@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
@@ -3,6 +3,19 @@ import type { ComponentPropsWithoutRef, ReactNode } from "react";
3
3
  import { cn } from "../lib/cn";
4
4
  import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from "../i18n/kit-labels";
5
5
 
6
+ /** The words a progress bar adds of its own. */
7
+ export interface ProgressBarLabels {
8
+ /** Shown after the value when `max={null}`: a quota with no ceiling. */
9
+ unlimited: string;
10
+ /** The `overage` line: how far past `max` the value is. `amount` is formatted. */
11
+ overLimit: (amount: string) => string;
12
+ }
13
+
14
+ export const DEFAULT_PROGRESS_BAR_LABELS: ProgressBarLabels = {
15
+ unlimited: "Unlimited",
16
+ overLimit: (amount) => `${amount} over the limit`,
17
+ };
18
+
6
19
  /**
7
20
  * `income` / `expense` are the money pair (`--money-income` / `--money-expense`), the
8
21
  * names `Chip`, `StatTile` and `Sparkline` already use for them: a share bar of what
@@ -82,6 +95,13 @@ export interface ProgressBarSegment {
82
95
  label?: ReactNode;
83
96
  /** A stable React key, when `label` is not a string. */
84
97
  key?: string;
98
+ /**
99
+ * List it in the `legend` but leave it out of the bar — its value, its share of the
100
+ * `aria-valuetext`, and the sum. keksdose's cut card (reports/cut-card:132) names
101
+ * EVERY bucket under its bar, the empty ones included, while the bar draws only the
102
+ * ones with a share: pass the empty buckets as `legendOnly` entries.
103
+ */
104
+ legendOnly?: boolean;
85
105
  }
86
106
 
87
107
  export interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "role"> {
@@ -89,7 +109,14 @@ export interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">,
89
109
  * and nobody knows how much is left (an import that reports no count). */
90
110
  value?: number;
91
111
  min?: number;
92
- max?: number;
112
+ /**
113
+ * The top of the scale (default 100). `null` is a quota with NO ceiling — kastlan's
114
+ * billing usage on an unlimited plan (usage-progress-bar:25): nothing to fill, so no
115
+ * bar is drawn; the label row shows the value and "Unlimited" (`progressBar.unlimited`)
116
+ * as plain text. `formatValue` is then called with `max` = `Infinity`; without it,
117
+ * the value is a plain number in the kit's locale.
118
+ */
119
+ max?: number | null;
93
120
  /**
94
121
  * `progress` (default): a task moving toward completion — `role="progressbar"`.
95
122
  * `meter`: a share of a known whole that is not going anywhere (budget used, disk
@@ -126,6 +153,25 @@ export interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">,
126
153
  /** With `segments`: a list under the bar naming each part with its swatch and value
127
154
  * — the table view of the bar, in text tokens rather than the segment colour. */
128
155
  legend?: boolean;
156
+ /**
157
+ * What a legend row shows at its end, in place of the part's formatted value — which
158
+ * it is handed, with the segment and its index. keksdose's cut card shows the share
159
+ * AND a private outflow amount per bucket (cut-card:150), which one figure could not:
160
+ * `legendValue={(seg, share) => <>{share} <Amount … /></>}`.
161
+ */
162
+ legendValue?: (segment: ProgressBarSegment, formatted: string, index: number) => ReactNode;
163
+ /** A muted line under the bar — the plan's reset date, "3 of 5 seats in use". It
164
+ * describes the bar (`aria-describedby`). */
165
+ hint?: ReactNode;
166
+ /**
167
+ * When `value` is past `max`, say by how much, in a danger-coloured line under the bar
168
+ * (kastlan usage-progress-bar:46): `true` prints `progressBar.overLimit` with the
169
+ * excess as a plain number; a function renders the line itself from the excess (a
170
+ * currency, "2 seats"). The bar stays full at `max` either way — colour it with
171
+ * `tone="danger"` if it should read as over. Ignored for `segments` and `max={null}`.
172
+ */
173
+ overage?: boolean | ((over: number) => ReactNode);
174
+ labels?: Partial<ProgressBarLabels>;
129
175
  }
130
176
 
131
177
  /**
@@ -148,7 +194,7 @@ export interface ProgressBarProps extends Omit<ComponentPropsWithoutRef<"div">,
148
194
  export function ProgressBar({
149
195
  value,
150
196
  min = 0,
151
- max = 100,
197
+ max: maxProp = 100,
152
198
  variant = "progress",
153
199
  label,
154
200
  showValue = false,
@@ -158,21 +204,65 @@ export function ProgressBar({
158
204
  locale,
159
205
  segments,
160
206
  legend = false,
207
+ legendValue,
208
+ hint,
209
+ overage,
210
+ labels,
161
211
  className,
162
212
  "aria-label": ariaLabel,
163
213
  "aria-labelledby": ariaLabelledBy,
214
+ "aria-describedby": ariaDescribedBy,
164
215
  ...rest
165
216
  }: ProgressBarProps) {
166
217
  const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
218
+ const text = useKitLabels("progressBar", DEFAULT_PROGRESS_BAR_LABELS, labels);
167
219
  const kitLocale = useKitLocale(locale);
168
220
  const labelId = useId();
221
+ const hintId = useId();
222
+ const hintNode =
223
+ hint != null ? (
224
+ <p id={hintId} data-part="hint" className="mt-1 text-xs text-[var(--text-muted)]">
225
+ {hint}
226
+ </p>
227
+ ) : null;
228
+
229
+ if (maxProp === null) {
230
+ // No ceiling: no bar, no role — a meter with no maximum is not one. The value and
231
+ // "Unlimited" are the content, so they are plain visible text.
232
+ const shown =
233
+ value === undefined || !Number.isFinite(value)
234
+ ? undefined
235
+ : formatValue
236
+ ? formatValue(value, Infinity, min)
237
+ : new Intl.NumberFormat(kitLocale).format(value);
238
+ return (
239
+ <div {...rest} className={cn("min-w-0", className)} data-state="unlimited">
240
+ <div className="flex items-baseline gap-2 text-sm">
241
+ {label != null && (
242
+ <span id={labelId} className="min-w-0 text-[var(--text-secondary)]">
243
+ {label}
244
+ </span>
245
+ )}
246
+ <span data-part="unlimited" className="ms-auto shrink-0 tabular-nums text-[var(--text-muted)]">
247
+ {shown !== undefined && `${shown} · `}
248
+ {text.unlimited}
249
+ </span>
250
+ </div>
251
+ {hintNode}
252
+ </div>
253
+ );
254
+ }
255
+ const max = maxProp;
169
256
 
170
257
  const stacked = segments !== undefined;
171
258
  const meter = stacked || variant === "meter";
172
259
  const span = max - min;
173
260
  const partValue = (v: number) => (Number.isFinite(v) ? Math.max(0, v - min) : 0);
261
+ // Colours are dealt over EVERY segment, legend-only ones included, so an entry keeps
262
+ // its swatch whether or not it is in the bar this time.
174
263
  const fills = stacked ? segmentFills(segments) : [];
175
- const partTotal = stacked ? min + segments.reduce((sum, seg) => sum + partValue(seg.value), 0) : undefined;
264
+ const barParts = stacked ? segments.filter((seg) => !seg.legendOnly) : [];
265
+ const partTotal = stacked ? min + barParts.reduce((sum, seg) => sum + partValue(seg.value), 0) : undefined;
176
266
  const format = (v: number) =>
177
267
  formatValue
178
268
  ? formatValue(v, max, min)
@@ -180,7 +270,7 @@ export function ProgressBar({
180
270
  span <= 0 ? 0 : (Math.min(max, Math.max(min, v)) - min) / span,
181
271
  );
182
272
  const partTexts = stacked
183
- ? segments.map((seg) => {
273
+ ? barParts.map((seg) => {
184
274
  const text = format(min + partValue(seg.value));
185
275
  return typeof seg.label === "string" || typeof seg.label === "number"
186
276
  ? common.fieldValue(String(seg.label), text)
@@ -199,6 +289,19 @@ export function ProgressBar({
199
289
  ? new Intl.ListFormat(kitLocale, { style: "short", type: "unit" }).format(partTexts)
200
290
  : valueText;
201
291
 
292
+ const over =
293
+ overage && !stacked && value !== undefined && Number.isFinite(value) && value > max ? value - max : 0;
294
+ const overageNode =
295
+ over > 0 ? (
296
+ <p id={`${hintId}-over`} data-part="overage" className="mt-1 text-xs text-[var(--danger)]">
297
+ {typeof overage === "function"
298
+ ? overage(over)
299
+ : text.overLimit(new Intl.NumberFormat(kitLocale).format(over))}
300
+ </p>
301
+ ) : null;
302
+ const describedBy =
303
+ [ariaDescribedBy, hintNode && hintId, overageNode && `${hintId}-over`].filter(Boolean).join(" ") || undefined;
304
+
202
305
  const labelledBy = ariaLabelledBy ?? (label != null ? labelId : undefined);
203
306
  const name = labelledBy ? undefined : (ariaLabel ?? (indeterminate ? common.loading : undefined));
204
307
 
@@ -225,6 +328,7 @@ export function ProgressBar({
225
328
  role={meter ? "meter" : "progressbar"}
226
329
  aria-label={name}
227
330
  aria-labelledby={labelledBy}
331
+ aria-describedby={describedBy}
228
332
  aria-valuemin={indeterminate ? undefined : min}
229
333
  aria-valuemax={indeterminate ? undefined : max}
230
334
  aria-valuenow={clamped}
@@ -238,20 +342,22 @@ export function ProgressBar({
238
342
  // The parts in a flex row with a 2px gap, so two neighbouring steps of one
239
343
  // hue are parted by the track rather than by their difference in lightness.
240
344
  <div className="flex h-full w-full gap-0.5">
241
- {segments.map((seg, i) => (
242
- <div
243
- key={seg.key ?? i}
244
- data-part="segment"
245
- className={cn(
246
- "h-full shrink-0 rounded-full transition-[width] duration-300 motion-reduce:transition-none",
247
- fills[i],
248
- )}
249
- style={{
250
- width: `${span <= 0 ? 0 : (partValue(seg.value) / span) * 100}%`,
251
- backgroundColor: seg.className ? undefined : seg.color,
252
- }}
253
- />
254
- ))}
345
+ {segments.map((seg, i) =>
346
+ seg.legendOnly ? null : (
347
+ <div
348
+ key={seg.key ?? i}
349
+ data-part="segment"
350
+ className={cn(
351
+ "h-full shrink-0 rounded-full transition-[width] duration-300 motion-reduce:transition-none",
352
+ fills[i],
353
+ )}
354
+ style={{
355
+ width: `${span <= 0 ? 0 : (partValue(seg.value) / span) * 100}%`,
356
+ backgroundColor: seg.className ? undefined : seg.color,
357
+ }}
358
+ />
359
+ ),
360
+ )}
255
361
  </div>
256
362
  ) : indeterminate ? (
257
363
  <div
@@ -294,12 +400,16 @@ export function ProgressBar({
294
400
  <span className="min-w-0 flex-1 truncate text-[var(--text-secondary)]">{seg.label}</span>
295
401
  )}
296
402
  <span className="ms-auto shrink-0 text-xs tabular-nums text-[var(--text-muted)]">
297
- {format(min + partValue(seg.value))}
403
+ {legendValue
404
+ ? legendValue(seg, format(min + partValue(seg.value)), i)
405
+ : format(min + partValue(seg.value))}
298
406
  </span>
299
407
  </li>
300
408
  ))}
301
409
  </ul>
302
410
  )}
411
+ {overageNode}
412
+ {hintNode}
303
413
  </div>
304
414
  );
305
415
  }
@@ -0,0 +1,83 @@
1
+ import { useMemo } from "react";
2
+ import type { ComponentPropsWithoutRef } from "react";
3
+ import { cn } from "../lib/cn";
4
+ import { encodeQr } from "../lib/qr-encode";
5
+ import type { QrErrorCorrection } from "../lib/qr-encode";
6
+
7
+ export interface QrCodeProps extends Omit<ComponentPropsWithoutRef<"svg">, "children" | "viewBox"> {
8
+ /** The text to encode (UTF-8) — an `otpauth://` URI, a URL, a pairing code. */
9
+ value: string;
10
+ /** The accessible name. Without it the code is decorative (`aria-hidden`): say what
11
+ * it is for, not what it encodes — nobody wants a URI read out. */
12
+ label?: string;
13
+ /** Default `"M"`, raised automatically where the size allows it. */
14
+ errorCorrection?: QrErrorCorrection;
15
+ /** The quiet zone, in modules. The spec asks for 4; less reads worse. Default 4. */
16
+ margin?: number;
17
+ }
18
+
19
+ /**
20
+ * A QR code as one inline SVG — no canvas, no data URL, no dependency (see
21
+ * `lib/qr-encode.ts` for why the encoder lives in the kit).
22
+ *
23
+ * Sized by CSS (`className="size-48"`); the SVG scales without blurring because every
24
+ * module is a whole unit of the viewBox and `shape-rendering` is `crispEdges`.
25
+ *
26
+ * **Black on white, always — the one literal colour pair in the kit.** A camera looks
27
+ * for the contrast between the modules and their quiet zone; on the dark theme a
28
+ * surface-coloured background makes a code slow to acquire, and an inverted one is not
29
+ * read at all by several authenticator apps. The quiet zone is part of the SVG, so the
30
+ * code carries its own white border wherever it is placed.
31
+ *
32
+ * A value too long for any QR version (≈2.9 kB) renders nothing rather than throwing
33
+ * in render.
34
+ */
35
+ export function QrCode({
36
+ value,
37
+ label,
38
+ errorCorrection = "M",
39
+ margin = 4,
40
+ className,
41
+ ...rest
42
+ }: QrCodeProps) {
43
+ const path = useMemo(() => {
44
+ try {
45
+ const { modules, size } = encodeQr(value, { errorCorrection });
46
+ // One path, one sub-path per horizontal run of dark modules: a few hundred
47
+ // commands rather than one <rect> per module.
48
+ let d = "";
49
+ for (let y = 0; y < size; y++) {
50
+ for (let x = 0; x < size; ) {
51
+ if (!modules[y][x]) {
52
+ x++;
53
+ continue;
54
+ }
55
+ let run = 1;
56
+ while (x + run < size && modules[y][x + run]) run++;
57
+ d += `M${x + margin} ${y + margin}h${run}v1h-${run}z`;
58
+ x += run;
59
+ }
60
+ }
61
+ return { d, extent: size + margin * 2 };
62
+ } catch {
63
+ return null;
64
+ }
65
+ }, [value, errorCorrection, margin]);
66
+
67
+ if (!path) return null;
68
+ return (
69
+ <svg
70
+ {...rest}
71
+ xmlns="http://www.w3.org/2000/svg"
72
+ viewBox={`0 0 ${path.extent} ${path.extent}`}
73
+ shapeRendering="crispEdges"
74
+ role={label ? "img" : undefined}
75
+ aria-label={label}
76
+ aria-hidden={label ? undefined : true}
77
+ className={cn("block", className)}
78
+ >
79
+ <rect width={path.extent} height={path.extent} fill="#ffffff" />
80
+ <path d={path.d} fill="#000000" />
81
+ </svg>
82
+ );
83
+ }
@@ -3,6 +3,7 @@ import type { InputHTMLAttributes } from "react";
3
3
  import { Search, X } from "lucide-react";
4
4
  import { cn } from "../lib/cn";
5
5
  import { FIELD_BASE } from "./ui";
6
+ import { DEFAULT_COMMON_LABELS, useKitLabels } from "../i18n/kit-labels";
6
7
 
7
8
  interface SearchFieldOwnProps
8
9
  extends Omit<
@@ -23,10 +24,22 @@ interface SearchFieldOwnProps
23
24
  * absent, so a consumer can migrate one call site at a time.
24
25
  */
25
26
  label?: string;
26
- /** Names the clear button. Omit it and no clear button is rendered — which is a
27
- * decision, not a default: a filter you can type into and not untype is the
28
- * complaint that produced the button on three of these four screens. */
27
+ /** Names the clear button. Default: the catalogue's `common.clear`, in the
28
+ * provider's language. */
29
29
  clearLabel?: string;
30
+ /**
31
+ * Draw the "×" that empties the field once there is something in it. Default
32
+ * `true`.
33
+ *
34
+ * It used to appear only when a `clearLabel` was passed, and every kastlan call
35
+ * site passed one — lease-list-page.tsx:91, trial-balance-page.tsx:~196,
36
+ * account-ledger-page.tsx:~131 and data-table/resource-list-page.tsx:256 each
37
+ * spelled `clearLabel={t("table.clear")}` — so the opt-in was boilerplate: a filter
38
+ * you can type into and not untype is the complaint that produced the button in
39
+ * the first place, and the catalogue already names it. `false` is for a field
40
+ * whose clearing lives elsewhere (a "Reset filters" beside it).
41
+ */
42
+ clearable?: boolean;
30
43
  /**
31
44
  * `"field"` (default): the bordered filter box that sits on a page.
32
45
  *
@@ -96,7 +109,8 @@ export const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(functi
96
109
  value,
97
110
  onChange,
98
111
  label,
99
- clearLabel,
112
+ clearLabel: clearLabelProp,
113
+ clearable = true,
100
114
  className,
101
115
  inputClassName,
102
116
  placeholder,
@@ -107,6 +121,8 @@ export const SearchField = forwardRef<HTMLInputElement, SearchFieldProps>(functi
107
121
  ref,
108
122
  ) {
109
123
  const innerRef = useRef<HTMLInputElement>(null);
124
+ const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
125
+ const clearLabel = clearable ? (clearLabelProp ?? common.clear) : undefined;
110
126
  // Whichever spelling the caller used. It is the accessible name AND the placeholder
111
127
  // fallback, so a box named only by `aria-label` still says what it is before anyone
112
128
  // has typed in it — the same deal `label` has always had.
@@ -0,0 +1,295 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode } from "react";
2
+ import { ArrowDown, ArrowUp, Minus } from "lucide-react";
3
+ import { cn } from "../lib/cn";
4
+ import { formatMoney, formatNumber, formatPercent } from "../lib/format";
5
+ import { useKitLabels, useKitLocale } from "../i18n/kit-labels";
6
+ import { DEFAULT_STAT_TILE_LABELS } from "./stat-tile";
7
+ import type { StatTileLabels } from "./stat-tile";
8
+
9
+ /**
10
+ * The text colours a figure can carry, token-backed — `StatTile`'s tones, as a class
11
+ * any span can take.
12
+ *
13
+ * - `default` — the primary text colour. `muted` — the secondary, quiet one.
14
+ * - `income` / `expense` / `net` — the money trio (CVD-safe teal / amber / violet).
15
+ * - `success` / `warning` / `danger` / `info` — a verdict: a rent rise is the tenant's
16
+ * bad news (kastlan lease-columns), a vote "for" is green (meeting-agenda-tab).
17
+ * - `brand` — the app's accent.
18
+ *
19
+ * Colour is never the only carrier: pair a verdict with an arrow, a word, or a sign.
20
+ */
21
+ export type TextTone =
22
+ | "default"
23
+ | "muted"
24
+ | "brand"
25
+ | "income"
26
+ | "expense"
27
+ | "net"
28
+ | "success"
29
+ | "warning"
30
+ | "danger"
31
+ | "info";
32
+
33
+ const TEXT_TONE: Record<TextTone, string> = {
34
+ default: "text-[var(--text-primary)]",
35
+ muted: "text-[var(--text-muted)]",
36
+ brand: "text-[var(--brand)]",
37
+ income: "text-[var(--money-income)]",
38
+ expense: "text-[var(--money-expense)]",
39
+ net: "text-[var(--money-net)]",
40
+ success: "text-[var(--success)]",
41
+ warning: "text-[var(--warning)]",
42
+ danger: "text-[var(--danger)]",
43
+ info: "text-[var(--info)]",
44
+ };
45
+
46
+ /**
47
+ * The class that colours text in `tone` — for the spans kastlan colours by hand with
48
+ * `text-[var(--danger)]` / `text-emerald-600` (settlement-table, changelog-page,
49
+ * meeting-agenda-tab), so the palette they draw from is the kit's tokens.
50
+ */
51
+ export function toneTextClass(tone: TextTone): string {
52
+ return TEXT_TONE[tone];
53
+ }
54
+
55
+ export interface ToneProps extends ComponentPropsWithoutRef<"span"> {
56
+ tone: TextTone;
57
+ }
58
+
59
+ /** A `<span>` in a {@link TextTone} — `<Tone tone="success">{votesFor}</Tone>`. */
60
+ export function Tone({ tone, className, ...rest }: ToneProps) {
61
+ return <span {...rest} className={cn(TEXT_TONE[tone], className)} />;
62
+ }
63
+
64
+ /* ── Signed amounts ──────────────────────────────────────────────────────── */
65
+
66
+ /** Which way is +1: `up` for a figure where more is good news (income, savings),
67
+ * `down` where less is (a rent the tenant pays, a cost). */
68
+ export type GoodDirection = "up" | "down";
69
+
70
+ /**
71
+ * How a signed figure is coloured:
72
+ * - `signed` (default) — money read by its own sign: above zero `income`, below zero
73
+ * `expense`, zero muted. A balance, a net.
74
+ * - `income` / `expense` / `neutral` — one colour whatever the sign (`neutral` is the
75
+ * text's own colour).
76
+ * - `verdict` — the change judged by {@link SignedAmountProps.goodDirection}: good
77
+ * news `success`, bad news `danger`, zero muted. Setting `goodDirection` implies it.
78
+ */
79
+ export type SignedAmountTone = "signed" | "income" | "expense" | "neutral" | "verdict";
80
+
81
+ /** What a signed figure says to a screen reader, whose readers disagree about a
82
+ * leading "+" (most skip it) and a "−" (some read "dash", some nothing). */
83
+ export interface SignedAmountLabels {
84
+ /** A figure above zero. `amount` is formatted and unsigned. */
85
+ positive: (amount: string) => string;
86
+ /** A figure below zero. `amount` is formatted and unsigned. */
87
+ negative: (amount: string) => string;
88
+ }
89
+
90
+ export const DEFAULT_SIGNED_AMOUNT_LABELS: SignedAmountLabels = {
91
+ positive: (amount) => `plus ${amount}`,
92
+ negative: (amount) => `minus ${amount}`,
93
+ };
94
+
95
+ interface SignedFormatProps {
96
+ /** ISO 4217 code: format as money. */
97
+ currency?: string;
98
+ /** `"percent"` reads `value` as a RATIO (`0.12` → 12 %), as `StatTile`'s delta does. */
99
+ unit?: "value" | "percent";
100
+ /** Compact notation: 12 400 → "12K". */
101
+ compact?: boolean;
102
+ /** Fraction digits, as `formatNumber`'s `digits`. */
103
+ digits?: number | { min?: number; max?: number };
104
+ /** Formats the UNSIGNED magnitude, replacing the `Intl` default outright. The sign
105
+ * is the component's. */
106
+ format?: (magnitude: number) => string;
107
+ locale?: string;
108
+ }
109
+
110
+ function useMagnitudeFormatter({ currency, unit, compact, digits, format, locale }: SignedFormatProps) {
111
+ const kitLocale = useKitLocale(locale);
112
+ return (magnitude: number): string => {
113
+ if (format) return format(magnitude);
114
+ if (unit === "percent") return formatPercent(magnitude, { locale: kitLocale, compact, digits });
115
+ if (currency) return formatMoney(magnitude, currency, { locale: kitLocale, compact, digits });
116
+ return formatNumber(magnitude, { locale: kitLocale, compact, digits });
117
+ };
118
+ }
119
+
120
+ function directionOf(value: number): "up" | "down" | "flat" {
121
+ return value > 0 ? "up" : value < 0 ? "down" : "flat";
122
+ }
123
+
124
+ function signedToneClass(
125
+ tone: SignedAmountTone,
126
+ dir: "up" | "down" | "flat",
127
+ goodDirection: GoodDirection | undefined,
128
+ ): string {
129
+ if (tone === "verdict" || (goodDirection && tone === "signed")) {
130
+ if (dir === "flat" || !goodDirection) return TEXT_TONE.muted;
131
+ return dir === goodDirection ? TEXT_TONE.success : TEXT_TONE.danger;
132
+ }
133
+ if (tone === "signed") return dir === "flat" ? TEXT_TONE.muted : dir === "up" ? TEXT_TONE.income : TEXT_TONE.expense;
134
+ if (tone === "neutral") return "";
135
+ return TEXT_TONE[tone];
136
+ }
137
+
138
+ const ARROW = { up: ArrowUp, down: ArrowDown, flat: Minus } as const;
139
+
140
+ export interface SignedAmountProps
141
+ extends SignedFormatProps,
142
+ Omit<ComponentPropsWithoutRef<"span">, "children"> {
143
+ value: number;
144
+ /** See {@link SignedAmountTone}. Default `signed`, or `verdict` with `goodDirection`. */
145
+ tone?: SignedAmountTone;
146
+ /** Which way is good news — colours by verdict (`success` / `danger`). */
147
+ goodDirection?: GoodDirection;
148
+ /** Print "+" before a positive figure (default `true`). A negative always shows its
149
+ * minus; zero shows neither. */
150
+ showPlus?: boolean;
151
+ /** An arrow before the figure (up / down / a dash at zero) — so the direction does not
152
+ * ride on colour alone. Decorative; the words say the same. */
153
+ arrow?: boolean;
154
+ /** Tag the figure `data-private`, for the host's demo-mode blur. */
155
+ sensitive?: boolean;
156
+ labels?: Partial<SignedAmountLabels>;
157
+ }
158
+
159
+ /**
160
+ * A figure with a sign that matters: a balance, a rent change, a net.
161
+ *
162
+ * kastlan writes it by hand in five places — `{change > 0 ? "+" : ""}{formatCurrency(
163
+ * change)}` in a span coloured by a ternary (lease-columns:190, settlement-table:58,
164
+ * the tenancy `formatPct`) — and each picks its own colours for the same verdict.
165
+ *
166
+ * The visible figure (sign, digits, arrow) is hidden from assistive tech and spoken as
167
+ * words instead ("plus CHF 50.00" / "minus CHF 50.00", `signedAmount.*`): a "+" is
168
+ * skipped by most screen readers, and `Intl`'s "−" is read as "dash" or not at all.
169
+ */
170
+ export function SignedAmount({
171
+ value,
172
+ currency,
173
+ unit,
174
+ compact,
175
+ digits,
176
+ format,
177
+ locale,
178
+ tone = "signed",
179
+ goodDirection,
180
+ showPlus = true,
181
+ arrow = false,
182
+ sensitive = false,
183
+ labels,
184
+ className,
185
+ ...rest
186
+ }: SignedAmountProps) {
187
+ const text = useKitLabels("signedAmount", DEFAULT_SIGNED_AMOUNT_LABELS, labels);
188
+ const fmt = useMagnitudeFormatter({ currency, unit, compact, digits, format, locale });
189
+ const finite = Number.isFinite(value);
190
+ const dir = finite ? directionOf(value) : "flat";
191
+ const amount = finite ? fmt(Math.abs(value)) : "—";
192
+ // U+2212, the typographic minus: the same width as "+", so a column of signed figures
193
+ // lines up under `tabular-nums`.
194
+ const sign = dir === "up" ? (showPlus ? "+" : "") : dir === "down" ? "−" : "";
195
+ const spoken = dir === "up" ? text.positive(amount) : dir === "down" ? text.negative(amount) : amount;
196
+ const Icon = ARROW[dir];
197
+ return (
198
+ <span
199
+ {...rest}
200
+ data-private={sensitive ? "" : undefined}
201
+ data-direction={dir}
202
+ className={cn(
203
+ "relative inline-flex items-center gap-0.5 whitespace-nowrap tabular-nums",
204
+ signedToneClass(tone, dir, goodDirection),
205
+ className,
206
+ )}
207
+ >
208
+ {arrow && <Icon aria-hidden className="size-[1em] shrink-0" />}
209
+ <span aria-hidden>
210
+ {sign}
211
+ {amount}
212
+ </span>
213
+ <span className="sr-only">{spoken}</span>
214
+ </span>
215
+ );
216
+ }
217
+
218
+ export interface DeltaProps extends SignedFormatProps, Omit<ComponentPropsWithoutRef<"span">, "children"> {
219
+ /** The change. Above zero is "up". */
220
+ value: number;
221
+ /** Which way is good news. Unset, the change is stated and not judged (muted); set,
222
+ * it is coloured `success` / `danger` and its sentence says "(better)" / "(worse)". */
223
+ goodDirection?: GoodDirection;
224
+ /** What it is measured against, after it: "vs last month". */
225
+ label?: ReactNode;
226
+ /** Hide the arrow (shown by default, as on a StatTile). At zero the arrow is a flat
227
+ * dash BESIDE the figure ("– 0.00 %"), never in place of it. */
228
+ arrow?: boolean;
229
+ sensitive?: boolean;
230
+ /** The `statTile` namespace's direction words — a delta says the same as a tile's. */
231
+ labels?: Partial<Pick<StatTileLabels, "increase" | "decrease" | "unchanged" | "better" | "worse">>;
232
+ }
233
+
234
+ /**
235
+ * `StatTile`'s delta on its own: an arrow, the unsigned magnitude, and a sentence for
236
+ * screen readers ("Up 12% (worse)"), coloured only when `goodDirection` judges it —
237
+ * users up and cost up are the same arrow until someone says which is good.
238
+ *
239
+ * For a change shown outside a tile: kastlan's rent-change column
240
+ * (lease-columns:219, a "+1.25%" chip), the breakdown rows of the rent calculator
241
+ * (breakdown-row:5, a trend icon coloured by hand).
242
+ */
243
+ export function Delta({
244
+ value,
245
+ currency,
246
+ unit,
247
+ compact,
248
+ digits,
249
+ format,
250
+ locale,
251
+ goodDirection,
252
+ label,
253
+ arrow = true,
254
+ sensitive = false,
255
+ labels,
256
+ className,
257
+ ...rest
258
+ }: DeltaProps) {
259
+ const text = useKitLabels("statTile", DEFAULT_STAT_TILE_LABELS, labels);
260
+ const fmt = useMagnitudeFormatter({ currency, unit, compact, digits, format, locale });
261
+ if (!Number.isFinite(value)) return null;
262
+ const amount = fmt(Math.abs(value));
263
+ const dir = directionOf(value);
264
+ const Icon = ARROW[dir];
265
+ let spoken = dir === "up" ? text.increase(amount) : dir === "down" ? text.decrease(amount) : text.unchanged;
266
+ let tone = TEXT_TONE.muted;
267
+ if (goodDirection && dir !== "flat") {
268
+ const good = dir === goodDirection;
269
+ spoken = good ? text.better(spoken) : text.worse(spoken);
270
+ tone = good ? TEXT_TONE.success : TEXT_TONE.danger;
271
+ }
272
+ return (
273
+ <span
274
+ {...rest}
275
+ data-direction={dir}
276
+ className={cn("relative inline-flex flex-wrap items-center gap-x-1.5 tabular-nums", className)}
277
+ >
278
+ <span data-private={sensitive ? "" : undefined} className={cn("inline-flex items-center gap-0.5 font-medium", tone)}>
279
+ {arrow && <Icon className="size-[1em] shrink-0" aria-hidden />}
280
+ {/* The figure at zero too, beside the flat dash (kastlan feedback #48). Before
281
+ this a flat delta with an arrow drew the dash ALONE, as a StatTile does — so
282
+ a rent-change column that printed "0.00 %" for an unchanged lease showed a
283
+ bare "–" in its place, and a bare dash is what a table prints for a MISSING
284
+ value: "unchanged" and "not known" looked the same. The Minus icon is the
285
+ neutral indicator, the muted tone the neutral colour, and the figure says
286
+ how much (none) in the column's own digits. A value that is missing never
287
+ reaches here — a non-finite one renders nothing, above. Visible change for
288
+ every `<Delta value={0}>` with the default arrow: "–" became "– 0 %". */}
289
+ <span aria-hidden>{amount}</span>
290
+ <span className="sr-only">{spoken}</span>
291
+ </span>
292
+ {label != null && <span className="text-[var(--text-muted)]">{label}</span>}
293
+ </span>
294
+ );
295
+ }