@eifi1/ui-kit 0.31.1 → 0.32.1

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 (565) hide show
  1. package/README.md +51 -28
  2. package/dist/account/delete-account-setting.d.ts +8 -3
  3. package/dist/admin/admin-action-confirm.d.ts +8 -3
  4. package/dist/admin/admin-action-log.d.ts +1 -1
  5. package/dist/admin/admin-action-log.js +4 -2
  6. package/dist/admin/admin-action-log.js.map +1 -1
  7. package/dist/admin/invitations-panel.d.ts +8 -3
  8. package/dist/admin/invitations-panel.js +29 -26
  9. package/dist/admin/invitations-panel.js.map +1 -1
  10. package/dist/admin/reviewer-scope-editor.d.ts +8 -3
  11. package/dist/admin/role-select.d.ts +10 -5
  12. package/dist/admin/transfer-ownership-dialog.d.ts +8 -3
  13. package/dist/admin/user-roster.d.ts +10 -5
  14. package/dist/admin/user-roster.js +8 -47
  15. package/dist/admin/user-roster.js.map +1 -1
  16. package/dist/auth/auth-errors.d.ts +43 -8
  17. package/dist/auth/auth-errors.js +24 -1
  18. package/dist/auth/auth-errors.js.map +1 -1
  19. package/dist/auth/register-form.d.ts +8 -3
  20. package/dist/billing/billing-banner.d.ts +184 -0
  21. package/dist/billing/billing-banner.js +98 -0
  22. package/dist/billing/billing-banner.js.map +1 -0
  23. package/dist/billing/billing-labels.d.ts +100 -0
  24. package/dist/billing/billing-labels.js +76 -0
  25. package/dist/billing/billing-labels.js.map +1 -0
  26. package/dist/billing/billing-lock.d.ts +158 -0
  27. package/dist/billing/billing-lock.js +17 -0
  28. package/dist/billing/billing-lock.js.map +1 -0
  29. package/dist/billing/billing-standing.d.ts +149 -0
  30. package/dist/billing/billing-standing.js +28 -0
  31. package/dist/billing/billing-standing.js.map +1 -0
  32. package/dist/billing/plan-card.d.ts +248 -0
  33. package/dist/billing/plan-card.js +248 -0
  34. package/dist/billing/plan-card.js.map +1 -0
  35. package/dist/billing/plan-limit-notice.d.ts +156 -0
  36. package/dist/billing/plan-limit-notice.js +76 -0
  37. package/dist/billing/plan-limit-notice.js.map +1 -0
  38. package/dist/billing/plan-limit.d.ts +34 -0
  39. package/dist/billing/plan-limit.js +20 -0
  40. package/dist/billing/plan-limit.js.map +1 -0
  41. package/dist/billing/plan-price.d.ts +61 -0
  42. package/dist/billing/plan-price.js +45 -0
  43. package/dist/billing/plan-price.js.map +1 -0
  44. package/dist/billing/subscription-actions.d.ts +133 -0
  45. package/dist/billing/subscription-actions.js +28 -0
  46. package/dist/billing/subscription-actions.js.map +1 -0
  47. package/dist/billing/subscription-status-chip.d.ts +149 -0
  48. package/dist/billing/subscription-status-chip.js +48 -0
  49. package/dist/billing/subscription-status-chip.js.map +1 -0
  50. package/dist/chart.d.ts +9 -3
  51. package/dist/chart.js.map +1 -1
  52. package/dist/components/account-chips.d.ts +16 -4
  53. package/dist/components/account-chips.js +21 -3
  54. package/dist/components/account-chips.js.map +1 -1
  55. package/dist/components/alert-banner.d.ts +6 -1
  56. package/dist/components/alert-banner.js +5 -4
  57. package/dist/components/alert-banner.js.map +1 -1
  58. package/dist/components/amount-input.d.ts +8 -3
  59. package/dist/components/amount-input.js +5 -5
  60. package/dist/components/amount-input.js.map +1 -1
  61. package/dist/components/appearance-labels.d.ts +38 -0
  62. package/dist/components/appearance-labels.js +19 -0
  63. package/dist/components/appearance-labels.js.map +1 -0
  64. package/dist/components/autocomplete.js +2 -2
  65. package/dist/components/autocomplete.js.map +1 -1
  66. package/dist/components/breadcrumbs.js +3 -2
  67. package/dist/components/breadcrumbs.js.map +1 -1
  68. package/dist/components/bulk-action-bar.d.ts +2 -2
  69. package/dist/components/bulk-action-bar.js +7 -12
  70. package/dist/components/bulk-action-bar.js.map +1 -1
  71. package/dist/components/button-group.d.ts +8 -3
  72. package/dist/components/button-group.js +7 -1
  73. package/dist/components/button-group.js.map +1 -1
  74. package/dist/components/calculator.d.ts +8 -3
  75. package/dist/components/calculator.js +4 -3
  76. package/dist/components/calculator.js.map +1 -1
  77. package/dist/components/calendar-heatmap.js +5 -5
  78. package/dist/components/calendar-heatmap.js.map +1 -1
  79. package/dist/components/chart-height.d.ts +23 -0
  80. package/dist/components/chart-height.js +15 -0
  81. package/dist/components/chart-height.js.map +1 -0
  82. package/dist/components/chart-zoom.js +6 -1
  83. package/dist/components/chart-zoom.js.map +1 -1
  84. package/dist/components/checkbox-group.js +2 -2
  85. package/dist/components/checkbox-group.js.map +1 -1
  86. package/dist/components/checkbox.d.ts +10 -0
  87. package/dist/components/checkbox.js +25 -10
  88. package/dist/components/checkbox.js.map +1 -1
  89. package/dist/components/chip.d.ts +19 -3
  90. package/dist/components/chip.js +54 -23
  91. package/dist/components/chip.js.map +1 -1
  92. package/dist/components/choice-card.d.ts +1 -1
  93. package/dist/components/choice-card.js +9 -8
  94. package/dist/components/choice-card.js.map +1 -1
  95. package/dist/components/column-mapper.js +1 -1
  96. package/dist/components/column-mapper.js.map +1 -1
  97. package/dist/components/combobox-core.js +8 -7
  98. package/dist/components/combobox-core.js.map +1 -1
  99. package/dist/components/combobox.js +8 -8
  100. package/dist/components/combobox.js.map +1 -1
  101. package/dist/components/confirm-dialog.d.ts +8 -3
  102. package/dist/components/copy-button.d.ts +10 -5
  103. package/dist/components/country-select.js +1 -1
  104. package/dist/components/country-select.js.map +1 -1
  105. package/dist/components/currency-select.js +3 -3
  106. package/dist/components/currency-select.js.map +1 -1
  107. package/dist/components/danger-confirm.d.ts +8 -3
  108. package/dist/components/data-table-cells.d.ts +1 -1
  109. package/dist/components/data-table-filter-popover.d.ts +1 -1
  110. package/dist/components/data-table-filter-popover.js +4 -1
  111. package/dist/components/data-table-filter-popover.js.map +1 -1
  112. package/dist/components/data-table-filters.d.ts +1 -1
  113. package/dist/components/data-table-labels.d.ts +1 -1
  114. package/dist/components/data-table-pagination.d.ts +3 -2
  115. package/dist/components/data-table-pagination.js +15 -6
  116. package/dist/components/data-table-pagination.js.map +1 -1
  117. package/dist/components/data-table.d.ts +1 -1
  118. package/dist/components/data-table.js +123 -52
  119. package/dist/components/data-table.js.map +1 -1
  120. package/dist/components/date-picker.d.ts +1 -1
  121. package/dist/components/date-picker.js +18 -6
  122. package/dist/components/date-picker.js.map +1 -1
  123. package/dist/components/description-list.js +2 -2
  124. package/dist/components/description-list.js.map +1 -1
  125. package/dist/components/dialog-frame.js +2 -1
  126. package/dist/components/dialog-frame.js.map +1 -1
  127. package/dist/components/disclosure.js +3 -2
  128. package/dist/components/disclosure.js.map +1 -1
  129. package/dist/components/dropdown.d.ts +1 -1
  130. package/dist/components/dropdown.js +7 -1
  131. package/dist/components/dropdown.js.map +1 -1
  132. package/dist/components/facing-pair.d.ts +19 -6
  133. package/dist/components/facing-pair.js +3 -2
  134. package/dist/components/facing-pair.js.map +1 -1
  135. package/dist/components/field-parts.d.ts +102 -6
  136. package/dist/components/field-parts.js +81 -6
  137. package/dist/components/field-parts.js.map +1 -1
  138. package/dist/components/field-sync.d.ts +4 -1
  139. package/dist/components/field-sync.js +12 -2
  140. package/dist/components/field-sync.js.map +1 -1
  141. package/dist/components/field.js +2 -2
  142. package/dist/components/field.js.map +1 -1
  143. package/dist/components/file-button.d.ts +8 -3
  144. package/dist/components/file-dropzone.d.ts +8 -3
  145. package/dist/components/floating-panel.js +7 -4
  146. package/dist/components/floating-panel.js.map +1 -1
  147. package/dist/components/focus-ring.d.ts +12 -0
  148. package/dist/components/focus-ring.js +8 -0
  149. package/dist/components/focus-ring.js.map +1 -0
  150. package/dist/components/form-actions.d.ts +8 -3
  151. package/dist/components/form-actions.js +33 -19
  152. package/dist/components/form-actions.js.map +1 -1
  153. package/dist/components/full-bleed-dialog.d.ts +11 -1
  154. package/dist/components/full-bleed-dialog.js +3 -1
  155. package/dist/components/full-bleed-dialog.js.map +1 -1
  156. package/dist/components/iban-input.d.ts +8 -3
  157. package/dist/components/image-grid.js +1 -1
  158. package/dist/components/image-grid.js.map +1 -1
  159. package/dist/components/inline-edit-field.js +10 -4
  160. package/dist/components/inline-edit-field.js.map +1 -1
  161. package/dist/components/language-select.d.ts +8 -3
  162. package/dist/components/legal-page.d.ts +26 -5
  163. package/dist/components/legal-page.js +3 -1
  164. package/dist/components/legal-page.js.map +1 -1
  165. package/dist/components/legal.d.ts +8 -3
  166. package/dist/components/legal.js +2 -1
  167. package/dist/components/legal.js.map +1 -1
  168. package/dist/components/lightbox.js +24 -2
  169. package/dist/components/lightbox.js.map +1 -1
  170. package/dist/components/line-items.js +2 -1
  171. package/dist/components/line-items.js.map +1 -1
  172. package/dist/components/list.d.ts +5 -0
  173. package/dist/components/list.js +13 -9
  174. package/dist/components/list.js.map +1 -1
  175. package/dist/components/measured-grid.d.ts +3 -1
  176. package/dist/components/measured-grid.js +38 -20
  177. package/dist/components/measured-grid.js.map +1 -1
  178. package/dist/components/menu-item.js +2 -1
  179. package/dist/components/menu-item.js.map +1 -1
  180. package/dist/components/mini-calendar.js +8 -4
  181. package/dist/components/mini-calendar.js.map +1 -1
  182. package/dist/components/money-field.d.ts +8 -3
  183. package/dist/components/month-picker.js +50 -39
  184. package/dist/components/month-picker.js.map +1 -1
  185. package/dist/components/nav-pills.js +1 -1
  186. package/dist/components/nav-pills.js.map +1 -1
  187. package/dist/components/number-field.d.ts +8 -3
  188. package/dist/components/number-input.d.ts +11 -4
  189. package/dist/components/number-input.js +8 -6
  190. package/dist/components/number-input.js.map +1 -1
  191. package/dist/components/numpad-sheet.d.ts +8 -3
  192. package/dist/components/numpad-sheet.js +2 -1
  193. package/dist/components/numpad-sheet.js.map +1 -1
  194. package/dist/components/one-time-code-input.js +2 -2
  195. package/dist/components/one-time-code-input.js.map +1 -1
  196. package/dist/components/page-contents.js +2 -2
  197. package/dist/components/page-contents.js.map +1 -1
  198. package/dist/components/phone-input.d.ts +8 -3
  199. package/dist/components/phone-input.js +4 -4
  200. package/dist/components/phone-input.js.map +1 -1
  201. package/dist/components/pie-chart.d.ts +8 -2
  202. package/dist/components/pie-chart.js +11 -7
  203. package/dist/components/pie-chart.js.map +1 -1
  204. package/dist/components/popover.d.ts +9 -1
  205. package/dist/components/popover.js +8 -4
  206. package/dist/components/popover.js.map +1 -1
  207. package/dist/components/row-actions.d.ts +100 -0
  208. package/dist/components/row-actions.js +231 -0
  209. package/dist/components/row-actions.js.map +1 -0
  210. package/dist/components/scroll-area.js +1 -1
  211. package/dist/components/scroll-area.js.map +1 -1
  212. package/dist/components/search-field.js +3 -2
  213. package/dist/components/search-field.js.map +1 -1
  214. package/dist/components/series-chart-budget.d.ts +30 -7
  215. package/dist/components/series-chart-budget.js +12 -4
  216. package/dist/components/series-chart-budget.js.map +1 -1
  217. package/dist/components/series-chart-ticks.d.ts +7 -1
  218. package/dist/components/series-chart-ticks.js +6 -0
  219. package/dist/components/series-chart-ticks.js.map +1 -1
  220. package/dist/components/series-chart.d.ts +58 -28
  221. package/dist/components/series-chart.js +42 -32
  222. package/dist/components/series-chart.js.map +1 -1
  223. package/dist/components/settings-fields.d.ts +57 -8
  224. package/dist/components/settings-fields.js +39 -1
  225. package/dist/components/settings-fields.js.map +1 -1
  226. package/dist/components/share-card.d.ts +11 -6
  227. package/dist/components/share-card.js +8 -2
  228. package/dist/components/share-card.js.map +1 -1
  229. package/dist/components/sign-chip.d.ts +4 -0
  230. package/dist/components/sign-chip.js +17 -4
  231. package/dist/components/sign-chip.js.map +1 -1
  232. package/dist/components/signature-pad.d.ts +3 -1
  233. package/dist/components/signature-pad.js +8 -5
  234. package/dist/components/signature-pad.js.map +1 -1
  235. package/dist/components/slider.js +7 -6
  236. package/dist/components/slider.js.map +1 -1
  237. package/dist/components/sparkline.d.ts +5 -1
  238. package/dist/components/sparkline.js +4 -2
  239. package/dist/components/sparkline.js.map +1 -1
  240. package/dist/components/stat-tile.d.ts +17 -1
  241. package/dist/components/stat-tile.js +48 -20
  242. package/dist/components/stat-tile.js.map +1 -1
  243. package/dist/components/status-dot.d.ts +4 -0
  244. package/dist/components/status-dot.js +9 -2
  245. package/dist/components/status-dot.js.map +1 -1
  246. package/dist/components/swipeable-row.js +10 -2
  247. package/dist/components/swipeable-row.js.map +1 -1
  248. package/dist/components/switch.d.ts +21 -7
  249. package/dist/components/switch.js +24 -11
  250. package/dist/components/switch.js.map +1 -1
  251. package/dist/components/table.js +2 -2
  252. package/dist/components/table.js.map +1 -1
  253. package/dist/components/text-link.d.ts +8 -3
  254. package/dist/components/text-link.js +1 -1
  255. package/dist/components/text-link.js.map +1 -1
  256. package/dist/components/text.d.ts +7 -2
  257. package/dist/components/text.js +5 -5
  258. package/dist/components/text.js.map +1 -1
  259. package/dist/components/tile-radio.js +2 -1
  260. package/dist/components/tile-radio.js.map +1 -1
  261. package/dist/components/time-input.d.ts +8 -3
  262. package/dist/components/toast.d.ts +18 -2
  263. package/dist/components/toast.js +8 -5
  264. package/dist/components/toast.js.map +1 -1
  265. package/dist/components/toggle-group.d.ts +25 -3
  266. package/dist/components/toggle-group.js +40 -11
  267. package/dist/components/toggle-group.js.map +1 -1
  268. package/dist/components/toggle-legend.d.ts +10 -1
  269. package/dist/components/toggle-legend.js +21 -7
  270. package/dist/components/toggle-legend.js.map +1 -1
  271. package/dist/components/translation-review-editor.js +1 -1
  272. package/dist/components/translation-review-editor.js.map +1 -1
  273. package/dist/components/translation-review-parts.d.ts +4 -0
  274. package/dist/components/translation-review.d.ts +4 -0
  275. package/dist/components/translation-review.js +3 -3
  276. package/dist/components/translation-review.js.map +1 -1
  277. package/dist/components/tree-view.js +3 -2
  278. package/dist/components/tree-view.js.map +1 -1
  279. package/dist/components/treemap.d.ts +5 -2
  280. package/dist/components/treemap.js +33 -16
  281. package/dist/components/treemap.js.map +1 -1
  282. package/dist/components/ui.d.ts +8 -3
  283. package/dist/components/ui.js +207 -119
  284. package/dist/components/ui.js.map +1 -1
  285. package/dist/components/use-table-state.d.ts +1 -1
  286. package/dist/components/user-avatar.d.ts +5 -1
  287. package/dist/components/user-avatar.js +1 -1
  288. package/dist/components/user-avatar.js.map +1 -1
  289. package/dist/components/wizard-stepper.js +20 -14
  290. package/dist/components/wizard-stepper.js.map +1 -1
  291. package/dist/{data-table-labels-DUELeXpa.d.ts → data-table-labels-kpAAVi6g.d.ts} +26 -2
  292. package/dist/data-table.d.ts +1 -1
  293. package/dist/{delete-account-setting-CpIhzxxZ.d.ts → delete-account-setting-FLyJf-kZ.d.ts} +642 -60
  294. package/dist/demo/demo-banner.js +1 -0
  295. package/dist/demo/demo-banner.js.map +1 -1
  296. package/dist/feedback/feedback-attachment.d.ts +8 -3
  297. package/dist/feedback/feedback-context.d.ts +8 -3
  298. package/dist/feedback/feedback-crash.d.ts +8 -3
  299. package/dist/feedback/feedback-dialog.d.ts +8 -3
  300. package/dist/feedback/feedback-dialog.js +3 -3
  301. package/dist/feedback/feedback-dialog.js.map +1 -1
  302. package/dist/feedback/feedback-inbox.d.ts +8 -3
  303. package/dist/feedback/feedback-inbox.js +2 -2
  304. package/dist/feedback/feedback-inbox.js.map +1 -1
  305. package/dist/feedback/feedback-labels.d.ts +8 -3
  306. package/dist/feedback/feedback-menu.d.ts +9 -4
  307. package/dist/feedback/feedback-record.d.ts +8 -3
  308. package/dist/feedback/feedback-row-detail.d.ts +8 -3
  309. package/dist/feedback/feedback-status-undo.d.ts +8 -3
  310. package/dist/feedback/feedback-submit.d.ts +8 -3
  311. package/dist/feedback/feedback-swipe.d.ts +8 -3
  312. package/dist/feedback/feedback-table.d.ts +8 -3
  313. package/dist/feedback/feedback-thread.d.ts +8 -3
  314. package/dist/feedback/feedback-thread.js +3 -3
  315. package/dist/feedback/feedback-thread.js.map +1 -1
  316. package/dist/feedback.d.ts +8 -3
  317. package/dist/hooks/use-anchored-panel.d.ts +32 -3
  318. package/dist/hooks/use-anchored-panel.js +21 -3
  319. package/dist/hooks/use-anchored-panel.js.map +1 -1
  320. package/dist/hooks/use-breakpoint.d.ts +88 -0
  321. package/dist/hooks/use-breakpoint.js +50 -0
  322. package/dist/hooks/use-breakpoint.js.map +1 -0
  323. package/dist/hooks/use-file-drop.d.ts +8 -3
  324. package/dist/hooks/use-large-text.d.ts +29 -0
  325. package/dist/hooks/use-large-text.js +20 -0
  326. package/dist/hooks/use-large-text.js.map +1 -0
  327. package/dist/hooks/use-windowed-rows.d.ts +52 -8
  328. package/dist/hooks/use-windowed-rows.js +94 -5
  329. package/dist/hooks/use-windowed-rows.js.map +1 -1
  330. package/dist/i18n/defaults.d.ts +8 -3
  331. package/dist/i18n/defaults.js +8 -0
  332. package/dist/i18n/defaults.js.map +1 -1
  333. package/dist/i18n/german.d.ts +8 -3
  334. package/dist/i18n/german.js +94 -3
  335. package/dist/i18n/german.js.map +1 -1
  336. package/dist/i18n/kit-labels.d.ts +8 -3
  337. package/dist/i18n/kit-labels.js.map +1 -1
  338. package/dist/i18n/languages.d.ts +10 -5
  339. package/dist/i18n/locales/de-CH.d.ts +8 -3
  340. package/dist/i18n/locales/en.d.ts +8 -3
  341. package/dist/i18n/locales/es.d.ts +8 -3
  342. package/dist/i18n/locales/es.js +89 -3
  343. package/dist/i18n/locales/es.js.map +1 -1
  344. package/dist/i18n/locales/fr.d.ts +8 -3
  345. package/dist/i18n/locales/fr.js +91 -3
  346. package/dist/i18n/locales/fr.js.map +1 -1
  347. package/dist/i18n/locales/hu.d.ts +8 -3
  348. package/dist/i18n/locales/hu.js +91 -3
  349. package/dist/i18n/locales/hu.js.map +1 -1
  350. package/dist/i18n/locales/it.d.ts +8 -3
  351. package/dist/i18n/locales/it.js +92 -3
  352. package/dist/i18n/locales/it.js.map +1 -1
  353. package/dist/i18n/locales/zh.d.ts +8 -3
  354. package/dist/i18n/locales/zh.js +89 -3
  355. package/dist/i18n/locales/zh.js.map +1 -1
  356. package/dist/i18n/review.d.ts +8 -3
  357. package/dist/i18n/review.js +15 -1
  358. package/dist/i18n/review.js.map +1 -1
  359. package/dist/index.d.ts +27 -11
  360. package/dist/index.js +77 -4
  361. package/dist/index.js.map +1 -1
  362. package/dist/landing/landing-actions.d.ts +8 -3
  363. package/dist/landing/landing-sections.d.ts +8 -3
  364. package/dist/landing/landing-sections.js +7 -3
  365. package/dist/landing/landing-sections.js.map +1 -1
  366. package/dist/landing/public-header.d.ts +9 -4
  367. package/dist/landing/public-header.js +3 -3
  368. package/dist/landing/public-header.js.map +1 -1
  369. package/dist/landing/routing.d.ts +8 -3
  370. package/dist/lib/cn.js +3 -2
  371. package/dist/lib/cn.js.map +1 -1
  372. package/dist/rhf/fields.d.ts +8 -3
  373. package/dist/rhf/fields.js +1 -1
  374. package/dist/rhf/fields.js.map +1 -1
  375. package/dist/rhf/form.d.ts +8 -3
  376. package/dist/rhf/form.js +2 -2
  377. package/dist/rhf/form.js.map +1 -1
  378. package/dist/rhf.d.ts +8 -3
  379. package/dist/search/command-palette.d.ts +1 -1
  380. package/dist/search/command-palette.js +9 -8
  381. package/dist/search/command-palette.js.map +1 -1
  382. package/dist/settings/settings-layout.js +2 -1
  383. package/dist/settings/settings-layout.js.map +1 -1
  384. package/dist/settings/settings-section.d.ts +8 -3
  385. package/dist/settings/use-account-appearance.d.ts +92 -0
  386. package/dist/settings/use-account-appearance.js +63 -0
  387. package/dist/settings/use-account-appearance.js.map +1 -0
  388. package/dist/settings/use-settings-route.d.ts +1 -1
  389. package/dist/settings/use-settings-route.js +2 -3
  390. package/dist/settings/use-settings-route.js.map +1 -1
  391. package/dist/shell/app-shell.d.ts +12 -171
  392. package/dist/shell/app-shell.js +144 -28
  393. package/dist/shell/app-shell.js.map +1 -1
  394. package/dist/shell/auth-layout.js +1 -1
  395. package/dist/shell/auth-layout.js.map +1 -1
  396. package/dist/shell/option-switcher-menu.js +1 -1
  397. package/dist/shell/option-switcher-menu.js.map +1 -1
  398. package/dist/shell/top-bar-brand.d.ts +8 -3
  399. package/dist/shell/top-bar-brand.js +3 -1
  400. package/dist/shell/top-bar-brand.js.map +1 -1
  401. package/dist/shell/topbar-action-menu.d.ts +4 -0
  402. package/dist/shell/topbar-action-menu.js +3 -3
  403. package/dist/shell/topbar-action-menu.js.map +1 -1
  404. package/dist/shell/topbar-controls.js +4 -4
  405. package/dist/shell/topbar-controls.js.map +1 -1
  406. package/dist/shell.d.ts +9 -5
  407. package/dist/theme/contrast-tokens.d.ts +41 -0
  408. package/dist/theme/contrast-tokens.js +59 -0
  409. package/dist/theme/contrast-tokens.js.map +1 -0
  410. package/dist/theme/contrast.d.ts +110 -0
  411. package/dist/theme/contrast.js +128 -0
  412. package/dist/theme/contrast.js.map +1 -0
  413. package/dist/theme/palette-presets.d.ts +12 -1
  414. package/dist/theme/palette-presets.js +9 -4
  415. package/dist/theme/palette-presets.js.map +1 -1
  416. package/dist/theme/palette-store.js.map +1 -1
  417. package/dist/theme/text-size.d.ts +173 -0
  418. package/dist/theme/text-size.js +121 -0
  419. package/dist/theme/text-size.js.map +1 -0
  420. package/dist/wizard/stepper-nav.d.ts +8 -3
  421. package/dist/wizard/wizard-summary.js +1 -1
  422. package/dist/wizard/wizard-summary.js.map +1 -1
  423. package/dist/wizard.d.ts +8 -3
  424. package/package.json +1 -1
  425. package/src/admin/admin-action-log.tsx +6 -1
  426. package/src/admin/invitations-panel.tsx +36 -29
  427. package/src/admin/user-roster.tsx +17 -72
  428. package/src/auth/auth-errors.ts +68 -7
  429. package/src/billing/billing-banner.tsx +205 -0
  430. package/src/billing/billing-labels.ts +240 -0
  431. package/src/billing/billing-lock.ts +75 -0
  432. package/src/billing/billing-standing.ts +72 -0
  433. package/src/billing/plan-card.tsx +458 -0
  434. package/src/billing/plan-limit-notice.tsx +145 -0
  435. package/src/billing/plan-limit.ts +53 -0
  436. package/src/billing/plan-price.ts +101 -0
  437. package/src/billing/subscription-actions.tsx +76 -0
  438. package/src/billing/subscription-status-chip.tsx +95 -0
  439. package/src/chart.ts +2 -0
  440. package/src/components/account-chips.tsx +36 -10
  441. package/src/components/alert-banner.tsx +16 -7
  442. package/src/components/amount-input.tsx +5 -5
  443. package/src/components/appearance-labels.ts +47 -0
  444. package/src/components/autocomplete.tsx +2 -2
  445. package/src/components/breadcrumbs.tsx +3 -3
  446. package/src/components/bulk-action-bar.tsx +11 -14
  447. package/src/components/button-group.tsx +21 -0
  448. package/src/components/calculator.tsx +4 -3
  449. package/src/components/calendar-heatmap.tsx +5 -5
  450. package/src/components/chart-height.ts +30 -0
  451. package/src/components/chart-zoom.tsx +6 -1
  452. package/src/components/checkbox-group.tsx +2 -2
  453. package/src/components/checkbox.tsx +62 -20
  454. package/src/components/chip.tsx +139 -30
  455. package/src/components/choice-card.tsx +16 -11
  456. package/src/components/column-mapper.tsx +1 -1
  457. package/src/components/combobox-core.tsx +8 -7
  458. package/src/components/combobox.tsx +8 -8
  459. package/src/components/country-select.tsx +1 -1
  460. package/src/components/currency-select.tsx +3 -3
  461. package/src/components/data-table-filter-popover.tsx +7 -1
  462. package/src/components/data-table-pagination.tsx +60 -31
  463. package/src/components/data-table.tsx +164 -43
  464. package/src/components/date-picker.tsx +44 -10
  465. package/src/components/description-list.tsx +2 -2
  466. package/src/components/dialog-frame.tsx +2 -1
  467. package/src/components/disclosure.tsx +3 -2
  468. package/src/components/dropdown.tsx +10 -2
  469. package/src/components/facing-pair.ts +12 -5
  470. package/src/components/field-parts.tsx +190 -7
  471. package/src/components/field-sync.tsx +17 -3
  472. package/src/components/field.tsx +2 -2
  473. package/src/components/floating-panel.tsx +7 -4
  474. package/src/components/focus-ring.ts +12 -0
  475. package/src/components/form-actions.tsx +55 -18
  476. package/src/components/full-bleed-dialog.tsx +18 -4
  477. package/src/components/image-grid.tsx +1 -1
  478. package/src/components/inline-edit-field.tsx +26 -4
  479. package/src/components/legal-page.tsx +28 -2
  480. package/src/components/legal.tsx +19 -1
  481. package/src/components/lightbox.tsx +24 -2
  482. package/src/components/line-items.tsx +7 -1
  483. package/src/components/list.tsx +36 -10
  484. package/src/components/measured-grid.tsx +49 -24
  485. package/src/components/menu-item.tsx +2 -2
  486. package/src/components/mini-calendar.tsx +8 -4
  487. package/src/components/month-picker.tsx +15 -6
  488. package/src/components/nav-pills.tsx +1 -1
  489. package/src/components/number-input.tsx +14 -7
  490. package/src/components/numpad-sheet.tsx +2 -2
  491. package/src/components/one-time-code-input.tsx +2 -2
  492. package/src/components/page-contents.tsx +2 -2
  493. package/src/components/phone-input.tsx +9 -4
  494. package/src/components/pie-chart.tsx +19 -9
  495. package/src/components/popover.tsx +22 -5
  496. package/src/components/row-actions.tsx +450 -0
  497. package/src/components/scroll-area.tsx +1 -1
  498. package/src/components/search-field.tsx +4 -2
  499. package/src/components/series-chart-budget.ts +40 -8
  500. package/src/components/series-chart-ticks.ts +10 -0
  501. package/src/components/series-chart.tsx +123 -72
  502. package/src/components/settings-fields.tsx +89 -5
  503. package/src/components/share-card.tsx +31 -2
  504. package/src/components/sign-chip.tsx +28 -4
  505. package/src/components/signature-pad.tsx +13 -7
  506. package/src/components/slider.tsx +22 -9
  507. package/src/components/sparkline.tsx +11 -3
  508. package/src/components/stat-tile.tsx +70 -14
  509. package/src/components/status-dot.tsx +25 -2
  510. package/src/components/swipeable-row.tsx +10 -2
  511. package/src/components/switch.tsx +57 -23
  512. package/src/components/table.tsx +2 -2
  513. package/src/components/text-link.tsx +1 -1
  514. package/src/components/text.tsx +11 -6
  515. package/src/components/tile-radio.tsx +4 -1
  516. package/src/components/toast.tsx +27 -7
  517. package/src/components/toggle-group.tsx +115 -14
  518. package/src/components/toggle-legend.tsx +53 -8
  519. package/src/components/translation-review-editor.tsx +1 -1
  520. package/src/components/translation-review.tsx +11 -5
  521. package/src/components/tree-view.tsx +3 -2
  522. package/src/components/treemap.tsx +27 -10
  523. package/src/components/ui.tsx +353 -153
  524. package/src/components/user-avatar.tsx +1 -1
  525. package/src/components/wizard-stepper.tsx +5 -1
  526. package/src/demo/demo-banner.tsx +5 -0
  527. package/src/feedback/feedback-dialog.tsx +3 -3
  528. package/src/feedback/feedback-inbox.tsx +2 -2
  529. package/src/feedback/feedback-thread.tsx +3 -3
  530. package/src/hooks/use-anchored-panel.ts +67 -5
  531. package/src/hooks/use-breakpoint.ts +132 -0
  532. package/src/hooks/use-large-text.ts +41 -0
  533. package/src/hooks/use-windowed-rows.ts +168 -12
  534. package/src/i18n/defaults.ts +8 -0
  535. package/src/i18n/german.ts +107 -0
  536. package/src/i18n/kit-labels.tsx +12 -0
  537. package/src/i18n/locales/es.ts +98 -0
  538. package/src/i18n/locales/fr.ts +102 -0
  539. package/src/i18n/locales/hu.ts +100 -0
  540. package/src/i18n/locales/it.ts +103 -0
  541. package/src/i18n/locales/zh.ts +94 -0
  542. package/src/i18n/review.ts +14 -0
  543. package/src/index.ts +76 -6
  544. package/src/landing/landing-sections.tsx +7 -3
  545. package/src/landing/public-header.tsx +4 -4
  546. package/src/lib/cn.ts +13 -2
  547. package/src/rhf/fields.tsx +1 -1
  548. package/src/rhf/form.tsx +2 -2
  549. package/src/search/command-palette.tsx +10 -9
  550. package/src/settings/settings-layout.tsx +2 -1
  551. package/src/settings/use-account-appearance.ts +148 -0
  552. package/src/settings/use-settings-route.ts +3 -4
  553. package/src/shell/app-shell.tsx +278 -54
  554. package/src/shell/auth-layout.tsx +1 -1
  555. package/src/shell/option-switcher-menu.tsx +1 -1
  556. package/src/shell/top-bar-brand.tsx +3 -1
  557. package/src/shell/topbar-action-menu.tsx +3 -3
  558. package/src/shell/topbar-controls.tsx +4 -4
  559. package/src/theme/contrast-tokens.ts +112 -0
  560. package/src/theme/contrast.ts +225 -0
  561. package/src/theme/palette-presets.ts +24 -4
  562. package/src/theme/palette-store.ts +6 -1
  563. package/src/theme/text-size.ts +283 -0
  564. package/src/wizard/wizard-summary.tsx +4 -2
  565. package/tokens.css +158 -8
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/phone-input.tsx"],"sourcesContent":["import { forwardRef, useId, useState } from \"react\";\nimport type { ChangeEvent, FocusEvent } from \"react\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport {\n PHONE_COUNTRIES,\n PHONE_DIAL_CODES,\n formatNationalPhone,\n formatPhone,\n isE164,\n parsePhone,\n} from \"../lib/phone\";\nimport type { ParsedPhone, PhoneCountry, PhoneCountryCode } from \"../lib/phone\";\nimport { hasMessage, mergeDescribedBy } from \"./choice-parts\";\nimport { Input, Select, type InputProps } from \"./ui\";\n\n// ── Labels ────────────────────────────────────────────────────────────────────\n\n/** The words {@link PhoneInput} renders on its own behalf. */\nexport interface PhoneInputLabels {\n /** The country-code select's accessible name. */\n countryCode: string;\n /** The select's last option: a number from a country not listed, typed with its\n * own \"+\". */\n other: string;\n}\n\nexport const DEFAULT_PHONE_INPUT_LABELS: PhoneInputLabels = {\n countryCode: \"Country code\",\n other: \"Other\",\n};\n\n/**\n * The `phoneInput` namespace: prop > `<UiKitProvider labels>` > English (0.22.0).\n */\nfunction usePhoneInputLabels(prop: Partial<PhoneInputLabels> | undefined): PhoneInputLabels {\n const labels: PhoneInputLabels = useKitLabels(\"phoneInput\", DEFAULT_PHONE_INPUT_LABELS, prop);\n return labels;\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\n/** The caption and the error under the row — the type every kit field uses for them\n * (Select's caption, Input's `error`). */\nconst CAPTION_CLASS = \"mt-1 text-[11px] leading-tight text-[var(--text-muted)]\";\nconst ERROR_CLASS = \"mt-1 text-[11px] leading-tight text-[var(--danger)]\";\n\nconst isTextHint = (hint: unknown): hint is string | number =>\n (typeof hint === \"string\" && hint !== \"\") || typeof hint === \"number\";\n\n/** A stored value read back: an E.164 number these rules can read, else `null`. */\nfunction storedNumber(value: string): ParsedPhone | null {\n return isE164(value) ? parsePhone(value, \"other\") : null;\n}\n\n/** The select's country for a stored E.164 value, or `null` for free text. */\nfunction countryOf(value: string): PhoneCountry | null {\n const parsed = storedNumber(value);\n return parsed ? (parsed.country ?? \"other\") : null;\n}\n\n/** What the field emits for typed text: E.164 when it reads, the text unchanged when\n * it does not. */\nfunction emitFor(raw: string, country: PhoneCountry): string {\n return parsePhone(raw, country)?.e164 ?? raw;\n}\n\n/**\n * The text a stored value shows, outside of typing. Under its own country: the national\n * number grouped (\"21 000 00 01\" beside \"CH +41\"). Under another (a \"+33\" number while\n * the select says \"Other\"): the whole international number. Free text: as it is.\n */\nfunction displayOf(value: string, country: PhoneCountry): string {\n const parsed = storedNumber(value);\n if (!parsed) return value;\n if (parsed.country !== null && parsed.country === country) {\n return formatNationalPhone(parsed.national, parsed.country);\n }\n return formatPhone(parsed.e164);\n}\n\n// ── Component ─────────────────────────────────────────────────────────────────\n\nexport interface PhoneInputProps extends Omit<InputProps, \"type\" | \"value\" | \"defaultValue\"> {\n /** The number: E.164 (\"+41210000001\") for one the field could read, or free text —\n * a value saved before the field existed is shown exactly as it is. `\"\"` is no\n * number. */\n value: string;\n /** On every change: E.164 when the typed text reads as a number of the selected\n * country (or carries its own \"+\"), otherwise the typed text UNCHANGED. Nothing is\n * refused — see {@link PhoneInput}. Plain `onChange` still fires with the raw event;\n * bind this one to a form. */\n onValueChange?: (value: string) => void;\n /** The country a number typed without \"+\" belongs to, until the value says\n * otherwise. Default `\"CH\"`. */\n defaultCountry?: PhoneCountry;\n /** The countries the select offers, in order; \"Other\" always follows. Default\n * {@link PHONE_COUNTRIES} (CH, LI, DE, AT, FR, IT). A stored number from a country\n * left out still shows under its own code. */\n countries?: readonly PhoneCountryCode[];\n /** See {@link PhoneInputLabels}. */\n labels?: Partial<PhoneInputLabels>;\n /** Classes for the whole field — the select, the input and the lines under them.\n * `inputClassName` reaches the `<input>`. */\n className?: string;\n}\n\n/**\n * A phone number field (kastlan): a country-code select and the number, stored as\n * E.164 and shown grouped.\n *\n * kastlan's contacts kept whatever was typed — \"079 …\", \"+41 (0)79 …\", \"0041 79 …\" —\n * which nobody could dial from the app or match against the next import. This field\n * turns all of those into one stored form, \"+41210000001\", without taking anything\n * away from the person typing:\n *\n * - **A country to type against.** The select (default CH, then LI, DE, AT, FR, IT\n * and \"Other\") says which country a number without \"+\" belongs to; the national\n * trunk 0 may be typed or left out. Typing a \"+\" or \"00\" overrides it, and the\n * select follows the number (\"+49 …\" switches it to DE). Changing the select moves\n * the national number to the new country code.\n * - **E.164 when it reads, the text when it does not.** A number the rules read is\n * emitted as E.164; anything else (\"ask reception\", an extension) is emitted exactly\n * as typed. Never refused, never an error of its own: a contact's number is often\n * known only roughly, and a field that will not save \"079 … (mornings)\" makes\n * people type it into the notes instead. An app that needs a dialable number\n * checks {@link isE164} on submit.\n * - **Existing values untouched.** A free-text value from before is shown exactly as\n * it is, and normalised only when someone edits it — the field never rewrites a\n * record just by being opened.\n * - **Grouped when settled.** While typing the field shows what was typed; on blur a\n * number it read is regrouped by its country (\"21 000 00 01\").\n *\n * **The limitation** — there is no libphonenumber behind this (≈150 kB of metadata in\n * every bundle): the six countries have light rules (the trunk 0, a plausible length)\n * and every other country is any \"+\" with 8–15 digits. A well-formed number that does\n * not exist passes, and German and Austrian landlines, whose area codes vary in length,\n * are grouped only where the split is known (mobile prefixes, the largest cities) and\n * otherwise shown as one block. See `lib/phone.ts`.\n *\n * `label`, `hint`, `error` and `invalid` read as on {@link Input}: the label floats in\n * the number's box, and a text hint and the error go under the whole row, attached to\n * the number with `aria-describedby`. The select is named by `labels.countryCode`, and\n * the pair is a group named by a string `label`.\n */\nexport const PhoneInput = forwardRef<HTMLInputElement, PhoneInputProps>(function PhoneInput(\n {\n value,\n onValueChange,\n onChange,\n onBlur,\n defaultCountry = \"CH\",\n countries = PHONE_COUNTRIES,\n labels,\n className,\n inputClassName,\n label,\n hint,\n error,\n invalid,\n disabled,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n },\n ref,\n) {\n const text = usePhoneInputLabels(labels);\n const [country, setCountry] = useState<PhoneCountry>(() => countryOf(value) ?? defaultCountry);\n // What was typed, shown for as long as it still reads as the value (AmountInput's\n // draft): the person's own spacing stays under their fingers, and is regrouped on\n // blur.\n const [draft, setDraft] = useState<string | null>(null);\n // The last value this field emitted, to tell its own echo from a value set outside.\n const [emitted, setEmitted] = useState<string | null>(null);\n const [seen, setSeen] = useState(value);\n if (seen !== value) {\n setSeen(value);\n if (value !== emitted) {\n // Set from outside (a reset, another record): its own country, none of our text.\n const own = countryOf(value);\n if (own) setCountry(own);\n setDraft(null);\n }\n }\n\n const emit = (next: string) => {\n setEmitted(next);\n onValueChange?.(next);\n };\n\n const handleChange = (e: ChangeEvent<HTMLInputElement>) => {\n onChange?.(e);\n const raw = e.target.value;\n setDraft(raw);\n const parsed = parsePhone(raw, country);\n // A number typed with its own \"+\" takes the select with it.\n if (parsed && parsed.country !== country) setCountry(parsed.country ?? \"other\");\n emit(parsed?.e164 ?? raw);\n };\n\n const handleCountry = (next: PhoneCountry) => {\n setCountry(next);\n const stored = storedNumber(value);\n if (stored) {\n // \"Other\", the same country, or a generic number with its own \"+\": the number\n // stays, only the way it is shown changes.\n if (next === \"other\" || stored.country === null || stored.country === next) {\n setDraft(null);\n return;\n }\n // A different listed country: the national number moves to its code. If it does\n // not read there, the digits are kept as text for the person to finish.\n emit(parsePhone(stored.national, next)?.e164 ?? stored.national);\n setDraft(null);\n return;\n }\n // Free text (or a draft): read it again under the new country.\n const source = draft ?? value;\n const parsed = source.trim() === \"\" ? null : parsePhone(source, next);\n if (parsed && parsed.e164 !== value) {\n setDraft(source);\n emit(parsed.e164);\n }\n };\n\n const display = draft !== null && emitFor(draft, country) === value ? draft : displayOf(value, country);\n\n // The select offers the configured countries, plus the stored number's own if it\n // is not among them — a select cannot show a value it has no option for.\n const options: PhoneCountryCode[] = [...countries];\n if (country !== \"other\" && !options.includes(country)) options.push(country);\n\n const captionId = useId();\n const errorId = useId();\n const caption = isTextHint(hint);\n const showError = hasMessage(error);\n const named = typeof label === \"string\" && label !== \"\";\n\n return (\n <div\n className={className}\n role={named ? \"group\" : undefined}\n aria-label={named ? label : undefined}\n >\n <div className=\"flex items-stretch gap-2\">\n <Select\n aria-label={text.countryCode}\n value={country}\n disabled={disabled}\n onChange={(e) => handleCountry(e.target.value as PhoneCountry)}\n className=\"w-28 shrink-0\"\n // Stretched to the number's height: a labelled field is taller than an\n // unlabelled select, and the pair should read as one row.\n selectClassName=\"h-full tabular-nums\"\n >\n {options.map((code) => (\n <option key={code} value={code}>\n {`${code} +${PHONE_DIAL_CODES[code]}`}\n </option>\n ))}\n <option value=\"other\">{text.other}</option>\n </Select>\n {/* Its own flex item, so nothing Input wraps itself in can take the sizing. */}\n <div className=\"min-w-0 flex-1\">\n <Input\n ref={ref}\n {...rest}\n type=\"tel\"\n autoComplete={rest.autoComplete ?? \"tel\"}\n label={label}\n // A FieldHint \"?\" rides the label line; text goes under the whole row.\n hint={caption ? undefined : hint}\n invalid={invalid || showError}\n aria-describedby={mergeDescribedBy(ariaDescribedBy, caption && captionId, showError && errorId)}\n disabled={disabled}\n value={display}\n onChange={handleChange}\n onBlur={(e: FocusEvent<HTMLInputElement>) => {\n setDraft(null);\n onBlur?.(e);\n }}\n inputClassName={cn(\"tabular-nums\", inputClassName)}\n />\n </div>\n </div>\n {caption && (\n <p id={captionId} className={CAPTION_CLASS}>\n {hint}\n </p>\n )}\n {showError && (\n <p id={errorId} className={ERROR_CLASS}>\n {error}\n </p>\n )}\n </div>\n );\n});\nPhoneInput.displayName = \"PhoneInput\";\n"],"mappings":"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","names":["PhoneInput"]}
1
+ {"version":3,"sources":["../../src/components/phone-input.tsx"],"sourcesContent":["import { forwardRef, useId, useState } from \"react\";\nimport type { ChangeEvent, FocusEvent } from \"react\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport {\n PHONE_COUNTRIES,\n PHONE_DIAL_CODES,\n formatNationalPhone,\n formatPhone,\n isE164,\n parsePhone,\n} from \"../lib/phone\";\nimport type { ParsedPhone, PhoneCountry, PhoneCountryCode } from \"../lib/phone\";\nimport { hasMessage, mergeDescribedBy } from \"./choice-parts\";\nimport { Input, Select, type InputProps } from \"./ui\";\n\n// ── Labels ────────────────────────────────────────────────────────────────────\n\n/** The words {@link PhoneInput} renders on its own behalf. */\nexport interface PhoneInputLabels {\n /** The country-code select's accessible name. */\n countryCode: string;\n /** The select's last option: a number from a country not listed, typed with its\n * own \"+\". */\n other: string;\n}\n\nexport const DEFAULT_PHONE_INPUT_LABELS: PhoneInputLabels = {\n countryCode: \"Country code\",\n other: \"Other\",\n};\n\n/**\n * The `phoneInput` namespace: prop > `<UiKitProvider labels>` > English (0.22.0).\n */\nfunction usePhoneInputLabels(prop: Partial<PhoneInputLabels> | undefined): PhoneInputLabels {\n const labels: PhoneInputLabels = useKitLabels(\"phoneInput\", DEFAULT_PHONE_INPUT_LABELS, prop);\n return labels;\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\n/** The caption and the error under the row — the type every kit field uses for them\n * (Select's caption, Input's `error`). */\nconst CAPTION_CLASS = \"mt-1 text-caption leading-tight text-[var(--text-muted)]\";\nconst ERROR_CLASS = \"mt-1 text-caption leading-tight text-[var(--danger)]\";\n\nconst isTextHint = (hint: unknown): hint is string | number =>\n (typeof hint === \"string\" && hint !== \"\") || typeof hint === \"number\";\n\n/** A stored value read back: an E.164 number these rules can read, else `null`. */\nfunction storedNumber(value: string): ParsedPhone | null {\n return isE164(value) ? parsePhone(value, \"other\") : null;\n}\n\n/** The select's country for a stored E.164 value, or `null` for free text. */\nfunction countryOf(value: string): PhoneCountry | null {\n const parsed = storedNumber(value);\n return parsed ? (parsed.country ?? \"other\") : null;\n}\n\n/** What the field emits for typed text: E.164 when it reads, the text unchanged when\n * it does not. */\nfunction emitFor(raw: string, country: PhoneCountry): string {\n return parsePhone(raw, country)?.e164 ?? raw;\n}\n\n/**\n * The text a stored value shows, outside of typing. Under its own country: the national\n * number grouped (\"21 000 00 01\" beside \"CH +41\"). Under another (a \"+33\" number while\n * the select says \"Other\"): the whole international number. Free text: as it is.\n */\nfunction displayOf(value: string, country: PhoneCountry): string {\n const parsed = storedNumber(value);\n if (!parsed) return value;\n if (parsed.country !== null && parsed.country === country) {\n return formatNationalPhone(parsed.national, parsed.country);\n }\n return formatPhone(parsed.e164);\n}\n\n// ── Component ─────────────────────────────────────────────────────────────────\n\nexport interface PhoneInputProps extends Omit<InputProps, \"type\" | \"value\" | \"defaultValue\"> {\n /** The number: E.164 (\"+41210000001\") for one the field could read, or free text —\n * a value saved before the field existed is shown exactly as it is. `\"\"` is no\n * number. */\n value: string;\n /** On every change: E.164 when the typed text reads as a number of the selected\n * country (or carries its own \"+\"), otherwise the typed text UNCHANGED. Nothing is\n * refused — see {@link PhoneInput}. Plain `onChange` still fires with the raw event;\n * bind this one to a form. */\n onValueChange?: (value: string) => void;\n /** The country a number typed without \"+\" belongs to, until the value says\n * otherwise. Default `\"CH\"`. */\n defaultCountry?: PhoneCountry;\n /** The countries the select offers, in order; \"Other\" always follows. Default\n * {@link PHONE_COUNTRIES} (CH, LI, DE, AT, FR, IT). A stored number from a country\n * left out still shows under its own code. */\n countries?: readonly PhoneCountryCode[];\n /** See {@link PhoneInputLabels}. */\n labels?: Partial<PhoneInputLabels>;\n /** Classes for the whole field — the select, the input and the lines under them.\n * `inputClassName` reaches the `<input>`. */\n className?: string;\n}\n\n/**\n * A phone number field (kastlan): a country-code select and the number, stored as\n * E.164 and shown grouped.\n *\n * kastlan's contacts kept whatever was typed — \"079 …\", \"+41 (0)79 …\", \"0041 79 …\" —\n * which nobody could dial from the app or match against the next import. This field\n * turns all of those into one stored form, \"+41210000001\", without taking anything\n * away from the person typing:\n *\n * - **A country to type against.** The select (default CH, then LI, DE, AT, FR, IT\n * and \"Other\") says which country a number without \"+\" belongs to; the national\n * trunk 0 may be typed or left out. Typing a \"+\" or \"00\" overrides it, and the\n * select follows the number (\"+49 …\" switches it to DE). Changing the select moves\n * the national number to the new country code.\n * - **E.164 when it reads, the text when it does not.** A number the rules read is\n * emitted as E.164; anything else (\"ask reception\", an extension) is emitted exactly\n * as typed. Never refused, never an error of its own: a contact's number is often\n * known only roughly, and a field that will not save \"079 … (mornings)\" makes\n * people type it into the notes instead. An app that needs a dialable number\n * checks {@link isE164} on submit.\n * - **Existing values untouched.** A free-text value from before is shown exactly as\n * it is, and normalised only when someone edits it — the field never rewrites a\n * record just by being opened.\n * - **Grouped when settled.** While typing the field shows what was typed; on blur a\n * number it read is regrouped by its country (\"21 000 00 01\").\n *\n * **The limitation** — there is no libphonenumber behind this (≈150 kB of metadata in\n * every bundle): the six countries have light rules (the trunk 0, a plausible length)\n * and every other country is any \"+\" with 8–15 digits. A well-formed number that does\n * not exist passes, and German and Austrian landlines, whose area codes vary in length,\n * are grouped only where the split is known (mobile prefixes, the largest cities) and\n * otherwise shown as one block. See `lib/phone.ts`.\n *\n * `label`, `hint`, `error` and `invalid` read as on {@link Input}: the label floats in\n * the number's box, and a text hint and the error go under the whole row, attached to\n * the number with `aria-describedby`. The select is named by `labels.countryCode`, and\n * the pair is a group named by a string `label`.\n */\nexport const PhoneInput = forwardRef<HTMLInputElement, PhoneInputProps>(function PhoneInput(\n {\n value,\n onValueChange,\n onChange,\n onBlur,\n defaultCountry = \"CH\",\n countries = PHONE_COUNTRIES,\n labels,\n className,\n inputClassName,\n label,\n hint,\n error,\n invalid,\n disabled,\n \"aria-describedby\": ariaDescribedBy,\n ...rest\n },\n ref,\n) {\n const text = usePhoneInputLabels(labels);\n const [country, setCountry] = useState<PhoneCountry>(() => countryOf(value) ?? defaultCountry);\n // What was typed, shown for as long as it still reads as the value (AmountInput's\n // draft): the person's own spacing stays under their fingers, and is regrouped on\n // blur.\n const [draft, setDraft] = useState<string | null>(null);\n // The last value this field emitted, to tell its own echo from a value set outside.\n const [emitted, setEmitted] = useState<string | null>(null);\n const [seen, setSeen] = useState(value);\n if (seen !== value) {\n setSeen(value);\n if (value !== emitted) {\n // Set from outside (a reset, another record): its own country, none of our text.\n const own = countryOf(value);\n if (own) setCountry(own);\n setDraft(null);\n }\n }\n\n const emit = (next: string) => {\n setEmitted(next);\n onValueChange?.(next);\n };\n\n const handleChange = (e: ChangeEvent<HTMLInputElement>) => {\n onChange?.(e);\n const raw = e.target.value;\n setDraft(raw);\n const parsed = parsePhone(raw, country);\n // A number typed with its own \"+\" takes the select with it.\n if (parsed && parsed.country !== country) setCountry(parsed.country ?? \"other\");\n emit(parsed?.e164 ?? raw);\n };\n\n const handleCountry = (next: PhoneCountry) => {\n setCountry(next);\n const stored = storedNumber(value);\n if (stored) {\n // \"Other\", the same country, or a generic number with its own \"+\": the number\n // stays, only the way it is shown changes.\n if (next === \"other\" || stored.country === null || stored.country === next) {\n setDraft(null);\n return;\n }\n // A different listed country: the national number moves to its code. If it does\n // not read there, the digits are kept as text for the person to finish.\n emit(parsePhone(stored.national, next)?.e164 ?? stored.national);\n setDraft(null);\n return;\n }\n // Free text (or a draft): read it again under the new country.\n const source = draft ?? value;\n const parsed = source.trim() === \"\" ? null : parsePhone(source, next);\n if (parsed && parsed.e164 !== value) {\n setDraft(source);\n emit(parsed.e164);\n }\n };\n\n const display = draft !== null && emitFor(draft, country) === value ? draft : displayOf(value, country);\n\n // The select offers the configured countries, plus the stored number's own if it\n // is not among them — a select cannot show a value it has no option for.\n const options: PhoneCountryCode[] = [...countries];\n if (country !== \"other\" && !options.includes(country)) options.push(country);\n\n const captionId = useId();\n const errorId = useId();\n const caption = isTextHint(hint);\n const showError = hasMessage(error);\n const named = typeof label === \"string\" && label !== \"\";\n\n return (\n <div\n className={className}\n role={named ? \"group\" : undefined}\n aria-label={named ? label : undefined}\n >\n {/* On a phone at Large the number takes a line of its own under the country (0.32.1,\n the 360 px Extra-large sweep; docs/text-size-harmonization.md §4 \"nothing\n truncates\"): the 7rem select is 168 px at 150 %, and on a 360 px phone it left\n the number 82 px — \"Ph…\" over \"021 (\". `large:max-sm:`, the scaled phone width,\n so a wider screen at Large and Normal keep the one row. */}\n <div className=\"flex items-stretch gap-2 large:max-sm:flex-wrap\">\n <Select\n aria-label={text.countryCode}\n value={country}\n disabled={disabled}\n onChange={(e) => handleCountry(e.target.value as PhoneCountry)}\n className=\"w-28 shrink-0\"\n // Stretched to the number's height: a labelled field is taller than an\n // unlabelled select, and the pair should read as one row.\n selectClassName=\"h-full tabular-nums\"\n >\n {options.map((code) => (\n <option key={code} value={code}>\n {`${code} +${PHONE_DIAL_CODES[code]}`}\n </option>\n ))}\n <option value=\"other\">{text.other}</option>\n </Select>\n {/* Its own flex item, so nothing Input wraps itself in can take the sizing. */}\n <div className=\"min-w-0 flex-1 large:max-sm:basis-full\">\n <Input\n ref={ref}\n {...rest}\n type=\"tel\"\n autoComplete={rest.autoComplete ?? \"tel\"}\n label={label}\n // A FieldHint \"?\" rides the label line; text goes under the whole row.\n hint={caption ? undefined : hint}\n invalid={invalid || showError}\n aria-describedby={mergeDescribedBy(ariaDescribedBy, caption && captionId, showError && errorId)}\n disabled={disabled}\n value={display}\n onChange={handleChange}\n onBlur={(e: FocusEvent<HTMLInputElement>) => {\n setDraft(null);\n onBlur?.(e);\n }}\n inputClassName={cn(\"tabular-nums\", inputClassName)}\n />\n </div>\n </div>\n {caption && (\n <p id={captionId} className={CAPTION_CLASS}>\n {hint}\n </p>\n )}\n {showError && (\n <p id={errorId} className={ERROR_CLASS}>\n {error}\n </p>\n )}\n </div>\n );\n});\nPhoneInput.displayName = \"PhoneInput\";\n"],"mappings":"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","names":["PhoneInput"]}
@@ -1,6 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ComponentPropsWithoutRef, ReactNode } from 'react';
3
3
  import { PieChartLabels } from './pie-chart-labels.js';
4
+ import { ChartHeight } from './chart-height.js';
4
5
 
5
6
  /** One slice. */
6
7
  interface PieChartSlice {
@@ -28,8 +29,13 @@ interface PieChartProps extends Omit<ComponentPropsWithoutRef<"div">, "children"
28
29
  data: readonly PieChartSlice[];
29
30
  /** `"donut"` (default) leaves a hole with the total in it; `"pie"` is the full disc. */
30
31
  variant?: "donut" | "pie";
31
- /** Height of the plot in px (the legend is under it). Default 300. */
32
- height?: number;
32
+ /**
33
+ * Height of the plot (the legend is under it): a CSS length (`"20rem"`,
34
+ * `"min(20rem, 50dvh)"`), a Tailwind class, or px. Default `"18.75rem"` — 300 px at
35
+ * Normal, growing with the text size so the slice labels keep their room (0.32,
36
+ * docs/text-size-harmonization.md §10.10). A number does not follow the text size.
37
+ */
38
+ height?: ChartHeight;
33
39
  /** How a value is written in the tooltip, the slice's name, a `"value"` slice label
34
40
  * and the donut's total. Default: `Intl.NumberFormat` in `locale`. */
35
41
  formatValue?: (value: number) => string;
@@ -9,6 +9,8 @@ import { paletteFor } from "../theme/chart-palette.js";
9
9
  import { ChartContainer, ChartTooltip, ChartTooltipContent } from "./chart.js";
10
10
  import { DEFAULT_PIE_CHART_LABELS } from "./pie-chart-labels.js";
11
11
  import { StaticLegend, ToggleLegend, toggleHidden } from "./toggle-legend.js";
12
+ import { useTextSize } from "../theme/text-size.js";
13
+ import { chartHeightProps } from "./chart-height.js";
12
14
  const PIE_CONFIG = {};
13
15
  const RADIAN = Math.PI / 180;
14
16
  function RedactedTooltipContent(props) {
@@ -17,7 +19,7 @@ function RedactedTooltipContent(props) {
17
19
  function PieChart({
18
20
  data,
19
21
  variant = "donut",
20
- height = 300,
22
+ height = "18.75rem",
21
23
  formatValue: formatValueProp,
22
24
  sliceLabels = "percent",
23
25
  minLabelShare = 0.04,
@@ -38,6 +40,8 @@ function PieChart({
38
40
  }) {
39
41
  const labels = useKitLabels("pieChart", DEFAULT_PIE_CHART_LABELS, labelsProp);
40
42
  const locale = useKitLocale(localeProp);
43
+ const { scale } = useTextSize();
44
+ const box = chartHeightProps(height);
41
45
  const numberFormat = useMemo(() => new Intl.NumberFormat(locale), [locale]);
42
46
  const percentFormats = useMemo(
43
47
  () => ({
@@ -150,7 +154,7 @@ function PieChart({
150
154
  const radius = Number(props.outerRadius);
151
155
  const mid = Number(props.midAngle);
152
156
  if (!d || d.share < minLabelShare || ![cx, cy, radius, mid].every(Number.isFinite)) return null;
153
- const r = radius + 10;
157
+ const r = radius + 10 * scale;
154
158
  const x = cx + r * Math.cos(-mid * RADIAN);
155
159
  const y = cy + r * Math.sin(-mid * RADIAN);
156
160
  const isValue = sliceLabels === "value";
@@ -161,7 +165,7 @@ function PieChart({
161
165
  y,
162
166
  textAnchor: x >= cx ? "start" : "end",
163
167
  dominantBaseline: "central",
164
- fontSize: 11,
168
+ fontSize: 11 * scale,
165
169
  fill: "var(--text-secondary)",
166
170
  "data-private": redact && isValue ? "" : void 0,
167
171
  style: { pointerEvents: "none" },
@@ -169,7 +173,7 @@ function PieChart({
169
173
  }
170
174
  );
171
175
  },
172
- [drawn, minLabelShare, sliceLabels, redact, formatValue, formatShare]
176
+ [drawn, minLabelShare, sliceLabels, redact, formatValue, formatShare, scale]
173
177
  );
174
178
  const withLabels = sliceLabels !== "none";
175
179
  const outer = withLabels ? 72 : 88;
@@ -213,8 +217,8 @@ function PieChart({
213
217
  const emptyBox = /* @__PURE__ */ jsx(
214
218
  "div",
215
219
  {
216
- className: "flex items-center justify-center text-sm text-[var(--text-muted)]",
217
- style: { height },
220
+ className: cn("flex items-center justify-center text-sm text-[var(--text-muted)]", box.className),
221
+ style: box.style,
218
222
  children: empty ?? labels.empty
219
223
  }
220
224
  );
@@ -226,7 +230,7 @@ function PieChart({
226
230
  drawn.length === 0 ? (
227
231
  // Every slice switched off: the legend below is how they come back.
228
232
  emptyBox
229
- ) : /* @__PURE__ */ jsxs("div", { role: "group", "aria-label": labels.slices, className: "relative", style: { height }, children: [
233
+ ) : /* @__PURE__ */ jsxs("div", { role: "group", "aria-label": labels.slices, className: cn("relative", box.className), style: box.style, children: [
230
234
  /* @__PURE__ */ jsx(
231
235
  ChartContainer,
232
236
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/pie-chart.tsx"],"sourcesContent":["// A part-to-whole chart: a donut (or a full pie), one slice per category, its ANGLE\n// the category's share of what is drawn. Lifted out of kastlan, where the dashboard's\n// occupancy card and the reports' maintenance card each drew a recharts `<Pie>` by\n// hand inside the kit's `ChartContainer` — with a legend that could not switch\n// anything, slices a keyboard could not reach, and colours from a local hex list.\n//\n// It uses the kit's own chart shell (`ChartContainer`, `ChartTooltipContent`) and\n// ramp (`paletteFor`), so it re-skins with the palette like every other chart, and the\n// kit's own legends (`ToggleLegend`, `StaticLegend`).\nimport { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { ComponentProps, ComponentPropsWithoutRef, KeyboardEvent, ReactNode } from \"react\";\nimport { Pie, PieChart as RechartsPieChart, Sector } from \"recharts\";\nimport type { PieLabelRenderProps, PieSectorShapeProps } from \"recharts\";\nimport { cn } from \"../lib/cn\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport { paletteFor } from \"../theme/chart-palette\";\nimport { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from \"./chart\";\nimport { DEFAULT_PIE_CHART_LABELS, type PieChartLabels } from \"./pie-chart-labels\";\nimport { StaticLegend, ToggleLegend, toggleHidden, type LegendEntry } from \"./toggle-legend\";\n\n/** One slice. */\nexport interface PieChartSlice {\n /** Stable key, handed back by {@link PieChartProps.onSliceClick} and used by the\n * legend's hidden set. Labels can collide; keys must not. */\n key: string;\n /** What the slice is called — in the legend, the tooltip and its accessible name. */\n label: string;\n /**\n * The slice's size. A value that is not a positive finite number — `null` for a\n * figure that is missing, a zero, a negative — is NOT drawn and not in the legend:\n * an angle cannot be negative, and a zero-degree slice is one a pointer and a\n * keyboard would both stop on to find nothing.\n */\n value: number | null | undefined;\n /** Overrides the by-index palette colour (`paletteFor(i)`, `i` the slice's index in\n * `data` AS GIVEN) — a status that has a colour of its own. Any CSS colour. */\n color?: string;\n}\n\n/**\n * Everything a `<div>` takes reaches the chart's root, so a test id or a `data-tour`\n * anchor can find it. `children` is the chart's own.\n */\nexport interface PieChartProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n data: readonly PieChartSlice[];\n /** `\"donut\"` (default) leaves a hole with the total in it; `\"pie\"` is the full disc. */\n variant?: \"donut\" | \"pie\";\n /** Height of the plot in px (the legend is under it). Default 300. */\n height?: number;\n /** How a value is written in the tooltip, the slice's name, a `\"value\"` slice label\n * and the donut's total. Default: `Intl.NumberFormat` in `locale`. */\n formatValue?: (value: number) => string;\n /**\n * What is written beside each slice: its share (`\"percent\"`, the default), its\n * value, or nothing. Only on slices of at least {@link minLabelShare} — a label on a\n * sliver sits on its neighbour's. The figure is in the tooltip and the slice's name\n * either way.\n */\n sliceLabels?: \"percent\" | \"value\" | \"none\";\n /** The smallest share (0–1) that gets a slice label. Default 0.04. */\n minLabelShare?: number;\n /** Draw the donut's centre: {@link centerLabel} over {@link centerValue}. Default\n * `true`; ignored for `variant=\"pie\"`, which has no centre to draw in. */\n center?: boolean;\n /** The caption in the donut's centre. Default: `labels.total`. */\n centerLabel?: ReactNode;\n /** The figure in the donut's centre. Default: the drawn total through `formatValue`. */\n centerValue?: ReactNode;\n /**\n * The legend under the chart. `\"toggle\"` (default): each entry is a switch that\n * takes its slice off the chart, and the percentages are then of what is left —\n * the shares always read \"of this picture\". `\"static\"`: a key with nothing to press.\n */\n legend?: \"toggle\" | \"static\" | \"none\";\n /** The slices switched off, for a caller that owns the set (a URL, a saved view).\n * Left out, the chart keeps its own. */\n hiddenSlices?: ReadonlySet<string>;\n /** Called with the new hidden set whenever the legend switches a slice. */\n onHiddenSlicesChange?: (hidden: ReadonlySet<string>) => void;\n /** A slice was activated — by click, or Enter/Space on a focused slice. Makes each\n * slice a `role=\"button\"`. */\n onSliceClick?: (key: string, slice: PieChartSlice) => void;\n /**\n * Tag every figure ON SCREEN `data-private` — the tooltip bubble, the centre and\n * `\"value\"` slice labels — so the host's demo-mode rule blurs it. A percentage alone\n * reveals no amount, so `\"percent\"` labels stay readable, and the slices themselves\n * are not tagged: a blurred ring is no picture at all.\n */\n redact?: boolean;\n /** What to show with nothing to draw. Default: `labels.empty`. */\n empty?: ReactNode;\n /** BCP 47 tag for the numbers and percentages. Default: the provider's. */\n locale?: string;\n labels?: Partial<PieChartLabels>;\n}\n\n/** A slice as drawn: only the ones with a positive value, in `data` order. */\ninterface DrawnSlice {\n slice: PieChartSlice;\n value: number;\n color: string;\n /** Of what is drawn, 0–1. */\n share: number;\n}\n\n// Slices carry their own `fill`, and `ChartTooltipContent` falls back to it for the\n// swatch, so no series config is needed — and none is wanted, because a config key is a\n// CSS identifier and a slice key is the caller's data (see `ChartStyle`).\nconst PIE_CONFIG: ChartConfig = {};\n\nconst RADIAN = Math.PI / 180;\n\n/** The shared tooltip with the bubble tagged `data-private` — see the treemap's\n * `RedactedTooltipContent` for why it has to BE the element recharts clones. */\nfunction RedactedTooltipContent(props: ComponentProps<typeof ChartTooltipContent>) {\n return (\n <div data-private>\n <ChartTooltipContent {...props} />\n </div>\n );\n}\n\n/**\n * Part-to-whole chart: a donut (or `variant=\"pie\"`), one slice per\n * {@link PieChartSlice}, coloured from the chart ramp by index.\n *\n * **Every slice is reachable from the keyboard.** The slices are ONE tab stop (a roving\n * tabindex, as in the kit's grids): ←/→ and ↑/↓ step to the next and previous slice —\n * clockwise from twelve o'clock, the order of the legend — with ←/→ following the\n * reading direction in RTL; Home/End go to the first and last. Each slice is named\n * \"Label: value (xx%)\" (`labels.slice`), because a colour and an angle are nothing a\n * screen reader can say, and focusing one reads it out in the donut's centre. With\n * `onSliceClick`, Enter and Space activate it as a click does.\n *\n * The legend switches slices off (`legend=\"toggle\"`); the percentages are always of\n * what is drawn. The plot is not mirrored in RTL (see `ChartContainer`); the legend\n * and the centre follow the page.\n *\n * With nothing to draw it says so (`empty`) rather than drawing an empty box.\n *\n * Needs `recharts`, like everything behind `@eifi1/ui-kit/chart`.\n */\nexport function PieChart({\n data,\n variant = \"donut\",\n height = 300,\n formatValue: formatValueProp,\n sliceLabels = \"percent\",\n minLabelShare = 0.04,\n center = true,\n centerLabel,\n centerValue,\n legend = \"toggle\",\n hiddenSlices: hiddenProp,\n onHiddenSlicesChange,\n onSliceClick,\n redact = false,\n empty,\n locale: localeProp,\n labels: labelsProp,\n className,\n style,\n ...rest\n}: PieChartProps) {\n const labels = useKitLabels(\"pieChart\", DEFAULT_PIE_CHART_LABELS, labelsProp);\n const locale = useKitLocale(localeProp);\n const numberFormat = useMemo(() => new Intl.NumberFormat(locale), [locale]);\n const percentFormats = useMemo(\n () => ({\n whole: new Intl.NumberFormat(locale, { style: \"percent\", maximumFractionDigits: 0 }),\n fine: new Intl.NumberFormat(locale, { style: \"percent\", maximumFractionDigits: 1 }),\n }),\n [locale],\n );\n // Memoised, like everything the sectors are drawn from: they must not be rebuilt on\n // this component's own re-renders (see `pie` below).\n const formatValue = useCallback(\n (v: number) => (formatValueProp ? formatValueProp(v) : numberFormat.format(v)),\n [formatValueProp, numberFormat],\n );\n // One decimal under 10 %, where rounding to whole percents starts to lie — a slice\n // of 0.4 % labelled \"0%\" says it is not there.\n const formatShare = useCallback(\n (share: number) => (share < 0.1 ? percentFormats.fine : percentFormats.whole).format(share),\n [percentFormats],\n );\n\n const [ownHidden, setOwnHidden] = useState<ReadonlySet<string>>(() => new Set());\n const hidden = hiddenProp ?? ownHidden;\n const toggle = (key: string) => {\n const next = toggleHidden(hidden, key);\n if (hiddenProp === undefined) setOwnHidden(next);\n onHiddenSlicesChange?.(next);\n };\n\n // Coloured by index into `data` AS GIVEN, before anything is dropped, so a slice\n // keeps its colour when a neighbour is switched off or has no value.\n const { drawable, drawn, total, pieData } = useMemo(() => {\n const drawable = data\n .map((slice, i) => ({ slice, value: slice.value, color: slice.color ?? paletteFor(i) }))\n .filter((d): d is { slice: PieChartSlice; value: number; color: string } =>\n typeof d.value === \"number\" && Number.isFinite(d.value) && d.value > 0,\n );\n const shown = drawable.filter((d) => !hidden.has(d.slice.key));\n const total = shown.reduce((acc, d) => acc + d.value, 0);\n const drawn: DrawnSlice[] = shown.map((d) => ({ ...d, share: total > 0 ? d.value / total : 0 }));\n const pieData = drawn.map((d) => ({\n key: d.slice.key,\n label: d.slice.label,\n value: d.value,\n fill: d.color,\n }));\n return { drawable, drawn, total, pieData };\n }, [data, hidden]);\n\n // The roving tab stop, and the slice that has focus (which the centre reads out).\n const [stop, setStop] = useState(0);\n const [focused, setFocused] = useState<number | undefined>(undefined);\n const current = Math.min(stop, Math.max(0, drawn.length - 1));\n const refs = useRef<(SVGGElement | null)[]>([]);\n const focusedSlice = focused !== undefined ? drawn[focused] : undefined;\n\n // Read through a ref: the sectors are drawn once per `data`, and a caller's inline\n // `onSliceClick` is a new function every render.\n const clickRef = useRef(onSliceClick);\n useLayoutEffect(() => {\n clickRef.current = onSliceClick;\n });\n const clickable = onSliceClick !== undefined;\n\n /**\n * One sector, wrapped in its keyboard stop. recharts renders it inside a layer of its\n * own that carries the pointer's hover and click, so those are untouched.\n *\n * Focus does NOT drive the recharts tooltip (`defaultIndex`), as it does in\n * `SeriesChart`: an active pie sector is re-parented into an extra layer, which\n * remounts this element and drops the focus it just took. The donut's centre is the\n * keyboard's read-out instead.\n */\n const shape = useCallback(\n (props: PieSectorShapeProps) => {\n const {\n index,\n isActive: _isActive,\n tabIndex: _tabIndex,\n animationElapsedTime: _elapsed,\n isAnimating: _animating,\n isEntrance: _entrance,\n // recharts hands the shape its own React `key` among the props. Spread into\n // JSX, a `key` inside a props object is a warning on every render (React 19),\n // so it is lifted out here and passed on its own.\n key,\n ...sector\n } = props as PieSectorShapeProps & {\n animationElapsedTime?: number;\n isAnimating?: boolean;\n isEntrance?: boolean;\n key?: string | number | null;\n };\n const d = drawn[index];\n if (!d) return <Sector key={key ?? undefined} {...sector} />;\n const onKeyDown = (event: KeyboardEvent<SVGGElement>) => {\n const step = horizontalStep(event.key, event.currentTarget);\n let target: number | undefined;\n if (step) target = index + step;\n else if (event.key === \"ArrowDown\") target = index + 1;\n else if (event.key === \"ArrowUp\") target = index - 1;\n else if (event.key === \"Home\") target = 0;\n else if (event.key === \"End\") target = drawn.length - 1;\n if (target !== undefined) {\n event.preventDefault();\n // Round the circle: past the last slice is the first, as the eye goes.\n refs.current[((target % drawn.length) + drawn.length) % drawn.length]?.focus();\n } else if (clickable && (event.key === \"Enter\" || event.key === \" \")) {\n event.preventDefault();\n clickRef.current?.(d.slice.key, d.slice);\n }\n };\n return (\n <g\n ref={(node) => {\n refs.current[index] = node;\n }}\n role={clickable ? \"button\" : \"img\"}\n // The first slice until the effect below moves the stop.\n tabIndex={index === 0 ? 0 : -1}\n aria-label={labels.slice(d.slice.label, formatValue(d.value), formatShare(d.share))}\n data-slice={d.slice.key}\n onKeyDown={onKeyDown}\n onFocus={() => {\n setStop(index);\n setFocused(index);\n }}\n onBlur={() => setFocused(undefined)}\n // CSS beats a presentation attribute, so this recolours the sector's own\n // stroke for as long as it has keyboard focus.\n className=\"outline-none [&:focus-visible>path]:stroke-[var(--text-primary)]\"\n >\n <Sector key={key ?? undefined} {...sector} />\n </g>\n );\n },\n [drawn, labels, formatValue, formatShare, clickable],\n );\n\n const label = useCallback(\n (props: PieLabelRenderProps) => {\n const d = drawn[props.index ?? -1];\n const cx = Number(props.cx);\n const cy = Number(props.cy);\n const radius = Number(props.outerRadius);\n const mid = Number(props.midAngle);\n if (!d || d.share < minLabelShare || ![cx, cy, radius, mid].every(Number.isFinite)) return null;\n const r = radius + 10;\n const x = cx + r * Math.cos(-mid * RADIAN);\n const y = cy + r * Math.sin(-mid * RADIAN);\n const isValue = sliceLabels === \"value\";\n return (\n <text\n x={x}\n y={y}\n textAnchor={x >= cx ? \"start\" : \"end\"}\n dominantBaseline=\"central\"\n fontSize={11}\n fill=\"var(--text-secondary)\"\n data-private={redact && isValue ? \"\" : undefined}\n style={{ pointerEvents: \"none\" }}\n >\n {isValue ? formatValue(d.value) : formatShare(d.share)}\n </text>\n );\n },\n [drawn, minLabelShare, sliceLabels, redact, formatValue, formatShare],\n );\n\n const withLabels = sliceLabels !== \"none\";\n // Room outside the ring for the labels, or the ring fills the box.\n const outer = withLabels ? 72 : 88;\n const inner = variant === \"donut\" ? Math.round(outer * 0.6) : 0;\n\n /**\n * The `<Pie>`, as ONE element for as long as what it draws is unchanged. recharts\n * keys its sectors by an animation id that is renewed whenever the Pie's props object\n * is — i.e. on every render of the Pie — so re-rendering it remounts every sector,\n * the one with focus included, which dropped the focus to <body> on the very state\n * update that focusing a slice makes. An unchanged element is one React does not\n * re-render, so the sectors stay put, and the roving tab stop is moved on them in\n * place (the effect below).\n */\n const pie = useMemo(\n () => (\n <Pie\n data={pieData}\n dataKey=\"value\"\n nameKey=\"label\"\n cx=\"50%\"\n cy=\"50%\"\n outerRadius={`${outer}%`}\n innerRadius={`${inner}%`}\n startAngle={90}\n endAngle={-270}\n // The card showing through between the slices, as the treemap's gutter.\n stroke=\"var(--bg-surface)\"\n strokeWidth={2}\n rootTabIndex={-1}\n isAnimationActive={false}\n shape={shape}\n label={withLabels ? label : false}\n labelLine={false}\n onClick={\n clickable\n ? (_entry: unknown, index: number) => {\n const d = drawn[index];\n if (d) clickRef.current?.(d.slice.key, d.slice);\n }\n : undefined\n }\n />\n ),\n [pieData, drawn, shape, label, withLabels, outer, inner, clickable],\n );\n\n // The roving tab stop, moved on the sectors in place (they are not re-rendered).\n useEffect(() => {\n refs.current.forEach((el, i) => el?.setAttribute(\"tabindex\", i === current ? \"0\" : \"-1\"));\n });\n\n const legendEntries: LegendEntry[] = drawable.map((d) => ({\n key: d.slice.key,\n label: d.slice.label,\n color: d.color,\n }));\n\n const emptyBox = (\n <div\n className=\"flex items-center justify-center text-sm text-[var(--text-muted)]\"\n style={{ height }}\n >\n {empty ?? labels.empty}\n </div>\n );\n\n // Nothing drawable at all: no chart, and no legend of switches for nothing.\n if (drawable.length === 0) {\n return (\n <div {...rest} className={cn(\"w-full\", className)} style={style}>\n {emptyBox}\n </div>\n );\n }\n\n const Content = redact ? RedactedTooltipContent : ChartTooltipContent;\n\n return (\n <div {...rest} className={cn(\"w-full\", className)} style={style}>\n {drawn.length === 0 ? (\n // Every slice switched off: the legend below is how they come back.\n emptyBox\n ) : (\n <div role=\"group\" aria-label={labels.slices} className=\"relative\" style={{ height }}>\n <ChartContainer\n config={PIE_CONFIG}\n className={cn(\"h-full\", clickable && \"[&_.recharts-pie-sector]:cursor-pointer\")}\n >\n <RechartsPieChart accessibilityLayer={false}>\n <ChartTooltip\n content={\n <Content\n hideLabel\n valueFormatter={(v) =>\n `${formatValue(v)} (${formatShare(total > 0 ? v / total : 0)})`\n }\n />\n }\n />\n {pie}\n </RechartsPieChart>\n </ChartContainer>\n {variant === \"donut\" && center && (\n <div\n data-private={redact ? \"\" : undefined}\n // While a slice has keyboard focus the centre reads it out — the\n // keyboard's tooltip. Hidden from assistive technology then: the slice's\n // own name has just said the same.\n aria-hidden={focusedSlice ? true : undefined}\n className=\"pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center\"\n >\n <span className=\"max-w-[40%] truncate text-xs text-[var(--text-muted)]\">\n {focusedSlice ? focusedSlice.slice.label : (centerLabel ?? labels.total)}\n </span>\n <span className=\"max-w-[40%] truncate text-lg font-semibold tabular-nums text-[var(--text-primary)]\">\n {focusedSlice ? formatValue(focusedSlice.value) : (centerValue ?? formatValue(total))}\n </span>\n {focusedSlice && (\n <span className=\"text-xs tabular-nums text-[var(--text-muted)]\">\n {formatShare(focusedSlice.share)}\n </span>\n )}\n </div>\n )}\n </div>\n )}\n {legend === \"toggle\" && (\n <ToggleLegend\n entries={legendEntries}\n hidden={hidden}\n onToggle={toggle}\n showSingle\n className=\"justify-center\"\n labels={{ legend: labels.legend }}\n />\n )}\n {legend === \"static\" && (\n <StaticLegend entries={legendEntries} className=\"justify-center\" aria-label={labels.legend} />\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["drawable","total","drawn","pieData"]}
1
+ {"version":3,"sources":["../../src/components/pie-chart.tsx"],"sourcesContent":["// A part-to-whole chart: a donut (or a full pie), one slice per category, its ANGLE\n// the category's share of what is drawn. Lifted out of kastlan, where the dashboard's\n// occupancy card and the reports' maintenance card each drew a recharts `<Pie>` by\n// hand inside the kit's `ChartContainer` — with a legend that could not switch\n// anything, slices a keyboard could not reach, and colours from a local hex list.\n//\n// It uses the kit's own chart shell (`ChartContainer`, `ChartTooltipContent`) and\n// ramp (`paletteFor`), so it re-skins with the palette like every other chart, and the\n// kit's own legends (`ToggleLegend`, `StaticLegend`).\nimport { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { ComponentProps, ComponentPropsWithoutRef, KeyboardEvent, ReactNode } from \"react\";\nimport { Pie, PieChart as RechartsPieChart, Sector } from \"recharts\";\nimport type { PieLabelRenderProps, PieSectorShapeProps } from \"recharts\";\nimport { cn } from \"../lib/cn\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport { paletteFor } from \"../theme/chart-palette\";\nimport { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from \"./chart\";\nimport { DEFAULT_PIE_CHART_LABELS, type PieChartLabels } from \"./pie-chart-labels\";\nimport { StaticLegend, ToggleLegend, toggleHidden, type LegendEntry } from \"./toggle-legend\";\nimport { useTextSize } from \"../theme/text-size\";\nimport { chartHeightProps, type ChartHeight } from \"./chart-height\";\n\n/** One slice. */\nexport interface PieChartSlice {\n /** Stable key, handed back by {@link PieChartProps.onSliceClick} and used by the\n * legend's hidden set. Labels can collide; keys must not. */\n key: string;\n /** What the slice is called — in the legend, the tooltip and its accessible name. */\n label: string;\n /**\n * The slice's size. A value that is not a positive finite number — `null` for a\n * figure that is missing, a zero, a negative — is NOT drawn and not in the legend:\n * an angle cannot be negative, and a zero-degree slice is one a pointer and a\n * keyboard would both stop on to find nothing.\n */\n value: number | null | undefined;\n /** Overrides the by-index palette colour (`paletteFor(i)`, `i` the slice's index in\n * `data` AS GIVEN) — a status that has a colour of its own. Any CSS colour. */\n color?: string;\n}\n\n/**\n * Everything a `<div>` takes reaches the chart's root, so a test id or a `data-tour`\n * anchor can find it. `children` is the chart's own.\n */\nexport interface PieChartProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n data: readonly PieChartSlice[];\n /** `\"donut\"` (default) leaves a hole with the total in it; `\"pie\"` is the full disc. */\n variant?: \"donut\" | \"pie\";\n /**\n * Height of the plot (the legend is under it): a CSS length (`\"20rem\"`,\n * `\"min(20rem, 50dvh)\"`), a Tailwind class, or px. Default `\"18.75rem\"` — 300 px at\n * Normal, growing with the text size so the slice labels keep their room (0.32,\n * docs/text-size-harmonization.md §10.10). A number does not follow the text size.\n */\n height?: ChartHeight;\n /** How a value is written in the tooltip, the slice's name, a `\"value\"` slice label\n * and the donut's total. Default: `Intl.NumberFormat` in `locale`. */\n formatValue?: (value: number) => string;\n /**\n * What is written beside each slice: its share (`\"percent\"`, the default), its\n * value, or nothing. Only on slices of at least {@link minLabelShare} — a label on a\n * sliver sits on its neighbour's. The figure is in the tooltip and the slice's name\n * either way.\n */\n sliceLabels?: \"percent\" | \"value\" | \"none\";\n /** The smallest share (0–1) that gets a slice label. Default 0.04. */\n minLabelShare?: number;\n /** Draw the donut's centre: {@link centerLabel} over {@link centerValue}. Default\n * `true`; ignored for `variant=\"pie\"`, which has no centre to draw in. */\n center?: boolean;\n /** The caption in the donut's centre. Default: `labels.total`. */\n centerLabel?: ReactNode;\n /** The figure in the donut's centre. Default: the drawn total through `formatValue`. */\n centerValue?: ReactNode;\n /**\n * The legend under the chart. `\"toggle\"` (default): each entry is a switch that\n * takes its slice off the chart, and the percentages are then of what is left —\n * the shares always read \"of this picture\". `\"static\"`: a key with nothing to press.\n */\n legend?: \"toggle\" | \"static\" | \"none\";\n /** The slices switched off, for a caller that owns the set (a URL, a saved view).\n * Left out, the chart keeps its own. */\n hiddenSlices?: ReadonlySet<string>;\n /** Called with the new hidden set whenever the legend switches a slice. */\n onHiddenSlicesChange?: (hidden: ReadonlySet<string>) => void;\n /** A slice was activated — by click, or Enter/Space on a focused slice. Makes each\n * slice a `role=\"button\"`. */\n onSliceClick?: (key: string, slice: PieChartSlice) => void;\n /**\n * Tag every figure ON SCREEN `data-private` — the tooltip bubble, the centre and\n * `\"value\"` slice labels — so the host's demo-mode rule blurs it. A percentage alone\n * reveals no amount, so `\"percent\"` labels stay readable, and the slices themselves\n * are not tagged: a blurred ring is no picture at all.\n */\n redact?: boolean;\n /** What to show with nothing to draw. Default: `labels.empty`. */\n empty?: ReactNode;\n /** BCP 47 tag for the numbers and percentages. Default: the provider's. */\n locale?: string;\n labels?: Partial<PieChartLabels>;\n}\n\n/** A slice as drawn: only the ones with a positive value, in `data` order. */\ninterface DrawnSlice {\n slice: PieChartSlice;\n value: number;\n color: string;\n /** Of what is drawn, 0–1. */\n share: number;\n}\n\n// Slices carry their own `fill`, and `ChartTooltipContent` falls back to it for the\n// swatch, so no series config is needed — and none is wanted, because a config key is a\n// CSS identifier and a slice key is the caller's data (see `ChartStyle`).\nconst PIE_CONFIG: ChartConfig = {};\n\nconst RADIAN = Math.PI / 180;\n\n/** The shared tooltip with the bubble tagged `data-private` — see the treemap's\n * `RedactedTooltipContent` for why it has to BE the element recharts clones. */\nfunction RedactedTooltipContent(props: ComponentProps<typeof ChartTooltipContent>) {\n return (\n <div data-private>\n <ChartTooltipContent {...props} />\n </div>\n );\n}\n\n/**\n * Part-to-whole chart: a donut (or `variant=\"pie\"`), one slice per\n * {@link PieChartSlice}, coloured from the chart ramp by index.\n *\n * **Every slice is reachable from the keyboard.** The slices are ONE tab stop (a roving\n * tabindex, as in the kit's grids): ←/→ and ↑/↓ step to the next and previous slice —\n * clockwise from twelve o'clock, the order of the legend — with ←/→ following the\n * reading direction in RTL; Home/End go to the first and last. Each slice is named\n * \"Label: value (xx%)\" (`labels.slice`), because a colour and an angle are nothing a\n * screen reader can say, and focusing one reads it out in the donut's centre. With\n * `onSliceClick`, Enter and Space activate it as a click does.\n *\n * The legend switches slices off (`legend=\"toggle\"`); the percentages are always of\n * what is drawn. The plot is not mirrored in RTL (see `ChartContainer`); the legend\n * and the centre follow the page.\n *\n * With nothing to draw it says so (`empty`) rather than drawing an empty box.\n *\n * Needs `recharts`, like everything behind `@eifi1/ui-kit/chart`.\n */\nexport function PieChart({\n data,\n variant = \"donut\",\n height = \"18.75rem\",\n formatValue: formatValueProp,\n sliceLabels = \"percent\",\n minLabelShare = 0.04,\n center = true,\n centerLabel,\n centerValue,\n legend = \"toggle\",\n hiddenSlices: hiddenProp,\n onHiddenSlicesChange,\n onSliceClick,\n redact = false,\n empty,\n locale: localeProp,\n labels: labelsProp,\n className,\n style,\n ...rest\n}: PieChartProps) {\n const labels = useKitLabels(\"pieChart\", DEFAULT_PIE_CHART_LABELS, labelsProp);\n const locale = useKitLocale(localeProp);\n // The slice labels are SVG text in px: they follow the text size by this (§10.10).\n const { scale } = useTextSize();\n const box = chartHeightProps(height);\n const numberFormat = useMemo(() => new Intl.NumberFormat(locale), [locale]);\n const percentFormats = useMemo(\n () => ({\n whole: new Intl.NumberFormat(locale, { style: \"percent\", maximumFractionDigits: 0 }),\n fine: new Intl.NumberFormat(locale, { style: \"percent\", maximumFractionDigits: 1 }),\n }),\n [locale],\n );\n // Memoised, like everything the sectors are drawn from: they must not be rebuilt on\n // this component's own re-renders (see `pie` below).\n const formatValue = useCallback(\n (v: number) => (formatValueProp ? formatValueProp(v) : numberFormat.format(v)),\n [formatValueProp, numberFormat],\n );\n // One decimal under 10 %, where rounding to whole percents starts to lie — a slice\n // of 0.4 % labelled \"0%\" says it is not there.\n const formatShare = useCallback(\n (share: number) => (share < 0.1 ? percentFormats.fine : percentFormats.whole).format(share),\n [percentFormats],\n );\n\n const [ownHidden, setOwnHidden] = useState<ReadonlySet<string>>(() => new Set());\n const hidden = hiddenProp ?? ownHidden;\n const toggle = (key: string) => {\n const next = toggleHidden(hidden, key);\n if (hiddenProp === undefined) setOwnHidden(next);\n onHiddenSlicesChange?.(next);\n };\n\n // Coloured by index into `data` AS GIVEN, before anything is dropped, so a slice\n // keeps its colour when a neighbour is switched off or has no value.\n const { drawable, drawn, total, pieData } = useMemo(() => {\n const drawable = data\n .map((slice, i) => ({ slice, value: slice.value, color: slice.color ?? paletteFor(i) }))\n .filter((d): d is { slice: PieChartSlice; value: number; color: string } =>\n typeof d.value === \"number\" && Number.isFinite(d.value) && d.value > 0,\n );\n const shown = drawable.filter((d) => !hidden.has(d.slice.key));\n const total = shown.reduce((acc, d) => acc + d.value, 0);\n const drawn: DrawnSlice[] = shown.map((d) => ({ ...d, share: total > 0 ? d.value / total : 0 }));\n const pieData = drawn.map((d) => ({\n key: d.slice.key,\n label: d.slice.label,\n value: d.value,\n fill: d.color,\n }));\n return { drawable, drawn, total, pieData };\n }, [data, hidden]);\n\n // The roving tab stop, and the slice that has focus (which the centre reads out).\n const [stop, setStop] = useState(0);\n const [focused, setFocused] = useState<number | undefined>(undefined);\n const current = Math.min(stop, Math.max(0, drawn.length - 1));\n const refs = useRef<(SVGGElement | null)[]>([]);\n const focusedSlice = focused !== undefined ? drawn[focused] : undefined;\n\n // Read through a ref: the sectors are drawn once per `data`, and a caller's inline\n // `onSliceClick` is a new function every render.\n const clickRef = useRef(onSliceClick);\n useLayoutEffect(() => {\n clickRef.current = onSliceClick;\n });\n const clickable = onSliceClick !== undefined;\n\n /**\n * One sector, wrapped in its keyboard stop. recharts renders it inside a layer of its\n * own that carries the pointer's hover and click, so those are untouched.\n *\n * Focus does NOT drive the recharts tooltip (`defaultIndex`), as it does in\n * `SeriesChart`: an active pie sector is re-parented into an extra layer, which\n * remounts this element and drops the focus it just took. The donut's centre is the\n * keyboard's read-out instead.\n */\n const shape = useCallback(\n (props: PieSectorShapeProps) => {\n const {\n index,\n isActive: _isActive,\n tabIndex: _tabIndex,\n animationElapsedTime: _elapsed,\n isAnimating: _animating,\n isEntrance: _entrance,\n // recharts hands the shape its own React `key` among the props. Spread into\n // JSX, a `key` inside a props object is a warning on every render (React 19),\n // so it is lifted out here and passed on its own.\n key,\n ...sector\n } = props as PieSectorShapeProps & {\n animationElapsedTime?: number;\n isAnimating?: boolean;\n isEntrance?: boolean;\n key?: string | number | null;\n };\n const d = drawn[index];\n if (!d) return <Sector key={key ?? undefined} {...sector} />;\n const onKeyDown = (event: KeyboardEvent<SVGGElement>) => {\n const step = horizontalStep(event.key, event.currentTarget);\n let target: number | undefined;\n if (step) target = index + step;\n else if (event.key === \"ArrowDown\") target = index + 1;\n else if (event.key === \"ArrowUp\") target = index - 1;\n else if (event.key === \"Home\") target = 0;\n else if (event.key === \"End\") target = drawn.length - 1;\n if (target !== undefined) {\n event.preventDefault();\n // Round the circle: past the last slice is the first, as the eye goes.\n refs.current[((target % drawn.length) + drawn.length) % drawn.length]?.focus();\n } else if (clickable && (event.key === \"Enter\" || event.key === \" \")) {\n event.preventDefault();\n clickRef.current?.(d.slice.key, d.slice);\n }\n };\n return (\n <g\n ref={(node) => {\n refs.current[index] = node;\n }}\n role={clickable ? \"button\" : \"img\"}\n // The first slice until the effect below moves the stop.\n tabIndex={index === 0 ? 0 : -1}\n aria-label={labels.slice(d.slice.label, formatValue(d.value), formatShare(d.share))}\n data-slice={d.slice.key}\n onKeyDown={onKeyDown}\n onFocus={() => {\n setStop(index);\n setFocused(index);\n }}\n onBlur={() => setFocused(undefined)}\n // CSS beats a presentation attribute, so this recolours the sector's own\n // stroke for as long as it has keyboard focus.\n className=\"outline-none [&:focus-visible>path]:stroke-[var(--text-primary)]\"\n >\n <Sector key={key ?? undefined} {...sector} />\n </g>\n );\n },\n [drawn, labels, formatValue, formatShare, clickable],\n );\n\n const label = useCallback(\n (props: PieLabelRenderProps) => {\n const d = drawn[props.index ?? -1];\n const cx = Number(props.cx);\n const cy = Number(props.cy);\n const radius = Number(props.outerRadius);\n const mid = Number(props.midAngle);\n if (!d || d.share < minLabelShare || ![cx, cy, radius, mid].every(Number.isFinite)) return null;\n const r = radius + 10 * scale;\n const x = cx + r * Math.cos(-mid * RADIAN);\n const y = cy + r * Math.sin(-mid * RADIAN);\n const isValue = sliceLabels === \"value\";\n return (\n <text\n x={x}\n y={y}\n textAnchor={x >= cx ? \"start\" : \"end\"}\n dominantBaseline=\"central\"\n fontSize={11 * scale}\n fill=\"var(--text-secondary)\"\n data-private={redact && isValue ? \"\" : undefined}\n style={{ pointerEvents: \"none\" }}\n >\n {isValue ? formatValue(d.value) : formatShare(d.share)}\n </text>\n );\n },\n [drawn, minLabelShare, sliceLabels, redact, formatValue, formatShare, scale],\n );\n\n const withLabels = sliceLabels !== \"none\";\n // Room outside the ring for the labels, or the ring fills the box.\n const outer = withLabels ? 72 : 88;\n const inner = variant === \"donut\" ? Math.round(outer * 0.6) : 0;\n\n /**\n * The `<Pie>`, as ONE element for as long as what it draws is unchanged. recharts\n * keys its sectors by an animation id that is renewed whenever the Pie's props object\n * is — i.e. on every render of the Pie — so re-rendering it remounts every sector,\n * the one with focus included, which dropped the focus to <body> on the very state\n * update that focusing a slice makes. An unchanged element is one React does not\n * re-render, so the sectors stay put, and the roving tab stop is moved on them in\n * place (the effect below).\n */\n const pie = useMemo(\n () => (\n <Pie\n data={pieData}\n dataKey=\"value\"\n nameKey=\"label\"\n cx=\"50%\"\n cy=\"50%\"\n outerRadius={`${outer}%`}\n innerRadius={`${inner}%`}\n startAngle={90}\n endAngle={-270}\n // The card showing through between the slices, as the treemap's gutter.\n stroke=\"var(--bg-surface)\"\n strokeWidth={2}\n rootTabIndex={-1}\n isAnimationActive={false}\n shape={shape}\n label={withLabels ? label : false}\n labelLine={false}\n onClick={\n clickable\n ? (_entry: unknown, index: number) => {\n const d = drawn[index];\n if (d) clickRef.current?.(d.slice.key, d.slice);\n }\n : undefined\n }\n />\n ),\n [pieData, drawn, shape, label, withLabels, outer, inner, clickable],\n );\n\n // The roving tab stop, moved on the sectors in place (they are not re-rendered).\n useEffect(() => {\n refs.current.forEach((el, i) => el?.setAttribute(\"tabindex\", i === current ? \"0\" : \"-1\"));\n });\n\n const legendEntries: LegendEntry[] = drawable.map((d) => ({\n key: d.slice.key,\n label: d.slice.label,\n color: d.color,\n }));\n\n const emptyBox = (\n <div\n className={cn(\"flex items-center justify-center text-sm text-[var(--text-muted)]\", box.className)}\n style={box.style}\n >\n {empty ?? labels.empty}\n </div>\n );\n\n // Nothing drawable at all: no chart, and no legend of switches for nothing.\n if (drawable.length === 0) {\n return (\n <div {...rest} className={cn(\"w-full\", className)} style={style}>\n {emptyBox}\n </div>\n );\n }\n\n const Content = redact ? RedactedTooltipContent : ChartTooltipContent;\n\n return (\n <div {...rest} className={cn(\"w-full\", className)} style={style}>\n {drawn.length === 0 ? (\n // Every slice switched off: the legend below is how they come back.\n emptyBox\n ) : (\n <div role=\"group\" aria-label={labels.slices} className={cn(\"relative\", box.className)} style={box.style}>\n <ChartContainer\n config={PIE_CONFIG}\n className={cn(\"h-full\", clickable && \"[&_.recharts-pie-sector]:cursor-pointer\")}\n >\n <RechartsPieChart accessibilityLayer={false}>\n <ChartTooltip\n content={\n <Content\n hideLabel\n valueFormatter={(v) =>\n `${formatValue(v)} (${formatShare(total > 0 ? v / total : 0)})`\n }\n />\n }\n />\n {pie}\n </RechartsPieChart>\n </ChartContainer>\n {variant === \"donut\" && center && (\n <div\n data-private={redact ? \"\" : undefined}\n // While a slice has keyboard focus the centre reads it out — the\n // keyboard's tooltip. Hidden from assistive technology then: the slice's\n // own name has just said the same.\n aria-hidden={focusedSlice ? true : undefined}\n className=\"pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center\"\n >\n <span className=\"max-w-[40%] truncate text-xs text-[var(--text-muted)]\">\n {focusedSlice ? focusedSlice.slice.label : (centerLabel ?? labels.total)}\n </span>\n <span className=\"max-w-[40%] truncate text-lg font-semibold tabular-nums text-[var(--text-primary)]\">\n {focusedSlice ? formatValue(focusedSlice.value) : (centerValue ?? formatValue(total))}\n </span>\n {focusedSlice && (\n <span className=\"text-xs tabular-nums text-[var(--text-muted)]\">\n {formatShare(focusedSlice.share)}\n </span>\n )}\n </div>\n )}\n </div>\n )}\n {legend === \"toggle\" && (\n <ToggleLegend\n entries={legendEntries}\n hidden={hidden}\n onToggle={toggle}\n showSingle\n className=\"justify-center\"\n labels={{ legend: labels.legend }}\n />\n )}\n {legend === \"static\" && (\n <StaticLegend entries={legendEntries} className=\"justify-center\" aria-label={labels.legend} />\n )}\n </div>\n );\n}\n"],"mappings":";AA6HM,cAsTM,YAtTN;AApHN,SAAS,aAAa,WAAW,iBAAiB,SAAS,QAAQ,gBAAgB;AAEnF,SAAS,KAAK,YAAY,kBAAkB,cAAc;AAE1D,SAAS,UAAU;AACnB,SAAS,sBAAsB;AAC/B,SAAS,cAAc,oBAAoB;AAC3C,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB,cAAc,2BAA6C;AACpF,SAAS,gCAAqD;AAC9D,SAAS,cAAc,cAAc,oBAAsC;AAC3E,SAAS,mBAAmB;AAC5B,SAAS,wBAA0C;AA+FnD,MAAM,aAA0B,CAAC;AAEjC,MAAM,SAAS,KAAK,KAAK;AAIzB,SAAS,uBAAuB,OAAmD;AACjF,SACE,oBAAC,SAAI,gBAAY,MACf,8BAAC,uBAAqB,GAAG,OAAO,GAClC;AAEJ;AAsBO,SAAS,SAAS;AAAA,EACvB;AAAA,EACA,UAAU;AAAA,EACV,SAAS;AAAA,EACT,aAAa;AAAA,EACb,cAAc;AAAA,EACd,gBAAgB;AAAA,EAChB,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAkB;AAChB,QAAM,SAAS,aAAa,YAAY,0BAA0B,UAAU;AAC5E,QAAM,SAAS,aAAa,UAAU;AAEtC,QAAM,EAAE,MAAM,IAAI,YAAY;AAC9B,QAAM,MAAM,iBAAiB,MAAM;AACnC,QAAM,eAAe,QAAQ,MAAM,IAAI,KAAK,aAAa,MAAM,GAAG,CAAC,MAAM,CAAC;AAC1E,QAAM,iBAAiB;AAAA,IACrB,OAAO;AAAA,MACL,OAAO,IAAI,KAAK,aAAa,QAAQ,EAAE,OAAO,WAAW,uBAAuB,EAAE,CAAC;AAAA,MACnF,MAAM,IAAI,KAAK,aAAa,QAAQ,EAAE,OAAO,WAAW,uBAAuB,EAAE,CAAC;AAAA,IACpF;AAAA,IACA,CAAC,MAAM;AAAA,EACT;AAGA,QAAM,cAAc;AAAA,IAClB,CAAC,MAAe,kBAAkB,gBAAgB,CAAC,IAAI,aAAa,OAAO,CAAC;AAAA,IAC5E,CAAC,iBAAiB,YAAY;AAAA,EAChC;AAGA,QAAM,cAAc;AAAA,IAClB,CAAC,WAAmB,QAAQ,MAAM,eAAe,OAAO,eAAe,OAAO,OAAO,KAAK;AAAA,IAC1F,CAAC,cAAc;AAAA,EACjB;AAEA,QAAM,CAAC,WAAW,YAAY,IAAI,SAA8B,MAAM,oBAAI,IAAI,CAAC;AAC/E,QAAM,SAAS,cAAc;AAC7B,QAAM,SAAS,CAAC,QAAgB;AAC9B,UAAM,OAAO,aAAa,QAAQ,GAAG;AACrC,QAAI,eAAe,OAAW,cAAa,IAAI;AAC/C,2BAAuB,IAAI;AAAA,EAC7B;AAIA,QAAM,EAAE,UAAU,OAAO,OAAO,QAAQ,IAAI,QAAQ,MAAM;AACxD,UAAMA,YAAW,KACd,IAAI,CAAC,OAAO,OAAO,EAAE,OAAO,OAAO,MAAM,OAAO,OAAO,MAAM,SAAS,WAAW,CAAC,EAAE,EAAE,EACtF;AAAA,MAAO,CAAC,MACP,OAAO,EAAE,UAAU,YAAY,OAAO,SAAS,EAAE,KAAK,KAAK,EAAE,QAAQ;AAAA,IACvE;AACF,UAAM,QAAQA,UAAS,OAAO,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,MAAM,GAAG,CAAC;AAC7D,UAAMC,SAAQ,MAAM,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,OAAO,CAAC;AACvD,UAAMC,SAAsB,MAAM,IAAI,CAAC,OAAO,EAAE,GAAG,GAAG,OAAOD,SAAQ,IAAI,EAAE,QAAQA,SAAQ,EAAE,EAAE;AAC/F,UAAME,WAAUD,OAAM,IAAI,CAAC,OAAO;AAAA,MAChC,KAAK,EAAE,MAAM;AAAA,MACb,OAAO,EAAE,MAAM;AAAA,MACf,OAAO,EAAE;AAAA,MACT,MAAM,EAAE;AAAA,IACV,EAAE;AACF,WAAO,EAAE,UAAAF,WAAU,OAAAE,QAAO,OAAAD,QAAO,SAAAE,SAAQ;AAAA,EAC3C,GAAG,CAAC,MAAM,MAAM,CAAC;AAGjB,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,CAAC;AAClC,QAAM,CAAC,SAAS,UAAU,IAAI,SAA6B,MAAS;AACpE,QAAM,UAAU,KAAK,IAAI,MAAM,KAAK,IAAI,GAAG,MAAM,SAAS,CAAC,CAAC;AAC5D,QAAM,OAAO,OAA+B,CAAC,CAAC;AAC9C,QAAM,eAAe,YAAY,SAAY,MAAM,OAAO,IAAI;AAI9D,QAAM,WAAW,OAAO,YAAY;AACpC,kBAAgB,MAAM;AACpB,aAAS,UAAU;AAAA,EACrB,CAAC;AACD,QAAM,YAAY,iBAAiB;AAWnC,QAAM,QAAQ;AAAA,IACZ,CAAC,UAA+B;AAC9B,YAAM;AAAA,QACJ;AAAA,QACA,UAAU;AAAA,QACV,UAAU;AAAA,QACV,sBAAsB;AAAA,QACtB,aAAa;AAAA,QACb,YAAY;AAAA;AAAA;AAAA;AAAA,QAIZ;AAAA,QACA,GAAG;AAAA,MACL,IAAI;AAMJ,YAAM,IAAI,MAAM,KAAK;AACrB,UAAI,CAAC,EAAG,QAAO,oBAAC,UAA+B,GAAG,UAAtB,OAAO,MAAuB;AAC1D,YAAM,YAAY,CAAC,UAAsC;AACvD,cAAM,OAAO,eAAe,MAAM,KAAK,MAAM,aAAa;AAC1D,YAAI;AACJ,YAAI,KAAM,UAAS,QAAQ;AAAA,iBAClB,MAAM,QAAQ,YAAa,UAAS,QAAQ;AAAA,iBAC5C,MAAM,QAAQ,UAAW,UAAS,QAAQ;AAAA,iBAC1C,MAAM,QAAQ,OAAQ,UAAS;AAAA,iBAC/B,MAAM,QAAQ,MAAO,UAAS,MAAM,SAAS;AACtD,YAAI,WAAW,QAAW;AACxB,gBAAM,eAAe;AAErB,eAAK,SAAU,SAAS,MAAM,SAAU,MAAM,UAAU,MAAM,MAAM,GAAG,MAAM;AAAA,QAC/E,WAAW,cAAc,MAAM,QAAQ,WAAW,MAAM,QAAQ,MAAM;AACpE,gBAAM,eAAe;AACrB,mBAAS,UAAU,EAAE,MAAM,KAAK,EAAE,KAAK;AAAA,QACzC;AAAA,MACF;AACA,aACE;AAAA,QAAC;AAAA;AAAA,UACC,KAAK,CAAC,SAAS;AACb,iBAAK,QAAQ,KAAK,IAAI;AAAA,UACxB;AAAA,UACA,MAAM,YAAY,WAAW;AAAA,UAE7B,UAAU,UAAU,IAAI,IAAI;AAAA,UAC5B,cAAY,OAAO,MAAM,EAAE,MAAM,OAAO,YAAY,EAAE,KAAK,GAAG,YAAY,EAAE,KAAK,CAAC;AAAA,UAClF,cAAY,EAAE,MAAM;AAAA,UACpB;AAAA,UACA,SAAS,MAAM;AACb,oBAAQ,KAAK;AACb,uBAAW,KAAK;AAAA,UAClB;AAAA,UACA,QAAQ,MAAM,WAAW,MAAS;AAAA,UAGlC,WAAU;AAAA,UAEV,8BAAC,UAA+B,GAAG,UAAtB,OAAO,MAAuB;AAAA;AAAA,MAC7C;AAAA,IAEJ;AAAA,IACA,CAAC,OAAO,QAAQ,aAAa,aAAa,SAAS;AAAA,EACrD;AAEA,QAAM,QAAQ;AAAA,IACZ,CAAC,UAA+B;AAC9B,YAAM,IAAI,MAAM,MAAM,SAAS,EAAE;AACjC,YAAM,KAAK,OAAO,MAAM,EAAE;AAC1B,YAAM,KAAK,OAAO,MAAM,EAAE;AAC1B,YAAM,SAAS,OAAO,MAAM,WAAW;AACvC,YAAM,MAAM,OAAO,MAAM,QAAQ;AACjC,UAAI,CAAC,KAAK,EAAE,QAAQ,iBAAiB,CAAC,CAAC,IAAI,IAAI,QAAQ,GAAG,EAAE,MAAM,OAAO,QAAQ,EAAG,QAAO;AAC3F,YAAM,IAAI,SAAS,KAAK;AACxB,YAAM,IAAI,KAAK,IAAI,KAAK,IAAI,CAAC,MAAM,MAAM;AACzC,YAAM,IAAI,KAAK,IAAI,KAAK,IAAI,CAAC,MAAM,MAAM;AACzC,YAAM,UAAU,gBAAgB;AAChC,aACE;AAAA,QAAC;AAAA;AAAA,UACC;AAAA,UACA;AAAA,UACA,YAAY,KAAK,KAAK,UAAU;AAAA,UAChC,kBAAiB;AAAA,UACjB,UAAU,KAAK;AAAA,UACf,MAAK;AAAA,UACL,gBAAc,UAAU,UAAU,KAAK;AAAA,UACvC,OAAO,EAAE,eAAe,OAAO;AAAA,UAE9B,oBAAU,YAAY,EAAE,KAAK,IAAI,YAAY,EAAE,KAAK;AAAA;AAAA,MACvD;AAAA,IAEJ;AAAA,IACA,CAAC,OAAO,eAAe,aAAa,QAAQ,aAAa,aAAa,KAAK;AAAA,EAC7E;AAEA,QAAM,aAAa,gBAAgB;AAEnC,QAAM,QAAQ,aAAa,KAAK;AAChC,QAAM,QAAQ,YAAY,UAAU,KAAK,MAAM,QAAQ,GAAG,IAAI;AAW9D,QAAM,MAAM;AAAA,IACV,MACE;AAAA,MAAC;AAAA;AAAA,QACC,MAAM;AAAA,QACN,SAAQ;AAAA,QACR,SAAQ;AAAA,QACR,IAAG;AAAA,QACH,IAAG;AAAA,QACH,aAAa,GAAG,KAAK;AAAA,QACrB,aAAa,GAAG,KAAK;AAAA,QACrB,YAAY;AAAA,QACZ,UAAU;AAAA,QAEV,QAAO;AAAA,QACP,aAAa;AAAA,QACb,cAAc;AAAA,QACd,mBAAmB;AAAA,QACnB;AAAA,QACA,OAAO,aAAa,QAAQ;AAAA,QAC5B,WAAW;AAAA,QACX,SACE,YACI,CAAC,QAAiB,UAAkB;AAClC,gBAAM,IAAI,MAAM,KAAK;AACrB,cAAI,EAAG,UAAS,UAAU,EAAE,MAAM,KAAK,EAAE,KAAK;AAAA,QAChD,IACA;AAAA;AAAA,IAER;AAAA,IAEF,CAAC,SAAS,OAAO,OAAO,OAAO,YAAY,OAAO,OAAO,SAAS;AAAA,EACpE;AAGA,YAAU,MAAM;AACd,SAAK,QAAQ,QAAQ,CAAC,IAAI,MAAM,IAAI,aAAa,YAAY,MAAM,UAAU,MAAM,IAAI,CAAC;AAAA,EAC1F,CAAC;AAED,QAAM,gBAA+B,SAAS,IAAI,CAAC,OAAO;AAAA,IACxD,KAAK,EAAE,MAAM;AAAA,IACb,OAAO,EAAE,MAAM;AAAA,IACf,OAAO,EAAE;AAAA,EACX,EAAE;AAEF,QAAM,WACJ;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,GAAG,qEAAqE,IAAI,SAAS;AAAA,MAChG,OAAO,IAAI;AAAA,MAEV,mBAAS,OAAO;AAAA;AAAA,EACnB;AAIF,MAAI,SAAS,WAAW,GAAG;AACzB,WACE,oBAAC,SAAK,GAAG,MAAM,WAAW,GAAG,UAAU,SAAS,GAAG,OAChD,oBACH;AAAA,EAEJ;AAEA,QAAM,UAAU,SAAS,yBAAyB;AAElD,SACE,qBAAC,SAAK,GAAG,MAAM,WAAW,GAAG,UAAU,SAAS,GAAG,OAChD;AAAA,UAAM,WAAW;AAAA;AAAA,MAEhB;AAAA,QAEA,qBAAC,SAAI,MAAK,SAAQ,cAAY,OAAO,QAAQ,WAAW,GAAG,YAAY,IAAI,SAAS,GAAG,OAAO,IAAI,OAChG;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,QAAQ;AAAA,UACR,WAAW,GAAG,UAAU,aAAa,yCAAyC;AAAA,UAE9E,+BAAC,oBAAiB,oBAAoB,OACpC;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,SACE;AAAA,kBAAC;AAAA;AAAA,oBACC,WAAS;AAAA,oBACT,gBAAgB,CAAC,MACf,GAAG,YAAY,CAAC,CAAC,KAAK,YAAY,QAAQ,IAAI,IAAI,QAAQ,CAAC,CAAC;AAAA;AAAA,gBAEhE;AAAA;AAAA,YAEJ;AAAA,YACC;AAAA,aACH;AAAA;AAAA,MACF;AAAA,MACC,YAAY,WAAW,UACtB;AAAA,QAAC;AAAA;AAAA,UACC,gBAAc,SAAS,KAAK;AAAA,UAI5B,eAAa,eAAe,OAAO;AAAA,UACnC,WAAU;AAAA,UAEV;AAAA,gCAAC,UAAK,WAAU,yDACb,yBAAe,aAAa,MAAM,QAAS,eAAe,OAAO,OACpE;AAAA,YACA,oBAAC,UAAK,WAAU,sFACb,yBAAe,YAAY,aAAa,KAAK,IAAK,eAAe,YAAY,KAAK,GACrF;AAAA,YACC,gBACC,oBAAC,UAAK,WAAU,iDACb,sBAAY,aAAa,KAAK,GACjC;AAAA;AAAA;AAAA,MAEJ;AAAA,OAEJ;AAAA,IAED,WAAW,YACV;AAAA,MAAC;AAAA;AAAA,QACC,SAAS;AAAA,QACT;AAAA,QACA,UAAU;AAAA,QACV,YAAU;AAAA,QACV,WAAU;AAAA,QACV,QAAQ,EAAE,QAAQ,OAAO,OAAO;AAAA;AAAA,IAClC;AAAA,IAED,WAAW,YACV,oBAAC,gBAAa,SAAS,eAAe,WAAU,kBAAiB,cAAY,OAAO,QAAQ;AAAA,KAEhG;AAEJ;","names":["drawable","total","drawn","pieData"]}
@@ -1,5 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ComponentPropsWithoutRef, RefObject, ReactNode } from 'react';
3
+ import { RemLength } from '../hooks/use-breakpoint.js';
3
4
 
4
5
  /** The panel's accessible name. A `role="dialog"` with no name announces as "dialog"
5
6
  * and nothing else, which is worse than the unlabelled div it replaced — so it is a
@@ -26,7 +27,14 @@ interface PopoverProps extends Omit<ComponentPropsWithoutRef<"div">, "children">
26
27
  ref: RefObject<HTMLButtonElement | null>;
27
28
  }) => ReactNode;
28
29
  children: (close: () => void) => ReactNode;
29
- width?: number;
30
+ /**
31
+ * The panel's width: `"18rem"` (the default) grows with the text size — the panel
32
+ * holds text, and a 288 px calendar at 150 % cut its own month off (0.32,
33
+ * docs/text-size-harmonization.md §3.2). A plain number is px, fixed, as before.
34
+ * Either way the panel is never wider than the viewport less 8 px a side: 18rem is
35
+ * 432 px at Extra large, more than a 360 px phone has.
36
+ */
37
+ width?: number | RemLength;
30
38
  labels?: Partial<PopoverLabels>;
31
39
  /** Id for the panel, so a trigger wearing `role="combobox"` can `aria-controls` it.
32
40
  * Required by ARIA on that role, and the panel is rendered here rather than by the
@@ -5,6 +5,7 @@ import { createPortal } from "react-dom";
5
5
  import { useAnchoredPanel } from "../hooks/use-anchored-panel.js";
6
6
  import { useEscapeKey, useOutsideClick } from "../hooks/use-dismiss.js";
7
7
  import { useFocusTrap } from "../hooks/use-focus-trap.js";
8
+ import { lengthPx, useRemPx } from "../hooks/use-breakpoint.js";
8
9
  import { cn } from "../lib/cn.js";
9
10
  import { dirOf } from "../lib/direction.js";
10
11
  import { useKitLabels } from "../i18n/kit-labels.js";
@@ -14,7 +15,8 @@ const DEFAULT_POPOVER_LABELS = {
14
15
  // bare Popover falls back to this — the kit's own pickers name their panels.
15
16
  panel: "Pop-up"
16
17
  };
17
- const POPOVER_WIDTH = 288;
18
+ const POPOVER_WIDTH = "18rem";
19
+ const EDGE = 8;
18
20
  function Popover({
19
21
  trigger,
20
22
  children,
@@ -38,11 +40,13 @@ function Popover({
38
40
  const text = useKitLabels("popover", DEFAULT_POPOVER_LABELS, labels);
39
41
  const placement = useAnchoredPanel(triggerRef, open);
40
42
  const rect = placement.rect;
43
+ const remPx = useRemPx();
44
+ const panelWidth = rect ? Math.min(lengthPx(width, remPx), Math.max(0, window.innerWidth - 2 * EDGE)) : 0;
41
45
  const pos = rect ? {
42
46
  top: placement.top,
43
47
  left: Math.min(
44
- Math.max(8, dir === "rtl" ? rect.left : rect.right - width),
45
- window.innerWidth - width - 8
48
+ Math.max(EDGE, dir === "rtl" ? rect.left : rect.right - panelWidth),
49
+ window.innerWidth - panelWidth - EDGE
46
50
  )
47
51
  } : null;
48
52
  useOutsideClick([triggerRef, panelRef], close, open);
@@ -90,7 +94,7 @@ function Popover({
90
94
  position: "fixed",
91
95
  top: pos.top,
92
96
  left: pos.left,
93
- width,
97
+ width: panelWidth,
94
98
  maxHeight: placement.maxHeight
95
99
  },
96
100
  className: cn(
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/popover.tsx"],"sourcesContent":["import { useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, RefObject } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useAnchoredPanel } from \"../hooks/use-anchored-panel\";\nimport { useEscapeKey, useOutsideClick } from \"../hooks/use-dismiss\";\nimport { useFocusTrap } from \"../hooks/use-focus-trap\";\nimport { cn } from \"../lib/cn\";\nimport { dirOf } from \"../lib/direction\";\nimport type { Direction } from \"../lib/direction\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\n\n/** The panel's accessible name. A `role=\"dialog\"` with no name announces as \"dialog\"\n * and nothing else, which is worse than the unlabelled div it replaced — so it is a\n * prop with an English default, like every other string this package renders. Callers\n * should pass the control's own name (\"Filter\", \"Choose a date\"); the default is only\n * there so an unmigrated call site is still named. */\nexport interface PopoverLabels {\n panel: string;\n}\n\n// Exported since the kit grew one label tree (`UiKitLabels`, src/i18n): the complete\n// English reference a translator works from has to be able to name every namespace.\nexport const DEFAULT_POPOVER_LABELS: PopoverLabels = {\n // \"Pop-up\", not \"Popover\": the panel's accessible name is read to USERS, and\n // \"popover\" is a developer's word for it (reported by keksdose, 0.5.0). Only a\n // bare Popover falls back to this — the kit's own pickers name their panels.\n panel: \"Pop-up\",\n};\n\n/**\n * Exported, and extending the `<div>` attributes, so a consumer wrapping this — which is\n * what `DateField` and `FilterPopover` in this very package do — can hand anything\n * through to the PANEL: a `data-tour` anchor for the kit's own guided tour, a test id,\n * an `aria-describedby`, or an `aria-label` naming this particular popover.\n *\n * `children` is omitted from those attributes rather than inherited: here it is a render\n * prop taking the panel's `close`, not a `ReactNode`, so the two cannot be reconciled.\n */\nexport interface PopoverProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n trigger: (state: { open: boolean; toggle: () => void; ref: RefObject<HTMLButtonElement | null> }) => ReactNode;\n children: (close: () => void) => ReactNode;\n width?: number;\n labels?: Partial<PopoverLabels>;\n /** Id for the panel, so a trigger wearing `role=\"combobox\"` can `aria-controls` it.\n * Required by ARIA on that role, and the panel is rendered here rather than by the\n * caller, so the id has to come in from outside. */\n panelId?: string;\n /** Extra classes for the PANEL. The trigger is the caller's own element. */\n className?: string;\n}\n\nconst POPOVER_WIDTH = 288;\n\n/**\n * Portal-rendered popover anchored under its trigger button. The panel is fixed\n * to the viewport (so table/overflow ancestors can't clip it) and re-aligns on\n * scroll/resize; it closes on outside-click and Escape.\n *\n * ## Trapped, not merely focused — and why, because it is the closer call\n *\n * The audit's finding was that this panel *\"portals out of the tab order with no role\n * and no focus move\"*: Tab from the trigger went straight past the panel into the page\n * behind it, so the calendar, the calculator keypad and every column filter in the kit\n * were mouse-only. Two shapes fix that, and a popover is not automatically a dialog, so\n * this was chosen rather than inherited:\n *\n * - **The light one** — move focus in on open, hand it back on close, and let Tab walk\n * OUT of the panel into whatever follows the trigger. That is the right shape for a\n * non-modal popover sitting inline in the document.\n * - **The trap** — Tab cycles inside the panel; Escape and a click outside are the\n * ways out. What is here.\n *\n * The deciding fact is the portal. This panel is rendered to the END of `<body>`, not\n * beside its trigger, so \"let Tab walk out\" does not mean \"carry on from the trigger\" —\n * it means land at the end of the document, several hundred controls from where the\n * user was. Tabbing out of it would be a worse place to arrive than the one the finding\n * complains about, and fixing that properly needs a sentinel-and-return dance that\n * nothing in this package has. The trap gets the user back to the trigger every time,\n * because `useFocusTrap` restores focus on close.\n *\n * And the panels this kit actually opens are self-contained: pick a date, tap out a\n * sum, narrow a column. Each of them commits and closes, and each already has two\n * documented ways out wired above — Escape and an outside press — so containment costs\n * a keyboard user nothing they had.\n *\n * **It stays non-modal in every other respect**, deliberately: no backdrop, no body\n * scroll lock, and no `aria-modal`, so the page behind is still readable by a screen\n * reader, still scrollable, and still clickable — clicking it is what dismisses this.\n * Only Tab is contained. A popover that took the page away as well would be a\n * {@link Modal}, and there is one of those.\n */\nexport function Popover({\n trigger,\n children,\n width = POPOVER_WIDTH,\n labels,\n panelId,\n className,\n style,\n \"aria-label\": ariaLabel,\n ...rest\n}: PopoverProps) {\n const triggerRef = useRef<HTMLButtonElement>(null);\n const panelRef = useRef<HTMLDivElement>(null);\n const [open, setOpen] = useState(false);\n // The trigger's reading direction, read when it is pressed. The panel is portalled to\n // <body>, out of any `dir=\"rtl\"` subtree the trigger sits in, so without carrying it\n // across a right-to-left calendar or filter rendered left-to-right.\n const [dir, setDir] = useState<Direction>(\"ltr\");\n const close = () => setOpen(false);\n const toggle = () => {\n setDir(dirOf(triggerRef.current));\n setOpen((v) => !v);\n };\n // prop > `<UiKitProvider labels={{ popover }}>` > English. The provider's\n // `popover.panel` is only the last-resort name for a panel no caller named.\n const text = useKitLabels(\"popover\", DEFAULT_POPOVER_LABELS, labels);\n\n // Align the panel's END edge to the trigger's — right in LTR, left in RTL; it was\n // always `rect.right`, so an RTL panel hung off the wrong side — and clamp it\n // horizontally; the\n // hook owns the vertical half — below by default, flipped above (and height-capped)\n // when that is where the room is, e.g. once a mobile keyboard has eaten the bottom\n // of the screen (feedback #135). It re-measures on scroll/resize.\n const placement = useAnchoredPanel(triggerRef, open);\n const rect = placement.rect;\n const pos = rect\n ? {\n top: placement.top,\n left: Math.min(\n Math.max(8, dir === \"rtl\" ? rect.left : rect.right - width),\n window.innerWidth - width - 8,\n ),\n }\n : null;\n\n useOutsideClick([triggerRef, panelRef], close, open);\n useEscapeKey(close, open);\n\n useFocusTrap(panelRef, {\n active: open && pos !== null,\n // The panel keeps a focus one of its own children has already taken. `FilterPopover`\n // autofocuses its text box on mount — that is what the press on \"Filter\" was FOR,\n // and it is a prop there precisely so the popover keeps it — and a trap that\n // unconditionally focused the container would take it straight back off again,\n // one effect later and invisibly.\n initialFocus: () => {\n const panel = panelRef.current;\n const active = document.activeElement;\n return panel?.contains(active) ? (active as HTMLElement) : panel;\n },\n });\n\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if (e.key !== \"Escape\") return;\n // `useEscapeKey` above listens on the document and still covers the case where\n // focus somehow is not in here. This handler is for the case that focus IS, which\n // is now the normal one — and it exists to STOP the key, because a popover opened\n // from inside a `Modal` (the calculator on an amount field is exactly that) would\n // otherwise have its Escape travel on up the React tree and close the dialog\n // underneath as well. Closing twice is harmless — `close` is a state setter, not a\n // caller's callback — so the two paths cannot double-fire anything.\n e.stopPropagation();\n close();\n };\n\n return (\n <>\n {trigger({ open, toggle, ref: triggerRef })}\n {open && pos &&\n createPortal(\n /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- the\n dialog container, per the APG dialog pattern: `onKeyDown` is Escape-to-close for\n whatever inside it holds focus. */\n <div\n // `...rest` first: the role, the id the trigger's `aria-controls` points at,\n // the `tabIndex` the trap needs and the measured position are what make this\n // a popover, and a caller reaching for a `data-tour` anchor must not be able\n // to remove one of them by accident.\n {...rest}\n ref={panelRef}\n role=\"dialog\"\n id={panelId}\n // The caller's own `aria-label` wins over {@link PopoverLabels.panel}, which\n // is the package's English fallback for an unnamed panel. Both name the same\n // element; the DOM spelling is the one being standardised on, so where both\n // are present it is the one that survives.\n aria-label={ariaLabel ?? text.panel}\n dir={rest.dir ?? dir}\n // Without it the panel cannot take focus at all, and the trap above would\n // silently do nothing on a panel whose children are not yet tabbable.\n tabIndex={-1}\n onKeyDown={handleKeyDown}\n style={{\n // Merged over the caller's, never replaced by it: these four are measured\n // against the trigger every scroll and resize, and a panel that dropped\n // them would paint at the top-left of the viewport.\n ...style,\n position: \"fixed\",\n top: pos.top,\n left: pos.left,\n width,\n maxHeight: placement.maxHeight,\n }}\n className={cn(\n \"z-50 overflow-y-auto rounded-md border border-[var(--border)] bg-[var(--bg-surface)] p-3 shadow-lg outline-none\",\n className,\n )}\n >\n {children(close)}\n </div>,\n document.body,\n )}\n </>\n );\n}\n"],"mappings":";AAuKI,mBAOM,KAPN;AAvKJ,SAAS,QAAQ,gBAAgB;AAEjC,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB;AACjC,SAAS,cAAc,uBAAuB;AAC9C,SAAS,oBAAoB;AAC7B,SAAS,UAAU;AACnB,SAAS,aAAa;AAEtB,SAAS,oBAAoB;AAatB,MAAM,yBAAwC;AAAA;AAAA;AAAA;AAAA,EAInD,OAAO;AACT;AAwBA,MAAM,gBAAgB;AAwCf,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,GAAG;AACL,GAAiB;AACf,QAAM,aAAa,OAA0B,IAAI;AACjD,QAAM,WAAW,OAAuB,IAAI;AAC5C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AAItC,QAAM,CAAC,KAAK,MAAM,IAAI,SAAoB,KAAK;AAC/C,QAAM,QAAQ,MAAM,QAAQ,KAAK;AACjC,QAAM,SAAS,MAAM;AACnB,WAAO,MAAM,WAAW,OAAO,CAAC;AAChC,YAAQ,CAAC,MAAM,CAAC,CAAC;AAAA,EACnB;AAGA,QAAM,OAAO,aAAa,WAAW,wBAAwB,MAAM;AAQnE,QAAM,YAAY,iBAAiB,YAAY,IAAI;AACnD,QAAM,OAAO,UAAU;AACvB,QAAM,MAAM,OACR;AAAA,IACE,KAAK,UAAU;AAAA,IACf,MAAM,KAAK;AAAA,MACT,KAAK,IAAI,GAAG,QAAQ,QAAQ,KAAK,OAAO,KAAK,QAAQ,KAAK;AAAA,MAC1D,OAAO,aAAa,QAAQ;AAAA,IAC9B;AAAA,EACF,IACA;AAEJ,kBAAgB,CAAC,YAAY,QAAQ,GAAG,OAAO,IAAI;AACnD,eAAa,OAAO,IAAI;AAExB,eAAa,UAAU;AAAA,IACrB,QAAQ,QAAQ,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMxB,cAAc,MAAM;AAClB,YAAM,QAAQ,SAAS;AACvB,YAAM,SAAS,SAAS;AACxB,aAAO,OAAO,SAAS,MAAM,IAAK,SAAyB;AAAA,IAC7D;AAAA,EACF,CAAC;AAED,QAAM,gBAAgB,CAAC,MAAqC;AAC1D,QAAI,EAAE,QAAQ,SAAU;AAQxB,MAAE,gBAAgB;AAClB,UAAM;AAAA,EACR;AAEA,SACE,iCACG;AAAA,YAAQ,EAAE,MAAM,QAAQ,KAAK,WAAW,CAAC;AAAA,IACzC,QAAQ,OACP;AAAA;AAAA;AAAA;AAAA,MAIE;AAAA,QAAC;AAAA;AAAA,UAKE,GAAG;AAAA,UACJ,KAAK;AAAA,UACL,MAAK;AAAA,UACL,IAAI;AAAA,UAKJ,cAAY,aAAa,KAAK;AAAA,UAC9B,KAAK,KAAK,OAAO;AAAA,UAGjB,UAAU;AAAA,UACV,WAAW;AAAA,UACX,OAAO;AAAA;AAAA;AAAA;AAAA,YAIL,GAAG;AAAA,YACH,UAAU;AAAA,YACV,KAAK,IAAI;AAAA,YACT,MAAM,IAAI;AAAA,YACV;AAAA,YACA,WAAW,UAAU;AAAA,UACvB;AAAA,UACA,WAAW;AAAA,YACT;AAAA,YACA;AAAA,UACF;AAAA,UAEC,mBAAS,KAAK;AAAA;AAAA,MACjB;AAAA,MACA,SAAS;AAAA,IACX;AAAA,KACJ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/popover.tsx"],"sourcesContent":["import { useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, RefObject } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useAnchoredPanel } from \"../hooks/use-anchored-panel\";\nimport { useEscapeKey, useOutsideClick } from \"../hooks/use-dismiss\";\nimport { useFocusTrap } from \"../hooks/use-focus-trap\";\nimport { lengthPx, useRemPx, type RemLength } from \"../hooks/use-breakpoint\";\nimport { cn } from \"../lib/cn\";\nimport { dirOf } from \"../lib/direction\";\nimport type { Direction } from \"../lib/direction\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\n\n/** The panel's accessible name. A `role=\"dialog\"` with no name announces as \"dialog\"\n * and nothing else, which is worse than the unlabelled div it replaced — so it is a\n * prop with an English default, like every other string this package renders. Callers\n * should pass the control's own name (\"Filter\", \"Choose a date\"); the default is only\n * there so an unmigrated call site is still named. */\nexport interface PopoverLabels {\n panel: string;\n}\n\n// Exported since the kit grew one label tree (`UiKitLabels`, src/i18n): the complete\n// English reference a translator works from has to be able to name every namespace.\nexport const DEFAULT_POPOVER_LABELS: PopoverLabels = {\n // \"Pop-up\", not \"Popover\": the panel's accessible name is read to USERS, and\n // \"popover\" is a developer's word for it (reported by keksdose, 0.5.0). Only a\n // bare Popover falls back to this — the kit's own pickers name their panels.\n panel: \"Pop-up\",\n};\n\n/**\n * Exported, and extending the `<div>` attributes, so a consumer wrapping this — which is\n * what `DateField` and `FilterPopover` in this very package do — can hand anything\n * through to the PANEL: a `data-tour` anchor for the kit's own guided tour, a test id,\n * an `aria-describedby`, or an `aria-label` naming this particular popover.\n *\n * `children` is omitted from those attributes rather than inherited: here it is a render\n * prop taking the panel's `close`, not a `ReactNode`, so the two cannot be reconciled.\n */\nexport interface PopoverProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n trigger: (state: { open: boolean; toggle: () => void; ref: RefObject<HTMLButtonElement | null> }) => ReactNode;\n children: (close: () => void) => ReactNode;\n /**\n * The panel's width: `\"18rem\"` (the default) grows with the text size — the panel\n * holds text, and a 288 px calendar at 150 % cut its own month off (0.32,\n * docs/text-size-harmonization.md §3.2). A plain number is px, fixed, as before.\n * Either way the panel is never wider than the viewport less 8 px a side: 18rem is\n * 432 px at Extra large, more than a 360 px phone has.\n */\n width?: number | RemLength;\n labels?: Partial<PopoverLabels>;\n /** Id for the panel, so a trigger wearing `role=\"combobox\"` can `aria-controls` it.\n * Required by ARIA on that role, and the panel is rendered here rather than by the\n * caller, so the id has to come in from outside. */\n panelId?: string;\n /** Extra classes for the PANEL. The trigger is the caller's own element. */\n className?: string;\n}\n\n/** 288 px at Normal, in rem so it follows the text size (§3.2). */\nconst POPOVER_WIDTH: RemLength = \"18rem\";\n\n/** The gap the panel keeps from either edge of the viewport, in px. */\nconst EDGE = 8;\n\n/**\n * Portal-rendered popover anchored under its trigger button. The panel is fixed\n * to the viewport (so table/overflow ancestors can't clip it) and re-aligns on\n * scroll/resize; it closes on outside-click and Escape.\n *\n * ## Trapped, not merely focused — and why, because it is the closer call\n *\n * The audit's finding was that this panel *\"portals out of the tab order with no role\n * and no focus move\"*: Tab from the trigger went straight past the panel into the page\n * behind it, so the calendar, the calculator keypad and every column filter in the kit\n * were mouse-only. Two shapes fix that, and a popover is not automatically a dialog, so\n * this was chosen rather than inherited:\n *\n * - **The light one** — move focus in on open, hand it back on close, and let Tab walk\n * OUT of the panel into whatever follows the trigger. That is the right shape for a\n * non-modal popover sitting inline in the document.\n * - **The trap** — Tab cycles inside the panel; Escape and a click outside are the\n * ways out. What is here.\n *\n * The deciding fact is the portal. This panel is rendered to the END of `<body>`, not\n * beside its trigger, so \"let Tab walk out\" does not mean \"carry on from the trigger\" —\n * it means land at the end of the document, several hundred controls from where the\n * user was. Tabbing out of it would be a worse place to arrive than the one the finding\n * complains about, and fixing that properly needs a sentinel-and-return dance that\n * nothing in this package has. The trap gets the user back to the trigger every time,\n * because `useFocusTrap` restores focus on close.\n *\n * And the panels this kit actually opens are self-contained: pick a date, tap out a\n * sum, narrow a column. Each of them commits and closes, and each already has two\n * documented ways out wired above — Escape and an outside press — so containment costs\n * a keyboard user nothing they had.\n *\n * **It stays non-modal in every other respect**, deliberately: no backdrop, no body\n * scroll lock, and no `aria-modal`, so the page behind is still readable by a screen\n * reader, still scrollable, and still clickable — clicking it is what dismisses this.\n * Only Tab is contained. A popover that took the page away as well would be a\n * {@link Modal}, and there is one of those.\n */\nexport function Popover({\n trigger,\n children,\n width = POPOVER_WIDTH,\n labels,\n panelId,\n className,\n style,\n \"aria-label\": ariaLabel,\n ...rest\n}: PopoverProps) {\n const triggerRef = useRef<HTMLButtonElement>(null);\n const panelRef = useRef<HTMLDivElement>(null);\n const [open, setOpen] = useState(false);\n // The trigger's reading direction, read when it is pressed. The panel is portalled to\n // <body>, out of any `dir=\"rtl\"` subtree the trigger sits in, so without carrying it\n // across a right-to-left calendar or filter rendered left-to-right.\n const [dir, setDir] = useState<Direction>(\"ltr\");\n const close = () => setOpen(false);\n const toggle = () => {\n setDir(dirOf(triggerRef.current));\n setOpen((v) => !v);\n };\n // prop > `<UiKitProvider labels={{ popover }}>` > English. The provider's\n // `popover.panel` is only the last-resort name for a panel no caller named.\n const text = useKitLabels(\"popover\", DEFAULT_POPOVER_LABELS, labels);\n\n // Align the panel's END edge to the trigger's — right in LTR, left in RTL; it was\n // always `rect.right`, so an RTL panel hung off the wrong side — and clamp it\n // horizontally; the\n // hook owns the vertical half — below by default, flipped above (and height-capped)\n // when that is where the room is, e.g. once a mobile keyboard has eaten the bottom\n // of the screen (feedback #135). It re-measures on scroll/resize.\n const placement = useAnchoredPanel(triggerRef, open);\n const rect = placement.rect;\n // In px for the arithmetic below, at the text size in force; capped at the viewport.\n const remPx = useRemPx();\n const panelWidth = rect\n ? Math.min(lengthPx(width, remPx), Math.max(0, window.innerWidth - 2 * EDGE))\n : 0;\n const pos = rect\n ? {\n top: placement.top,\n left: Math.min(\n Math.max(EDGE, dir === \"rtl\" ? rect.left : rect.right - panelWidth),\n window.innerWidth - panelWidth - EDGE,\n ),\n }\n : null;\n\n useOutsideClick([triggerRef, panelRef], close, open);\n useEscapeKey(close, open);\n\n useFocusTrap(panelRef, {\n active: open && pos !== null,\n // The panel keeps a focus one of its own children has already taken. `FilterPopover`\n // autofocuses its text box on mount — that is what the press on \"Filter\" was FOR,\n // and it is a prop there precisely so the popover keeps it — and a trap that\n // unconditionally focused the container would take it straight back off again,\n // one effect later and invisibly.\n initialFocus: () => {\n const panel = panelRef.current;\n const active = document.activeElement;\n return panel?.contains(active) ? (active as HTMLElement) : panel;\n },\n });\n\n const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if (e.key !== \"Escape\") return;\n // `useEscapeKey` above listens on the document and still covers the case where\n // focus somehow is not in here. This handler is for the case that focus IS, which\n // is now the normal one — and it exists to STOP the key, because a popover opened\n // from inside a `Modal` (the calculator on an amount field is exactly that) would\n // otherwise have its Escape travel on up the React tree and close the dialog\n // underneath as well. Closing twice is harmless — `close` is a state setter, not a\n // caller's callback — so the two paths cannot double-fire anything.\n e.stopPropagation();\n close();\n };\n\n return (\n <>\n {trigger({ open, toggle, ref: triggerRef })}\n {open && pos &&\n createPortal(\n /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- the\n dialog container, per the APG dialog pattern: `onKeyDown` is Escape-to-close for\n whatever inside it holds focus. */\n <div\n // `...rest` first: the role, the id the trigger's `aria-controls` points at,\n // the `tabIndex` the trap needs and the measured position are what make this\n // a popover, and a caller reaching for a `data-tour` anchor must not be able\n // to remove one of them by accident.\n {...rest}\n ref={panelRef}\n role=\"dialog\"\n id={panelId}\n // The caller's own `aria-label` wins over {@link PopoverLabels.panel}, which\n // is the package's English fallback for an unnamed panel. Both name the same\n // element; the DOM spelling is the one being standardised on, so where both\n // are present it is the one that survives.\n aria-label={ariaLabel ?? text.panel}\n dir={rest.dir ?? dir}\n // Without it the panel cannot take focus at all, and the trap above would\n // silently do nothing on a panel whose children are not yet tabbable.\n tabIndex={-1}\n onKeyDown={handleKeyDown}\n style={{\n // Merged over the caller's, never replaced by it: these four are measured\n // against the trigger every scroll and resize, and a panel that dropped\n // them would paint at the top-left of the viewport.\n ...style,\n position: \"fixed\",\n top: pos.top,\n left: pos.left,\n width: panelWidth,\n maxHeight: placement.maxHeight,\n }}\n className={cn(\n \"z-50 overflow-y-auto rounded-md border border-[var(--border)] bg-[var(--bg-surface)] p-3 shadow-lg outline-none\",\n className,\n )}\n >\n {children(close)}\n </div>,\n document.body,\n )}\n </>\n );\n}\n"],"mappings":";AAwLI,mBAOM,KAPN;AAxLJ,SAAS,QAAQ,gBAAgB;AAEjC,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB;AACjC,SAAS,cAAc,uBAAuB;AAC9C,SAAS,oBAAoB;AAC7B,SAAS,UAAU,gBAAgC;AACnD,SAAS,UAAU;AACnB,SAAS,aAAa;AAEtB,SAAS,oBAAoB;AAatB,MAAM,yBAAwC;AAAA;AAAA;AAAA;AAAA,EAInD,OAAO;AACT;AAgCA,MAAM,gBAA2B;AAGjC,MAAM,OAAO;AAwCN,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAAA,EACd,GAAG;AACL,GAAiB;AACf,QAAM,aAAa,OAA0B,IAAI;AACjD,QAAM,WAAW,OAAuB,IAAI;AAC5C,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AAItC,QAAM,CAAC,KAAK,MAAM,IAAI,SAAoB,KAAK;AAC/C,QAAM,QAAQ,MAAM,QAAQ,KAAK;AACjC,QAAM,SAAS,MAAM;AACnB,WAAO,MAAM,WAAW,OAAO,CAAC;AAChC,YAAQ,CAAC,MAAM,CAAC,CAAC;AAAA,EACnB;AAGA,QAAM,OAAO,aAAa,WAAW,wBAAwB,MAAM;AAQnE,QAAM,YAAY,iBAAiB,YAAY,IAAI;AACnD,QAAM,OAAO,UAAU;AAEvB,QAAM,QAAQ,SAAS;AACvB,QAAM,aAAa,OACf,KAAK,IAAI,SAAS,OAAO,KAAK,GAAG,KAAK,IAAI,GAAG,OAAO,aAAa,IAAI,IAAI,CAAC,IAC1E;AACJ,QAAM,MAAM,OACR;AAAA,IACE,KAAK,UAAU;AAAA,IACf,MAAM,KAAK;AAAA,MACT,KAAK,IAAI,MAAM,QAAQ,QAAQ,KAAK,OAAO,KAAK,QAAQ,UAAU;AAAA,MAClE,OAAO,aAAa,aAAa;AAAA,IACnC;AAAA,EACF,IACA;AAEJ,kBAAgB,CAAC,YAAY,QAAQ,GAAG,OAAO,IAAI;AACnD,eAAa,OAAO,IAAI;AAExB,eAAa,UAAU;AAAA,IACrB,QAAQ,QAAQ,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMxB,cAAc,MAAM;AAClB,YAAM,QAAQ,SAAS;AACvB,YAAM,SAAS,SAAS;AACxB,aAAO,OAAO,SAAS,MAAM,IAAK,SAAyB;AAAA,IAC7D;AAAA,EACF,CAAC;AAED,QAAM,gBAAgB,CAAC,MAAqC;AAC1D,QAAI,EAAE,QAAQ,SAAU;AAQxB,MAAE,gBAAgB;AAClB,UAAM;AAAA,EACR;AAEA,SACE,iCACG;AAAA,YAAQ,EAAE,MAAM,QAAQ,KAAK,WAAW,CAAC;AAAA,IACzC,QAAQ,OACP;AAAA;AAAA;AAAA;AAAA,MAIE;AAAA,QAAC;AAAA;AAAA,UAKE,GAAG;AAAA,UACJ,KAAK;AAAA,UACL,MAAK;AAAA,UACL,IAAI;AAAA,UAKJ,cAAY,aAAa,KAAK;AAAA,UAC9B,KAAK,KAAK,OAAO;AAAA,UAGjB,UAAU;AAAA,UACV,WAAW;AAAA,UACX,OAAO;AAAA;AAAA;AAAA;AAAA,YAIL,GAAG;AAAA,YACH,UAAU;AAAA,YACV,KAAK,IAAI;AAAA,YACT,MAAM,IAAI;AAAA,YACV,OAAO;AAAA,YACP,WAAW,UAAU;AAAA,UACvB;AAAA,UACA,WAAW;AAAA,YACT;AAAA,YACA;AAAA,UACF;AAAA,UAEC,mBAAS,KAAK;AAAA;AAAA,MACjB;AAAA,MACA,SAAS;AAAA,IACX;AAAA,KACJ;AAEJ;","names":[]}
@@ -0,0 +1,100 @@
1
+ import 'react';
2
+ import 'lucide-react';
3
+ export { cO as DEFAULT_ROW_ACTIONS_LABELS, eO as RowAction, eP as RowActionList, eQ as RowActions, eR as RowActionsCollapse, eS as RowActionsColumnOptions, eT as RowActionsLabels, eU as RowActionsProps, eV as RowActionsSize, fY as rowActionsColumn } from '../delete-account-setting-FLyJf-kZ.js';
4
+ import '../data-table-labels-kpAAVi6g.js';
5
+ import './tooltip.js';
6
+ import '../hooks/use-anchored-rect.js';
7
+ import '../lib/clipping.js';
8
+ import './mini-calendar.js';
9
+ import './calendar-heatmap.js';
10
+ import './popover.js';
11
+ import '../hooks/use-breakpoint.js';
12
+ import './chip.js';
13
+ import './field-parts.js';
14
+ import './field-sync.js';
15
+ import '../wizard/types.js';
16
+ import '../tour/tour.js';
17
+ import '../search/command-palette.js';
18
+ import '../search/global-search.js';
19
+ import '../search/search-index.js';
20
+ import './month-picker.js';
21
+ import './page-contents.js';
22
+ import './series-chart-labels.js';
23
+ import './pie-chart-labels.js';
24
+ import './sparkline.js';
25
+ import './stat-tile.js';
26
+ import './signature-pad.js';
27
+ import './password-strength.js';
28
+ import './swatch-picker.js';
29
+ import './tile-radio.js';
30
+ import './icon-picker.js';
31
+ import './dialog-frame.js';
32
+ import './modal.js';
33
+ import '../hooks/use-search-param-state.js';
34
+ import './measured-grid.js';
35
+ import './user-avatar.js';
36
+ import '../lib/person-name.js';
37
+ import './status-dot.js';
38
+ import './progress-bar.js';
39
+ import './signed-amount.js';
40
+ import '../shell/topbar-action-menu.js';
41
+ import './menu-item.js';
42
+ import './floating-panel.js';
43
+ import '../hooks/use-authed-src.js';
44
+ import './account-settings-labels.js';
45
+ import './bulk-action-bar.js';
46
+ import './list.js';
47
+ import './breadcrumbs.js';
48
+ import './toast.js';
49
+ import './description-list.js';
50
+ import './line-items.js';
51
+ import './error-boundary.js';
52
+ import './authed-image.js';
53
+ import './image-grid.js';
54
+ import './lightbox.js';
55
+ import './write-lock.js';
56
+ import '../auth/sign-in-form.js';
57
+ import '../landing/access.js';
58
+ import '../landing/landing-labels.js';
59
+ import '../shell/topbar-controls.js';
60
+ import '../theme/palette-presets.js';
61
+ import '../theme/chart-palette.js';
62
+ import '../theme/theme-store.js';
63
+ import 'zustand/middleware';
64
+ import 'zustand';
65
+ import '../auth/complete-name-dialog.js';
66
+ import '../auth/forgot-password-form.js';
67
+ import '../auth/status-parts.js';
68
+ import '../auth/reset-password-form.js';
69
+ import '../auth/verify-email.js';
70
+ import '../auth/not-found-page.js';
71
+ import '../auth/accept-invitation.js';
72
+ import '../shell/company-switcher.js';
73
+ import '../account/email-change-setting.js';
74
+ import '../account/sessions-setting.js';
75
+ import '../lib/format.js';
76
+ import '../account/data-export-setting.js';
77
+ import './account-chips.js';
78
+ import './data-table-sort.js';
79
+ import './swipeable-row.js';
80
+ import '../admin/admin-parts.js';
81
+ import '../admin/admin-action-log.js';
82
+ import '../settings/settings-labels.js';
83
+ import '../demo/demo-labels.js';
84
+ import './appearance-labels.js';
85
+ import '../theme/contrast.js';
86
+ import '../theme/text-size.js';
87
+ import './reauth-dialog.js';
88
+ import './server-wake.js';
89
+ import '../lib/server-wake.js';
90
+ import './translation-review-labels.js';
91
+ import '../lib/translation-review.js';
92
+ import './country-select.js';
93
+ import './inline-edit-field.js';
94
+ import '../lib/iban.js';
95
+ import '../lib/phone.js';
96
+ import './sign-chip.js';
97
+ import './column-mapper.js';
98
+ import '../lib/column-mapping.js';
99
+ import '../lib/table-text.js';
100
+ import './focus-ring.js';