@lerianstudio/sindarian-ui 2.0.0-beta.25 → 2.0.0-beta.26

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 (305) hide show
  1. package/dist/charts/chart.js +4 -231
  2. package/dist/charts/index.js +1 -11
  3. package/dist/components/breadcrumb/get-breadcrumb-paths.js +1 -18
  4. package/dist/components/breadcrumb/index.js +1 -37
  5. package/dist/components/card/account-card/index.js +1 -69
  6. package/dist/components/card/basic-card/index.js +1 -20
  7. package/dist/components/card/dashboard-card/index.js +1 -20
  8. package/dist/components/card/entity-card/index.js +1 -68
  9. package/dist/components/card/index.js +1 -20
  10. package/dist/components/dialog/confirmation-dialog/index.js +1 -96
  11. package/dist/components/dialog/confirmation-dialog/use-confirm-dialog.js +1 -55
  12. package/dist/components/dialog/index.js +1 -19
  13. package/dist/components/dialog/large-dialog/index.js +1 -21
  14. package/dist/components/entity-box/index.js +1 -67
  15. package/dist/components/entity-data-table/index.js +1 -20
  16. package/dist/components/form/combo-box-field/index.js +1 -35
  17. package/dist/components/form/country-field/index.js +1 -11
  18. package/dist/components/form/date-picker-field/index.js +1 -83
  19. package/dist/components/form/date-range-field/index.js +1 -58
  20. package/dist/components/form/date-time-picker-field/index.js +1 -114
  21. package/dist/components/form/file-upload-field/index.js +1 -69
  22. package/dist/components/form/index.js +1 -31
  23. package/dist/components/form/input-field/index.js +1 -96
  24. package/dist/components/form/otp-field/index.js +1 -19
  25. package/dist/components/form/pagination-limit-field/index.js +1 -10
  26. package/dist/components/form/password-field/index.js +1 -16
  27. package/dist/components/form/radio-group-field/index.js +1 -36
  28. package/dist/components/form/renderable-label.js +1 -57
  29. package/dist/components/form/select-field/index.js +1 -68
  30. package/dist/components/form/state-field/index.js +1 -42
  31. package/dist/components/form/switch-field/index.js +1 -16
  32. package/dist/components/form/textarea-field/index.js +1 -23
  33. package/dist/components/page/index.js +1 -20
  34. package/dist/components/page-header/index.js +1 -80
  35. package/dist/components/pagination/index.js +1 -10
  36. package/dist/components/table/id-table-cell.js +1 -109
  37. package/dist/components/table/index.js +1 -19
  38. package/dist/components/table/locked-table-actions.js +1 -41
  39. package/dist/components/table/name-table-cell.js +1 -58
  40. package/dist/components/ui/accordion/index.js +1 -62
  41. package/dist/components/ui/alert/index.js +1 -45
  42. package/dist/components/ui/alert-dialog/index.js +1 -84
  43. package/dist/components/ui/autocomplete/index.js +1 -440
  44. package/dist/components/ui/autosize-textarea/index.js +1 -89
  45. package/dist/components/ui/avatar/index.js +1 -51
  46. package/dist/components/ui/badge/index.js +1 -83
  47. package/dist/components/ui/breadcrumb/index.js +1 -35
  48. package/dist/components/ui/button/index.js +1 -65
  49. package/dist/components/ui/calendar/index.js +1 -118
  50. package/dist/components/ui/card/index.js +1 -32
  51. package/dist/components/ui/checkbox/index.js +1 -44
  52. package/dist/components/ui/collapsible/index.js +1 -50
  53. package/dist/components/ui/command/index.js +1 -95
  54. package/dist/components/ui/copy-field/index.js +1 -163
  55. package/dist/components/ui/dialog/index.js +1 -80
  56. package/dist/components/ui/dropdown-menu/index.js +1 -110
  57. package/dist/components/ui/field/index.js +1 -82
  58. package/dist/components/ui/file-upload/index.js +1 -574
  59. package/dist/components/ui/form.js +1 -162
  60. package/dist/components/ui/hover-card/index.js +1 -51
  61. package/dist/components/ui/icon-button/index.js +1 -35
  62. package/dist/components/ui/input/adornment.js +1 -21
  63. package/dist/components/ui/input/index.js +1 -99
  64. package/dist/components/ui/label/index.js +1 -45
  65. package/dist/components/ui/lerian-wordmark/index.js +1 -7
  66. package/dist/components/ui/loading-button/index.js +1 -9
  67. package/dist/components/ui/multiple-select/index.js +1 -235
  68. package/dist/components/ui/otp-input/index.js +1 -62
  69. package/dist/components/ui/paper/index.js +1 -8
  70. package/dist/components/ui/paper-collapsible/index.js +1 -24
  71. package/dist/components/ui/popover/index.js +1 -55
  72. package/dist/components/ui/progress/index.js +1 -66
  73. package/dist/components/ui/qr-code/index.js +1 -27
  74. package/dist/components/ui/radio-group/index.js +1 -48
  75. package/dist/components/ui/scroll-area/index.js +1 -49
  76. package/dist/components/ui/select/index.js +1 -119
  77. package/dist/components/ui/separator/index.js +1 -43
  78. package/dist/components/ui/sheet/index.js +1 -136
  79. package/dist/components/ui/sidebar/index.js +1 -27
  80. package/dist/components/ui/sidebar/next-router.cjs +1 -119
  81. package/dist/components/ui/sidebar/sidebar-back-button.js +1 -8
  82. package/dist/components/ui/sidebar/sidebar-components.js +1 -23
  83. package/dist/components/ui/sidebar/sidebar-expand-button.js +1 -49
  84. package/dist/components/ui/sidebar/sidebar-item-button.js +1 -21
  85. package/dist/components/ui/sidebar/sidebar-item-collapsible.js +1 -52
  86. package/dist/components/ui/sidebar/sidebar-item-icon-button.js +1 -18
  87. package/dist/components/ui/sidebar/sidebar-item.js +1 -44
  88. package/dist/components/ui/sidebar/sidebar-provider.js +1 -201
  89. package/dist/components/ui/sidebar/sidebar-root.js +1 -130
  90. package/dist/components/ui/sidebar/sidebar-router.js +1 -126
  91. package/dist/components/ui/sidebar/sidebar-trigger.js +1 -57
  92. package/dist/components/ui/skeleton/index.js +1 -8
  93. package/dist/components/ui/stepper/index.js +1 -29
  94. package/dist/components/ui/switch/index.js +1 -43
  95. package/dist/components/ui/table/index.js +1 -56
  96. package/dist/components/ui/tabs/index.js +1 -55
  97. package/dist/components/ui/textarea/index.js +1 -8
  98. package/dist/components/ui/time-picker/index.js +1 -85
  99. package/dist/components/ui/toast/index.js +1 -3
  100. package/dist/components/ui/toast/toaster.js +1 -16
  101. package/dist/components/ui/toggle-group/index.js +1 -156
  102. package/dist/components/ui/tooltip/index.js +1 -81
  103. package/dist/domain/blotter/index.js +1 -20
  104. package/dist/domain/delinquency-aging/aging-buckets.js +1 -120
  105. package/dist/domain/delinquency-aging/index.js +1 -222
  106. package/dist/domain/figure/index.js +1 -19
  107. package/dist/domain/format.js +1 -137
  108. package/dist/domain/index.js +1 -43
  109. package/dist/domain/key-id/index.js +1 -174
  110. package/dist/domain/ledger-sheet/index.js +1 -28
  111. package/dist/domain/money-diff.js +1 -48
  112. package/dist/domain/money-math.js +1 -104
  113. package/dist/domain/money-text/index.js +1 -123
  114. package/dist/domain/section-label/index.js +1 -17
  115. package/dist/domain/status-rail/index.js +1 -76
  116. package/dist/domain/threshold-gauge/index.js +1 -161
  117. package/dist/enterprise/alert-banner/index.js +1 -24
  118. package/dist/enterprise/app-shell/index.js +1 -87
  119. package/dist/enterprise/cursor-pager/index.js +1 -26
  120. package/dist/enterprise/data-table/column-size.js +1 -103
  121. package/dist/enterprise/data-table/index.js +1 -345
  122. package/dist/enterprise/date-range-picker/index.js +1 -115
  123. package/dist/enterprise/detail-panel/index.js +1 -20
  124. package/dist/enterprise/empty-state/index.js +1 -14
  125. package/dist/enterprise/index.js +1 -39
  126. package/dist/enterprise/number-input/index.js +1 -252
  127. package/dist/enterprise/search-input/index.js +1 -121
  128. package/dist/enterprise/stat-card/index.js +1 -75
  129. package/dist/enterprise/status-badge/index.js +1 -120
  130. package/dist/enterprise/use-is-mobile.js +1 -68
  131. package/dist/enterprise/virtualized-table/index.js +1 -141
  132. package/dist/esm/charts/chart.js +4 -190
  133. package/dist/esm/charts/index.js +1 -3
  134. package/dist/esm/components/breadcrumb/get-breadcrumb-paths.js +1 -12
  135. package/dist/esm/components/breadcrumb/index.js +1 -30
  136. package/dist/esm/components/card/account-card/index.js +1 -53
  137. package/dist/esm/components/card/basic-card/index.js +1 -14
  138. package/dist/esm/components/card/dashboard-card/index.js +1 -14
  139. package/dist/esm/components/card/entity-card/index.js +1 -52
  140. package/dist/esm/components/card/index.js +1 -4
  141. package/dist/esm/components/dialog/confirmation-dialog/index.js +1 -60
  142. package/dist/esm/components/dialog/confirmation-dialog/use-confirm-dialog.js +1 -49
  143. package/dist/esm/components/dialog/index.js +1 -3
  144. package/dist/esm/components/dialog/large-dialog/index.js +1 -16
  145. package/dist/esm/components/entity-box/index.js +1 -56
  146. package/dist/esm/components/entity-data-table/index.js +1 -17
  147. package/dist/esm/components/form/combo-box-field/index.js +1 -28
  148. package/dist/esm/components/form/country-field/index.js +1 -7
  149. package/dist/esm/components/form/date-picker-field/index.js +1 -43
  150. package/dist/esm/components/form/date-range-field/index.js +1 -18
  151. package/dist/esm/components/form/date-time-picker-field/index.js +1 -74
  152. package/dist/esm/components/form/file-upload-field/index.js +1 -65
  153. package/dist/esm/components/form/index.js +1 -15
  154. package/dist/esm/components/form/input-field/index.js +1 -92
  155. package/dist/esm/components/form/otp-field/index.js +1 -15
  156. package/dist/esm/components/form/pagination-limit-field/index.js +1 -6
  157. package/dist/esm/components/form/password-field/index.js +1 -10
  158. package/dist/esm/components/form/radio-group-field/index.js +1 -32
  159. package/dist/esm/components/form/renderable-label.js +1 -54
  160. package/dist/esm/components/form/select-field/index.js +1 -64
  161. package/dist/esm/components/form/state-field/index.js +1 -35
  162. package/dist/esm/components/form/switch-field/index.js +1 -12
  163. package/dist/esm/components/form/textarea-field/index.js +1 -19
  164. package/dist/esm/components/page/index.js +1 -15
  165. package/dist/esm/components/page-header/index.js +1 -71
  166. package/dist/esm/components/pagination/index.js +1 -6
  167. package/dist/esm/components/table/id-table-cell.js +1 -105
  168. package/dist/esm/components/table/index.js +1 -3
  169. package/dist/esm/components/table/locked-table-actions.js +1 -37
  170. package/dist/esm/components/table/name-table-cell.js +1 -54
  171. package/dist/esm/components/ui/accordion/index.js +1 -24
  172. package/dist/esm/components/ui/alert/index.js +1 -38
  173. package/dist/esm/components/ui/alert-dialog/index.js +1 -39
  174. package/dist/esm/components/ui/autocomplete/index.js +1 -392
  175. package/dist/esm/components/ui/autosize-textarea/index.js +1 -51
  176. package/dist/esm/components/ui/avatar/index.js +1 -14
  177. package/dist/esm/components/ui/badge/index.js +1 -79
  178. package/dist/esm/components/ui/breadcrumb/index.js +1 -27
  179. package/dist/esm/components/ui/button/index.js +1 -60
  180. package/dist/esm/components/ui/calendar/index.js +1 -82
  181. package/dist/esm/components/ui/card/index.js +1 -24
  182. package/dist/esm/components/ui/checkbox/index.js +1 -9
  183. package/dist/esm/components/ui/collapsible/index.js +1 -13
  184. package/dist/esm/components/ui/command/index.js +1 -51
  185. package/dist/esm/components/ui/copy-field/index.js +1 -127
  186. package/dist/esm/components/ui/dialog/index.js +1 -36
  187. package/dist/esm/components/ui/dropdown-menu/index.js +1 -60
  188. package/dist/esm/components/ui/field/index.js +1 -71
  189. package/dist/esm/components/ui/file-upload/index.js +1 -536
  190. package/dist/esm/components/ui/form.js +1 -118
  191. package/dist/esm/components/ui/hover-card/index.js +1 -14
  192. package/dist/esm/components/ui/icon-button/index.js +1 -31
  193. package/dist/esm/components/ui/input/adornment.js +1 -17
  194. package/dist/esm/components/ui/input/index.js +1 -60
  195. package/dist/esm/components/ui/label/index.js +1 -10
  196. package/dist/esm/components/ui/lerian-wordmark/index.js +1 -4
  197. package/dist/esm/components/ui/loading-button/index.js +1 -7
  198. package/dist/esm/components/ui/multiple-select/index.js +1 -196
  199. package/dist/esm/components/ui/otp-input/index.js +1 -21
  200. package/dist/esm/components/ui/paper/index.js +1 -5
  201. package/dist/esm/components/ui/paper-collapsible/index.js +1 -19
  202. package/dist/esm/components/ui/popover/index.js +1 -17
  203. package/dist/esm/components/ui/progress/index.js +1 -31
  204. package/dist/esm/components/ui/qr-code/index.js +1 -24
  205. package/dist/esm/components/ui/radio-group/index.js +1 -12
  206. package/dist/esm/components/ui/scroll-area/index.js +1 -13
  207. package/dist/esm/components/ui/select/index.js +1 -74
  208. package/dist/esm/components/ui/separator/index.js +1 -8
  209. package/dist/esm/components/ui/sheet/index.js +1 -92
  210. package/dist/esm/components/ui/sidebar/index.js +1 -11
  211. package/dist/esm/components/ui/sidebar/next-router.cjs +1 -119
  212. package/dist/esm/components/ui/sidebar/sidebar-back-button.js +1 -5
  213. package/dist/esm/components/ui/sidebar/sidebar-components.js +1 -15
  214. package/dist/esm/components/ui/sidebar/sidebar-expand-button.js +1 -42
  215. package/dist/esm/components/ui/sidebar/sidebar-item-button.js +1 -18
  216. package/dist/esm/components/ui/sidebar/sidebar-item-collapsible.js +1 -44
  217. package/dist/esm/components/ui/sidebar/sidebar-item-icon-button.js +1 -14
  218. package/dist/esm/components/ui/sidebar/sidebar-item.js +1 -40
  219. package/dist/esm/components/ui/sidebar/sidebar-provider.js +1 -193
  220. package/dist/esm/components/ui/sidebar/sidebar-root.js +1 -126
  221. package/dist/esm/components/ui/sidebar/sidebar-router.js +1 -89
  222. package/dist/esm/components/ui/sidebar/sidebar-trigger.js +1 -53
  223. package/dist/esm/components/ui/skeleton/index.js +1 -6
  224. package/dist/esm/components/ui/stepper/index.js +1 -20
  225. package/dist/esm/components/ui/switch/index.js +1 -8
  226. package/dist/esm/components/ui/table/index.js +1 -44
  227. package/dist/esm/components/ui/tabs/index.js +1 -17
  228. package/dist/esm/components/ui/textarea/index.js +1 -6
  229. package/dist/esm/components/ui/time-picker/index.js +1 -49
  230. package/dist/esm/components/ui/toast/index.js +1 -2
  231. package/dist/esm/components/ui/toast/toaster.js +1 -13
  232. package/dist/esm/components/ui/toggle-group/index.js +1 -120
  233. package/dist/esm/components/ui/tooltip/index.js +1 -43
  234. package/dist/esm/domain/blotter/index.js +1 -16
  235. package/dist/esm/domain/delinquency-aging/aging-buckets.js +1 -115
  236. package/dist/esm/domain/delinquency-aging/index.js +1 -218
  237. package/dist/esm/domain/figure/index.js +1 -15
  238. package/dist/esm/domain/format.js +1 -128
  239. package/dist/esm/domain/index.js +1 -19
  240. package/dist/esm/domain/key-id/index.js +1 -169
  241. package/dist/esm/domain/ledger-sheet/index.js +1 -24
  242. package/dist/esm/domain/money-diff.js +1 -45
  243. package/dist/esm/domain/money-math.js +1 -98
  244. package/dist/esm/domain/money-text/index.js +1 -118
  245. package/dist/esm/domain/section-label/index.js +1 -14
  246. package/dist/esm/domain/status-rail/index.js +1 -71
  247. package/dist/esm/domain/threshold-gauge/index.js +1 -157
  248. package/dist/esm/enterprise/alert-banner/index.js +1 -21
  249. package/dist/esm/enterprise/app-shell/index.js +1 -51
  250. package/dist/esm/enterprise/cursor-pager/index.js +1 -23
  251. package/dist/esm/enterprise/data-table/column-size.js +1 -97
  252. package/dist/esm/enterprise/data-table/index.js +1 -342
  253. package/dist/esm/enterprise/date-range-picker/index.js +1 -112
  254. package/dist/esm/enterprise/detail-panel/index.js +1 -17
  255. package/dist/esm/enterprise/empty-state/index.js +1 -11
  256. package/dist/esm/enterprise/index.js +1 -22
  257. package/dist/esm/enterprise/number-input/index.js +1 -214
  258. package/dist/esm/enterprise/search-input/index.js +1 -85
  259. package/dist/esm/enterprise/stat-card/index.js +1 -72
  260. package/dist/esm/enterprise/status-badge/index.js +1 -116
  261. package/dist/esm/enterprise/use-is-mobile.js +1 -31
  262. package/dist/esm/enterprise/virtualized-table/index.js +1 -138
  263. package/dist/esm/hooks/use-click-away.js +1 -22
  264. package/dist/esm/hooks/use-pagination.js +1 -38
  265. package/dist/esm/hooks/use-stepper.js +1 -22
  266. package/dist/esm/hooks/use-time.js +1 -13
  267. package/dist/esm/hooks/use-toast.js +1 -107
  268. package/dist/esm/index.js +1 -77
  269. package/dist/esm/lib/storage/get-storage-object.js +1 -17
  270. package/dist/esm/lib/storage/get-storage.js +1 -14
  271. package/dist/esm/lib/storage/index.js +1 -2
  272. package/dist/esm/lib/typography.js +1 -31
  273. package/dist/esm/lib/utils.js +1 -41
  274. package/dist/esm/theme/get-theme-script.js +1 -41
  275. package/dist/esm/theme/index.js +1 -4
  276. package/dist/esm/theme/mode-toggle.js +1 -45
  277. package/dist/esm/theme/theme-provider.js +1 -122
  278. package/dist/esm/toast/helpers.js +1 -36
  279. package/dist/esm/toast/index.js +1 -4
  280. package/dist/esm/types/metadata-type.js +1 -1
  281. package/dist/esm/utils/countries.cjs +1 -17
  282. package/dist/esm/utils/country-utils.js +1 -18
  283. package/dist/esm/utils/currency-codes.js +1 -181
  284. package/dist/hooks/use-click-away.js +1 -29
  285. package/dist/hooks/use-pagination.js +1 -41
  286. package/dist/hooks/use-stepper.js +1 -26
  287. package/dist/hooks/use-time.js +1 -20
  288. package/dist/hooks/use-toast.js +1 -111
  289. package/dist/index.js +1 -96
  290. package/dist/lib/storage/get-storage-object.js +1 -23
  291. package/dist/lib/storage/get-storage.js +1 -17
  292. package/dist/lib/storage/index.js +1 -18
  293. package/dist/lib/typography.js +1 -34
  294. package/dist/lib/utils.js +1 -45
  295. package/dist/theme/get-theme-script.js +1 -44
  296. package/dist/theme/index.js +1 -11
  297. package/dist/theme/mode-toggle.js +1 -48
  298. package/dist/theme/theme-provider.js +1 -160
  299. package/dist/toast/helpers.js +1 -41
  300. package/dist/toast/index.js +1 -10
  301. package/dist/types/metadata-type.js +1 -2
  302. package/dist/utils/countries.cjs +1 -17
  303. package/dist/utils/country-utils.js +1 -25
  304. package/dist/utils/currency-codes.js +1 -184
  305. package/package.json +4 -3
@@ -1,218 +1 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- /**
3
- * DelinquencyAging — a loan/receivable delinquency surface: the ordered overdue
4
- * aging distribution PLUS the headline delinquency rate (the share of portfolio
5
- * value that is past due).
6
- *
7
- * <DelinquencyAging
8
- * currency="BRL"
9
- * locale="pt-BR"
10
- * warn={0.05}
11
- * breach={0.1}
12
- * buckets={[
13
- * { label: 'A vencer', count: 312, total: '1840500.00', overdue: false },
14
- * { label: '1–30', count: 84, total: '402300.00', overdue: true },
15
- * { label: '31–60', count: 21, total: '118900.00', overdue: true },
16
- * { label: '61–90', count: 9, total: '54200.00', overdue: true },
17
- * { label: '90+', count: 4, total: '38100.00', overdue: true }
18
- * ]}
19
- * />
20
- *
21
- * The distribution is delegated whole to `AgingBuckets`: it owns the ordered
22
- * current → 90+ schedule, the per-bucket count/total, the ordinal band cue, and
23
- * the money-math-exact grand total. This composite adds only the one fact aging
24
- * alone does not state — the delinquency RATE.
25
- *
26
- * THIRD RAIL (money): the rate is overdueTotal / portfolioTotal, and BOTH totals
27
- * are summed in integer minor units (BigInt, at the currency's CLDR scale) via
28
- * `money-math` — never an IEEE-754 sum of decimals (0.1 + 0.2 ≠ 0.3 in float, a
29
- * silent basis-point error on a portfolio). The single floating-point operation
30
- * is the FINAL division of two already-exact integers, which is the correct,
31
- * lossless-enough place for it (a ratio is not money). A zero portfolio yields a
32
- * null rate — an explicit empty readout, never a NaN. A single unparseable
33
- * bucket total poisons the rate to indeterminate rather than reporting a wrong
34
- * share.
35
- *
36
- * NON-COLOR CUE (WCAG 1.4.1): the rate band (healthy / elevated / distressed) is
37
- * resolved through the gauge's strict-edge `gaugeBand` (higher-is-worse: more
38
- * overdue is worse) and named with a glyph + an sr-only word + a tint — the tint
39
- * is reinforcement only, so the band survives grayscale and color-vision
40
- * deficiency. A distressed rate reads loud (credit-red, role='alert').
41
- *
42
- * Pure display: no directive, no interactivity, server-safe for RSC.
43
- */
44
- import { AlertOctagon, ShieldCheck, TriangleAlert } from 'lucide-react';
45
- import { LABEL_VOICE_CLASS } from '../../lib/typography.js';
46
- import { cn } from '../../lib/utils.js';
47
- import { Figure } from '../figure/index.js';
48
- import { formatPercent, NO_VALUE } from '../format.js';
49
- import { minorDigitsOf, toMinor } from '../money-math.js';
50
- import { SectionLabel } from '../section-label/index.js';
51
- import { AgingBuckets } from './aging-buckets.js';
52
- /** Fixed-point scale for the rate division: 1e12 keeps twelve decimal places of
53
- * a 0..1 ratio, and both it and any in-range scaled quotient stay well inside
54
- * the exact-integer range of a double (2^53), so the conversion is lossless. */
55
- const RATE_PRECISION = 1000000000000n;
56
- /**
57
- * THIRD RAIL: the delinquency rate = overdue total / portfolio total, with both
58
- * totals summed through integer minor units (BigInt) at `scale`. The only float
59
- * operation is the final division of two EXACT integers — never a float sum of
60
- * decimals. A zero portfolio gives a null rate (explicit empty, no NaN); an
61
- * unparseable total poisons everything to indeterminate. Pure: unit-tested.
62
- *
63
- * Internal: exported for its unit tests, never from `src/domain/index.ts`.
64
- */
65
- export function delinquencyRate(buckets, scale) {
66
- // Parse each bucket exactly once: the sums and the per-bucket SIGN check both
67
- // come off the same pass, so the two can never disagree about the same input.
68
- const parsed = buckets.map((b) => toMinor(b.total, scale));
69
- if (parsed.some((m) => m === null)) {
70
- return {
71
- rate: null,
72
- overdueMinor: null,
73
- totalMinor: null,
74
- indeterminate: true
75
- };
76
- }
77
- let totalMinor = 0n;
78
- let overdueMinor = 0n;
79
- let hasNegativeBucket = false;
80
- parsed.forEach((m, i) => {
81
- const minor = m;
82
- if (minor < 0n)
83
- hasNegativeBucket = true;
84
- totalMinor += minor;
85
- if (buckets[i].overdue)
86
- overdueMinor += minor;
87
- });
88
- // A NEGATIVE bucket total is parseable but not interpretable as a share of a
89
- // portfolio: overdue/total stops being bounded by 0..1 the moment a component
90
- // is negative (a -50 current band against a 100 overdue band yields "200%
91
- // delinquent"), and it can also cancel the denominator toward zero. A credit
92
- // adjustment is real data, but it is not aging data, so the honest answer is
93
- // that the RATE cannot be determined — the sums stay, since they are still
94
- // correct, and the readout takes the no-value path. Legacy divided anyway and
95
- // printed the out-of-range percentage.
96
- if (hasNegativeBucket) {
97
- return { rate: null, overdueMinor, totalMinor, indeterminate: true };
98
- }
99
- // A zero portfolio has no rate to report — null, never overdueMinor/0 (NaN).
100
- if (totalMinor === 0n) {
101
- return { rate: null, overdueMinor, totalMinor, indeterminate: false };
102
- }
103
- // The division is done as a BOUNDED BigInt ratio first, then converted once.
104
- // Converting the two totals to Number separately overflowed on a large
105
- // portfolio: past ~1.8e308 each side becomes Infinity and Infinity/Infinity is
106
- // NaN — a portfolio big enough to matter reported no rate at all. Scaling the
107
- // numerator by RATE_PRECISION keeps the quotient the size of a ratio (not the
108
- // size of the money), so the single Number() sees a value it can hold exactly.
109
- const scaled = (overdueMinor * RATE_PRECISION) / totalMinor;
110
- const rate = Number(scaled) / Number(RATE_PRECISION);
111
- // A ratio that still cannot be represented is no rate at all — the readout
112
- // shows the no-value placeholder rather than a NaN/Infinity artifact.
113
- if (!Number.isFinite(rate)) {
114
- return { rate: null, overdueMinor, totalMinor, indeterminate: false };
115
- }
116
- return { rate, overdueMinor, totalMinor, indeterminate: false };
117
- }
118
- /** Fixed-point scale for comparing the ratio against a float threshold as exact
119
- * integers. 1e18 is well past the ~1e-16 relative precision a double carries,
120
- * so the comparison is exact to the limit of the threshold value itself. */
121
- const THRESHOLD_SCALE = 10n ** 18n;
122
- /**
123
- * THIRD RAIL: `overdue / total > threshold`, decided on EXACT integers.
124
- *
125
- * The float `rate` is a TRUNCATED view of the ratio — RATE_PRECISION drops every
126
- * digit past 1e-12 — so a rate a hair ABOVE a threshold rounds down onto it, and
127
- * the strict-edge rule then reads it as the calmer band. 500000000001 /
128
- * 10000000000000 is 0.0500000000001, genuinely past a 0.05 warn edge, and
129
- * truncates to exactly 0.05, which reported "Saudável" on a portfolio that had
130
- * crossed into "Elevada". Cross-multiplying keeps the comparison in integers,
131
- * where no digit is dropped.
132
- */
133
- function ratioExceeds(overdue, total, threshold) {
134
- // A non-finite threshold is no threshold: nothing can be past it.
135
- if (!Number.isFinite(threshold))
136
- return false;
137
- // Scaling overflows to ±Infinity for a threshold that is finite but huge —
138
- // 1e300 is enough, MAX_VALUE certainly — and `BigInt(Infinity)` throws a
139
- // RangeError, mid-render, taking the surface down. Both overflow directions
140
- // have an EXACT answer, so this degrades to the right result rather than a
141
- // merely safe one: an overflowed positive threshold is one no 0..1 ratio could
142
- // ever reach (not exceeded), an overflowed negative one sits below every ratio
143
- // (always exceeded).
144
- const scaled = Math.round(threshold * Number(THRESHOLD_SCALE));
145
- if (!Number.isFinite(scaled))
146
- return scaled < 0;
147
- let scaledInt = BigInt(scaled);
148
- // A threshold FINER than one unit of the scale rounds to zero, and a zero
149
- // threshold means "anything above zero is past it" — the exact opposite of what
150
- // a tiny positive threshold asks for. 4e-19 scaled to 0, so a ratio of 1e-19
151
- // was reported as exceeding it. Clamp to one unit instead, keeping the sign, so
152
- // a sub-scale threshold behaves as the finest one this comparison supports.
153
- //
154
- // ponytail: the floor is 1e-18 (THRESHOLD_SCALE), so a threshold between 0 and
155
- // 1e-18 is treated as 1e-18 and a ratio in that sliver reads calmer than the
156
- // literal number asks. Immaterial for a delinquency ratio — a portfolio would
157
- // need ~1e18 currency units for one minor unit to land there — and the honest
158
- // alternative is exact rational threshold conversion, which buys nothing real.
159
- // An EXACT zero is left alone: every positive ratio does exceed it.
160
- if (scaledInt === 0n && threshold !== 0) {
161
- scaledInt = threshold < 0 ? -1n : 1n;
162
- }
163
- return overdue * THRESHOLD_SCALE > scaledInt * total;
164
- }
165
- /**
166
- * The rate band, resolved from the exact minor-unit ratio. Mirrors `gaugeBand`'s
167
- * higher-is-worse STRICT-edge canon (a ratio exactly ON a threshold stays in the
168
- * calmer band) rather than calling it: `gaugeBand` takes a float, and handing it
169
- * the truncated rate is precisely the defect above.
170
- */
171
- function exactBand(overdue, total, warn, breach) {
172
- if (ratioExceeds(overdue, total, breach))
173
- return 'breach';
174
- if (ratioExceeds(overdue, total, warn))
175
- return 'warn';
176
- return 'low';
177
- }
178
- /** Band → glyph + tint + accessible word. The glyph and sr-only word are the
179
- * load-bearing cue; the tint only reinforces, so the band survives grayscale
180
- * and color-vision deficiency. Mirrors the gauge's three-band canon, renamed
181
- * to the delinquency vocabulary.
182
- *
183
- * The tint is an INK token (`-h1a`), because it is painted on the `Figure`
184
- * below as well as on the glyph. The bare fill tokens read 1.78:1
185
- * (`--system-alert`) and 3.35:1 (`--system-success`) on every light surface in
186
- * the kit — the alert one under even the 3:1 floor a glyph answers to. */
187
- const RATE_BAND = {
188
- low: { Icon: ShieldCheck, tint: 'text-system-success-h1a', word: 'Saudável' },
189
- warn: { Icon: TriangleAlert, tint: 'text-system-alert-h1a', word: 'Elevada' },
190
- breach: { Icon: AlertOctagon, tint: 'text-credit', word: 'Em estresse' }
191
- };
192
- export function DelinquencyAging({ buckets, currency, locale, rateLabel = 'Inadimplência', rateBandLabels, emptyLabel = 'sem carteira', indeterminateLabel = 'indeterminada', bucketLabels, warn = 0.05, breach = 0.1, showTotal = false, healthyTone = 'success', className }) {
193
- // THIRD RAIL: the rate scale and the two totals come from money-math, never
194
- // float. The division below is of two exact BigInt-derived integers.
195
- const scale = currency ? minorDigitsOf(currency) : 2;
196
- const { rate, overdueMinor, totalMinor, indeterminate } = delinquencyRate(buckets, scale);
197
- // The band is only meaningful when there is a rate; an empty/indeterminate
198
- // portfolio has no band to name (rendered as the explicit no-value readout).
199
- const hasRate = rate !== null;
200
- // higher-is-worse: more value past due is worse. The band comes off the EXACT
201
- // minor-unit ratio, never `rate` — that float is truncated at 1e-12 and is for
202
- // DISPLAY only (one decimal place, where the truncation cannot show).
203
- const band = hasRate && overdueMinor !== null && totalMinor !== null
204
- ? exactBand(overdueMinor, totalMinor, warn, breach)
205
- : 'low';
206
- const { Icon, word } = RATE_BAND[band];
207
- // `ink` retones ONLY the healthy band; `warn`/`breach` keep their alarm tints.
208
- const tint = healthyTone === 'ink' && band === 'low'
209
- ? 'text-foreground'
210
- : RATE_BAND[band].tint;
211
- // Shallow merge: an unlisted band keeps its pt-BR default.
212
- const bandWord = rateBandLabels?.[band] ?? word;
213
- const distressed = band === 'breach';
214
- return (_jsxs("div", { className: cn('flex flex-col gap-4', className), children: [_jsxs("header", { className: "flex items-baseline justify-between gap-3", children: [_jsx(SectionLabel, { as: "span", children: rateLabel }), hasRate ? (_jsxs("span", {
215
- // A distressed rate is a credit alarm: announced the moment it
216
- // crosses the band, so assistive tech does not lean on the tint.
217
- role: distressed ? 'alert' : undefined, className: cn('inline-flex items-center gap-1.5', tint), children: [_jsx(Icon, { "aria-hidden": true, className: "size-4 shrink-0" }), _jsxs("span", { className: "sr-only", children: [bandWord, ": "] }), _jsx(Figure, { size: "row", className: tint, children: formatPercent(rate, { unit: 'ratio', locale }) })] })) : (_jsxs("span", { className: "text-muted-foreground inline-flex items-center gap-1.5", children: [_jsx("span", { className: LABEL_VOICE_CLASS, children: indeterminate ? indeterminateLabel : emptyLabel }), _jsx(Figure, { size: "row", className: "text-muted-foreground", children: NO_VALUE })] }))] }), _jsx(AgingBuckets, { buckets: buckets, currency: currency, locale: locale, showTotal: showTotal, labels: bucketLabels, healthyTone: healthyTone })] }));
218
- }
1
+ import{jsx as s,jsxs as c}from"react/jsx-runtime";import{AlertOctagon as y,ShieldCheck as C,TriangleAlert as T}from"lucide-react";import{LABEL_VOICE_CLASS as k}from"../../lib/typography.js";import{cn as w}from"../../lib/utils.js";import{Figure as E}from"../figure/index.js";import{formatPercent as D,NO_VALUE as F}from"../format.js";import{minorDigitsOf as z,toMinor as q}from"../money-math.js";import{SectionLabel as H}from"../section-label/index.js";import{AgingBuckets as P}from"./aging-buckets.js";const A=1000000000000n;export function delinquencyRate(r,i){const n=r.map(a=>q(a.total,i));if(n.some(a=>a===null)){return{rate:null,overdueMinor:null,totalMinor:null,indeterminate:true}}let e=0n;let t=0n;let m=false;n.forEach((a,f)=>{const o=a;if(o<0n)m=true;e+=o;if(r[f].overdue)t+=o});if(m){return{rate:null,overdueMinor:t,totalMinor:e,indeterminate:true}}if(e===0n){return{rate:null,overdueMinor:t,totalMinor:e,indeterminate:false}}const d=t*A/e;const u=Number(d)/Number(A);if(!Number.isFinite(u)){return{rate:null,overdueMinor:t,totalMinor:e,indeterminate:false}}return{rate:u,overdueMinor:t,totalMinor:e,indeterminate:false}}const I=10n**18n;function M(r,i,n){if(!Number.isFinite(n))return false;const e=Math.round(n*Number(I));if(!Number.isFinite(e))return e<0;let t=BigInt(e);if(t===0n&&n!==0){t=n<0?-1n:1n}return r*I>t*i}function V(r,i,n,e){if(M(r,i,e))return"breach";if(M(r,i,n))return"warn";return"low"}const S={low:{Icon:C,tint:"text-system-success-h1a",word:"Saud\xE1vel"},warn:{Icon:T,tint:"text-system-alert-h1a",word:"Elevada"},breach:{Icon:y,tint:"text-credit",word:"Em estresse"}};export function DelinquencyAging({buckets:r,currency:i,locale:n,rateLabel:e="Inadimpl\xEAncia",rateBandLabels:t,emptyLabel:m="sem carteira",indeterminateLabel:d="indeterminada",bucketLabels:u,warn:a=.05,breach:f=.1,showTotal:o=false,healthyTone:p="success",className:_}){const v=i?z(i):2;const{rate:x,overdueMinor:N,totalMinor:h,indeterminate:L}=delinquencyRate(r,v);const g=x!==null;const l=g&&N!==null&&h!==null?V(N,h,a,f):"low";const{Icon:B,word:O}=S[l];const b=p==="ink"&&l==="low"?"text-foreground":S[l].tint;const R=t?.[l]??O;const j=l==="breach";return c("div",{className:w("flex flex-col gap-4",_),children:[c("header",{className:"flex items-baseline justify-between gap-3",children:[s(H,{as:"span",children:e}),g?c("span",{role:j?"alert":void 0,className:w("inline-flex items-center gap-1.5",b),children:[s(B,{"aria-hidden":true,className:"size-4 shrink-0"}),c("span",{className:"sr-only",children:[R,": "]}),s(E,{size:"row",className:b,children:D(x,{unit:"ratio",locale:n})})]}):c("span",{className:"text-muted-foreground inline-flex items-center gap-1.5",children:[s("span",{className:k,children:L?d:m}),s(E,{size:"row",className:"text-muted-foreground",children:F})]})]}),s(P,{buckets:r,currency:i,locale:n,showTotal:o,labels:u,healthyTone:p})]})}
@@ -1,15 +1 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { cn } from '../../lib/utils.js';
3
- /** Canonical class strings per figure size. The `tabular-nums` invariant lives
4
- * here so it can never be forgotten at a call site. */
5
- export const FIGURE_CLASS = {
6
- hero: 'text-4xl font-semibold tracking-tight tabular-nums lg:text-5xl',
7
- 'money-hero': 'text-2xl font-semibold tracking-tight tabular-nums lg:text-3xl',
8
- panel: 'text-3xl font-semibold tracking-tight tabular-nums',
9
- count: 'text-2xl font-semibold tracking-tight tabular-nums',
10
- row: 'text-sm tabular-nums',
11
- tick: 'text-xs leading-none tabular-nums'
12
- };
13
- export function Figure({ size, children, className, as: Tag = 'span' }) {
14
- return _jsx(Tag, { className: cn(FIGURE_CLASS[size], className), children: children });
15
- }
1
+ import{jsx as r}from"react/jsx-runtime";import{cn as l}from"../../lib/utils.js";export const FIGURE_CLASS={hero:"text-4xl font-semibold tracking-tight tabular-nums lg:text-5xl","money-hero":"text-2xl font-semibold tracking-tight tabular-nums lg:text-3xl",panel:"text-3xl font-semibold tracking-tight tabular-nums",count:"text-2xl font-semibold tracking-tight tabular-nums",row:"text-sm tabular-nums",tick:"text-xs leading-none tabular-nums"};export function Figure({size:t,children:n,className:e,as:a="span"}){return r(a,{className:l(FIGURE_CLASS[t],e),children:n})}
@@ -1,128 +1 @@
1
- /**
2
- * Ledger-kit formatting helpers. Pure functions, no React. `MoneyText` owns
3
- * monetary formatting; these cover percent, integer counts, and humanized
4
- * durations (e.g. an average resolution time arriving as milliseconds).
5
- */
6
- /** No-value placeholder. A middle dot reads as "empty" without an em dash, and
7
- * stays narrow enough not to disturb tabular alignment. */
8
- export const NO_VALUE = '·';
9
- /**
10
- * Whether a digit count is one `Intl.NumberFormat` (and `Number#toFixed`, and
11
- * `String#repeat`) will actually accept: a finite integer in 0..100. Anything
12
- * else — a negative, a fraction, NaN, Infinity — makes those constructors throw
13
- * a RangeError, which on a money surface means a blank screen instead of a
14
- * number. Every caller turns an invalid count into the no-value path instead.
15
- *
16
- * Internal: shared by `format`, `money-text` and `money-math` so the three
17
- * cannot drift on what "valid" means. Never exported from `src/domain/index.ts`.
18
- */
19
- export function isValidDigitCount(digits) {
20
- return Number.isInteger(digits) && digits >= 0 && digits <= 100;
21
- }
22
- /**
23
- * A locale tag `Intl` will accept, or `undefined` — which every `Intl`
24
- * constructor reads as "the runtime locale", the documented default for every
25
- * `locale` prop in this layer.
26
- *
27
- * A structurally invalid tag makes EVERY `Intl` constructor throw a RangeError:
28
- * `''`, `'en_US'` (underscore for hyphen — the commonest mistake, and what a
29
- * POSIX `LANG` value looks like), a truncated tag out of a stored user
30
- * preference. Unguarded, that RangeError escapes during render, so one bad
31
- * preference string takes down the whole money surface instead of printing a
32
- * number with the wrong decimal mark. `Intl.getCanonicalLocales` rejects exactly
33
- * the tags the formatters reject, so it is the predicate rather than a
34
- * hand-rolled BCP 47 regex.
35
- *
36
- * The locale is the least important input to a figure — the DIGITS are the
37
- * point — so an unusable tag degrades to the runtime locale and the number still
38
- * renders.
39
- *
40
- * Internal: shared by `format`, `money-text` and `number-input` so they cannot
41
- * drift. Never exported from `src/domain/index.ts`.
42
- */
43
- export function safeLocale(locale) {
44
- if (locale === undefined)
45
- return undefined;
46
- try {
47
- Intl.getCanonicalLocales(locale);
48
- return locale;
49
- }
50
- catch {
51
- return undefined;
52
- }
53
- }
54
- /**
55
- * Format a rate as a percent string. `unit` is explicit, never inferred from
56
- * magnitude: 'ratio' (default) treats `value` as 0..1 and scales ×100;
57
- * 'percent' treats `value` as already 0..100.
58
- *
59
- * Uses Intl style:'percent' for correct rounding and a locale-aware decimal
60
- * mark. Intl multiplies its input by 100, so we always feed it the RATIO —
61
- * a 'percent' input is divided by 100 first.
62
- */
63
- export function formatPercent(value, options = {}) {
64
- if (value === null || value === undefined || !Number.isFinite(value))
65
- return NO_VALUE;
66
- const { digits = 1, unit = 'ratio', locale } = options;
67
- // A caller-supplied digit count reaches Intl directly; an out-of-range one
68
- // would throw a RangeError and take the whole surface down with it.
69
- if (!isValidDigitCount(digits))
70
- return NO_VALUE;
71
- const ratio = unit === 'percent' ? value / 100 : value;
72
- return new Intl.NumberFormat(safeLocale(locale), {
73
- style: 'percent',
74
- minimumFractionDigits: digits,
75
- maximumFractionDigits: digits
76
- }).format(ratio);
77
- }
78
- /**
79
- * Normalize a rate to the 0..100 range for chart geometry. `unit` is explicit:
80
- * 'ratio' (default) scales a 0..1 input ×100; 'percent' is already 0..100.
81
- */
82
- export function toPercentValue(value, unit = 'ratio') {
83
- if (value === null || value === undefined || !Number.isFinite(value))
84
- return 0;
85
- const pct = unit === 'percent' ? value : value * 100;
86
- return Math.max(0, Math.min(100, pct));
87
- }
88
- /** Thousands-grouped integer count. `locale` defaults to the runtime locale. */
89
- export function formatCount(value, locale) {
90
- if (value === null || value === undefined || !Number.isFinite(value))
91
- return NO_VALUE;
92
- return new Intl.NumberFormat(safeLocale(locale), {
93
- maximumFractionDigits: 0
94
- }).format(value);
95
- }
96
- /**
97
- * Humanize a millisecond duration into the largest two sensible units.
98
- * 0ms → "0s". Sub-minute → seconds. Otherwise the two most-significant
99
- * CONTIGUOUS d/h/m units from the largest non-zero unit down — so "2d 0h 5m"
100
- * renders as "2d 0h", not the misleading "2d 5m".
101
- *
102
- * Note: the d/h/m/s suffixes are English; localizing them is deferred.
103
- */
104
- export function humanizeDurationMs(ms) {
105
- if (ms == null || !Number.isFinite(ms) || ms < 0)
106
- return NO_VALUE;
107
- if (ms === 0)
108
- return '0s';
109
- const totalSeconds = Math.round(ms / 1000);
110
- if (totalSeconds < 60)
111
- return `${totalSeconds}s`;
112
- const days = Math.floor(totalSeconds / 86400);
113
- const hours = Math.floor((totalSeconds % 86400) / 3600);
114
- const minutes = Math.floor((totalSeconds % 3600) / 60);
115
- // Two contiguous units starting at the largest non-zero unit, in order.
116
- const units = [
117
- [days, 'd'],
118
- [hours, 'h'],
119
- [minutes, 'm']
120
- ];
121
- const start = units.findIndex(([n]) => n > 0);
122
- if (start === -1)
123
- return `${minutes}m`;
124
- return units
125
- .slice(start, start + 2)
126
- .map(([n, suffix]) => `${n}${suffix}`)
127
- .join(' ');
128
- }
1
+ export const NO_VALUE="\xB7";export function isValidDigitCount(t){return Number.isInteger(t)&&t>=0&&t<=100}export function safeLocale(t){if(t===void 0)return void 0;try{Intl.getCanonicalLocales(t);return t}catch{return void 0}}export function formatPercent(t,n={}){if(t===null||t===void 0||!Number.isFinite(t))return NO_VALUE;const{digits:r=1,unit:o="ratio",locale:i}=n;if(!isValidDigitCount(r))return NO_VALUE;const e=o==="percent"?t/100:t;return new Intl.NumberFormat(safeLocale(i),{style:"percent",minimumFractionDigits:r,maximumFractionDigits:r}).format(e)}export function toPercentValue(t,n="ratio"){if(t===null||t===void 0||!Number.isFinite(t))return 0;const r=n==="percent"?t:t*100;return Math.max(0,Math.min(100,r))}export function formatCount(t,n){if(t===null||t===void 0||!Number.isFinite(t))return NO_VALUE;return new Intl.NumberFormat(safeLocale(n),{maximumFractionDigits:0}).format(t)}export function humanizeDurationMs(t){if(t==null||!Number.isFinite(t)||t<0)return NO_VALUE;if(t===0)return"0s";const n=Math.round(t/1e3);if(n<60)return`${n}s`;const r=Math.floor(n/86400);const o=Math.floor(n%86400/3600);const i=Math.floor(n%3600/60);const e=[[r,"d"],[o,"h"],[i,"m"]];const u=e.findIndex(([c])=>c>0);if(u===-1)return`${i}m`;return e.slice(u,u+2).map(([c,f])=>`${c}${f}`).join(" ")}
@@ -1,19 +1 @@
1
- // Owned by lane domain — see docs/plans/2026-08-26-sindarian-x-retirement/
2
- //
3
- // The finance domain grammar absorbed from @lerianstudio/sindarian-x@0.15.0.
4
- // Exports are NAMED (never `export *`) on purpose: the port census freezes the
5
- // public surface to exactly the symbols the four migrating apps import, and the
6
- // money-math internals (`toMinor`, `Amount`, `sumMinor`, `formatMoneyParts`,
7
- // `maskKeyId`, `AgingBuckets`, …) stay module-private so they never become an
8
- // API this package has to keep.
9
- export { NO_VALUE, formatCount, formatPercent, humanizeDurationMs, toPercentValue } from './format.js';
10
- export { moneyDiff } from './money-diff.js';
11
- export { MoneyText } from './money-text/index.js';
12
- export { FIGURE_CLASS, Figure } from './figure/index.js';
13
- export { SectionLabel } from './section-label/index.js';
14
- export { Blotter, BlotterRow } from './blotter/index.js';
15
- export { LedgerPanel, LedgerSheet } from './ledger-sheet/index.js';
16
- export { KeyId } from './key-id/index.js';
17
- export { Dot, LivePulse, StatusRail } from './status-rail/index.js';
18
- export { ThresholdGauge, gaugeBand } from './threshold-gauge/index.js';
19
- export { DelinquencyAging } from './delinquency-aging/index.js';
1
+ export{NO_VALUE,formatCount,formatPercent,humanizeDurationMs,toPercentValue}from"./format.js";export{moneyDiff}from"./money-diff.js";export{MoneyText}from"./money-text/index.js";export{FIGURE_CLASS,Figure}from"./figure/index.js";export{SectionLabel}from"./section-label/index.js";export{Blotter,BlotterRow}from"./blotter/index.js";export{LedgerPanel,LedgerSheet}from"./ledger-sheet/index.js";export{KeyId}from"./key-id/index.js";export{Dot,LivePulse,StatusRail}from"./status-rail/index.js";export{ThresholdGauge,gaugeBand}from"./threshold-gauge/index.js";export{DelinquencyAging}from"./delinquency-aging/index.js";
@@ -1,169 +1 @@
1
- 'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- /**
4
- * KeyId — a masked, copyable, traceable identifier for Brazilian rails: a Pix
5
- * key (CPF / CNPJ / email / phone / EVP-random), an End-to-End id (E2E), an
6
- * idempotency key, or a generic id.
7
- *
8
- * Two jobs at once. PII keys (CPF/CNPJ/email/phone) are masked by default so a
9
- * shared screen or a screenshot doesn't leak a person's document or contact —
10
- * the middle of a document is revealed, the identifying head/tail is dotted.
11
- * Long-but-not-secret ids (EVP/E2E/idempotency/generic) are merely truncated in
12
- * the MIDDLE so the head and tail — the parts an operator eyeballs against a
13
- * log — stay legible.
14
- *
15
- * <KeyId value="529.982.247-25" kind="pix-cpf" label="Payer" copyable />
16
- * <KeyId value="E32074158202406211200abc123def456" kind="e2e" copyable />
17
- *
18
- * The copy button always lifts the RAW, unmasked value to the clipboard — the
19
- * mask is a display concern, never a data one. The masked render and the copy
20
- * affordance are what make this a client component.
21
- */
22
- import { Check, Copy } from 'lucide-react';
23
- import { useState } from 'react';
24
- import { LABEL_VOICE_CLASS } from '../../lib/typography.js';
25
- import { cn } from '../../lib/utils.js';
26
- /** The dot used to redact a character. A bullet, not an asterisk: it reads as
27
- * "hidden" without looking like a password field's shouting. */
28
- const DOT = '•';
29
- /** Kinds that carry PII and are masked by default. The id-shaped kinds are long
30
- * but not secret, so they default to revealed (just truncated). */
31
- const PII_KINDS = new Set([
32
- 'pix-cpf',
33
- 'pix-cnpj',
34
- 'pix-email',
35
- 'pix-phone'
36
- ]);
37
- /** Whether a kind masks by default (PII) — used when `mask` is not supplied.
38
- * Internal: exported for its unit tests, never from `src/domain/index.ts`. */
39
- export function masksByDefault(kind) {
40
- return PII_KINDS.has(kind);
41
- }
42
- /** Keep only the digits — Brazilian documents arrive punctuated or bare. */
43
- function digitsOf(value) {
44
- return value.replace(/\D/g, '');
45
- }
46
- /**
47
- * Truncate the MIDDLE of a long value, keeping a head and tail so an operator
48
- * can eyeball it against a log. Short values are returned whole.
49
- */
50
- function truncateMiddle(value, head = 6, tail = 4) {
51
- if (value.length <= head + tail + 1)
52
- return value;
53
- return `${value.slice(0, head)}…${value.slice(-tail)}`;
54
- }
55
- /**
56
- * The fallback for a PII value that is MALFORMED for its kind. It must not be
57
- * `truncateMiddle`: a short malformed value (a CPF missing a digit, a hand-typed
58
- * document, a name-shaped email that failed the `@` check) is returned WHOLE by
59
- * truncation, so the "masked" render leaks the very PII the mask exists to hide.
60
- * A fixed-width dot run reveals nothing — not the content, not even the length.
61
- *
62
- * DELIBERATE DIVERGENCE from sindarian-x@0.15.0, which truncated instead. It
63
- * costs the operator the ability to eyeball a malformed document against a log;
64
- * that is the correct trade, because the alternative is a screenshot with a real
65
- * CPF in it. An operator who needs the value passes `mask={false}` explicitly.
66
- */
67
- function redactPii() {
68
- return DOT.repeat(6);
69
- }
70
- /** Mask all but the central block of a digit string, then re-punctuate it. */
71
- function maskCpf(value) {
72
- const d = digitsOf(value);
73
- if (d.length !== 11)
74
- return redactPii();
75
- // Reveal the middle two blocks (456.789), dot the leading block and check digits.
76
- return `${DOT.repeat(3)}.${d.slice(3, 6)}.${d.slice(6, 9)}-${DOT.repeat(2)}`;
77
- }
78
- function maskCnpj(value) {
79
- const d = digitsOf(value);
80
- if (d.length !== 14)
81
- return redactPii();
82
- // Reveal the central registration blocks, dot the head and the check digits.
83
- return `${DOT.repeat(2)}.${d.slice(2, 5)}.${d.slice(5, 8)}/${d.slice(8, 12)}-${DOT.repeat(2)}`;
84
- }
85
- function maskEmail(value) {
86
- const at = value.indexOf('@');
87
- if (at <= 0 || at === value.length - 1)
88
- return redactPii();
89
- // EXACTLY one '@', or it is not an address and the domain half cannot be
90
- // trusted to be a domain. `a@private@domain.test` split on the FIRST '@' and
91
- // published `private@domain.test` verbatim as the "domain" — the local part of
92
- // a second address, revealed in a view whose whole purpose is to hide it.
93
- // Malformed PII redacts, exactly like a wrong-length CPF.
94
- if (value.indexOf('@', at + 1) !== -1)
95
- return redactPii();
96
- const first = value.slice(0, 1);
97
- const domain = value.slice(at + 1);
98
- return `${first}${DOT.repeat(3)}@${domain}`;
99
- }
100
- function maskPhone(value) {
101
- const d = digitsOf(value);
102
- // Expect a BR mobile in E.164-ish form: 55 + 2-digit area + 9 subscriber digits.
103
- if (d.length !== 13 || !d.startsWith('55'))
104
- return redactPii();
105
- const area = d.slice(2, 4);
106
- const last2 = d.slice(-2);
107
- // Keep country + area, reveal the last two, dot the rest of the subscriber run.
108
- return `+55 (${area}) ${DOT.repeat(5)}-${DOT.repeat(2)}${last2}`;
109
- }
110
- /**
111
- * Mask a key for display. PII kinds get a structured mask that reveals a
112
- * non-identifying middle and dots the rest; a value MALFORMED for its kind is
113
- * redacted outright (see `redactPii`) rather than truncated, so a mask can never
114
- * fall through to the raw secret. Id-shaped kinds are always middle-truncated
115
- * (long, not secret). The copy path never uses this — it always sends the raw
116
- * value.
117
- *
118
- * Internal: exported for its unit tests, never from `src/domain/index.ts`.
119
- */
120
- export function maskKeyId(value, kind) {
121
- switch (kind) {
122
- case 'pix-cpf':
123
- return maskCpf(value);
124
- case 'pix-cnpj':
125
- return maskCnpj(value);
126
- case 'pix-email':
127
- return maskEmail(value);
128
- case 'pix-phone':
129
- return maskPhone(value);
130
- default:
131
- return truncateMiddle(value);
132
- }
133
- }
134
- export function KeyId({ value, kind = 'generic', label, mask, copyable = false, className }) {
135
- const masked = mask ?? masksByDefault(kind);
136
- const display = masked ? maskKeyId(value, kind) : truncateMiddle(value);
137
- return (_jsxs("span", { className: cn('inline-flex min-w-0 items-center gap-2', className), children: [label ? (_jsx("span", { className: cn(LABEL_VOICE_CLASS, 'shrink-0 text-xs'), children: label })) : null, _jsx("span", { className: "text-foreground truncate font-mono text-xs tabular-nums",
138
- // The raw value as a tooltip only when nothing is hidden — never leak a
139
- // masked PII value through the title attribute.
140
- title: masked ? undefined : value, children: display }), copyable ? _jsx(CopyValue, { value: value }) : null] }));
141
- }
142
- /** The copy affordance: lifts the RAW value to the clipboard and flips to a
143
- * check for a beat. The transient confirmation is announced for assistive tech;
144
- * the icon is paired with a text aria-label so the state isn't color-only. */
145
- function CopyValue({ value }) {
146
- const [copied, setCopied] = useState(false);
147
- const copy = () => {
148
- // ponytail: best-effort clipboard write — no toast, no error surface. The
149
- // ceiling is silent failure in an insecure context (clipboard unavailable);
150
- // upgrade path is to lift a result up via an onCopy callback when a caller
151
- // needs to react.
152
- //
153
- // The rejection handler is NOT optional: `writeText` rejects on a denied
154
- // permission or a non-secure context, and an unhandled rejection in a render
155
- // tree is a console error at best and a crashed handler at worst. On failure
156
- // the button simply does not flip to the copied state, so the operator sees
157
- // that nothing was copied rather than a false confirmation.
158
- void navigator.clipboard
159
- ?.writeText(value)
160
- .then(() => {
161
- setCopied(true);
162
- setTimeout(() => setCopied(false), 1500);
163
- })
164
- .catch(() => {
165
- setCopied(false);
166
- });
167
- };
168
- return (_jsx("button", { type: "button", onClick: copy, "aria-label": copied ? 'Copiado para a área de transferência' : 'Copiar valor', className: "text-muted-foreground hover:text-foreground focus:ring-ring inline-flex size-5 shrink-0 items-center justify-center rounded-sm transition-colors duration-150 ease-out focus:ring-2 focus:outline-none", children: copied ? (_jsx(Check, { "aria-hidden": true, className: "text-system-success-h1a size-3" })) : (_jsx(Copy, { "aria-hidden": true, className: "size-3" })) }));
169
- }
1
+ "use client";import{jsx as i,jsxs as m}from"react/jsx-runtime";import{Check as x,Copy as h}from"lucide-react";import{useState as $}from"react";import{LABEL_VOICE_CLASS as g}from"../../lib/typography.js";import{cn as u}from"../../lib/utils.js";const r="\u2022";const C=new Set(["pix-cpf","pix-cnpj","pix-email","pix-phone"]);export function masksByDefault(e){return C.has(e)}function o(e){return e.replace(/\D/g,"")}function p(e,t=6,n=4){if(e.length<=t+n+1)return e;return`${e.slice(0,t)}\u2026${e.slice(-n)}`}function c(){return r.repeat(6)}function k(e){const t=o(e);if(t.length!==11)return c();return`${r.repeat(3)}.${t.slice(3,6)}.${t.slice(6,9)}-${r.repeat(2)}`}function y(e){const t=o(e);if(t.length!==14)return c();return`${r.repeat(2)}.${t.slice(2,5)}.${t.slice(5,8)}/${t.slice(8,12)}-${r.repeat(2)}`}function j(e){const t=e.indexOf("@");if(t<=0||t===e.length-1)return c();if(e.indexOf("@",t+1)!==-1)return c();const n=e.slice(0,1);const s=e.slice(t+1);return`${n}${r.repeat(3)}@${s}`}function N(e){const t=o(e);if(t.length!==13||!t.startsWith("55"))return c();const n=t.slice(2,4);const s=t.slice(-2);return`+55 (${n}) ${r.repeat(5)}-${r.repeat(2)}${s}`}export function maskKeyId(e,t){switch(t){case"pix-cpf":return k(e);case"pix-cnpj":return y(e);case"pix-email":return j(e);case"pix-phone":return N(e);default:return p(e)}}export function KeyId({value:e,kind:t="generic",label:n,mask:s,copyable:f=false,className:l}){const a=s??masksByDefault(t);const d=a?maskKeyId(e,t):p(e);return m("span",{className:u("inline-flex min-w-0 items-center gap-2",l),children:[n?i("span",{className:u(g,"shrink-0 text-xs"),children:n}):null,i("span",{className:"text-foreground truncate font-mono text-xs tabular-nums",title:a?void 0:e,children:d}),f?i(I,{value:e}):null]})}function I({value:e}){const[t,n]=$(false);const s=()=>{void navigator.clipboard?.writeText(e).then(()=>{n(true);setTimeout(()=>n(false),1500)}).catch(()=>{n(false)})};return i("button",{type:"button",onClick:s,"aria-label":t?"Copiado para a \xE1rea de transfer\xEAncia":"Copiar valor",className:"text-muted-foreground hover:text-foreground focus:ring-ring inline-flex size-5 shrink-0 items-center justify-center rounded-sm transition-colors duration-150 ease-out focus:ring-2 focus:outline-none",children:t?i(x,{"aria-hidden":true,className:"text-system-success-h1a size-3"}):i(h,{"aria-hidden":true,className:"size-3"})})}
@@ -1,24 +1 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { cn } from '../../lib/utils.js';
3
- /** Tailwind needs literal class names to survive purge; map cols → class. */
4
- const COLS_CLASS = {
5
- 1: 'lg:grid-cols-1',
6
- 2: 'lg:grid-cols-2',
7
- 3: 'lg:grid-cols-3',
8
- 4: 'lg:grid-cols-4'
9
- };
10
- export function LedgerSheet({ children, cols = 1, className }) {
11
- return (_jsx("div", { className: cn('border-border bg-border grid grid-cols-1 gap-px overflow-hidden rounded-lg border', COLS_CLASS[cols] ?? COLS_CLASS[1], className), children: children }));
12
- }
13
- /**
14
- * One cell of a ledger sheet. Flush — no own border or shadow — with `bg-card`,
15
- * internal `p-5` padding and `flex h-full flex-col gap-4` so panels stretch to
16
- * equal height. Start each panel with a `<SectionLabel>` as its heading.
17
- *
18
- * Remaining props land on the rendered element, so a composed panel can carry
19
- * its own `data-*` hooks (StatCard's `data-tone`, for one) without wrapping the
20
- * cell in an extra div that would break the hairline seams.
21
- */
22
- export function LedgerPanel({ children, className, as: Tag = 'section', ...rest }) {
23
- return (_jsx(Tag, { className: cn('bg-card flex h-full flex-col gap-4 p-5', className), ...rest, children: children }));
24
- }
1
+ import{jsx as l}from"react/jsx-runtime";import{cn as d}from"../../lib/utils.js";const g={1:"lg:grid-cols-1",2:"lg:grid-cols-2",3:"lg:grid-cols-3",4:"lg:grid-cols-4"};export function LedgerSheet({children:r,cols:e=1,className:o}){return l("div",{className:d("border-border bg-border grid grid-cols-1 gap-px overflow-hidden rounded-lg border",g[e]??g[1],o),children:r})}export function LedgerPanel({children:r,className:e,as:o="section",...c}){return l(o,{className:d("bg-card flex h-full flex-col gap-4 p-5",e),...c,children:r})}
@@ -1,45 +1 @@
1
- /**
2
- * moneyDiff — the signed difference between two reconciliation sides, computed
3
- * on integer minor units and never on floats.
4
- *
5
- * THIRD RAIL (money): `left − right` runs through `toMinor` on both operands →
6
- * BigInt subtraction → `minorToDecimal`, and the tolerance breach test compares
7
- * MAGNITUDES in minor units. A float subtraction of two decimals would drift by
8
- * a cent at reconciliation scale, which is the difference between a matched and
9
- * an unmatched pair.
10
- *
11
- * A null `decimal` means an unparseable operand — surfaced, never coerced to
12
- * zero, so a caller renders the no-value placeholder rather than a fabricated
13
- * tie.
14
- */
15
- import { minorDigitsOf, minorToDecimal, toMinor } from './money-math.js';
16
- export function moneyDiff(money) {
17
- const scale = money.minorDigits ?? minorDigitsOf(money.currency);
18
- const l = toMinor(money.left, scale);
19
- const r = toMinor(money.right, scale);
20
- if (l === null || r === null)
21
- return { decimal: null, breach: false };
22
- const diff = l - r;
23
- // A tolerance that was SUPPLIED but does not parse ('', 'oops', '-.') is not
24
- // the same thing as no tolerance at all. Degrading to the no-tolerance rule
25
- // made every nonzero difference a breach, so one typo in a tolerance field
26
- // turned a reconciled book into a screen of false breaches — the loudest
27
- // possible wrong answer. A supplied-invalid tolerance is INDETERMINATE,
28
- // exactly like an unparseable operand: no number, no verdict.
29
- const supplied = money.tolerance != null;
30
- const rawTol = supplied ? toMinor(money.tolerance, scale) : null;
31
- if (supplied && rawTol === null)
32
- return { decimal: null, breach: false };
33
- // A tolerance is a BAND WIDTH, so only its magnitude is meaningful. Taken
34
- // signed, a negative tolerance made `|diff| > tol` true for every value
35
- // including an exact tie (0n > -5n), flagging perfectly reconciled pairs as
36
- // breaches. Normalizing to the magnitude means `-0.05` and `0.05` describe the
37
- // same band, which is the only reading of a negative tolerance that is not a
38
- // silent false alarm.
39
- const tol = rawTol === null ? null : rawTol < 0n ? -rawTol : rawTol;
40
- const abs = diff < 0n ? -diff : diff;
41
- // A breach is a magnitude STRICTLY beyond the band; no tolerance → no band, so
42
- // any nonzero difference is itself the breach.
43
- const breach = tol === null ? diff !== 0n : abs > tol;
44
- return { decimal: minorToDecimal(diff, scale), breach };
45
- }
1
+ import{minorDigitsOf as f,minorToDecimal as d,toMinor as c}from"./money-math.js";export function moneyDiff(l){const t=l.minorDigits??f(l.currency);const i=c(l.left,t);const o=c(l.right,t);if(i===null||o===null)return{decimal:null,breach:false};const n=i-o;const e=l.tolerance!=null;const r=e?c(l.tolerance,t):null;if(e&&r===null)return{decimal:null,breach:false};const s=r===null?null:r<0n?-r:r;const u=n<0n?-n:n;const a=s===null?n!==0n:u>s;return{decimal:d(n,t),breach:a}}