@eifi1/ui-kit 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (397) hide show
  1. package/README.md +59 -34
  2. package/dist/chart.d.ts +3 -1
  3. package/dist/chart.js +2 -0
  4. package/dist/chart.js.map +1 -1
  5. package/dist/components/account-settings-labels.d.ts +105 -0
  6. package/dist/components/account-settings-labels.js +81 -0
  7. package/dist/components/account-settings-labels.js.map +1 -0
  8. package/dist/components/account-settings.d.ts +36 -52
  9. package/dist/components/account-settings.js +44 -8
  10. package/dist/components/account-settings.js.map +1 -1
  11. package/dist/components/amount-input.d.ts +40 -4
  12. package/dist/components/amount-input.js +50 -12
  13. package/dist/components/amount-input.js.map +1 -1
  14. package/dist/components/authed-image.d.ts +68 -0
  15. package/dist/components/authed-image.js +117 -0
  16. package/dist/components/authed-image.js.map +1 -0
  17. package/dist/components/breadcrumbs.d.ts +2 -1
  18. package/dist/components/breadcrumbs.js +5 -2
  19. package/dist/components/breadcrumbs.js.map +1 -1
  20. package/dist/components/bulk-action-bar.d.ts +10 -2
  21. package/dist/components/bulk-action-bar.js.map +1 -1
  22. package/dist/components/button-group.d.ts +51 -2
  23. package/dist/components/button-group.js +5 -1
  24. package/dist/components/button-group.js.map +1 -1
  25. package/dist/components/calculator.d.ts +15 -4
  26. package/dist/components/calendar-heatmap.d.ts +41 -3
  27. package/dist/components/calendar-heatmap.js +48 -7
  28. package/dist/components/calendar-heatmap.js.map +1 -1
  29. package/dist/components/chart.js +14 -8
  30. package/dist/components/chart.js.map +1 -1
  31. package/dist/components/chip.d.ts +2 -1
  32. package/dist/components/chip.js +26 -18
  33. package/dist/components/chip.js.map +1 -1
  34. package/dist/components/choice-card.d.ts +35 -3
  35. package/dist/components/choice-card.js +83 -25
  36. package/dist/components/choice-card.js.map +1 -1
  37. package/dist/components/combobox-core.d.ts +3 -1
  38. package/dist/components/combobox-core.js +18 -5
  39. package/dist/components/combobox-core.js.map +1 -1
  40. package/dist/components/combobox.d.ts +2 -2
  41. package/dist/components/combobox.js +31 -9
  42. package/dist/components/combobox.js.map +1 -1
  43. package/dist/components/copy-button.d.ts +51 -70
  44. package/dist/components/copy-button.js +1 -1
  45. package/dist/components/copy-button.js.map +1 -1
  46. package/dist/components/data-table-cells.d.ts +63 -0
  47. package/dist/components/data-table-cells.js +64 -0
  48. package/dist/components/data-table-cells.js.map +1 -0
  49. package/dist/components/data-table-filter-popover.d.ts +1 -1
  50. package/dist/components/data-table-filter-popover.js +8 -1
  51. package/dist/components/data-table-filter-popover.js.map +1 -1
  52. package/dist/components/data-table-filters.d.ts +1 -1
  53. package/dist/components/data-table-filters.js +31 -1
  54. package/dist/components/data-table-filters.js.map +1 -1
  55. package/dist/components/data-table-labels.d.ts +13 -0
  56. package/dist/components/data-table-labels.js +8 -1
  57. package/dist/components/data-table-labels.js.map +1 -1
  58. package/dist/components/data-table.d.ts +1 -1
  59. package/dist/components/data-table.js +125 -27
  60. package/dist/components/data-table.js.map +1 -1
  61. package/dist/components/date-picker.d.ts +3 -1
  62. package/dist/components/date-picker.js +15 -2
  63. package/dist/components/date-picker.js.map +1 -1
  64. package/dist/components/description-list.d.ts +23 -3
  65. package/dist/components/description-list.js +23 -3
  66. package/dist/components/description-list.js.map +1 -1
  67. package/dist/components/dialog-frame.d.ts +1 -0
  68. package/dist/components/entity-combobox.d.ts +1 -1
  69. package/dist/components/entity-combobox.js +40 -20
  70. package/dist/components/entity-combobox.js.map +1 -1
  71. package/dist/components/error-boundary.d.ts +281 -0
  72. package/dist/components/error-boundary.js +302 -0
  73. package/dist/components/error-boundary.js.map +1 -0
  74. package/dist/components/field-sync.js +90 -83
  75. package/dist/components/field-sync.js.map +1 -1
  76. package/dist/components/field.d.ts +30 -4
  77. package/dist/components/field.js +40 -17
  78. package/dist/components/field.js.map +1 -1
  79. package/dist/components/file-button.d.ts +50 -208
  80. package/dist/components/file-dropzone.d.ts +49 -2
  81. package/dist/components/floating-panel.d.ts +13 -4
  82. package/dist/components/floating-panel.js +7 -3
  83. package/dist/components/floating-panel.js.map +1 -1
  84. package/dist/components/form-actions.d.ts +53 -0
  85. package/dist/components/form-actions.js +98 -0
  86. package/dist/components/form-actions.js.map +1 -0
  87. package/dist/components/full-bleed-dialog.js +4 -0
  88. package/dist/components/full-bleed-dialog.js.map +1 -1
  89. package/dist/components/hover-menu.js +64 -56
  90. package/dist/components/hover-menu.js.map +1 -1
  91. package/dist/components/image-grid.d.ts +67 -0
  92. package/dist/components/image-grid.js +112 -0
  93. package/dist/components/image-grid.js.map +1 -0
  94. package/dist/components/lightbox.d.ts +104 -0
  95. package/dist/components/lightbox.js +229 -0
  96. package/dist/components/lightbox.js.map +1 -0
  97. package/dist/components/line-items.d.ts +114 -0
  98. package/dist/components/line-items.js +238 -0
  99. package/dist/components/line-items.js.map +1 -0
  100. package/dist/components/list.d.ts +37 -4
  101. package/dist/components/list.js +35 -14
  102. package/dist/components/list.js.map +1 -1
  103. package/dist/components/loading-state.d.ts +24 -0
  104. package/dist/components/loading-state.js +32 -0
  105. package/dist/components/loading-state.js.map +1 -0
  106. package/dist/components/menu-item.d.ts +2 -1
  107. package/dist/components/menu-item.js +5 -1
  108. package/dist/components/menu-item.js.map +1 -1
  109. package/dist/components/mini-calendar.d.ts +13 -1
  110. package/dist/components/mini-calendar.js +46 -2
  111. package/dist/components/mini-calendar.js.map +1 -1
  112. package/dist/components/modal.d.ts +20 -1
  113. package/dist/components/modal.js +26 -2
  114. package/dist/components/modal.js.map +1 -1
  115. package/dist/components/month-picker.d.ts +21 -1
  116. package/dist/components/month-picker.js +139 -26
  117. package/dist/components/month-picker.js.map +1 -1
  118. package/dist/components/multi-entity-combobox.d.ts +1 -1
  119. package/dist/components/multi-entity-combobox.js +42 -20
  120. package/dist/components/multi-entity-combobox.js.map +1 -1
  121. package/dist/components/nav-pills.d.ts +2 -1
  122. package/dist/components/nav-pills.js +5 -1
  123. package/dist/components/nav-pills.js.map +1 -1
  124. package/dist/components/number-field.d.ts +24 -5
  125. package/dist/components/number-field.js +5 -7
  126. package/dist/components/number-field.js.map +1 -1
  127. package/dist/components/number-input.d.ts +25 -5
  128. package/dist/components/number-input.js +7 -1
  129. package/dist/components/number-input.js.map +1 -1
  130. package/dist/components/numpad-sheet.d.ts +20 -5
  131. package/dist/components/numpad-sheet.js +3 -2
  132. package/dist/components/numpad-sheet.js.map +1 -1
  133. package/dist/components/page-header.d.ts +35 -6
  134. package/dist/components/page-header.js +7 -1
  135. package/dist/components/page-header.js.map +1 -1
  136. package/dist/components/passkeys-setting.d.ts +67 -0
  137. package/dist/components/passkeys-setting.js +214 -0
  138. package/dist/components/passkeys-setting.js.map +1 -0
  139. package/dist/components/picker-sheet.js +5 -1
  140. package/dist/components/picker-sheet.js.map +1 -1
  141. package/dist/components/pie-chart-labels.d.ts +20 -0
  142. package/dist/components/pie-chart-labels.js +12 -0
  143. package/dist/components/pie-chart-labels.js.map +1 -0
  144. package/dist/components/pie-chart.d.ts +101 -0
  145. package/dist/components/pie-chart.js +283 -0
  146. package/dist/components/pie-chart.js.map +1 -0
  147. package/dist/components/popover.js +3 -0
  148. package/dist/components/popover.js.map +1 -1
  149. package/dist/components/progress-bar.d.ts +44 -3
  150. package/dist/components/progress-bar.js +58 -22
  151. package/dist/components/progress-bar.js.map +1 -1
  152. package/dist/components/qr-code.d.ts +34 -0
  153. package/dist/components/qr-code.js +57 -0
  154. package/dist/components/qr-code.js.map +1 -0
  155. package/dist/components/search-field.d.ts +15 -3
  156. package/dist/components/search-field.js +5 -1
  157. package/dist/components/search-field.js.map +1 -1
  158. package/dist/components/settings-fields.d.ts +49 -1
  159. package/dist/components/signed-amount.d.ts +122 -0
  160. package/dist/components/signed-amount.js +152 -0
  161. package/dist/components/signed-amount.js.map +1 -0
  162. package/dist/components/skeleton.d.ts +10 -3
  163. package/dist/components/skeleton.js +12 -2
  164. package/dist/components/skeleton.js.map +1 -1
  165. package/dist/components/stat-tile.d.ts +2 -1
  166. package/dist/components/stat-tile.js +6 -3
  167. package/dist/components/stat-tile.js.map +1 -1
  168. package/dist/components/swipeable-row.js +4 -3
  169. package/dist/components/swipeable-row.js.map +1 -1
  170. package/dist/components/table.d.ts +39 -5
  171. package/dist/components/table.js +45 -11
  172. package/dist/components/table.js.map +1 -1
  173. package/dist/components/text-link.d.ts +238 -0
  174. package/dist/components/text-link.js +144 -0
  175. package/dist/components/text-link.js.map +1 -0
  176. package/dist/components/text.d.ts +26 -2
  177. package/dist/components/text.js +16 -2
  178. package/dist/components/text.js.map +1 -1
  179. package/dist/components/time-input.d.ts +49 -1
  180. package/dist/components/toggle-group.d.ts +35 -0
  181. package/dist/components/toggle-group.js +43 -14
  182. package/dist/components/toggle-group.js.map +1 -1
  183. package/dist/components/toggle-legend.d.ts +53 -3
  184. package/dist/components/toggle-legend.js +54 -1
  185. package/dist/components/toggle-legend.js.map +1 -1
  186. package/dist/components/tooltip.d.ts +51 -1
  187. package/dist/components/tooltip.js +59 -35
  188. package/dist/components/tooltip.js.map +1 -1
  189. package/dist/components/ui.d.ts +51 -779
  190. package/dist/components/ui.js +364 -34
  191. package/dist/components/ui.js.map +1 -1
  192. package/dist/components/use-table-state.d.ts +3 -52
  193. package/dist/components/use-table-state.js +128 -30
  194. package/dist/components/use-table-state.js.map +1 -1
  195. package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
  196. package/dist/data-table.d.ts +2 -1
  197. package/dist/data-table.js +5 -1
  198. package/dist/data-table.js.map +1 -1
  199. package/dist/feedback/feedback-attachment.js +4 -6
  200. package/dist/feedback/feedback-attachment.js.map +1 -1
  201. package/dist/feedback/feedback-dialog.d.ts +26 -3
  202. package/dist/feedback/feedback-dialog.js +21 -5
  203. package/dist/feedback/feedback-dialog.js.map +1 -1
  204. package/dist/feedback/feedback-inbox.js +86 -75
  205. package/dist/feedback/feedback-inbox.js.map +1 -1
  206. package/dist/feedback/feedback-thread.d.ts +173 -0
  207. package/dist/feedback/feedback-thread.js +264 -0
  208. package/dist/feedback/feedback-thread.js.map +1 -0
  209. package/dist/feedback.d.ts +2 -1
  210. package/dist/feedback.js +1 -0
  211. package/dist/feedback.js.map +1 -1
  212. package/dist/hooks/use-authed-src.d.ts +71 -0
  213. package/dist/hooks/use-authed-src.js +63 -0
  214. package/dist/hooks/use-authed-src.js.map +1 -0
  215. package/dist/hooks/use-file-drop.d.ts +49 -2
  216. package/dist/hooks/use-hotkey.d.ts +59 -0
  217. package/dist/hooks/use-hotkey.js +105 -0
  218. package/dist/hooks/use-hotkey.js.map +1 -0
  219. package/dist/hooks/use-search-param-state.d.ts +83 -0
  220. package/dist/hooks/use-search-param-state.js +94 -0
  221. package/dist/hooks/use-search-param-state.js.map +1 -0
  222. package/dist/i18n/defaults.d.ts +15 -4
  223. package/dist/i18n/defaults.js +31 -1
  224. package/dist/i18n/defaults.js.map +1 -1
  225. package/dist/i18n/kit-labels.d.ts +46 -312
  226. package/dist/i18n/kit-labels.js +20 -5
  227. package/dist/i18n/kit-labels.js.map +1 -1
  228. package/dist/i18n/locales/de-CH-informal.d.ts +15 -4
  229. package/dist/i18n/locales/de-CH.d.ts +15 -4
  230. package/dist/i18n/locales/de-informal.d.ts +15 -4
  231. package/dist/i18n/locales/de.d.ts +15 -4
  232. package/dist/i18n/locales/de.js +164 -3
  233. package/dist/i18n/locales/de.js.map +1 -1
  234. package/dist/i18n/locales/es.d.ts +15 -4
  235. package/dist/i18n/locales/es.js +161 -3
  236. package/dist/i18n/locales/es.js.map +1 -1
  237. package/dist/i18n/locales/fr.d.ts +15 -4
  238. package/dist/i18n/locales/fr.js +161 -3
  239. package/dist/i18n/locales/fr.js.map +1 -1
  240. package/dist/i18n/locales/hu.d.ts +15 -4
  241. package/dist/i18n/locales/hu.js +161 -3
  242. package/dist/i18n/locales/hu.js.map +1 -1
  243. package/dist/i18n/locales/it.d.ts +15 -4
  244. package/dist/i18n/locales/it.js +161 -3
  245. package/dist/i18n/locales/it.js.map +1 -1
  246. package/dist/i18n/locales/zh.d.ts +15 -4
  247. package/dist/i18n/locales/zh.js +161 -3
  248. package/dist/i18n/locales/zh.js.map +1 -1
  249. package/dist/index.d.ts +39 -19
  250. package/dist/index.js +40 -1
  251. package/dist/index.js.map +1 -1
  252. package/dist/kit-labels-Mz8zZHIF.d.ts +1744 -0
  253. package/dist/lib/format.d.ts +162 -0
  254. package/dist/lib/format.js +151 -0
  255. package/dist/lib/format.js.map +1 -0
  256. package/dist/lib/qr-encode.d.ts +44 -0
  257. package/dist/lib/qr-encode.js +338 -0
  258. package/dist/lib/qr-encode.js.map +1 -0
  259. package/dist/rhf/fields.d.ts +283 -0
  260. package/dist/rhf/fields.js +631 -0
  261. package/dist/rhf/fields.js.map +1 -0
  262. package/dist/rhf/form.d.ts +49 -1
  263. package/dist/rhf/line-items.d.ts +31 -0
  264. package/dist/rhf/line-items.js +59 -0
  265. package/dist/rhf/line-items.js.map +1 -0
  266. package/dist/rhf.d.ts +61 -1
  267. package/dist/rhf.js +2 -0
  268. package/dist/rhf.js.map +1 -1
  269. package/dist/search/command-palette.js +20 -14
  270. package/dist/search/command-palette.js.map +1 -1
  271. package/dist/shell/app-shell.d.ts +99 -2
  272. package/dist/shell/app-shell.js +120 -57
  273. package/dist/shell/app-shell.js.map +1 -1
  274. package/dist/shell/auth-layout.d.ts +74 -0
  275. package/dist/shell/auth-layout.js +97 -0
  276. package/dist/shell/auth-layout.js.map +1 -0
  277. package/dist/shell/top-bar-brand.d.ts +81 -0
  278. package/dist/shell/top-bar-brand.js +47 -0
  279. package/dist/shell/top-bar-brand.js.map +1 -0
  280. package/dist/shell/topbar-action-menu.d.ts +21 -4
  281. package/dist/shell/topbar-action-menu.js +18 -2
  282. package/dist/shell/topbar-action-menu.js.map +1 -1
  283. package/dist/shell.d.ts +56 -1
  284. package/dist/shell.js +2 -0
  285. package/dist/shell.js.map +1 -1
  286. package/dist/tour/tour.js +10 -4
  287. package/dist/tour/tour.js.map +1 -1
  288. package/dist/wizard/stepper-nav.d.ts +48 -1
  289. package/dist/wizard/use-wizard.js +4 -2
  290. package/dist/wizard/use-wizard.js.map +1 -1
  291. package/dist/wizard/wizard-context.d.ts +24 -1
  292. package/dist/wizard/wizard-context.js +19 -2
  293. package/dist/wizard/wizard-context.js.map +1 -1
  294. package/dist/wizard/wizard-step.d.ts +30 -5
  295. package/dist/wizard/wizard-step.js +34 -3
  296. package/dist/wizard/wizard-step.js.map +1 -1
  297. package/dist/wizard.d.ts +50 -3
  298. package/package.json +6 -6
  299. package/src/chart.ts +4 -0
  300. package/src/components/account-settings-labels.ts +190 -0
  301. package/src/components/account-settings.tsx +110 -63
  302. package/src/components/amount-input.tsx +109 -10
  303. package/src/components/authed-image.tsx +190 -0
  304. package/src/components/breadcrumbs.tsx +8 -2
  305. package/src/components/bulk-action-bar.tsx +16 -3
  306. package/src/components/button-group.tsx +7 -2
  307. package/src/components/calendar-heatmap.tsx +105 -10
  308. package/src/components/chart.tsx +19 -8
  309. package/src/components/chip.tsx +14 -4
  310. package/src/components/choice-card.tsx +136 -25
  311. package/src/components/combobox-core.tsx +33 -9
  312. package/src/components/combobox.tsx +33 -9
  313. package/src/components/copy-button.tsx +3 -2
  314. package/src/components/data-table-cells.tsx +119 -0
  315. package/src/components/data-table-filter-popover.tsx +13 -5
  316. package/src/components/data-table-filters.ts +77 -0
  317. package/src/components/data-table-labels.ts +23 -0
  318. package/src/components/data-table.tsx +230 -13
  319. package/src/components/date-picker.tsx +19 -3
  320. package/src/components/description-list.tsx +52 -3
  321. package/src/components/entity-combobox.tsx +25 -7
  322. package/src/components/error-boundary.tsx +640 -0
  323. package/src/components/field-sync.tsx +48 -40
  324. package/src/components/field.tsx +56 -5
  325. package/src/components/floating-panel.tsx +20 -7
  326. package/src/components/form-actions.tsx +191 -0
  327. package/src/components/full-bleed-dialog.tsx +4 -0
  328. package/src/components/hover-menu.tsx +26 -17
  329. package/src/components/image-grid.tsx +181 -0
  330. package/src/components/lightbox.tsx +374 -0
  331. package/src/components/line-items.tsx +385 -0
  332. package/src/components/list.tsx +82 -19
  333. package/src/components/loading-state.tsx +47 -0
  334. package/src/components/menu-item.tsx +7 -2
  335. package/src/components/mini-calendar.tsx +72 -3
  336. package/src/components/modal.tsx +65 -3
  337. package/src/components/month-picker.tsx +181 -26
  338. package/src/components/multi-entity-combobox.tsx +27 -7
  339. package/src/components/nav-pills.tsx +8 -1
  340. package/src/components/number-field.tsx +15 -10
  341. package/src/components/number-input.tsx +17 -1
  342. package/src/components/numpad-sheet.tsx +6 -1
  343. package/src/components/page-header.tsx +42 -5
  344. package/src/components/passkeys-setting.tsx +331 -0
  345. package/src/components/picker-sheet.tsx +10 -1
  346. package/src/components/pie-chart-labels.ts +32 -0
  347. package/src/components/pie-chart.tsx +479 -0
  348. package/src/components/popover.tsx +3 -0
  349. package/src/components/progress-bar.tsx +129 -19
  350. package/src/components/qr-code.tsx +83 -0
  351. package/src/components/search-field.tsx +20 -4
  352. package/src/components/signed-amount.tsx +295 -0
  353. package/src/components/skeleton.tsx +21 -3
  354. package/src/components/stat-tile.tsx +11 -3
  355. package/src/components/swipeable-row.tsx +7 -7
  356. package/src/components/table.tsx +102 -13
  357. package/src/components/text-link.tsx +356 -0
  358. package/src/components/text.tsx +46 -2
  359. package/src/components/toggle-group.tsx +93 -20
  360. package/src/components/toggle-legend.tsx +108 -2
  361. package/src/components/tooltip.tsx +79 -8
  362. package/src/components/ui.tsx +707 -49
  363. package/src/components/use-table-state.ts +265 -33
  364. package/src/data-table.ts +10 -0
  365. package/src/feedback/feedback-attachment.tsx +10 -7
  366. package/src/feedback/feedback-dialog.tsx +49 -7
  367. package/src/feedback/feedback-inbox.tsx +7 -0
  368. package/src/feedback/feedback-thread.tsx +510 -0
  369. package/src/feedback.ts +1 -0
  370. package/src/hooks/use-authed-src.ts +161 -0
  371. package/src/hooks/use-hotkey.ts +172 -0
  372. package/src/hooks/use-search-param-state.ts +187 -0
  373. package/src/i18n/defaults.ts +30 -0
  374. package/src/i18n/kit-labels.tsx +118 -8
  375. package/src/i18n/locales/de.ts +161 -0
  376. package/src/i18n/locales/es.ts +158 -0
  377. package/src/i18n/locales/fr.ts +161 -0
  378. package/src/i18n/locales/hu.ts +159 -0
  379. package/src/i18n/locales/it.ts +159 -0
  380. package/src/i18n/locales/zh.ts +158 -0
  381. package/src/index.ts +59 -0
  382. package/src/lib/format.ts +348 -0
  383. package/src/lib/qr-encode.ts +420 -0
  384. package/src/rhf/fields.tsx +1135 -0
  385. package/src/rhf/line-items.tsx +130 -0
  386. package/src/rhf.ts +6 -0
  387. package/src/search/command-palette.tsx +29 -19
  388. package/src/shell/app-shell.tsx +153 -3
  389. package/src/shell/auth-layout.tsx +190 -0
  390. package/src/shell/top-bar-brand.tsx +76 -0
  391. package/src/shell/topbar-action-menu.tsx +32 -4
  392. package/src/shell.ts +2 -0
  393. package/src/tour/tour.tsx +16 -5
  394. package/src/wizard/use-wizard.ts +8 -4
  395. package/src/wizard/wizard-context.tsx +41 -1
  396. package/src/wizard/wizard-step.tsx +75 -6
  397. package/tokens.css +12 -0
@@ -57,6 +57,15 @@ interface NumberInputProps {
57
57
  hint?: ReactNode;
58
58
  /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */
59
59
  invalid?: boolean;
60
+ /**
61
+ * Set on the `<input>`, with `id` above. Declared so `Field`'s render-prop spread —
62
+ * `{(ids) => <NumberInput {...ids} … />}` — is typed and reaches the input: the hint
63
+ * and error are described, a required number is announced as required, and
64
+ * `aria-invalid` paints {@link FIELD_INVALID} the way `invalid` does.
65
+ */
66
+ "aria-describedby"?: string;
67
+ "aria-invalid"?: boolean | "true" | "false";
68
+ "aria-required"?: boolean | "true" | "false";
60
69
  /**
61
70
  * A static unit shown at the field's right edge — "%", "kg", "km/h".
62
71
  *
@@ -156,7 +165,10 @@ export function NumberInput({
156
165
  calculator = true,
157
166
  variant = "field",
158
167
  hint,
159
- invalid,
168
+ invalid: invalidProp,
169
+ "aria-describedby": ariaDescribedBy,
170
+ "aria-invalid": ariaInvalid,
171
+ "aria-required": ariaRequired,
160
172
  suffix,
161
173
  step,
162
174
  min,
@@ -164,6 +176,7 @@ export function NumberInput({
164
176
  }: NumberInputProps) {
165
177
  const generatedId = useId();
166
178
  const fieldId = id ?? generatedId;
179
+ const invalid = Boolean(invalidProp) || ariaInvalid === true || ariaInvalid === "true";
167
180
  const labelled = label !== undefined;
168
181
  // On phones we suppress the OS keyboard (inputMode="none" below) for our own
169
182
  // calculator numpad, so the desktop popover trigger is hidden (feedback #334).
@@ -219,6 +232,7 @@ export function NumberInput({
219
232
  // keeps focus/caret); desktop keeps the native decimal keypad.
220
233
  inputMode={isMobile ? "none" : "decimal"}
221
234
  autoComplete="off"
235
+ // eslint-disable-next-line jsx-a11y/no-autofocus -- a documented prop the caller opts into (off by default); the field never takes focus on its own.
222
236
  autoFocus={autoFocus}
223
237
  disabled={disabled}
224
238
  // Display mode has no floating label to feed the blank-placeholder trick,
@@ -234,6 +248,8 @@ export function NumberInput({
234
248
  }}
235
249
  onKeyDown={onKeyDown}
236
250
  aria-invalid={invalid || undefined}
251
+ aria-describedby={ariaDescribedBy}
252
+ aria-required={ariaRequired}
237
253
  // The end padding sits last so it always wins over an inputClassName that
238
254
  // sets its own px. Both trailing controls can be on at once, so the room
239
255
  // they need is reserved together rather than by whichever happens to render:
@@ -99,11 +99,16 @@ export function NumberPadSheet({
99
99
  onDone,
100
100
  label,
101
101
  labels,
102
+ decimalMark = ".",
102
103
  }: {
103
104
  value: string;
104
105
  /** Fired with the raw (sanitised) field text on every key — same contract as
105
106
  * the host field's own `onChange`. */
106
107
  onChange: (value: string) => void;
108
+ /** The glyph on the decimal key — "," where the host shows a comma (AmountInput
109
+ * in fr-CH, kastlan 40). The key still inserts a dot; the host's text is dot-form
110
+ * underneath and `sanitizeLive` reads either. */
111
+ decimalMark?: "." | ",";
107
112
  /** Fired on "Done": the host blurs the input, which commits (evaluates) and
108
113
  * unmounts the sheet via its existing blur handler. */
109
114
  onDone: () => void;
@@ -231,7 +236,7 @@ export function NumberPadSheet({
231
236
  onClick={() => insert(key.ins)}
232
237
  className={cn(PAD_BTN, key.accent ? PAD_ACCENT : PAD_DIGIT)}
233
238
  >
234
- {key.label}
239
+ {key.kind === "ins" && key.name === "decimal" ? decimalMark : key.label}
235
240
  </button>
236
241
  ),
237
242
  )}
@@ -2,21 +2,44 @@ import type { ComponentPropsWithoutRef, ElementType, ReactNode } from "react";
2
2
  import { cn } from "../lib/cn";
3
3
  import { SECTION_LABEL_CLASS } from "./text";
4
4
 
5
- /** `md` (default) is kastlan's `text-2xl` page title from `sm` up, a step smaller on a
6
- * phone; `sm` is lenkbank's `text-lg` title on a dense tool page (projects-page.tsx). */
7
- export type PageHeaderSize = "sm" | "md";
5
+ /**
6
+ * `md` (default) is kastlan's `text-2xl` page title from `sm` up, a step smaller on a
7
+ * phone; `sm` is lenkbank's `text-lg` title on a dense tool page (projects-page.tsx).
8
+ *
9
+ * `compact` (0.13) is keksdose's page title (keksdose F4): `text-xl font-semibold` at
10
+ * EVERY width — budgets, rules, accounts, transactions and budget pages all write
11
+ * exactly that `<h1>` by hand. It is `md`'s phone size without `md`'s step up to
12
+ * `text-2xl` on a wider screen, and without its `tracking-tight`, which keksdose's
13
+ * titles never had. A word and not a letter for the reason `ProgressBar`'s `slim` is
14
+ * one: the letters are taken in order, and there is none between `sm` and `md` — an
15
+ * `lg` or `xl` at `text-xl` would read as LARGER than `md`, which from `sm` up it is
16
+ * not.
17
+ */
18
+ export type PageHeaderSize = "sm" | "compact" | "md";
8
19
 
9
20
  const TITLE: Record<PageHeaderSize, string> = {
10
21
  sm: "text-lg font-semibold",
22
+ compact: "text-xl font-semibold",
11
23
  md: "text-xl font-semibold tracking-tight sm:text-2xl",
12
24
  };
13
25
 
26
+ /**
27
+ * How the title block and the actions sit below the `sm` breakpoint. From `sm` up both
28
+ * layouts are one row: title block at the start, actions at the end.
29
+ */
30
+ export type PageHeaderMobileLayout = "stacked" | "inline";
31
+
32
+ const ROW: Record<PageHeaderMobileLayout, string> = {
33
+ stacked: "flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4",
34
+ inline: "flex-row items-center justify-between gap-2 sm:gap-4",
35
+ };
36
+
14
37
  export interface PageHeaderProps extends Omit<ComponentPropsWithoutRef<"div">, "title"> {
15
38
  title: ReactNode;
16
39
  /** A sentence under the title. */
17
40
  description?: ReactNode;
18
41
  /** Buttons for the page — "New", "Export". At the title's end from `sm` up; under it,
19
- * wrapping, on a phone. */
42
+ * wrapping, on a phone — unless {@link mobileLayout} is `inline`. */
20
43
  actions?: ReactNode;
21
44
  /** A `Breadcrumbs` trail, drawn above everything else. */
22
45
  breadcrumbs?: ReactNode;
@@ -27,6 +50,19 @@ export interface PageHeaderProps extends Omit<ComponentPropsWithoutRef<"div">, "
27
50
  * header inside a page that already has its `h1` (a tab's own header). */
28
51
  as?: "h1" | "h2" | "h3";
29
52
  size?: PageHeaderSize;
53
+ /**
54
+ * `stacked` (default): on a phone the actions drop under the title and wrap, so a
55
+ * row of three buttons never pushes the title off the screen.
56
+ *
57
+ * `inline`: the actions stay beside the title on a phone too, vertically centred on
58
+ * it — keksdose's headers (transactions-page.tsx:515, `flex items-center
59
+ * justify-between gap-2`), which carry one or two icon-sized controls and were tuned
60
+ * for the phone in live #263. Adopting `PageHeader` must not undo that tuning, so the
61
+ * row stays a row (keksdose F4). The actions do NOT wrap or shrink here: `inline` is
62
+ * for a header whose actions are known to fit beside a phone-width title (an icon
63
+ * button, a month picker); a long title wraps to make room for them instead.
64
+ */
65
+ mobileLayout?: PageHeaderMobileLayout;
30
66
  }
31
67
 
32
68
  /**
@@ -48,6 +84,7 @@ export function PageHeader({
48
84
  eyebrow,
49
85
  as = "h1",
50
86
  size = "md",
87
+ mobileLayout = "stacked",
51
88
  className,
52
89
  ...rest
53
90
  }: PageHeaderProps) {
@@ -55,7 +92,7 @@ export function PageHeader({
55
92
  return (
56
93
  <div {...rest} className={cn("flex min-w-0 flex-col gap-2", className)}>
57
94
  {breadcrumbs}
58
- <div className="flex min-w-0 flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-4">
95
+ <div className={cn("flex min-w-0", ROW[mobileLayout])}>
59
96
  <div className="min-w-0 flex-1">
60
97
  {eyebrow != null && <p className={cn(SECTION_LABEL_CLASS.xs, "mb-1")}>{eyebrow}</p>}
61
98
  <Heading className={cn("break-words text-[var(--text-primary)]", TITLE[size])}>{title}</Heading>
@@ -0,0 +1,331 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import type { KeyboardEvent, ReactNode } from "react";
3
+ import { KeyRound } from "lucide-react";
4
+ import { cn } from "../lib/cn";
5
+ import { useKitLocale } from "../i18n/kit-labels";
6
+ import { Button, Card, EmptyState, Input } from "./ui";
7
+ import { Skeleton } from "./skeleton";
8
+ import { useAccountSettingsLabels } from "./account-settings-labels";
9
+ import type { PasskeysSettingLabels } from "./account-settings-labels";
10
+
11
+ /** One registered passkey, as the app's API lists it. */
12
+ export interface PasskeyItem {
13
+ id: string | number;
14
+ name: string;
15
+ /** A `Date`, an ISO string or epoch ms. */
16
+ createdAt?: Date | string | number | null;
17
+ /** Null (or left out) reads as "Never used". */
18
+ lastUsedAt?: Date | string | number | null;
19
+ }
20
+
21
+ export interface PasskeysSettingProps {
22
+ /** `undefined` while the first load is in flight (or pass `loading`). */
23
+ passkeys: readonly PasskeyItem[] | undefined;
24
+ loading?: boolean;
25
+ /**
26
+ * Run the WebAuthn ceremony and register the passkey — app-side, since the options
27
+ * and the attestation are the app's server's business. Called with the trimmed name
28
+ * field, which may be EMPTY: pick your own default ("Passkey", the device name).
29
+ * Return the promise and the field clears when it resolves; a rejection (including
30
+ * the user cancelling the OS prompt) keeps what was typed.
31
+ */
32
+ onAdd: (name: string) => void | Promise<unknown>;
33
+ /** The ceremony is running. The add button says so and does not start a second one. */
34
+ adding?: boolean;
35
+ /** Offer "Rename" on each row. Return a promise to leave edit mode only on success. */
36
+ onRename?: (id: PasskeyItem["id"], name: string) => void | Promise<unknown>;
37
+ /** Offer "Delete" on each row — always behind an inline confirmation. */
38
+ onDelete?: (id: PasskeyItem["id"]) => void | Promise<unknown>;
39
+ /** A row whose rename or delete is in flight; its buttons are disabled. */
40
+ busyId?: PasskeyItem["id"] | null;
41
+ /** Show the name field above the add button. Default true. */
42
+ nameField?: boolean;
43
+ /**
44
+ * Shown INSTEAD of the list and the add button — passkeys are not supported by this
45
+ * browser, or the page is not a secure context. Its wording is the app's: only the
46
+ * app knows which of those it detected.
47
+ */
48
+ unavailable?: ReactNode;
49
+ /** A date for the "Added …" / "Last used …" lines. Default: the locale's medium date. */
50
+ formatDate?: (date: Date) => string;
51
+ /** Overrides the provider's locale for the default date format. */
52
+ locale?: string;
53
+ /** Prop > `<UiKitProvider labels={{ accountSettings: { passkeys } }}>` > English. */
54
+ labels?: Partial<PasskeysSettingLabels>;
55
+ className?: string;
56
+ }
57
+
58
+ /**
59
+ * The passkeys section of an account page: what is registered, when it was last used,
60
+ * rename and delete, and "Add passkey" — the fourth card beside `ProfileSetting`,
61
+ * `PasswordSetting` and `TwoFactorSetting`, and built the same way (a self-contained
62
+ * `<Card>` owning its transient form state; the app owns the data and the API).
63
+ *
64
+ * Lifted from kastlan's passkeys-card. **The WebAuthn ceremony stays app-side**:
65
+ * `navigator.credentials.create` needs options minted by the app's server and hands
66
+ * back an attestation only that server can verify, so the kit calls `onAdd` and waits.
67
+ *
68
+ * **Delete confirms inline**, in the row, rather than through `useConfirm`: this card
69
+ * must work in an app without a `ConfirmProvider`, and the question belongs next to
70
+ * the passkey it names. Focus moves to Cancel, as in every destructive confirm in the
71
+ * kit, so the Enter that pressed Delete cannot also answer it.
72
+ */
73
+ export function PasskeysSetting({
74
+ passkeys,
75
+ loading,
76
+ onAdd,
77
+ adding = false,
78
+ onRename,
79
+ onDelete,
80
+ busyId,
81
+ nameField = true,
82
+ unavailable,
83
+ formatDate,
84
+ locale: localeProp,
85
+ labels: labelsProp,
86
+ className,
87
+ }: PasskeysSettingProps) {
88
+ const labels = useAccountSettingsLabels("passkeys", labelsProp);
89
+ const locale = useKitLocale(localeProp);
90
+ const [name, setName] = useState("");
91
+
92
+ const date = (value: PasskeyItem["createdAt"]): string | null => {
93
+ if (value === null || value === undefined || value === "") return null;
94
+ const d = value instanceof Date ? value : new Date(value);
95
+ if (Number.isNaN(d.getTime())) return null;
96
+ return formatDate ? formatDate(d) : new Intl.DateTimeFormat(locale, { dateStyle: "medium" }).format(d);
97
+ };
98
+
99
+ const add = () => {
100
+ if (adding) return;
101
+ const typed = name;
102
+ Promise.resolve(onAdd(typed.trim()))
103
+ // Clear only what was sent: the field stays editable while the OS prompt is up.
104
+ .then(() => setName((current) => (current === typed ? "" : current)))
105
+ .catch(() => {});
106
+ };
107
+
108
+ const isLoading = loading || passkeys === undefined;
109
+ const items = passkeys ?? [];
110
+
111
+ const body = (() => {
112
+ if (unavailable != null && unavailable !== false) {
113
+ return <EmptyState variant="inline" size="sm" icon={<KeyRound />} title={unavailable} />;
114
+ }
115
+ return (
116
+ <>
117
+ {isLoading ? (
118
+ <Skeleton lines={2} label={labels.loading} />
119
+ ) : items.length === 0 ? (
120
+ <EmptyState variant="inline" size="sm" icon={<KeyRound />} title={labels.empty} />
121
+ ) : (
122
+ <ul aria-label={labels.list} className="divide-y divide-[var(--border)]">
123
+ {items.map((item) => (
124
+ <PasskeyRow
125
+ key={item.id}
126
+ item={item}
127
+ created={date(item.createdAt)}
128
+ lastUsed={date(item.lastUsedAt)}
129
+ busy={busyId !== undefined && busyId !== null && busyId === item.id}
130
+ onRename={onRename}
131
+ onDelete={onDelete}
132
+ labels={labels}
133
+ />
134
+ ))}
135
+ </ul>
136
+ )}
137
+ <div className="space-y-2 pt-1">
138
+ {nameField && (
139
+ <Input
140
+ label={labels.nameLabel}
141
+ placeholder={labels.namePlaceholder}
142
+ value={name}
143
+ maxLength={120}
144
+ onChange={(e) => setName(e.target.value)}
145
+ onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
146
+ if (e.key === "Enter") {
147
+ e.preventDefault();
148
+ add();
149
+ }
150
+ }}
151
+ />
152
+ )}
153
+ <Button onClick={add} disabled={adding} aria-busy={adding || undefined}>
154
+ <KeyRound className="size-4" aria-hidden />
155
+ {adding ? labels.adding : labels.add}
156
+ </Button>
157
+ </div>
158
+ </>
159
+ );
160
+ })();
161
+
162
+ return (
163
+ <Card className={cn("p-4 space-y-3", className)}>
164
+ <div>
165
+ <div className="text-sm font-medium">{labels.title}</div>
166
+ <div className="text-xs text-[var(--text-muted)]">
167
+ {labels.description}
168
+ </div>
169
+ </div>
170
+ {body}
171
+ </Card>
172
+ );
173
+ }
174
+
175
+ type RowMode = "view" | "rename" | "delete";
176
+
177
+ function PasskeyRow({
178
+ item,
179
+ created,
180
+ lastUsed,
181
+ busy,
182
+ onRename,
183
+ onDelete,
184
+ labels,
185
+ }: {
186
+ item: PasskeyItem;
187
+ created: string | null;
188
+ lastUsed: string | null;
189
+ busy: boolean;
190
+ onRename?: PasskeysSettingProps["onRename"];
191
+ onDelete?: PasskeysSettingProps["onDelete"];
192
+ labels: PasskeysSettingLabels;
193
+ }) {
194
+ const [mode, setMode] = useState<RowMode>("view");
195
+ const [draft, setDraft] = useState(item.name);
196
+ // Which button opened the sub-mode, so leaving it puts focus back there — the
197
+ // confirm or the field unmounts under the focus, which otherwise falls to <body>.
198
+ const [returnTo, setReturnTo] = useState<"rename" | "delete" | null>(null);
199
+ const cancelDeleteRef = useRef<HTMLButtonElement>(null);
200
+ const renameRef = useRef<HTMLInputElement>(null);
201
+ const renameButtonRef = useRef<HTMLButtonElement>(null);
202
+ const deleteButtonRef = useRef<HTMLButtonElement>(null);
203
+
204
+ useEffect(() => {
205
+ if (mode === "delete") cancelDeleteRef.current?.focus();
206
+ else if (mode === "rename") renameRef.current?.select();
207
+ else if (returnTo) (returnTo === "rename" ? renameButtonRef : deleteButtonRef).current?.focus();
208
+ }, [mode, returnTo]);
209
+
210
+ const open = (next: "rename" | "delete") => {
211
+ setReturnTo(next);
212
+ setDraft(item.name);
213
+ setMode(next);
214
+ };
215
+ const leave = () => setMode("view");
216
+ const escapeLeaves = (e: KeyboardEvent<HTMLButtonElement>) => {
217
+ if (e.key === "Escape") {
218
+ e.preventDefault();
219
+ e.stopPropagation();
220
+ leave();
221
+ }
222
+ };
223
+
224
+ const saveRename = () => {
225
+ const next = draft.trim();
226
+ if (!onRename || !next || busy) return;
227
+ if (next === item.name) {
228
+ leave();
229
+ return;
230
+ }
231
+ Promise.resolve(onRename(item.id, next))
232
+ .then(leave)
233
+ .catch(() => {});
234
+ };
235
+
236
+ const confirmDelete = () => {
237
+ if (!onDelete || busy) return;
238
+ // The row unmounts once the list refreshes; on a failure it stays, confirm open.
239
+ Promise.resolve(onDelete(item.id))
240
+ .then(() => setMode("view"))
241
+ .catch(() => {});
242
+ };
243
+
244
+ const meta = [created && labels.created(created), lastUsed ? labels.lastUsed(lastUsed) : labels.neverUsed]
245
+ .filter(Boolean)
246
+ .join(" · ");
247
+
248
+ if (mode === "rename") {
249
+ return (
250
+ <li className="flex flex-wrap items-center gap-2 py-2">
251
+ <Input
252
+ ref={renameRef}
253
+ aria-label={labels.renameField(item.name)}
254
+ className="min-w-0 flex-1"
255
+ value={draft}
256
+ maxLength={120}
257
+ onChange={(e) => setDraft(e.target.value)}
258
+ onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
259
+ if (e.key === "Enter") {
260
+ e.preventDefault();
261
+ saveRename();
262
+ } else if (e.key === "Escape") {
263
+ e.preventDefault();
264
+ e.stopPropagation();
265
+ leave();
266
+ }
267
+ }}
268
+ />
269
+ <Button size="sm" onClick={saveRename} disabled={busy || !draft.trim()}>
270
+ {labels.save}
271
+ </Button>
272
+ <Button size="sm" variant="ghost" onClick={leave}>
273
+ {labels.cancel}
274
+ </Button>
275
+ </li>
276
+ );
277
+ }
278
+
279
+ if (mode === "delete") {
280
+ return (
281
+ <li className="space-y-2 py-2">
282
+ <p role="alert" className="text-sm">
283
+ {labels.deleteConfirm(item.name)}
284
+ </p>
285
+ <div className="flex flex-wrap gap-2">
286
+ <Button size="sm" variant="danger" onClick={confirmDelete} onKeyDown={escapeLeaves} disabled={busy}>
287
+ {labels.confirmDelete}
288
+ </Button>
289
+ <Button ref={cancelDeleteRef} size="sm" variant="ghost" onClick={leave} onKeyDown={escapeLeaves}>
290
+ {labels.cancel}
291
+ </Button>
292
+ </div>
293
+ </li>
294
+ );
295
+ }
296
+
297
+ return (
298
+ <li className="flex items-center gap-3 py-2">
299
+ <KeyRound className="size-4 shrink-0 text-[var(--text-muted)]" aria-hidden />
300
+ <div className="min-w-0 flex-1">
301
+ <div className="truncate text-sm font-medium">{item.name}</div>
302
+ <div className="text-xs text-[var(--text-muted)]">{meta}</div>
303
+ </div>
304
+ {onRename && (
305
+ <Button
306
+ ref={renameButtonRef}
307
+ size="sm"
308
+ variant="ghost"
309
+ disabled={busy}
310
+ onClick={() => open("rename")}
311
+ aria-label={labels.renameItem(item.name)}
312
+ >
313
+ {labels.rename}
314
+ </Button>
315
+ )}
316
+ {onDelete && (
317
+ <Button
318
+ ref={deleteButtonRef}
319
+ size="sm"
320
+ variant="ghost"
321
+ tone="danger"
322
+ disabled={busy}
323
+ onClick={() => open("delete")}
324
+ aria-label={labels.deleteItem(item.name)}
325
+ >
326
+ {labels.delete}
327
+ </Button>
328
+ )}
329
+ </li>
330
+ );
331
+ }
@@ -3,6 +3,7 @@ import { createPortal } from "react-dom";
3
3
  import { X } from "lucide-react";
4
4
  import type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, RefObject } from "react";
5
5
  import { cn } from "../lib/cn";
6
+ import { CLIPS_ATTRIBUTE } from "../lib/clipping";
6
7
  import { DropdownSearchHeader } from "./dropdown";
7
8
  import { useBodyScrollLock } from "../hooks/use-body-scroll-lock";
8
9
  import { useOverlayHistory } from "../hooks/use-overlay-history";
@@ -153,6 +154,9 @@ export function PickerSheet({
153
154
 
154
155
  if (!open || typeof document === "undefined") return null;
155
156
  return createPortal(
157
+ /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- the dialog
158
+ container, per the APG dialog pattern: `onKeyDown` is Escape-to-close for whatever
159
+ inside it holds focus, and `onMouseDown` only stops propagation (live #328). */
156
160
  <div
157
161
  // Spread FIRST, and every attribute below it deliberately wins: `style`,
158
162
  // `onMouseDown` and `onKeyDown` here are not styling choices, they are live
@@ -237,7 +241,12 @@ export function PickerSheet({
237
241
  />
238
242
  {/* min-h-0 so the LIST scrolls rather than the dialog growing past the
239
243
  viewport — a flex child defaults to min-height:auto. */}
240
- <div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">{children}</div>
244
+ {/* Marked as the scroller it is (`CLIPS_ATTRIBUTE`), so a Tooltip in a row —
245
+ `Combobox`'s `optionAdornment` — portals out under jsdom as it does in the
246
+ browser rather than joining the option's name (keksdose E8). */}
247
+ <div {...{ [CLIPS_ATTRIBUTE]: "" }} className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
248
+ {children}
249
+ </div>
241
250
  </div>,
242
251
  document.body,
243
252
  );
@@ -0,0 +1,32 @@
1
+ // All user-facing strings for `PieChart`, as the `pieChart` namespace of
2
+ // `<UiKitProvider labels>`, and per chart the `labels` prop over that.
3
+ //
4
+ // A module of its own, with no recharts in it, for the reason `series-chart-labels`
5
+ // is one: the label registry (src/i18n) imports the defaults, and must not pull the
6
+ // chart library in behind them.
7
+
8
+ export interface PieChartLabels {
9
+ /** Accessible name of the group of keyboard stops laid over the slices — one tab
10
+ * stop, arrow keys between them. */
11
+ slices: string;
12
+ /**
13
+ * One slice's accessible name. `value` arrives through `formatValue`, `percent` as
14
+ * the slice's share of what is drawn, already formatted in the locale ("42 %" in
15
+ * German, "42%" in English) — the function only arranges them.
16
+ */
17
+ slice: (label: string, value: string, percent: string) => string;
18
+ /** The caption over the donut's centre figure. */
19
+ total: string;
20
+ /** What a chart with nothing to draw says, when the caller passes no `empty`. */
21
+ empty: string;
22
+ /** Accessible name of the chart's legend (a group of switches, one per slice). */
23
+ legend: string;
24
+ }
25
+
26
+ export const DEFAULT_PIE_CHART_LABELS: PieChartLabels = {
27
+ slices: "Chart slices",
28
+ slice: (label, value, percent) => `${label}: ${value} (${percent})`,
29
+ total: "Total",
30
+ empty: "No data",
31
+ legend: "Categories",
32
+ };