@eifi1/ui-kit 0.48.2 → 0.50.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 (377) hide show
  1. package/README.md +34 -16
  2. package/dist/admin/admin-action-confirm.js +1 -1
  3. package/dist/admin/admin-action-confirm.js.map +1 -1
  4. package/dist/admin/invitations-panel.js +5 -1
  5. package/dist/admin/invitations-panel.js.map +1 -1
  6. package/dist/admin/user-roster.d.ts +24 -12
  7. package/dist/admin/user-roster.js +21 -6
  8. package/dist/admin/user-roster.js.map +1 -1
  9. package/dist/auth/accept-invitation.js +2 -2
  10. package/dist/auth/accept-invitation.js.map +1 -1
  11. package/dist/auth/not-found-page.js +1 -1
  12. package/dist/auth/not-found-page.js.map +1 -1
  13. package/dist/auth/reset-password-form.js +2 -2
  14. package/dist/auth/reset-password-form.js.map +1 -1
  15. package/dist/auth/verify-email.d.ts +9 -1
  16. package/dist/auth/verify-email.js +5 -3
  17. package/dist/auth/verify-email.js.map +1 -1
  18. package/dist/billing/plan-card.js +2 -2
  19. package/dist/billing/plan-card.js.map +1 -1
  20. package/dist/components/amount-input.d.ts +2 -1
  21. package/dist/components/amount-input.js +3 -2
  22. package/dist/components/amount-input.js.map +1 -1
  23. package/dist/components/autocomplete.js +1 -0
  24. package/dist/components/autocomplete.js.map +1 -1
  25. package/dist/components/breadcrumbs.d.ts +19 -1
  26. package/dist/components/breadcrumbs.js +11 -4
  27. package/dist/components/breadcrumbs.js.map +1 -1
  28. package/dist/components/button-look.d.ts +37 -7
  29. package/dist/components/button-look.js +33 -6
  30. package/dist/components/button-look.js.map +1 -1
  31. package/dist/components/button.d.ts +4 -1
  32. package/dist/components/button.js +2 -2
  33. package/dist/components/button.js.map +1 -1
  34. package/dist/components/calendar-heatmap.js +2 -1
  35. package/dist/components/calendar-heatmap.js.map +1 -1
  36. package/dist/components/chart-edge.d.ts +10 -0
  37. package/dist/components/chart-edge.js +57 -0
  38. package/dist/components/chart-edge.js.map +1 -0
  39. package/dist/components/chart.js +2 -1
  40. package/dist/components/chart.js.map +1 -1
  41. package/dist/components/chip.d.ts +8 -1
  42. package/dist/components/chip.js +4 -1
  43. package/dist/components/chip.js.map +1 -1
  44. package/dist/components/combobox-core.js +1 -0
  45. package/dist/components/combobox-core.js.map +1 -1
  46. package/dist/components/combobox-suggestions.js +1 -0
  47. package/dist/components/combobox-suggestions.js.map +1 -1
  48. package/dist/components/confirm-context.d.ts +5 -0
  49. package/dist/components/confirm-context.js +7 -0
  50. package/dist/components/confirm-context.js.map +1 -0
  51. package/dist/components/confirm-dialog.js +3 -3
  52. package/dist/components/confirm-dialog.js.map +1 -1
  53. package/dist/components/currency-select.d.ts +28 -2
  54. package/dist/components/currency-select.js +56 -10
  55. package/dist/components/currency-select.js.map +1 -1
  56. package/dist/components/danger-confirm.d.ts +2 -2
  57. package/dist/components/danger-confirm.js +1 -1
  58. package/dist/components/danger-confirm.js.map +1 -1
  59. package/dist/components/data-table-fit.js +21 -3
  60. package/dist/components/data-table-fit.js.map +1 -1
  61. package/dist/components/data-table-mobile.d.ts +58 -3
  62. package/dist/components/data-table-mobile.js +147 -16
  63. package/dist/components/data-table-mobile.js.map +1 -1
  64. package/dist/components/data-table-row-actions.d.ts +2 -1
  65. package/dist/components/data-table-row-actions.js +7 -0
  66. package/dist/components/data-table-row-actions.js.map +1 -1
  67. package/dist/components/data-table-row.d.ts +1 -1
  68. package/dist/components/data-table-row.js.map +1 -1
  69. package/dist/components/data-table.d.ts +34 -1
  70. package/dist/components/data-table.js +37 -10
  71. package/dist/components/data-table.js.map +1 -1
  72. package/dist/components/dropdown.js +1 -0
  73. package/dist/components/dropdown.js.map +1 -1
  74. package/dist/components/error-boundary.js +2 -2
  75. package/dist/components/error-boundary.js.map +1 -1
  76. package/dist/components/floating-panel.d.ts +36 -13
  77. package/dist/components/floating-panel.js +27 -7
  78. package/dist/components/floating-panel.js.map +1 -1
  79. package/dist/components/form-actions.d.ts +1 -1
  80. package/dist/components/form-actions.js +1 -1
  81. package/dist/components/form-actions.js.map +1 -1
  82. package/dist/components/full-bleed-dialog.js +1 -0
  83. package/dist/components/full-bleed-dialog.js.map +1 -1
  84. package/dist/components/icon-button.d.ts +11 -0
  85. package/dist/components/icon-button.js +7 -5
  86. package/dist/components/icon-button.js.map +1 -1
  87. package/dist/components/modal.js +1 -0
  88. package/dist/components/modal.js.map +1 -1
  89. package/dist/components/nav-pills.d.ts +8 -1
  90. package/dist/components/nav-pills.js +61 -38
  91. package/dist/components/nav-pills.js.map +1 -1
  92. package/dist/components/number-input.d.ts +2 -1
  93. package/dist/components/number-input.js +6 -2
  94. package/dist/components/number-input.js.map +1 -1
  95. package/dist/components/numpad-sheet.js +2 -0
  96. package/dist/components/numpad-sheet.js.map +1 -1
  97. package/dist/components/page-header-context.d.ts +8 -0
  98. package/dist/components/page-header-context.js +7 -0
  99. package/dist/components/page-header-context.js.map +1 -0
  100. package/dist/components/page-header.d.ts +14 -2
  101. package/dist/components/page-header.js +10 -1
  102. package/dist/components/page-header.js.map +1 -1
  103. package/dist/components/picker-sheet.js +2 -0
  104. package/dist/components/picker-sheet.js.map +1 -1
  105. package/dist/components/pie-chart.js +2 -1
  106. package/dist/components/pie-chart.js.map +1 -1
  107. package/dist/components/popover.js +1 -0
  108. package/dist/components/popover.js.map +1 -1
  109. package/dist/components/progress-bar.js +5 -4
  110. package/dist/components/progress-bar.js.map +1 -1
  111. package/dist/components/reauth-dialog.js +1 -1
  112. package/dist/components/reauth-dialog.js.map +1 -1
  113. package/dist/components/row-actions-labels.js +3 -1
  114. package/dist/components/row-actions-labels.js.map +1 -1
  115. package/dist/components/row-actions.d.ts +43 -9
  116. package/dist/components/row-actions.js +78 -15
  117. package/dist/components/row-actions.js.map +1 -1
  118. package/dist/components/section-nav.d.ts +37 -0
  119. package/dist/components/section-nav.js +33 -0
  120. package/dist/components/section-nav.js.map +1 -0
  121. package/dist/components/series-chart.js +8 -3
  122. package/dist/components/series-chart.js.map +1 -1
  123. package/dist/components/share-card.js +1 -1
  124. package/dist/components/share-card.js.map +1 -1
  125. package/dist/components/signature-pad.js +1 -1
  126. package/dist/components/signature-pad.js.map +1 -1
  127. package/dist/components/sparkline.js +2 -1
  128. package/dist/components/sparkline.js.map +1 -1
  129. package/dist/components/stat-tile.js +14 -7
  130. package/dist/components/stat-tile.js.map +1 -1
  131. package/dist/components/status-dot.d.ts +5 -1
  132. package/dist/components/status-dot.js +4 -1
  133. package/dist/components/status-dot.js.map +1 -1
  134. package/dist/components/tooltip.js +1 -1
  135. package/dist/components/tooltip.js.map +1 -1
  136. package/dist/components/translation-review-editor.js +2 -2
  137. package/dist/components/translation-review-editor.js.map +1 -1
  138. package/dist/components/translation-review.js +2 -2
  139. package/dist/components/translation-review.js.map +1 -1
  140. package/dist/components/treemap.d.ts +5 -2
  141. package/dist/components/treemap.js +26 -6
  142. package/dist/components/treemap.js.map +1 -1
  143. package/dist/components/use-table-state.js +37 -6
  144. package/dist/components/use-table-state.js.map +1 -1
  145. package/dist/data-table.js.map +1 -1
  146. package/dist/demo/demo-banner.js +1 -1
  147. package/dist/demo/demo-banner.js.map +1 -1
  148. package/dist/demo/demo-ended.js +1 -1
  149. package/dist/demo/demo-ended.js.map +1 -1
  150. package/dist/demo/demo-start.js +1 -1
  151. package/dist/demo/demo-start.js.map +1 -1
  152. package/dist/eslint/index.js +1 -1
  153. package/dist/feedback/feedback-inbox.js +1 -1
  154. package/dist/feedback/feedback-inbox.js.map +1 -1
  155. package/dist/hooks/use-dismiss.d.ts +9 -1
  156. package/dist/hooks/use-dismiss.js +25 -5
  157. package/dist/hooks/use-dismiss.js.map +1 -1
  158. package/dist/hooks/use-document-title.d.ts +14 -0
  159. package/dist/hooks/use-document-title.js +22 -0
  160. package/dist/hooks/use-document-title.js.map +1 -0
  161. package/dist/i18n/defaults.js +2 -0
  162. package/dist/i18n/defaults.js.map +1 -1
  163. package/dist/i18n/german.js +20 -3
  164. package/dist/i18n/german.js.map +1 -1
  165. package/dist/i18n/kit-labels.d.ts +34 -1
  166. package/dist/i18n/kit-labels.js +43 -2
  167. package/dist/i18n/kit-labels.js.map +1 -1
  168. package/dist/i18n/locales/en.js +22 -3
  169. package/dist/i18n/locales/en.js.map +3 -3
  170. package/dist/i18n/locales/es.js +20 -3
  171. package/dist/i18n/locales/es.js.map +1 -1
  172. package/dist/i18n/locales/fr.js +20 -3
  173. package/dist/i18n/locales/fr.js.map +1 -1
  174. package/dist/i18n/locales/hu.js +20 -3
  175. package/dist/i18n/locales/hu.js.map +1 -1
  176. package/dist/i18n/locales/it.js +20 -3
  177. package/dist/i18n/locales/it.js.map +1 -1
  178. package/dist/i18n/locales/zh.js +20 -3
  179. package/dist/i18n/locales/zh.js.map +1 -1
  180. package/dist/i18n/review.js +3 -0
  181. package/dist/i18n/review.js.map +1 -1
  182. package/dist/i18n/shared-labels.js +1 -0
  183. package/dist/i18n/shared-labels.js.map +1 -1
  184. package/dist/index.d.ts +8 -1
  185. package/dist/index.js +11 -0
  186. package/dist/index.js.map +1 -1
  187. package/dist/landing/landing-actions.js +2 -2
  188. package/dist/landing/landing-actions.js.map +1 -1
  189. package/dist/landing/public-header.js +2 -2
  190. package/dist/landing/public-header.js.map +1 -1
  191. package/dist/lib/clipboard-minus.d.ts +27 -0
  192. package/dist/lib/clipboard-minus.js +63 -0
  193. package/dist/lib/clipboard-minus.js.map +1 -0
  194. package/dist/lib/data-attributes.d.ts +11 -0
  195. package/dist/lib/data-attributes.js +10 -0
  196. package/dist/lib/data-attributes.js.map +1 -0
  197. package/dist/lib/document-title.d.ts +17 -0
  198. package/dist/lib/document-title.js +29 -0
  199. package/dist/lib/document-title.js.map +1 -0
  200. package/dist/lib/float-clearance.js +8 -2
  201. package/dist/lib/float-clearance.js.map +1 -1
  202. package/dist/lib/format.d.ts +36 -10
  203. package/dist/lib/format.js +57 -11
  204. package/dist/lib/format.js.map +1 -1
  205. package/dist/lib/minus.d.ts +32 -0
  206. package/dist/lib/minus.js +14 -0
  207. package/dist/lib/minus.js.map +1 -0
  208. package/dist/lib/status-tone.d.ts +30 -0
  209. package/dist/lib/status-tone.js +17 -0
  210. package/dist/lib/status-tone.js.map +1 -0
  211. package/dist/settings/settings-layout.js +3 -2
  212. package/dist/settings/settings-layout.js.map +1 -1
  213. package/dist/shell/account-menu-language.d.ts +1 -1
  214. package/dist/shell/account-menu-language.js +9 -1
  215. package/dist/shell/account-menu-language.js.map +1 -1
  216. package/dist/shell/app-shell-labels.js +2 -1
  217. package/dist/shell/app-shell-labels.js.map +1 -1
  218. package/dist/shell/app-shell.d.ts +58 -16
  219. package/dist/shell/app-shell.js +151 -43
  220. package/dist/shell/app-shell.js.map +1 -1
  221. package/dist/shell/language-code.d.ts +3 -0
  222. package/dist/shell/language-code.js +8 -0
  223. package/dist/shell/language-code.js.map +1 -0
  224. package/dist/shell/topbar-controls.d.ts +14 -6
  225. package/dist/shell/topbar-controls.js +14 -17
  226. package/dist/shell/topbar-controls.js.map +1 -1
  227. package/dist/testing/css-variables.generated.d.ts +1 -1
  228. package/dist/testing/css-variables.generated.js +1 -1
  229. package/dist/testing/css-variables.generated.js.map +1 -1
  230. package/dist/testing/react.d.ts +25 -0
  231. package/dist/testing/react.js +19 -0
  232. package/dist/testing/react.js.map +1 -0
  233. package/dist/theme/chart-palette.d.ts +82 -15
  234. package/dist/theme/chart-palette.js +51 -24
  235. package/dist/theme/chart-palette.js.map +1 -1
  236. package/dist/theme/palette-presets.d.ts +9 -0
  237. package/dist/theme/palette-presets.js +77 -62
  238. package/dist/theme/palette-presets.js.map +1 -1
  239. package/dist/tour/tour-dock.d.ts +42 -0
  240. package/dist/tour/tour-dock.js +80 -0
  241. package/dist/tour/tour-dock.js.map +1 -0
  242. package/dist/tour/tour-gates.d.ts +71 -0
  243. package/dist/tour/tour-gates.js +114 -0
  244. package/dist/tour/tour-gates.js.map +1 -0
  245. package/dist/tour/tour-guide-button.d.ts +27 -0
  246. package/dist/tour/tour-guide-button.js +45 -0
  247. package/dist/tour/tour-guide-button.js.map +1 -0
  248. package/dist/tour/tour-guide-labels.d.ts +8 -0
  249. package/dist/tour/tour-guide-labels.js +9 -0
  250. package/dist/tour/tour-guide-labels.js.map +1 -0
  251. package/dist/tour/tour-labels.js +9 -1
  252. package/dist/tour/tour-labels.js.map +1 -1
  253. package/dist/tour/tour.d.ts +133 -9
  254. package/dist/tour/tour.js +594 -70
  255. package/dist/tour/tour.js.map +1 -1
  256. package/dist/tour/use-tour-url-sync.d.ts +5 -2
  257. package/dist/tour/use-tour-url-sync.js +7 -1
  258. package/dist/tour/use-tour-url-sync.js.map +1 -1
  259. package/dist/tour.d.ts +2 -0
  260. package/dist/tour.js +3 -0
  261. package/dist/tour.js.map +1 -1
  262. package/dist/wizard/stepper-nav.d.ts +1 -1
  263. package/dist/wizard/stepper-nav.js +18 -5
  264. package/dist/wizard/stepper-nav.js.map +1 -1
  265. package/dist/wizard/types.d.ts +11 -0
  266. package/dist/wizard/types.js.map +1 -1
  267. package/dist/wizard/use-wizard.js +11 -5
  268. package/dist/wizard/use-wizard.js.map +1 -1
  269. package/package.json +9 -1
  270. package/src/admin/admin-action-confirm.tsx +1 -1
  271. package/src/admin/invitations-panel.tsx +7 -3
  272. package/src/admin/user-roster.tsx +41 -14
  273. package/src/auth/accept-invitation.tsx +3 -3
  274. package/src/auth/not-found-page.tsx +1 -1
  275. package/src/auth/reset-password-form.tsx +3 -3
  276. package/src/auth/verify-email.tsx +13 -3
  277. package/src/billing/plan-card.tsx +2 -2
  278. package/src/components/amount-input.tsx +5 -4
  279. package/src/components/autocomplete.tsx +2 -0
  280. package/src/components/breadcrumbs.tsx +32 -4
  281. package/src/components/button-look.ts +86 -15
  282. package/src/components/button.tsx +6 -3
  283. package/src/components/calendar-heatmap.tsx +2 -1
  284. package/src/components/chart-edge.ts +73 -0
  285. package/src/components/chart.tsx +3 -1
  286. package/src/components/chip.tsx +10 -1
  287. package/src/components/combobox-core.tsx +2 -0
  288. package/src/components/combobox-suggestions.tsx +2 -0
  289. package/src/components/confirm-context.ts +7 -0
  290. package/src/components/confirm-dialog.tsx +3 -3
  291. package/src/components/currency-select.tsx +95 -6
  292. package/src/components/danger-confirm.tsx +3 -3
  293. package/src/components/data-table-fit.ts +41 -6
  294. package/src/components/data-table-mobile.tsx +259 -17
  295. package/src/components/data-table-row-actions.tsx +9 -1
  296. package/src/components/data-table-row.tsx +3 -0
  297. package/src/components/data-table.tsx +91 -19
  298. package/src/components/dropdown.tsx +2 -0
  299. package/src/components/error-boundary.tsx +2 -2
  300. package/src/components/floating-panel.tsx +67 -16
  301. package/src/components/form-actions.tsx +2 -2
  302. package/src/components/full-bleed-dialog.tsx +3 -0
  303. package/src/components/icon-button.tsx +21 -8
  304. package/src/components/modal.tsx +3 -0
  305. package/src/components/nav-pills.tsx +29 -2
  306. package/src/components/number-input.tsx +8 -3
  307. package/src/components/numpad-sheet.tsx +4 -0
  308. package/src/components/page-header-context.ts +8 -0
  309. package/src/components/page-header.tsx +34 -2
  310. package/src/components/picker-sheet.tsx +4 -0
  311. package/src/components/pie-chart.tsx +2 -1
  312. package/src/components/popover.tsx +2 -0
  313. package/src/components/progress-bar.tsx +6 -3
  314. package/src/components/reauth-dialog.tsx +1 -1
  315. package/src/components/row-actions-labels.ts +2 -0
  316. package/src/components/row-actions.tsx +154 -33
  317. package/src/components/section-nav.tsx +65 -0
  318. package/src/components/series-chart.tsx +12 -3
  319. package/src/components/share-card.tsx +1 -1
  320. package/src/components/signature-pad.tsx +1 -1
  321. package/src/components/sparkline.tsx +2 -1
  322. package/src/components/stat-tile.tsx +19 -7
  323. package/src/components/status-dot.tsx +7 -1
  324. package/src/components/tooltip.tsx +3 -1
  325. package/src/components/translation-review-editor.tsx +2 -2
  326. package/src/components/translation-review.tsx +2 -2
  327. package/src/components/treemap.tsx +34 -8
  328. package/src/components/use-table-state.ts +63 -8
  329. package/src/data-table.ts +3 -3
  330. package/src/demo/demo-banner.tsx +1 -1
  331. package/src/demo/demo-ended.tsx +1 -1
  332. package/src/demo/demo-start.tsx +1 -1
  333. package/src/feedback/feedback-inbox.tsx +1 -1
  334. package/src/hooks/use-dismiss.ts +43 -4
  335. package/src/hooks/use-document-title.ts +31 -0
  336. package/src/i18n/defaults.ts +2 -0
  337. package/src/i18n/german.ts +17 -0
  338. package/src/i18n/kit-labels.tsx +79 -1
  339. package/src/i18n/locales/es.ts +17 -0
  340. package/src/i18n/locales/fr.ts +17 -0
  341. package/src/i18n/locales/hu.ts +17 -0
  342. package/src/i18n/locales/it.ts +17 -0
  343. package/src/i18n/locales/zh.ts +17 -0
  344. package/src/i18n/review.ts +3 -0
  345. package/src/i18n/shared-labels.ts +1 -0
  346. package/src/index.ts +13 -0
  347. package/src/landing/landing-actions.tsx +2 -2
  348. package/src/landing/public-header.tsx +2 -2
  349. package/src/lib/clipboard-minus.ts +94 -0
  350. package/src/lib/data-attributes.ts +14 -0
  351. package/src/lib/document-title.ts +41 -0
  352. package/src/lib/float-clearance.ts +19 -3
  353. package/src/lib/format.ts +117 -30
  354. package/src/lib/minus.ts +50 -0
  355. package/src/lib/status-tone.ts +43 -0
  356. package/src/settings/settings-layout.tsx +3 -2
  357. package/src/shell/account-menu-language.tsx +14 -2
  358. package/src/shell/app-shell-labels.ts +1 -0
  359. package/src/shell/app-shell.tsx +291 -68
  360. package/src/shell/language-code.ts +5 -0
  361. package/src/shell/topbar-controls.tsx +35 -15
  362. package/src/testing/css-variables.generated.ts +6 -6
  363. package/src/testing/react.ts +59 -0
  364. package/src/theme/chart-palette.ts +102 -20
  365. package/src/theme/palette-presets.ts +121 -84
  366. package/src/tour/tour-dock.ts +132 -0
  367. package/src/tour/tour-gates.ts +195 -0
  368. package/src/tour/tour-guide-button.tsx +64 -0
  369. package/src/tour/tour-guide-labels.ts +15 -0
  370. package/src/tour/tour-labels.ts +8 -0
  371. package/src/tour/tour.tsx +949 -86
  372. package/src/tour/use-tour-url-sync.ts +14 -4
  373. package/src/tour.ts +3 -0
  374. package/src/wizard/stepper-nav.tsx +27 -7
  375. package/src/wizard/types.ts +12 -0
  376. package/src/wizard/use-wizard.ts +16 -9
  377. package/tokens.css +28 -18
package/src/lib/format.ts CHANGED
@@ -1,8 +1,13 @@
1
1
  import { useMemo } from "react";
2
2
  import { useKitLocale, useKitTimeZone } from "../i18n/kit-labels";
3
3
  import { parseIsoDate } from "./dates";
4
+ import { formatWithMinus, type MinusStyle } from "./minus";
4
5
  import { roundHalfAway } from "./round";
5
6
 
7
+ export type { MinusStyle } from "./minus";
8
+ // 0.50 (KIT-1 follow-up): the kit's minus for an app's OWN `Intl.NumberFormat` wrapper.
9
+ export { formatWithMinus } from "./minus";
10
+
6
11
  /**
7
12
  * Number, money, percentage, date and relative-time formatting — `Intl`, in one place,
8
13
  * for the figures an app prints OUTSIDE a kit component.
@@ -19,9 +24,18 @@ import { roundHalfAway } from "./round";
19
24
  * functions bound to the `<UiKitProvider locale>`, so a page formats exactly like the
20
25
  * kit components on it.
21
26
  *
22
- * A missing value — `null`, `undefined`, `NaN`, an unparseable date — formats as
23
- * `empty` (default "—", the dash `StatTile` shows for no value), never as "NaN" or
24
- * "Invalid Date".
27
+ * A missing value — `null`, `undefined`, an unparseable date — formats as `empty`
28
+ * (default "—", the dash `StatTile` shows for no value), never as "NaN" or "Invalid
29
+ * Date". A number may also arrive as a numeric STRING (0.49, KIT-2) — "1850.00", "-12.5",
30
+ * "0": a Decimal field off a Python API — and is formatted like the number. Anything
31
+ * else (a non-numeric string, an object, `NaN`, ±`Infinity`) prints `empty` too, but is
32
+ * a bug upstream rather than a gap in the data, so a development build says so on the
33
+ * console, once per call site. Zero is a figure: `0` and `"0"` print "CHF 0.00", never
34
+ * the dash.
35
+ *
36
+ * A negative figure carries the typographic minus, U+2212 (0.49, KIT-1), in every
37
+ * locale — `Intl` writes a hyphen in most. `minus: "ascii"` for text a machine reads
38
+ * back (a CSV, the clipboard).
25
39
  */
26
40
 
27
41
  /** What every formatter here prints for a missing value, unless told otherwise. */
@@ -47,6 +61,14 @@ export interface FormatNumberBaseOptions extends BaseOptions {
47
61
  signDisplay?: Intl.NumberFormatOptions["signDisplay"];
48
62
  /** Any further `Intl.NumberFormat` options, merged over the ones above. */
49
63
  options?: Intl.NumberFormatOptions;
64
+ /**
65
+ * The minus sign of a negative figure (0.49, KIT-1). `"typographic"` (default):
66
+ * U+2212, as wide as "+" and the digits, as `SignedAmount` prints it — in every
67
+ * locale, also where `Intl` writes a hyphen. `"ascii"`: U+002D, for an export a
68
+ * machine parses back (a CSV download, the clipboard) — there, also pass
69
+ * `options: { useGrouping: false }` or write the raw number.
70
+ */
71
+ minus?: MinusStyle;
50
72
  }
51
73
 
52
74
  /** The options {@link formatNumber} takes: the shared ones plus a free-text `unit`. */
@@ -115,44 +137,113 @@ function numberOptions(o: FormatNumberBaseOptions): Intl.NumberFormatOptions {
115
137
  };
116
138
  }
117
139
 
118
- function isMissing(value: number | null | undefined): value is null | undefined {
119
- // ±Infinity too (lenkbank, adopting 0.13): a division by zero upstream is a missing
120
- // figure, and "∞" printed in a money or measurement column reads like data.
121
- return value === null || value === undefined || !Number.isFinite(value);
140
+ /**
141
+ * What the number formatters take (0.49, KIT-2): a number, a numeric string ("1850.00",
142
+ * "-12.5", "1e3" — what a Decimal arrives as in JSON), or `null` / `undefined` for no
143
+ * value. A string is read with `Number()`, so a figure with more significant digits than
144
+ * a double holds (about 15–17) is printed as its nearest double.
145
+ */
146
+ export type FormattableNumber = number | string | null | undefined;
147
+
148
+ /** A plain decimal: optional sign, digits with an optional point, an optional exponent.
149
+ * No grouping marks, no decimal comma, no currency — those are formatted text, and
150
+ * reading them back is a locale's guess. */
151
+ const NUMERIC_STRING = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
152
+
153
+ /** The kit's dev switch, as `lib/logger.ts` reads it: a development build, never a
154
+ * production one (the consumer's bundler makes `import.meta.env.DEV` false) and never
155
+ * under vitest. Read per call so a test can switch it on with `vi.stubEnv`. */
156
+ function devWarnings(): boolean {
157
+ return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;
158
+ }
159
+
160
+ const warnedSites = new Set<string>();
161
+ /** This module's own frames, skipped to find the caller ("Object.formatMoney" is a
162
+ * `useKitFormat` wrapper). */
163
+ const OWN_FRAME = /\b(?:formatNumber|formatMoney|formatPercent|toFormattable|warnInvalid|callSite)\b/;
164
+
165
+ /** The first stack frame outside the formatters: where the bad value was passed. */
166
+ function callSite(): string {
167
+ const frames = (new Error().stack ?? "").split("\n").slice(1);
168
+ return frames.find((frame) => !OWN_FRAME.test(frame))?.trim() ?? "";
169
+ }
170
+
171
+ function describeValue(value: unknown): string {
172
+ if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") return String(value);
173
+ try {
174
+ return JSON.stringify(value) ?? String(value);
175
+ } catch {
176
+ return Object.prototype.toString.call(value);
177
+ }
178
+ }
179
+
180
+ /** A development build's console note about an invalid value — once per call site. */
181
+ function warnInvalid(fn: string, value: unknown): void {
182
+ if (!devWarnings()) return;
183
+ const site = callSite();
184
+ const key = `${fn}|${site || typeof value}`;
185
+ if (warnedSites.has(key)) return;
186
+ warnedSites.add(key);
187
+ const type = Array.isArray(value) ? "array" : typeof value;
188
+ console.warn(
189
+ `[ui-kit] ${fn} got ${describeValue(value)} (${type}), which is not a number: printed the empty value. ` +
190
+ "Pass a number, a numeric string, or null/undefined for a missing value." +
191
+ (site ? `\n at ${site.replace(/^at /, "")}` : ""),
192
+ );
193
+ }
194
+
195
+ /**
196
+ * `value` as a finite number to format, or `undefined` to print `empty`. `null` and
197
+ * `undefined` are a missing value and pass quietly; anything that is neither a finite
198
+ * number nor a numeric string is an invalid one — ±Infinity included (lenkbank, adopting
199
+ * 0.13: a division by zero upstream; "∞" in a money column reads like data) — and a
200
+ * development build warns. `-0` is folded into `0`: zero carries no sign.
201
+ */
202
+ function toFormattable(fn: string, value: unknown): number | undefined {
203
+ if (value === null || value === undefined) return undefined;
204
+ let n: number | undefined;
205
+ if (typeof value === "number") n = value;
206
+ else if (typeof value === "string" && NUMERIC_STRING.test(value.trim())) n = Number(value.trim());
207
+ if (n === undefined || !Number.isFinite(n)) {
208
+ warnInvalid(fn, value);
209
+ return undefined;
210
+ }
211
+ return n + 0;
122
212
  }
123
213
 
124
214
  /** A number in `locale`: "1,234.5", "1.234,5", "1’234.5" — with `unit`, "3.400 N". */
125
- export function formatNumber(value: number | null | undefined, options: FormatNumberOptions = {}): string {
126
- if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;
127
- const figure = numberFormatter(options.locale, numberOptions(options)).format(value);
215
+ export function formatNumber(value: FormattableNumber, options: FormatNumberOptions = {}): string {
216
+ const n = toFormattable("formatNumber", value);
217
+ if (n === undefined) return options.empty ?? EMPTY_FORMATTED_VALUE;
218
+ const figure = formatWithMinus(numberFormatter(options.locale, numberOptions(options)), n, options.minus);
128
219
  return options.unit ? `${figure}${UNIT_SEPARATOR}${options.unit}` : figure;
129
220
  }
130
221
 
131
222
  /** An amount of `currency` (ISO 4217) in `locale`: "CHF 1’234.50", "1.234,50 €". */
132
- export function formatMoney(
133
- value: number | null | undefined,
134
- currency: string,
135
- options: FormatMoneyOptions = {},
136
- ): string {
137
- if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;
138
- return numberFormatter(options.locale, {
223
+ export function formatMoney(value: FormattableNumber, currency: string, options: FormatMoneyOptions = {}): string {
224
+ const n = toFormattable("formatMoney", value);
225
+ if (n === undefined) return options.empty ?? EMPTY_FORMATTED_VALUE;
226
+ const formatter = numberFormatter(options.locale, {
139
227
  style: "currency",
140
228
  currency,
141
229
  ...(options.currencyDisplay ? { currencyDisplay: options.currencyDisplay } : null),
142
230
  ...numberOptions(options),
143
- }).format(value);
231
+ });
232
+ return formatWithMinus(formatter, n, options.minus);
144
233
  }
145
234
 
146
235
  /** A percentage in `locale` ("12%", "12 %", "+1.25%"). See {@link FormatPercentOptions.ratio}
147
236
  * for what `value` means. Default digits: at most one. */
148
- export function formatPercent(value: number | null | undefined, options: FormatPercentOptions = {}): string {
149
- if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;
150
- const ratio = options.ratio === false ? value / 100 : value;
151
- return numberFormatter(options.locale, {
237
+ export function formatPercent(value: FormattableNumber, options: FormatPercentOptions = {}): string {
238
+ const n = toFormattable("formatPercent", value);
239
+ if (n === undefined) return options.empty ?? EMPTY_FORMATTED_VALUE;
240
+ const ratio = options.ratio === false ? n / 100 : n;
241
+ const formatter = numberFormatter(options.locale, {
152
242
  style: "percent",
153
243
  ...(options.digits === undefined ? { maximumFractionDigits: 1 } : null),
154
244
  ...numberOptions(options),
155
- }).format(ratio);
245
+ });
246
+ return formatWithMinus(formatter, ratio, options.minus);
156
247
  }
157
248
 
158
249
  /* ── Dates ─────────────────────────────────────────────────────────────────── */
@@ -327,13 +418,9 @@ type WithoutLocale<T> = Omit<T, "locale"> & { locale?: string };
327
418
  export interface KitFormat {
328
419
  /** The locale they format in — the provider's, or `undefined` (runtime default). */
329
420
  locale: string | undefined;
330
- formatNumber: (value: number | null | undefined, options?: WithoutLocale<FormatNumberOptions>) => string;
331
- formatMoney: (
332
- value: number | null | undefined,
333
- currency: string,
334
- options?: WithoutLocale<FormatMoneyOptions>,
335
- ) => string;
336
- formatPercent: (value: number | null | undefined, options?: WithoutLocale<FormatPercentOptions>) => string;
421
+ formatNumber: (value: FormattableNumber, options?: WithoutLocale<FormatNumberOptions>) => string;
422
+ formatMoney: (value: FormattableNumber, currency: string, options?: WithoutLocale<FormatMoneyOptions>) => string;
423
+ formatPercent: (value: FormattableNumber, options?: WithoutLocale<FormatPercentOptions>) => string;
337
424
  formatDate: (
338
425
  value: DateInput,
339
426
  style?: FormatDateStyle | Intl.DateTimeFormatOptions,
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The one minus sign (0.49, KIT-1). `Intl.NumberFormat` writes a negative with the
3
+ * HYPHEN-MINUS (U+002D) in nearly every locale — en, de-CH, fr, it, es, hu, zh — and
4
+ * with the typographic MINUS SIGN (U+2212) in a few (sv, fi). The kit prints U+2212
5
+ * everywhere, as `SignedAmount` always has: it is as wide as "+" and the tabular
6
+ * digits, so a column of signed figures lines up, and it does not break a line the way
7
+ * a hyphen may.
8
+ *
9
+ * `"ascii"` is the other way round, for text a machine reads back — a CSV, the
10
+ * clipboard: every minus becomes U+002D, whatever the locale wrote, so `Number()` and a
11
+ * spreadsheet parse it.
12
+ */
13
+ export type MinusStyle = "typographic" | "ascii";
14
+
15
+ /** U+2212 MINUS SIGN. */
16
+ export const TYPOGRAPHIC_MINUS = "\u2212";
17
+
18
+ /** The parts `formatToParts` writes a minus in (the exponent's, under `notation:
19
+ * "scientific"`, too). */
20
+ const MINUS_PARTS: ReadonlySet<string> = new Set(["minusSign", "exponentMinusSign"]);
21
+
22
+ /**
23
+ * `formatter.format(value)` with every minus sign in `minus` style. Only the parts
24
+ * `Intl` marks as a minus are touched — a hyphen inside a currency's name
25
+ * ("Bosnia-Herzegovina convertible marks") stays — and only when the text holds the
26
+ * other sign at all, so a positive figure costs no `formatToParts`.
27
+ *
28
+ * Public since 0.50 (the KIT-1 follow-up): an app that keeps its OWN `Intl.NumberFormat`
29
+ * wrapper (a currency with app-specific digits, a quantity) calls it in place of
30
+ * `formatter.format(value)`, so its negatives match the kit's "−". Where the wrapper only
31
+ * re-states `formatMoney` / `formatNumber`, route it through those instead.
32
+ *
33
+ * @example
34
+ * const eur = new Intl.NumberFormat("en", { style: "currency", currency: "EUR" });
35
+ * formatWithMinus(eur, -150); // "−€150.00"
36
+ * formatWithMinus(eur, -150, "ascii"); // "-€150.00", for a CSV
37
+ */
38
+ export function formatWithMinus(
39
+ formatter: Intl.NumberFormat,
40
+ value: number,
41
+ minus: MinusStyle = "typographic",
42
+ ): string {
43
+ const text = formatter.format(value);
44
+ const [wanted, other] = minus === "ascii" ? ["-", TYPOGRAPHIC_MINUS] : [TYPOGRAPHIC_MINUS, "-"];
45
+ if (!text.includes(other)) return text;
46
+ return formatter
47
+ .formatToParts(value)
48
+ .map((part) => (MINUS_PARTS.has(part.type) ? wanted : part.value))
49
+ .join("");
50
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * What a status means, in five domain-free words, and the tone that says it (0.49 KIT-8).
3
+ *
4
+ * Every app paints its statuses by hand: kastlan's ~15 enums each carry a Chip tone
5
+ * (`shared/enums/enum-meta.ts`), keksdose's job and transaction states theirs, and the
6
+ * same meaning drifted into different colours — "unpaid" amber in one table, brand in
7
+ * the next. The kind is the decision ("does the reader have to act?"), the tone only
8
+ * its colour, so an app maps each of its states to a kind once and the colours agree
9
+ * everywhere:
10
+ *
11
+ * | kind | tone | for |
12
+ * |---|---|---|
13
+ * | `good` | `success` | done, settled, healthy: nothing to do (Paid, Verified) |
14
+ * | `attention` | `warning` | needs a look or an action soon, nothing broken yet (Unpaid, Due soon) |
15
+ * | `problem` | `danger` | something is wrong or late and needs action now (Overdue, Failed) |
16
+ * | `active` | `info` | in progress or current, neither good nor bad (Running, Sent) |
17
+ * | `inactive` | `neutral` | over, paused or not started: out of the way (Draft, Archived) |
18
+ *
19
+ * `brand` is not a status tone: it is the app's own colour, and a status in it reads as
20
+ * "this is the app" rather than as a state. Money (`income` / `expense`) is not one
21
+ * either: it says which way an amount flows, not how a record stands.
22
+ */
23
+ export type StatusKind = "good" | "attention" | "problem" | "active" | "inactive";
24
+
25
+ /** The tones a {@link StatusKind} maps to — a subset of `ChipTone` and `StatusDotTone`. */
26
+ export type StatusTone = "success" | "warning" | "danger" | "info" | "neutral";
27
+
28
+ /** The five kinds, in the order the table above lists them. */
29
+ export const STATUS_KINDS: readonly StatusKind[] = ["good", "attention", "problem", "active", "inactive"];
30
+
31
+ const TONES: Readonly<Record<StatusKind, StatusTone>> = {
32
+ good: "success",
33
+ attention: "warning",
34
+ problem: "danger",
35
+ active: "info",
36
+ inactive: "neutral",
37
+ };
38
+
39
+ /** The tone for a status kind: good → success, attention → warning, problem → danger,
40
+ * active → info, inactive → neutral. See {@link StatusKind}. */
41
+ export function statusTone(kind: StatusKind): StatusTone {
42
+ return TONES[kind];
43
+ }
@@ -323,6 +323,7 @@ export function SettingsLayout<G extends string>({
323
323
  <div className={pageClass} data-settings-view="group">
324
324
  <PageHeader
325
325
  size="compact"
326
+ documentTitle={false}
326
327
  title={open.title}
327
328
  description={open.help}
328
329
  breadcrumbs={
@@ -347,7 +348,7 @@ export function SettingsLayout<G extends string>({
347
348
  </div>
348
349
  ) : (
349
350
  <div className={pageClass} data-settings-view="list">
350
- <PageHeader size="compact" title={pageTitle} />
351
+ <PageHeader size="compact" documentTitle={false} title={pageTitle} />
351
352
  {searchField}
352
353
  {searching ? (
353
354
  results
@@ -409,7 +410,7 @@ export function SettingsLayout<G extends string>({
409
410
  );
410
411
  body = (
411
412
  <div className={pageClass} data-settings-view="desktop">
412
- <PageHeader size="compact" title={pageTitle} />
413
+ <PageHeader size="compact" documentTitle={false} title={pageTitle} />
413
414
  {searchField}
414
415
  <div className="md:grid md:grid-cols-[14rem_minmax(0,1fr)] md:gap-6">
415
416
  {withSidebarFooter(
@@ -4,6 +4,7 @@ import { Globe } from "lucide-react";
4
4
  import { Disclosure } from "../components/disclosure";
5
5
  import { MenuItem } from "../components/menu-item";
6
6
  import { DEFAULT_TOP_BAR_LABELS, useKitLabels } from "../i18n/kit-labels";
7
+ import { languageCodeLabel } from "./language-code";
7
8
 
8
9
  /** One offered language in the account menu's sub-list. */
9
10
  export interface AccountMenuLanguageOption {
@@ -56,7 +57,7 @@ export interface AccountMenuLanguageProps {
56
57
  /**
57
58
  * The language choice INSIDE the account menu (docs/top-bar-harmonization.md §2.3),
58
59
  * keksdose's sub-list: a `Disclosure variant="menu"` row — the globe, "Language", the
59
- * active language's flag and the chevron — that opens in place onto one checked
60
+ * active language's flag and code ("DE") and the chevron — that opens in place onto one checked
60
61
  * `MenuItem` per language. Inline rather than a nested popover: a popover inside a
61
62
  * popover fights the outside click and has nowhere to go on a phone (keksdose #140).
62
63
  *
@@ -87,7 +88,18 @@ export function AccountMenuLanguage({
87
88
  <Globe aria-hidden className="size-4" />
88
89
  {label ?? words.language}
89
90
  </span>
90
- {active && flagOf(active)}
91
+ {active && (
92
+ // The flag alone could not tell de-CH, fr-CH and it-CH apart (one Swiss
93
+ // flag each), so the code sits beside it (0.49 KIT-10); the row's name
94
+ // carries the language's own name.
95
+ <span className="flex items-center gap-1.5">
96
+ {flagOf(active)}
97
+ <span aria-hidden className="text-xs font-semibold tabular-nums tracking-wide text-secondary">
98
+ {languageCodeLabel(active.code)}
99
+ </span>
100
+ <span className="sr-only">{active.label}</span>
101
+ </span>
102
+ )}
91
103
  </span>
92
104
  }
93
105
  >
@@ -7,4 +7,5 @@ import type { AppShellMoreLabels } from "./app-shell";
7
7
  export const DEFAULT_APP_SHELL_MORE_LABELS: AppShellMoreLabels = {
8
8
  more: "More",
9
9
  moreTitle: "More pages",
10
+ overview: "Overview",
10
11
  };