@eifi1/ui-kit 0.10.0 → 0.12.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 (382) hide show
  1. package/README.md +66 -37
  2. package/dist/chart.d.ts +3 -1
  3. package/dist/chart.js +2 -0
  4. package/dist/chart.js.map +1 -1
  5. package/dist/components/account-settings-labels.d.ts +105 -0
  6. package/dist/components/account-settings-labels.js +81 -0
  7. package/dist/components/account-settings-labels.js.map +1 -0
  8. package/dist/components/account-settings.d.ts +36 -52
  9. package/dist/components/account-settings.js +44 -8
  10. package/dist/components/account-settings.js.map +1 -1
  11. package/dist/components/alert-banner.d.ts +26 -1
  12. package/dist/components/alert-banner.js +10 -7
  13. package/dist/components/alert-banner.js.map +1 -1
  14. package/dist/components/amount-input.d.ts +26 -4
  15. package/dist/components/amount-input.js +5 -2
  16. package/dist/components/amount-input.js.map +1 -1
  17. package/dist/components/authed-image.d.ts +68 -0
  18. package/dist/components/authed-image.js +117 -0
  19. package/dist/components/authed-image.js.map +1 -0
  20. package/dist/components/breadcrumbs.d.ts +2 -1
  21. package/dist/components/breadcrumbs.js +5 -2
  22. package/dist/components/breadcrumbs.js.map +1 -1
  23. package/dist/components/bulk-action-bar.d.ts +51 -5
  24. package/dist/components/bulk-action-bar.js +88 -41
  25. package/dist/components/bulk-action-bar.js.map +1 -1
  26. package/dist/components/button-group.d.ts +107 -3
  27. package/dist/components/button-group.js +60 -5
  28. package/dist/components/button-group.js.map +1 -1
  29. package/dist/components/calculator.d.ts +17 -4
  30. package/dist/components/calendar-heatmap.d.ts +162 -0
  31. package/dist/components/calendar-heatmap.js +336 -0
  32. package/dist/components/calendar-heatmap.js.map +1 -0
  33. package/dist/components/chip.d.ts +2 -1
  34. package/dist/components/chip.js +6 -3
  35. package/dist/components/chip.js.map +1 -1
  36. package/dist/components/choice-card.d.ts +66 -2
  37. package/dist/components/choice-card.js +108 -1
  38. package/dist/components/choice-card.js.map +1 -1
  39. package/dist/components/combobox-core.js +2 -0
  40. package/dist/components/combobox-core.js.map +1 -1
  41. package/dist/components/combobox.d.ts +2 -2
  42. package/dist/components/combobox.js +31 -9
  43. package/dist/components/combobox.js.map +1 -1
  44. package/dist/components/copy-button.d.ts +52 -70
  45. package/dist/components/copy-button.js +1 -1
  46. package/dist/components/copy-button.js.map +1 -1
  47. package/dist/components/data-table-cells.d.ts +63 -0
  48. package/dist/components/data-table-cells.js +64 -0
  49. package/dist/components/data-table-cells.js.map +1 -0
  50. package/dist/components/data-table-filter-popover.d.ts +1 -1
  51. package/dist/components/data-table-filters.d.ts +1 -1
  52. package/dist/components/data-table-filters.js +31 -1
  53. package/dist/components/data-table-filters.js.map +1 -1
  54. package/dist/components/data-table-labels.d.ts +13 -0
  55. package/dist/components/data-table-labels.js +8 -1
  56. package/dist/components/data-table-labels.js.map +1 -1
  57. package/dist/components/data-table.d.ts +1 -1
  58. package/dist/components/data-table.js +104 -12
  59. package/dist/components/data-table.js.map +1 -1
  60. package/dist/components/date-picker.d.ts +3 -1
  61. package/dist/components/date-picker.js +15 -2
  62. package/dist/components/date-picker.js.map +1 -1
  63. package/dist/components/description-list.d.ts +23 -3
  64. package/dist/components/description-list.js +23 -3
  65. package/dist/components/description-list.js.map +1 -1
  66. package/dist/components/dialog-frame.d.ts +1 -0
  67. package/dist/components/entity-combobox.d.ts +1 -1
  68. package/dist/components/entity-combobox.js +18 -3
  69. package/dist/components/entity-combobox.js.map +1 -1
  70. package/dist/components/error-boundary.d.ts +92 -0
  71. package/dist/components/error-boundary.js +111 -0
  72. package/dist/components/error-boundary.js.map +1 -0
  73. package/dist/components/field.d.ts +69 -0
  74. package/dist/components/field.js +75 -0
  75. package/dist/components/field.js.map +1 -0
  76. package/dist/components/file-button.d.ts +51 -208
  77. package/dist/components/file-dropzone.d.ts +50 -2
  78. package/dist/components/floating-panel.d.ts +156 -4
  79. package/dist/components/floating-panel.js +193 -29
  80. package/dist/components/floating-panel.js.map +1 -1
  81. package/dist/components/form-actions.d.ts +53 -0
  82. package/dist/components/form-actions.js +98 -0
  83. package/dist/components/form-actions.js.map +1 -0
  84. package/dist/components/image-grid.d.ts +67 -0
  85. package/dist/components/image-grid.js +112 -0
  86. package/dist/components/image-grid.js.map +1 -0
  87. package/dist/components/lightbox.d.ts +104 -0
  88. package/dist/components/lightbox.js +229 -0
  89. package/dist/components/lightbox.js.map +1 -0
  90. package/dist/components/line-items.d.ts +114 -0
  91. package/dist/components/line-items.js +238 -0
  92. package/dist/components/line-items.js.map +1 -0
  93. package/dist/components/list.d.ts +37 -3
  94. package/dist/components/list.js +39 -14
  95. package/dist/components/list.js.map +1 -1
  96. package/dist/components/loading-state.d.ts +24 -0
  97. package/dist/components/loading-state.js +32 -0
  98. package/dist/components/loading-state.js.map +1 -0
  99. package/dist/components/menu-item.d.ts +13 -2
  100. package/dist/components/menu-item.js +11 -2
  101. package/dist/components/menu-item.js.map +1 -1
  102. package/dist/components/mini-calendar.d.ts +82 -5
  103. package/dist/components/mini-calendar.js +212 -64
  104. package/dist/components/mini-calendar.js.map +1 -1
  105. package/dist/components/modal.d.ts +20 -1
  106. package/dist/components/modal.js +26 -2
  107. package/dist/components/modal.js.map +1 -1
  108. package/dist/components/month-picker.d.ts +21 -1
  109. package/dist/components/month-picker.js +139 -26
  110. package/dist/components/month-picker.js.map +1 -1
  111. package/dist/components/multi-entity-combobox.d.ts +1 -1
  112. package/dist/components/multi-entity-combobox.js +19 -3
  113. package/dist/components/multi-entity-combobox.js.map +1 -1
  114. package/dist/components/nav-pills.d.ts +67 -0
  115. package/dist/components/nav-pills.js +69 -0
  116. package/dist/components/nav-pills.js.map +1 -0
  117. package/dist/components/number-field.d.ts +26 -5
  118. package/dist/components/number-field.js +5 -7
  119. package/dist/components/number-field.js.map +1 -1
  120. package/dist/components/number-input.d.ts +27 -5
  121. package/dist/components/number-input.js +7 -1
  122. package/dist/components/number-input.js.map +1 -1
  123. package/dist/components/numpad-sheet.d.ts +17 -4
  124. package/dist/components/passkeys-setting.d.ts +67 -0
  125. package/dist/components/passkeys-setting.js +214 -0
  126. package/dist/components/passkeys-setting.js.map +1 -0
  127. package/dist/components/picker-sheet.js +2 -1
  128. package/dist/components/picker-sheet.js.map +1 -1
  129. package/dist/components/pie-chart-labels.d.ts +20 -0
  130. package/dist/components/pie-chart-labels.js +12 -0
  131. package/dist/components/pie-chart-labels.js.map +1 -0
  132. package/dist/components/pie-chart.d.ts +101 -0
  133. package/dist/components/pie-chart.js +283 -0
  134. package/dist/components/pie-chart.js.map +1 -0
  135. package/dist/components/progress-bar.d.ts +73 -3
  136. package/dist/components/progress-bar.js +110 -8
  137. package/dist/components/progress-bar.js.map +1 -1
  138. package/dist/components/qr-code.d.ts +34 -0
  139. package/dist/components/qr-code.js +57 -0
  140. package/dist/components/qr-code.js.map +1 -0
  141. package/dist/components/search-field.d.ts +15 -3
  142. package/dist/components/search-field.js +5 -1
  143. package/dist/components/search-field.js.map +1 -1
  144. package/dist/components/series-chart.d.ts +16 -0
  145. package/dist/components/series-chart.js +4 -1
  146. package/dist/components/series-chart.js.map +1 -1
  147. package/dist/components/settings-fields.d.ts +50 -1
  148. package/dist/components/signed-amount.d.ts +121 -0
  149. package/dist/components/signed-amount.js +152 -0
  150. package/dist/components/signed-amount.js.map +1 -0
  151. package/dist/components/skeleton.d.ts +10 -3
  152. package/dist/components/skeleton.js +12 -2
  153. package/dist/components/skeleton.js.map +1 -1
  154. package/dist/components/stat-tile.d.ts +2 -1
  155. package/dist/components/stat-tile.js +5 -2
  156. package/dist/components/stat-tile.js.map +1 -1
  157. package/dist/components/status-dot.d.ts +6 -2
  158. package/dist/components/status-dot.js +6 -1
  159. package/dist/components/status-dot.js.map +1 -1
  160. package/dist/components/table.d.ts +77 -11
  161. package/dist/components/table.js +85 -17
  162. package/dist/components/table.js.map +1 -1
  163. package/dist/components/text-link.d.ts +176 -0
  164. package/dist/components/text-link.js +109 -0
  165. package/dist/components/text-link.js.map +1 -0
  166. package/dist/components/text.d.ts +8 -1
  167. package/dist/components/text.js +1 -0
  168. package/dist/components/text.js.map +1 -1
  169. package/dist/components/time-input.d.ts +50 -1
  170. package/dist/components/toggle-group.d.ts +28 -0
  171. package/dist/components/toggle-group.js +58 -9
  172. package/dist/components/toggle-group.js.map +1 -1
  173. package/dist/components/toggle-legend.d.ts +36 -3
  174. package/dist/components/toggle-legend.js +49 -0
  175. package/dist/components/toggle-legend.js.map +1 -1
  176. package/dist/components/tooltip.d.ts +11 -3
  177. package/dist/components/tooltip.js +2 -0
  178. package/dist/components/tooltip.js.map +1 -1
  179. package/dist/components/ui.d.ts +52 -729
  180. package/dist/components/ui.js +296 -27
  181. package/dist/components/ui.js.map +1 -1
  182. package/dist/components/use-table-state.d.ts +3 -52
  183. package/dist/components/use-table-state.js +128 -30
  184. package/dist/components/use-table-state.js.map +1 -1
  185. package/dist/components/user-avatar.d.ts +25 -3
  186. package/dist/components/user-avatar.js +32 -3
  187. package/dist/components/user-avatar.js.map +1 -1
  188. package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
  189. package/dist/data-table.d.ts +2 -1
  190. package/dist/data-table.js +5 -1
  191. package/dist/data-table.js.map +1 -1
  192. package/dist/feedback/feedback-dialog.d.ts +26 -3
  193. package/dist/feedback/feedback-dialog.js +16 -2
  194. package/dist/feedback/feedback-dialog.js.map +1 -1
  195. package/dist/feedback/feedback-thread.d.ts +133 -0
  196. package/dist/feedback/feedback-thread.js +237 -0
  197. package/dist/feedback/feedback-thread.js.map +1 -0
  198. package/dist/feedback.d.ts +2 -1
  199. package/dist/feedback.js +1 -0
  200. package/dist/feedback.js.map +1 -1
  201. package/dist/hooks/use-authed-src.d.ts +71 -0
  202. package/dist/hooks/use-authed-src.js +63 -0
  203. package/dist/hooks/use-authed-src.js.map +1 -0
  204. package/dist/hooks/use-file-drop.d.ts +50 -2
  205. package/dist/hooks/use-hotkey.d.ts +59 -0
  206. package/dist/hooks/use-hotkey.js +105 -0
  207. package/dist/hooks/use-hotkey.js.map +1 -0
  208. package/dist/hooks/use-search-param-state.d.ts +83 -0
  209. package/dist/hooks/use-search-param-state.js +94 -0
  210. package/dist/hooks/use-search-param-state.js.map +1 -0
  211. package/dist/i18n/defaults.d.ts +17 -4
  212. package/dist/i18n/defaults.js +33 -1
  213. package/dist/i18n/defaults.js.map +1 -1
  214. package/dist/i18n/kit-labels.d.ts +47 -310
  215. package/dist/i18n/kit-labels.js +20 -5
  216. package/dist/i18n/kit-labels.js.map +1 -1
  217. package/dist/i18n/locales/de-CH-informal.d.ts +17 -4
  218. package/dist/i18n/locales/de-CH.d.ts +17 -4
  219. package/dist/i18n/locales/de-informal.d.ts +17 -4
  220. package/dist/i18n/locales/de.d.ts +17 -4
  221. package/dist/i18n/locales/de.js +165 -4
  222. package/dist/i18n/locales/de.js.map +1 -1
  223. package/dist/i18n/locales/es.d.ts +17 -4
  224. package/dist/i18n/locales/es.js +162 -4
  225. package/dist/i18n/locales/es.js.map +1 -1
  226. package/dist/i18n/locales/fr.d.ts +17 -4
  227. package/dist/i18n/locales/fr.js +162 -4
  228. package/dist/i18n/locales/fr.js.map +1 -1
  229. package/dist/i18n/locales/hu.d.ts +17 -4
  230. package/dist/i18n/locales/hu.js +162 -4
  231. package/dist/i18n/locales/hu.js.map +1 -1
  232. package/dist/i18n/locales/it.d.ts +17 -4
  233. package/dist/i18n/locales/it.js +162 -4
  234. package/dist/i18n/locales/it.js.map +1 -1
  235. package/dist/i18n/locales/zh.d.ts +17 -4
  236. package/dist/i18n/locales/zh.js +162 -4
  237. package/dist/i18n/locales/zh.js.map +1 -1
  238. package/dist/index.d.ts +45 -21
  239. package/dist/index.js +43 -1
  240. package/dist/index.js.map +1 -1
  241. package/dist/kit-labels-v3biUF1L.d.ts +1645 -0
  242. package/dist/lib/clipping.d.ts +21 -5
  243. package/dist/lib/clipping.js +3 -0
  244. package/dist/lib/clipping.js.map +1 -1
  245. package/dist/lib/dates.d.ts +17 -1
  246. package/dist/lib/dates.js +19 -0
  247. package/dist/lib/dates.js.map +1 -1
  248. package/dist/lib/format.d.ts +144 -0
  249. package/dist/lib/format.js +149 -0
  250. package/dist/lib/format.js.map +1 -0
  251. package/dist/lib/qr-encode.d.ts +44 -0
  252. package/dist/lib/qr-encode.js +338 -0
  253. package/dist/lib/qr-encode.js.map +1 -0
  254. package/dist/rhf/fields.d.ts +241 -0
  255. package/dist/rhf/fields.js +601 -0
  256. package/dist/rhf/fields.js.map +1 -0
  257. package/dist/rhf/form.d.ts +50 -1
  258. package/dist/rhf/line-items.d.ts +31 -0
  259. package/dist/rhf/line-items.js +59 -0
  260. package/dist/rhf/line-items.js.map +1 -0
  261. package/dist/rhf/use-rhf-wizard-step.d.ts +22 -0
  262. package/dist/rhf/use-rhf-wizard-step.js +38 -0
  263. package/dist/rhf/use-rhf-wizard-step.js.map +1 -0
  264. package/dist/rhf.d.ts +63 -1
  265. package/dist/rhf.js +3 -0
  266. package/dist/rhf.js.map +1 -1
  267. package/dist/shell/app-shell.d.ts +99 -2
  268. package/dist/shell/app-shell.js +58 -3
  269. package/dist/shell/app-shell.js.map +1 -1
  270. package/dist/shell/auth-layout.d.ts +74 -0
  271. package/dist/shell/auth-layout.js +97 -0
  272. package/dist/shell/auth-layout.js.map +1 -0
  273. package/dist/shell/top-bar-brand.d.ts +81 -0
  274. package/dist/shell/top-bar-brand.js +47 -0
  275. package/dist/shell/top-bar-brand.js.map +1 -0
  276. package/dist/shell/topbar-action-menu.d.ts +84 -3
  277. package/dist/shell/topbar-action-menu.js +115 -27
  278. package/dist/shell/topbar-action-menu.js.map +1 -1
  279. package/dist/shell.d.ts +58 -2
  280. package/dist/shell.js +2 -0
  281. package/dist/shell.js.map +1 -1
  282. package/dist/wizard/stepper-nav.d.ts +49 -1
  283. package/dist/wizard/wizard-context.d.ts +30 -1
  284. package/dist/wizard/wizard-context.js +23 -2
  285. package/dist/wizard/wizard-context.js.map +1 -1
  286. package/dist/wizard/wizard-step.d.ts +20 -5
  287. package/dist/wizard/wizard-step.js +16 -3
  288. package/dist/wizard/wizard-step.js.map +1 -1
  289. package/dist/wizard.d.ts +51 -3
  290. package/package.json +1 -1
  291. package/src/chart.ts +4 -0
  292. package/src/components/account-settings-labels.ts +190 -0
  293. package/src/components/account-settings.tsx +110 -63
  294. package/src/components/alert-banner.tsx +36 -7
  295. package/src/components/amount-input.tsx +13 -1
  296. package/src/components/authed-image.tsx +190 -0
  297. package/src/components/breadcrumbs.tsx +8 -2
  298. package/src/components/bulk-action-bar.tsx +163 -57
  299. package/src/components/button-group.tsx +129 -4
  300. package/src/components/calendar-heatmap.tsx +599 -0
  301. package/src/components/chip.tsx +8 -4
  302. package/src/components/choice-card.tsx +208 -1
  303. package/src/components/combobox-core.tsx +5 -0
  304. package/src/components/combobox.tsx +31 -9
  305. package/src/components/copy-button.tsx +3 -2
  306. package/src/components/data-table-cells.tsx +119 -0
  307. package/src/components/data-table-filters.ts +77 -0
  308. package/src/components/data-table-labels.ts +23 -0
  309. package/src/components/data-table.tsx +231 -13
  310. package/src/components/date-picker.tsx +19 -3
  311. package/src/components/description-list.tsx +52 -3
  312. package/src/components/entity-combobox.tsx +18 -3
  313. package/src/components/error-boundary.tsx +216 -0
  314. package/src/components/field.tsx +188 -0
  315. package/src/components/floating-panel.tsx +380 -16
  316. package/src/components/form-actions.tsx +191 -0
  317. package/src/components/image-grid.tsx +181 -0
  318. package/src/components/lightbox.tsx +374 -0
  319. package/src/components/line-items.tsx +385 -0
  320. package/src/components/list.tsx +93 -18
  321. package/src/components/loading-state.tsx +47 -0
  322. package/src/components/menu-item.tsx +29 -3
  323. package/src/components/mini-calendar.tsx +317 -57
  324. package/src/components/modal.tsx +61 -3
  325. package/src/components/month-picker.tsx +181 -26
  326. package/src/components/multi-entity-combobox.tsx +19 -3
  327. package/src/components/nav-pills.tsx +152 -0
  328. package/src/components/number-field.tsx +15 -10
  329. package/src/components/number-input.tsx +16 -1
  330. package/src/components/passkeys-setting.tsx +331 -0
  331. package/src/components/picker-sheet.tsx +7 -1
  332. package/src/components/pie-chart-labels.ts +32 -0
  333. package/src/components/pie-chart.tsx +479 -0
  334. package/src/components/progress-bar.tsx +241 -13
  335. package/src/components/qr-code.tsx +83 -0
  336. package/src/components/search-field.tsx +20 -4
  337. package/src/components/series-chart.tsx +21 -0
  338. package/src/components/signed-amount.tsx +286 -0
  339. package/src/components/skeleton.tsx +21 -3
  340. package/src/components/stat-tile.tsx +8 -2
  341. package/src/components/status-dot.tsx +10 -2
  342. package/src/components/table.tsx +186 -23
  343. package/src/components/text-link.tsx +248 -0
  344. package/src/components/text.tsx +9 -1
  345. package/src/components/toggle-group.tsx +106 -13
  346. package/src/components/toggle-legend.tsx +87 -2
  347. package/src/components/tooltip.tsx +17 -3
  348. package/src/components/ui.tsx +572 -44
  349. package/src/components/use-table-state.ts +265 -33
  350. package/src/components/user-avatar.tsx +57 -3
  351. package/src/data-table.ts +10 -0
  352. package/src/feedback/feedback-dialog.tsx +42 -3
  353. package/src/feedback/feedback-thread.tsx +434 -0
  354. package/src/feedback.ts +1 -0
  355. package/src/hooks/use-authed-src.ts +161 -0
  356. package/src/hooks/use-hotkey.ts +172 -0
  357. package/src/hooks/use-search-param-state.ts +187 -0
  358. package/src/i18n/defaults.ts +32 -0
  359. package/src/i18n/kit-labels.tsx +104 -8
  360. package/src/i18n/locales/de.ts +162 -0
  361. package/src/i18n/locales/es.ts +159 -0
  362. package/src/i18n/locales/fr.ts +162 -0
  363. package/src/i18n/locales/hu.ts +160 -0
  364. package/src/i18n/locales/it.ts +160 -0
  365. package/src/i18n/locales/zh.ts +159 -0
  366. package/src/index.ts +64 -0
  367. package/src/lib/clipping.ts +22 -4
  368. package/src/lib/dates.ts +37 -0
  369. package/src/lib/format.ts +325 -0
  370. package/src/lib/qr-encode.ts +420 -0
  371. package/src/rhf/fields.tsx +1054 -0
  372. package/src/rhf/line-items.tsx +130 -0
  373. package/src/rhf/use-rhf-wizard-step.ts +113 -0
  374. package/src/rhf.ts +8 -0
  375. package/src/shell/app-shell.tsx +147 -2
  376. package/src/shell/auth-layout.tsx +190 -0
  377. package/src/shell/top-bar-brand.tsx +76 -0
  378. package/src/shell/topbar-action-menu.tsx +260 -49
  379. package/src/shell.ts +2 -0
  380. package/src/wizard/wizard-context.tsx +50 -1
  381. package/src/wizard/wizard-step.tsx +43 -6
  382. package/tokens.css +12 -0
@@ -1,9 +1,25 @@
1
- /** Internal (not exported from the package): the Tooltip's auto-portal check. */
1
+ /** Internal (not exported from the package): the Tooltip's auto-portal check. The
2
+ * attribute name itself IS exported, as `CLIPS_ATTRIBUTE` from the tooltip module. */
3
+ /**
4
+ * The explicit "this element clips or scrolls" marker. A container carrying it counts
5
+ * as clipping whatever its computed `overflow` says.
6
+ *
7
+ * Why a marker as well as the computed style: the kit's containers get their
8
+ * `overflow` from Tailwind classes, and jsdom computes no stylesheet — so under test
9
+ * every scroller reads `overflow: visible`, the Tooltip stays in place, and its always-
10
+ * mounted bubble shows up in the cell's text (keksdose 0.10: "CheckingChecking"). The
11
+ * browser and the test then disagreed about where the bubble lives, and apps pinned
12
+ * `portal` on every tooltip in a table to make them agree. The kit's own scrollers
13
+ * (DataTable's body, Table's wrapper) carry the marker, so the answer is the same in
14
+ * both; an app marks its own with `{ [CLIPS_ATTRIBUTE]: "" }` or a literal `data-clips`.
15
+ */
16
+ declare const CLIPS_ATTRIBUTE = "data-clips";
2
17
  /** Whether anything between `el` and `<body>` clips or scrolls its content — the
3
18
  * containers an absolutely positioned bubble would be cut off by, or would widen.
4
- * `<body>` and `<html>` are not counted: their `overflow` belongs to the viewport (a
5
- * dialog's scroll lock sets it), and the viewport clips a portalled bubble just the
6
- * same. */
19
+ * Either the computed `overflow` says so or the element carries
20
+ * {@link CLIPS_ATTRIBUTE}. `<body>` and `<html>` are not counted: their `overflow`
21
+ * belongs to the viewport (a dialog's scroll lock sets it), and the viewport clips a
22
+ * portalled bubble just the same. */
7
23
  declare function hasClippingAncestor(el: Element | null): boolean;
8
24
 
9
- export { hasClippingAncestor };
25
+ export { CLIPS_ATTRIBUTE, hasClippingAncestor };
@@ -1,14 +1,17 @@
1
1
  "use client";
2
+ const CLIPS_ATTRIBUTE = "data-clips";
2
3
  function hasClippingAncestor(el) {
3
4
  if (typeof window === "undefined" || !el) return false;
4
5
  for (let node = el.parentElement; node; node = node.parentElement) {
5
6
  if (node === document.body || node === document.documentElement) return false;
7
+ if (node.hasAttribute(CLIPS_ATTRIBUTE)) return true;
6
8
  const style = window.getComputedStyle(node);
7
9
  if (style.overflowX !== "visible" || style.overflowY !== "visible") return true;
8
10
  }
9
11
  return false;
10
12
  }
11
13
  export {
14
+ CLIPS_ATTRIBUTE,
12
15
  hasClippingAncestor
13
16
  };
14
17
  //# sourceMappingURL=clipping.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/clipping.ts"],"sourcesContent":["/** Internal (not exported from the package): the Tooltip's auto-portal check. */\n\n/** Whether anything between `el` and `<body>` clips or scrolls its content — the\n * containers an absolutely positioned bubble would be cut off by, or would widen.\n * `<body>` and `<html>` are not counted: their `overflow` belongs to the viewport (a\n * dialog's scroll lock sets it), and the viewport clips a portalled bubble just the\n * same. */\nexport function hasClippingAncestor(el: Element | null): boolean {\n if (typeof window === \"undefined\" || !el) return false;\n for (let node = el.parentElement; node; node = node.parentElement) {\n if (node === document.body || node === document.documentElement) return false;\n const style = window.getComputedStyle(node);\n if (style.overflowX !== \"visible\" || style.overflowY !== \"visible\") return true;\n }\n return false;\n}\n"],"mappings":";AAOO,SAAS,oBAAoB,IAA6B;AAC/D,MAAI,OAAO,WAAW,eAAe,CAAC,GAAI,QAAO;AACjD,WAAS,OAAO,GAAG,eAAe,MAAM,OAAO,KAAK,eAAe;AACjE,QAAI,SAAS,SAAS,QAAQ,SAAS,SAAS,gBAAiB,QAAO;AACxE,UAAM,QAAQ,OAAO,iBAAiB,IAAI;AAC1C,QAAI,MAAM,cAAc,aAAa,MAAM,cAAc,UAAW,QAAO;AAAA,EAC7E;AACA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/lib/clipping.ts"],"sourcesContent":["/** Internal (not exported from the package): the Tooltip's auto-portal check. The\n * attribute name itself IS exported, as `CLIPS_ATTRIBUTE` from the tooltip module. */\n\n/**\n * The explicit \"this element clips or scrolls\" marker. A container carrying it counts\n * as clipping whatever its computed `overflow` says.\n *\n * Why a marker as well as the computed style: the kit's containers get their\n * `overflow` from Tailwind classes, and jsdom computes no stylesheet — so under test\n * every scroller reads `overflow: visible`, the Tooltip stays in place, and its always-\n * mounted bubble shows up in the cell's text (keksdose 0.10: \"CheckingChecking\"). The\n * browser and the test then disagreed about where the bubble lives, and apps pinned\n * `portal` on every tooltip in a table to make them agree. The kit's own scrollers\n * (DataTable's body, Table's wrapper) carry the marker, so the answer is the same in\n * both; an app marks its own with `{ [CLIPS_ATTRIBUTE]: \"\" }` or a literal `data-clips`.\n */\nexport const CLIPS_ATTRIBUTE = \"data-clips\";\n\n/** Whether anything between `el` and `<body>` clips or scrolls its content — the\n * containers an absolutely positioned bubble would be cut off by, or would widen.\n * Either the computed `overflow` says so or the element carries\n * {@link CLIPS_ATTRIBUTE}. `<body>` and `<html>` are not counted: their `overflow`\n * belongs to the viewport (a dialog's scroll lock sets it), and the viewport clips a\n * portalled bubble just the same. */\nexport function hasClippingAncestor(el: Element | null): boolean {\n if (typeof window === \"undefined\" || !el) return false;\n for (let node = el.parentElement; node; node = node.parentElement) {\n if (node === document.body || node === document.documentElement) return false;\n if (node.hasAttribute(CLIPS_ATTRIBUTE)) return true;\n const style = window.getComputedStyle(node);\n if (style.overflowX !== \"visible\" || style.overflowY !== \"visible\") return true;\n }\n return false;\n}\n"],"mappings":";AAgBO,MAAM,kBAAkB;AAQxB,SAAS,oBAAoB,IAA6B;AAC/D,MAAI,OAAO,WAAW,eAAe,CAAC,GAAI,QAAO;AACjD,WAAS,OAAO,GAAG,eAAe,MAAM,OAAO,KAAK,eAAe;AACjE,QAAI,SAAS,SAAS,QAAQ,SAAS,SAAS,gBAAiB,QAAO;AACxE,QAAI,KAAK,aAAa,eAAe,EAAG,QAAO;AAC/C,UAAM,QAAQ,OAAO,iBAAiB,IAAI;AAC1C,QAAI,MAAM,cAAc,aAAa,MAAM,cAAc,UAAW,QAAO;AAAA,EAC7E;AACA,SAAO;AACT;","names":[]}
@@ -67,6 +67,22 @@ declare function formatIsoDate(iso: string, locale: string, options?: Intl.DateT
67
67
  * {@link toLocalIso} reads local components back, so this never slides a day the way
68
68
  * a `toISOString()` round-trip does west of UTC. */
69
69
  declare function addDaysIso(iso: string, days: number): string;
70
+ /**
71
+ * `d` moved by whole months, keeping the day of the month where the target has one.
72
+ * `setMonth` alone ROLLS OVER — 31 January + 1 month is 3 March — which would skip
73
+ * February entirely for anyone paging through the year with PageDown. Returns a new
74
+ * Date; `d` is not touched.
75
+ */
76
+ declare function addMonthsClamped(d: Date, months: number): Date;
77
+ /**
78
+ * The first day of the week in `locale`, as a `Date#getDay` index (0 = Sunday).
79
+ *
80
+ * `Intl.Locale#getWeekInfo()` is the standard spelling; V8 shipped it first as the
81
+ * `weekInfo` accessor, and Firefox has neither yet — hence both reads and a Monday
82
+ * fallback, which is ISO 8601. Its `firstDay` counts 1 = Monday … 7 = Sunday, so `% 7`
83
+ * maps it onto `getDay`. `undefined` asks for the runtime's default locale.
84
+ */
85
+ declare function localeWeekStart(locale: string | undefined): 0 | 1 | 2 | 3 | 4 | 5 | 6;
70
86
  /** Whether two Dates fall on the same local calendar day. */
71
87
  declare function sameYmd(a: Date, b: Date): boolean;
72
88
  interface DateRangePreset {
@@ -117,4 +133,4 @@ interface CalendarMonthPresetOptions {
117
133
  */
118
134
  declare function calendarMonthPresets(options?: CalendarMonthPresetOptions): DateRangePreset[];
119
135
 
120
- export { type CalendarMonthPresetOptions, type DateRangePreset, addDaysIso, calendarMonthPresets, dateRangePresets, endOfMonthIso, endOfYearIso, formatIsoDate, lastFullMonthsRange, lastFullYearsRange, monthKey, pad, parseIsoDate, sameYmd, shiftIso, startOfMonthIso, startOfWeekIso, startOfYearIso, toLocalIso, todayIso };
136
+ export { type CalendarMonthPresetOptions, type DateRangePreset, addDaysIso, addMonthsClamped, calendarMonthPresets, dateRangePresets, endOfMonthIso, endOfYearIso, formatIsoDate, lastFullMonthsRange, lastFullYearsRange, localeWeekStart, monthKey, pad, parseIsoDate, sameYmd, shiftIso, startOfMonthIso, startOfWeekIso, startOfYearIso, toLocalIso, todayIso };
package/dist/lib/dates.js CHANGED
@@ -70,6 +70,23 @@ function addDaysIso(iso, days) {
70
70
  d.setDate(d.getDate() + days);
71
71
  return toLocalIso(d);
72
72
  }
73
+ function addMonthsClamped(d, months) {
74
+ const day = d.getDate();
75
+ const out = new Date(d.getFullYear(), d.getMonth() + months, 1);
76
+ const lastOfTarget = new Date(out.getFullYear(), out.getMonth() + 1, 0).getDate();
77
+ out.setDate(Math.min(day, lastOfTarget));
78
+ return out;
79
+ }
80
+ function localeWeekStart(locale) {
81
+ try {
82
+ const tag = locale ?? new Intl.DateTimeFormat().resolvedOptions().locale;
83
+ const loc = new Intl.Locale(tag);
84
+ const firstDay = (loc.getWeekInfo?.() ?? loc.weekInfo)?.firstDay;
85
+ if (typeof firstDay === "number") return firstDay % 7;
86
+ } catch {
87
+ }
88
+ return 1;
89
+ }
73
90
  function sameYmd(a, b) {
74
91
  return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
75
92
  }
@@ -107,6 +124,7 @@ function calendarMonthPresets(options = {}) {
107
124
  }
108
125
  export {
109
126
  addDaysIso,
127
+ addMonthsClamped,
110
128
  calendarMonthPresets,
111
129
  dateRangePresets,
112
130
  endOfMonthIso,
@@ -114,6 +132,7 @@ export {
114
132
  formatIsoDate,
115
133
  lastFullMonthsRange,
116
134
  lastFullYearsRange,
135
+ localeWeekStart,
117
136
  monthKey,
118
137
  pad,
119
138
  parseIsoDate,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/dates.ts"],"sourcesContent":["/**\n * ISO date helpers, collected from copies that were scattered across the pages.\n *\n * **Every \"what day is it\" helper here reads the LOCAL calendar** — one family, not\n * two. It used to be two: these all built a local Date and then read it back through\n * `toISOString()`, which is UTC, so east of Greenwich they all returned YESTERDAY\n * between local midnight and the UTC rollover. The old module note called that\n * deliberate and warned against changing it. It was not defensible, and dev#471 is\n * what it cost:\n *\n * > *\"Just entered a tx now from scheduled. But it still shows as upcoming. Is there\n * > a problem with timezones? It is currently 1 o clock middle european summer time.\"*\n *\n * There was: at 01:00 CEST `todayIso()` answered with the 14th, the row the scheduler\n * had just booked was dated the 15th, and `isUpcoming` compares the two strings — so a\n * transaction entered a second ago read as one still to come. The same slip moved\n * `startOfMonthIso()` onto the last day of the PREVIOUS month for an hour every first\n * of the month, which is a report range nobody would have questioned.\n *\n * A calendar day only means anything to the human reading it, and every caller here —\n * the register's today, the date-range presets, the budget cursor, a new\n * transaction's default date — is asking that question. There is no caller for whom\n * \"the UTC day\" is the right answer; the server compares plain dates with no zone at\n * all, so sending it the user's local day is also what it wants.\n */\n\n/** Zero-pad a number to two digits (\"3\" -> \"03\"). */\nexport function pad(n: number): string {\n return String(n).padStart(2, \"0\");\n}\n\n/** Today as \"YYYY-MM-DD\" on the LOCAL calendar (see the module note). */\nexport function todayIso(): string {\n return toLocalIso(new Date());\n}\n\n/** A Date as a local-time \"YYYY-MM-DD\" string. */\nexport function toLocalIso(d: Date): string {\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;\n}\n\n/** A Date as a local-time \"YYYY-MM\" month key. */\nexport function monthKey(d: Date): string {\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}`;\n}\n\n// ---------- Date-range presets + calendar helpers ----------\n// Local-calendar like todayIso (see the module note); used by the data-table date\n// filter and its MiniCalendar. Each one mutates a local Date and must therefore READ\n// it back locally — going out through toISOString() is what shifted every one of\n// these by a day near midnight (dev#471).\n\n/** Today shifted by `days`, as \"YYYY-MM-DD\". */\nexport function shiftIso(days: number): string {\n const d = new Date();\n d.setDate(d.getDate() + days);\n return toLocalIso(d);\n}\n\n/** First day (Monday) of the current week, as \"YYYY-MM-DD\". */\nexport function startOfWeekIso(): string {\n const d = new Date();\n const day = d.getDay();\n const diff = (day + 6) % 7; // monday = 0\n d.setDate(d.getDate() - diff);\n return toLocalIso(d);\n}\n\n/** First day of the month `offsetMonths` from now, as \"YYYY-MM-DD\". */\nexport function startOfMonthIso(offsetMonths = 0): string {\n const d = new Date();\n d.setDate(1);\n d.setMonth(d.getMonth() + offsetMonths);\n return toLocalIso(d);\n}\n\n/** Last day of the month `offsetMonths` from now, as \"YYYY-MM-DD\". */\nexport function endOfMonthIso(offsetMonths = 0): string {\n const d = new Date();\n d.setDate(1);\n d.setMonth(d.getMonth() + offsetMonths + 1);\n d.setDate(0);\n return toLocalIso(d);\n}\n\n/** First day of the year `offsetYears` from now, as \"YYYY-MM-DD\". */\nexport function startOfYearIso(offsetYears = 0): string {\n const d = new Date();\n d.setMonth(0);\n d.setDate(1);\n d.setFullYear(d.getFullYear() + offsetYears);\n return toLocalIso(d);\n}\n\n/** Last day of the year `offsetYears` from now, as \"YYYY-MM-DD\". */\nexport function endOfYearIso(offsetYears = 0): string {\n const d = new Date();\n d.setMonth(11);\n d.setDate(31);\n d.setFullYear(d.getFullYear() + offsetYears);\n return toLocalIso(d);\n}\n\n/** Parse a \"YYYY-MM-DD\" string to a local-midnight Date, or null if malformed —\n * where \"malformed\" includes a date that does not exist.\n *\n * The falsy check alone was not enough. `new Date(y, m - 1, d)` ROLLS OVER out-of-range\n * components rather than refusing them, so \"2026-13-45\" came back as 2027-02-14 and\n * \"2026-02-30\" as 2026-03-02: a valid-looking Date for a day that never happened. No\n * caller could tell — {@link formatIsoDate} blanks only on `null`, so the invented\n * date formatted and displayed exactly like a real one.\n *\n * The round-trip is the check: build the Date, then read the components back off it.\n * A value that rolled over cannot answer with the numbers it was given. That is the\n * same shape {@link sameYmd} already expresses, applied to a Date and its source. */\nexport function parseIsoDate(s: string): Date | null {\n if (!s) return null;\n const [y, m, d] = s.split(\"-\").map(Number);\n if (!y || !m || !d) return null;\n if (!Number.isInteger(y) || !Number.isInteger(m) || !Number.isInteger(d)) return null;\n const dt = new Date(y, m - 1, d);\n if (Number.isNaN(dt.getTime())) return null;\n if (dt.getFullYear() !== y || dt.getMonth() !== m - 1 || dt.getDate() !== d) return null;\n return dt;\n}\n\n/** Format a \"YYYY-MM-DD\" string for display in `locale` (empty string when\n * unparseable). Defaults to the locale's short numeric date; pass `options` to\n * override (e.g. `{ dateStyle: \"medium\" }`). */\nexport function formatIsoDate(\n iso: string,\n locale: string,\n options?: Intl.DateTimeFormatOptions,\n): string {\n const d = parseIsoDate(iso);\n return d ? d.toLocaleDateString(locale, options) : \"\";\n}\n\n/** A \"YYYY-MM-DD\" string shifted by `days` (negative shifts back), or the input\n * unchanged when it isn't a parseable date.\n *\n * Local-time throughout — {@link parseIsoDate} builds a local-midnight Date and\n * {@link toLocalIso} reads local components back, so this never slides a day the way\n * a `toISOString()` round-trip does west of UTC. */\nexport function addDaysIso(iso: string, days: number): string {\n const d = parseIsoDate(iso);\n if (!d) return iso;\n d.setDate(d.getDate() + days);\n return toLocalIso(d);\n}\n\n/** Whether two Dates fall on the same local calendar day. */\nexport function sameYmd(a: Date, b: Date): boolean {\n return (\n a.getFullYear() === b.getFullYear() &&\n a.getMonth() === b.getMonth() &&\n a.getDate() === b.getDate()\n );\n}\n\nexport interface DateRangePreset {\n key: string;\n from: string;\n to: string;\n}\n\n/**\n * The named ranges offered by the data-table date filter (local-calendar, like\n * {@link todayIso} — see the module note). `key` maps to the consumer's\n * `table.preset_*` labels.\n */\nexport function dateRangePresets(): DateRangePreset[] {\n const mondayOffset = (new Date().getDay() + 6) % 7; // monday = 0\n return [\n { key: \"today\", from: todayIso(), to: todayIso() },\n { key: \"yesterday\", from: shiftIso(-1), to: shiftIso(-1) },\n { key: \"this_week\", from: startOfWeekIso(), to: todayIso() },\n { key: \"last_week\", from: shiftIso(-7 - mondayOffset), to: shiftIso(-1 - mondayOffset) },\n { key: \"last_7_days\", from: shiftIso(-6), to: todayIso() },\n { key: \"last_30_days\", from: shiftIso(-29), to: todayIso() },\n { key: \"this_month\", from: startOfMonthIso(0), to: todayIso() },\n { key: \"last_month\", from: startOfMonthIso(-1), to: endOfMonthIso(-1) },\n { key: \"last_3_months\", from: shiftIso(-90), to: todayIso() },\n { key: \"ytd\", from: startOfYearIso(0), to: todayIso() },\n { key: \"last_year\", from: startOfYearIso(-1), to: endOfYearIso(-1) },\n ];\n}\n\n/** The last `n` WHOLE calendar months before the current one, as an inclusive\n * `{ from, to }` — on 15 May, `n = 3` is 1 February … 30 April. Local-calendar, like\n * everything here; `now` is for tests and for a host with its own clock. */\nexport function lastFullMonthsRange(n: number, now: Date = new Date()): { from: string; to: string } {\n const from = new Date(now.getFullYear(), now.getMonth() - n, 1);\n // Day 0 of the current month is the last day of the previous one.\n const to = new Date(now.getFullYear(), now.getMonth(), 0);\n return { from: toLocalIso(from), to: toLocalIso(to) };\n}\n\n/** The last `n` WHOLE calendar years before the current one — in 2026, `n = 2` is\n * 1 January 2024 … 31 December 2025. */\nexport function lastFullYearsRange(n: number, now: Date = new Date()): { from: string; to: string } {\n const year = now.getFullYear();\n return { from: `${year - n}-01-01`, to: `${year - 1}-12-31` };\n}\n\nexport interface CalendarMonthPresetOptions {\n /** How many whole months each month preset spans. Default `[3, 6]`. */\n months?: readonly number[];\n /** How many whole years each year preset spans. Default `[2]`. */\n years?: readonly number[];\n /** The day to count back from. Default: now, on the local calendar. */\n now?: Date;\n}\n\n/**\n * Month-ALIGNED presets, beside the rolling-day ones of {@link dateRangePresets}.\n *\n * kastlan's accounting reports are closed by the month: \"the last three months\" there\n * means February, March and April, never \"the 90 days up to this morning\", which\n * starts mid-month and ends with a day that is still being booked. `last_3_months`\n * above is the rolling kind and stays that way for the data table's filter, so these\n * are separate keys rather than a changed meaning under an old one.\n *\n * Keys are `last_<n>_full_months` and `last_<n>_full_years`; the host labels them (the\n * same arrangement as `dateRangePresets`' keys and its `table.preset_*` strings). With\n * `DateRangePicker`, pass the key as the preset's `id` so the choice keeps its\n * identity while the dates are recomputed on every render.\n */\nexport function calendarMonthPresets(options: CalendarMonthPresetOptions = {}): DateRangePreset[] {\n const { months = [3, 6], years = [2], now = new Date() } = options;\n return [\n ...months.map((n) => ({ key: `last_${n}_full_months`, ...lastFullMonthsRange(n, now) })),\n ...years.map((n) => ({ key: `last_${n}_full_years`, ...lastFullYearsRange(n, now) })),\n ];\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/lib/dates.ts"],"sourcesContent":["/**\n * ISO date helpers, collected from copies that were scattered across the pages.\n *\n * **Every \"what day is it\" helper here reads the LOCAL calendar** — one family, not\n * two. It used to be two: these all built a local Date and then read it back through\n * `toISOString()`, which is UTC, so east of Greenwich they all returned YESTERDAY\n * between local midnight and the UTC rollover. The old module note called that\n * deliberate and warned against changing it. It was not defensible, and dev#471 is\n * what it cost:\n *\n * > *\"Just entered a tx now from scheduled. But it still shows as upcoming. Is there\n * > a problem with timezones? It is currently 1 o clock middle european summer time.\"*\n *\n * There was: at 01:00 CEST `todayIso()` answered with the 14th, the row the scheduler\n * had just booked was dated the 15th, and `isUpcoming` compares the two strings — so a\n * transaction entered a second ago read as one still to come. The same slip moved\n * `startOfMonthIso()` onto the last day of the PREVIOUS month for an hour every first\n * of the month, which is a report range nobody would have questioned.\n *\n * A calendar day only means anything to the human reading it, and every caller here —\n * the register's today, the date-range presets, the budget cursor, a new\n * transaction's default date — is asking that question. There is no caller for whom\n * \"the UTC day\" is the right answer; the server compares plain dates with no zone at\n * all, so sending it the user's local day is also what it wants.\n */\n\n/** Zero-pad a number to two digits (\"3\" -> \"03\"). */\nexport function pad(n: number): string {\n return String(n).padStart(2, \"0\");\n}\n\n/** Today as \"YYYY-MM-DD\" on the LOCAL calendar (see the module note). */\nexport function todayIso(): string {\n return toLocalIso(new Date());\n}\n\n/** A Date as a local-time \"YYYY-MM-DD\" string. */\nexport function toLocalIso(d: Date): string {\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;\n}\n\n/** A Date as a local-time \"YYYY-MM\" month key. */\nexport function monthKey(d: Date): string {\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}`;\n}\n\n// ---------- Date-range presets + calendar helpers ----------\n// Local-calendar like todayIso (see the module note); used by the data-table date\n// filter and its MiniCalendar. Each one mutates a local Date and must therefore READ\n// it back locally — going out through toISOString() is what shifted every one of\n// these by a day near midnight (dev#471).\n\n/** Today shifted by `days`, as \"YYYY-MM-DD\". */\nexport function shiftIso(days: number): string {\n const d = new Date();\n d.setDate(d.getDate() + days);\n return toLocalIso(d);\n}\n\n/** First day (Monday) of the current week, as \"YYYY-MM-DD\". */\nexport function startOfWeekIso(): string {\n const d = new Date();\n const day = d.getDay();\n const diff = (day + 6) % 7; // monday = 0\n d.setDate(d.getDate() - diff);\n return toLocalIso(d);\n}\n\n/** First day of the month `offsetMonths` from now, as \"YYYY-MM-DD\". */\nexport function startOfMonthIso(offsetMonths = 0): string {\n const d = new Date();\n d.setDate(1);\n d.setMonth(d.getMonth() + offsetMonths);\n return toLocalIso(d);\n}\n\n/** Last day of the month `offsetMonths` from now, as \"YYYY-MM-DD\". */\nexport function endOfMonthIso(offsetMonths = 0): string {\n const d = new Date();\n d.setDate(1);\n d.setMonth(d.getMonth() + offsetMonths + 1);\n d.setDate(0);\n return toLocalIso(d);\n}\n\n/** First day of the year `offsetYears` from now, as \"YYYY-MM-DD\". */\nexport function startOfYearIso(offsetYears = 0): string {\n const d = new Date();\n d.setMonth(0);\n d.setDate(1);\n d.setFullYear(d.getFullYear() + offsetYears);\n return toLocalIso(d);\n}\n\n/** Last day of the year `offsetYears` from now, as \"YYYY-MM-DD\". */\nexport function endOfYearIso(offsetYears = 0): string {\n const d = new Date();\n d.setMonth(11);\n d.setDate(31);\n d.setFullYear(d.getFullYear() + offsetYears);\n return toLocalIso(d);\n}\n\n/** Parse a \"YYYY-MM-DD\" string to a local-midnight Date, or null if malformed —\n * where \"malformed\" includes a date that does not exist.\n *\n * The falsy check alone was not enough. `new Date(y, m - 1, d)` ROLLS OVER out-of-range\n * components rather than refusing them, so \"2026-13-45\" came back as 2027-02-14 and\n * \"2026-02-30\" as 2026-03-02: a valid-looking Date for a day that never happened. No\n * caller could tell — {@link formatIsoDate} blanks only on `null`, so the invented\n * date formatted and displayed exactly like a real one.\n *\n * The round-trip is the check: build the Date, then read the components back off it.\n * A value that rolled over cannot answer with the numbers it was given. That is the\n * same shape {@link sameYmd} already expresses, applied to a Date and its source. */\nexport function parseIsoDate(s: string): Date | null {\n if (!s) return null;\n const [y, m, d] = s.split(\"-\").map(Number);\n if (!y || !m || !d) return null;\n if (!Number.isInteger(y) || !Number.isInteger(m) || !Number.isInteger(d)) return null;\n const dt = new Date(y, m - 1, d);\n if (Number.isNaN(dt.getTime())) return null;\n if (dt.getFullYear() !== y || dt.getMonth() !== m - 1 || dt.getDate() !== d) return null;\n return dt;\n}\n\n/** Format a \"YYYY-MM-DD\" string for display in `locale` (empty string when\n * unparseable). Defaults to the locale's short numeric date; pass `options` to\n * override (e.g. `{ dateStyle: \"medium\" }`). */\nexport function formatIsoDate(\n iso: string,\n locale: string,\n options?: Intl.DateTimeFormatOptions,\n): string {\n const d = parseIsoDate(iso);\n return d ? d.toLocaleDateString(locale, options) : \"\";\n}\n\n/** A \"YYYY-MM-DD\" string shifted by `days` (negative shifts back), or the input\n * unchanged when it isn't a parseable date.\n *\n * Local-time throughout — {@link parseIsoDate} builds a local-midnight Date and\n * {@link toLocalIso} reads local components back, so this never slides a day the way\n * a `toISOString()` round-trip does west of UTC. */\nexport function addDaysIso(iso: string, days: number): string {\n const d = parseIsoDate(iso);\n if (!d) return iso;\n d.setDate(d.getDate() + days);\n return toLocalIso(d);\n}\n\n/**\n * `d` moved by whole months, keeping the day of the month where the target has one.\n * `setMonth` alone ROLLS OVER — 31 January + 1 month is 3 March — which would skip\n * February entirely for anyone paging through the year with PageDown. Returns a new\n * Date; `d` is not touched.\n */\nexport function addMonthsClamped(d: Date, months: number): Date {\n const day = d.getDate();\n const out = new Date(d.getFullYear(), d.getMonth() + months, 1);\n const lastOfTarget = new Date(out.getFullYear(), out.getMonth() + 1, 0).getDate();\n out.setDate(Math.min(day, lastOfTarget));\n return out;\n}\n\n/**\n * The first day of the week in `locale`, as a `Date#getDay` index (0 = Sunday).\n *\n * `Intl.Locale#getWeekInfo()` is the standard spelling; V8 shipped it first as the\n * `weekInfo` accessor, and Firefox has neither yet — hence both reads and a Monday\n * fallback, which is ISO 8601. Its `firstDay` counts 1 = Monday … 7 = Sunday, so `% 7`\n * maps it onto `getDay`. `undefined` asks for the runtime's default locale.\n */\nexport function localeWeekStart(locale: string | undefined): 0 | 1 | 2 | 3 | 4 | 5 | 6 {\n try {\n const tag = locale ?? new Intl.DateTimeFormat().resolvedOptions().locale;\n const loc = new Intl.Locale(tag) as Intl.Locale & {\n getWeekInfo?: () => { firstDay: number };\n weekInfo?: { firstDay: number };\n };\n const firstDay = (loc.getWeekInfo?.() ?? loc.weekInfo)?.firstDay;\n if (typeof firstDay === \"number\") return (firstDay % 7) as 0 | 1 | 2 | 3 | 4 | 5 | 6;\n } catch {\n // A malformed tag throws from `Intl.Locale`; a calendar still has to render.\n }\n return 1;\n}\n\n/** Whether two Dates fall on the same local calendar day. */\nexport function sameYmd(a: Date, b: Date): boolean {\n return (\n a.getFullYear() === b.getFullYear() &&\n a.getMonth() === b.getMonth() &&\n a.getDate() === b.getDate()\n );\n}\n\nexport interface DateRangePreset {\n key: string;\n from: string;\n to: string;\n}\n\n/**\n * The named ranges offered by the data-table date filter (local-calendar, like\n * {@link todayIso} — see the module note). `key` maps to the consumer's\n * `table.preset_*` labels.\n */\nexport function dateRangePresets(): DateRangePreset[] {\n const mondayOffset = (new Date().getDay() + 6) % 7; // monday = 0\n return [\n { key: \"today\", from: todayIso(), to: todayIso() },\n { key: \"yesterday\", from: shiftIso(-1), to: shiftIso(-1) },\n { key: \"this_week\", from: startOfWeekIso(), to: todayIso() },\n { key: \"last_week\", from: shiftIso(-7 - mondayOffset), to: shiftIso(-1 - mondayOffset) },\n { key: \"last_7_days\", from: shiftIso(-6), to: todayIso() },\n { key: \"last_30_days\", from: shiftIso(-29), to: todayIso() },\n { key: \"this_month\", from: startOfMonthIso(0), to: todayIso() },\n { key: \"last_month\", from: startOfMonthIso(-1), to: endOfMonthIso(-1) },\n { key: \"last_3_months\", from: shiftIso(-90), to: todayIso() },\n { key: \"ytd\", from: startOfYearIso(0), to: todayIso() },\n { key: \"last_year\", from: startOfYearIso(-1), to: endOfYearIso(-1) },\n ];\n}\n\n/** The last `n` WHOLE calendar months before the current one, as an inclusive\n * `{ from, to }` — on 15 May, `n = 3` is 1 February … 30 April. Local-calendar, like\n * everything here; `now` is for tests and for a host with its own clock. */\nexport function lastFullMonthsRange(n: number, now: Date = new Date()): { from: string; to: string } {\n const from = new Date(now.getFullYear(), now.getMonth() - n, 1);\n // Day 0 of the current month is the last day of the previous one.\n const to = new Date(now.getFullYear(), now.getMonth(), 0);\n return { from: toLocalIso(from), to: toLocalIso(to) };\n}\n\n/** The last `n` WHOLE calendar years before the current one — in 2026, `n = 2` is\n * 1 January 2024 … 31 December 2025. */\nexport function lastFullYearsRange(n: number, now: Date = new Date()): { from: string; to: string } {\n const year = now.getFullYear();\n return { from: `${year - n}-01-01`, to: `${year - 1}-12-31` };\n}\n\nexport interface CalendarMonthPresetOptions {\n /** How many whole months each month preset spans. Default `[3, 6]`. */\n months?: readonly number[];\n /** How many whole years each year preset spans. Default `[2]`. */\n years?: readonly number[];\n /** The day to count back from. Default: now, on the local calendar. */\n now?: Date;\n}\n\n/**\n * Month-ALIGNED presets, beside the rolling-day ones of {@link dateRangePresets}.\n *\n * kastlan's accounting reports are closed by the month: \"the last three months\" there\n * means February, March and April, never \"the 90 days up to this morning\", which\n * starts mid-month and ends with a day that is still being booked. `last_3_months`\n * above is the rolling kind and stays that way for the data table's filter, so these\n * are separate keys rather than a changed meaning under an old one.\n *\n * Keys are `last_<n>_full_months` and `last_<n>_full_years`; the host labels them (the\n * same arrangement as `dateRangePresets`' keys and its `table.preset_*` strings). With\n * `DateRangePicker`, pass the key as the preset's `id` so the choice keeps its\n * identity while the dates are recomputed on every render.\n */\nexport function calendarMonthPresets(options: CalendarMonthPresetOptions = {}): DateRangePreset[] {\n const { months = [3, 6], years = [2], now = new Date() } = options;\n return [\n ...months.map((n) => ({ key: `last_${n}_full_months`, ...lastFullMonthsRange(n, now) })),\n ...years.map((n) => ({ key: `last_${n}_full_years`, ...lastFullYearsRange(n, now) })),\n ];\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Number, money, percentage, date and relative-time formatting — `Intl`, in one place,
3
+ * for the figures an app prints OUTSIDE a kit component.
4
+ *
5
+ * Each app grew its own: kastlan's `utils/formatters.ts` (a Swiss-pinned
6
+ * `formatNumber`/`formatCurrency`, a date-fns `formatDate`, and a `formatRelativeTime`
7
+ * whose "5m ago" / "3h ago" / "2d ago" stayed English in every language), its tenancy
8
+ * `format.ts` (`formatChf`, a `formatPct` gluing a "+" onto `toFixed(2)`), keksdose's
9
+ * own set. Every one of them is `Intl` with a locale bolted on, so the only thing
10
+ * worth sharing is the locale — and the kit already has one, the provider's.
11
+ *
12
+ * The plain functions take a `locale` (none: the runtime default, as every `Intl` API
13
+ * reads `undefined`). Inside a component, {@link useKitFormat} hands back the same
14
+ * functions bound to the `<UiKitProvider locale>`, so a page formats exactly like the
15
+ * kit components on it.
16
+ *
17
+ * A missing value — `null`, `undefined`, `NaN`, an unparseable date — formats as
18
+ * `empty` (default "—", the dash `StatTile` shows for no value), never as "NaN" or
19
+ * "Invalid Date".
20
+ */
21
+ /** What every formatter here prints for a missing value, unless told otherwise. */
22
+ declare const EMPTY_FORMATTED_VALUE = "\u2014";
23
+ /** Fraction digits: a number pins both the minimum and the maximum (`2` → "3.10");
24
+ * an object sets them apart (a meter reading, 1–3). */
25
+ type FormatDigits = number | {
26
+ min?: number;
27
+ max?: number;
28
+ };
29
+ interface BaseOptions {
30
+ /** BCP 47 tag. Left out: the runtime default (the hook: the provider's). */
31
+ locale?: string;
32
+ /** Printed for a missing value. Default {@link EMPTY_FORMATTED_VALUE}. */
33
+ empty?: string;
34
+ }
35
+ interface FormatNumberOptions extends BaseOptions {
36
+ digits?: FormatDigits;
37
+ /** Compact notation: 12 400 → "12K". */
38
+ compact?: boolean;
39
+ /** `"exceptZero"` for a change ("+3", "−3", "0"). Default: the minus only. */
40
+ signDisplay?: Intl.NumberFormatOptions["signDisplay"];
41
+ /** Any further `Intl.NumberFormat` options, merged over the ones above. */
42
+ options?: Intl.NumberFormatOptions;
43
+ }
44
+ interface FormatMoneyOptions extends FormatNumberOptions {
45
+ /** `"symbol"` (default), `"narrowSymbol"`, `"code"` ("CHF 12.00") or `"name"`. */
46
+ currencyDisplay?: Intl.NumberFormatOptions["currencyDisplay"];
47
+ }
48
+ interface FormatPercentOptions extends FormatNumberOptions {
49
+ /**
50
+ * `true` (default): `value` is a RATIO, as `Intl` and `StatTile`'s percent delta read
51
+ * it — `0.12` is 12 %. `false`: it is already a percentage — `12` is 12 %
52
+ * (kastlan's rent `change_pct`).
53
+ */
54
+ ratio?: boolean;
55
+ }
56
+ /** A number in `locale`: "1,234.5", "1.234,5", "1’234.5". */
57
+ declare function formatNumber(value: number | null | undefined, options?: FormatNumberOptions): string;
58
+ /** An amount of `currency` (ISO 4217) in `locale`: "CHF 1’234.50", "1.234,50 €". */
59
+ declare function formatMoney(value: number | null | undefined, currency: string, options?: FormatMoneyOptions): string;
60
+ /** A percentage in `locale` ("12%", "12 %", "+1.25%"). See {@link FormatPercentOptions.ratio}
61
+ * for what `value` means. Default digits: at most one. */
62
+ declare function formatPercent(value: number | null | undefined, options?: FormatPercentOptions): string;
63
+ /**
64
+ * The shapes a date is printed in, each a set of `Intl.DateTimeFormat` options:
65
+ * - `short` / `medium` / `long` / `full` — the date alone, at that `dateStyle`
66
+ * ("08/07/2026", "8 Jul 2026", "8 July 2026", "Wednesday, 8 July 2026" in en-GB).
67
+ * - `dateTime` — medium date and short time: a timestamp ("8 Jul 2026, 14:05").
68
+ * - `time` — the short time alone ("14:05").
69
+ * - `monthYear` — "July 2026".
70
+ *
71
+ * Or pass `Intl.DateTimeFormatOptions` for anything else. A date-fns PATTERN
72
+ * ("dd.MM.yyyy") is deliberately not accepted: a pattern is one locale's order baked
73
+ * into a string, which is how an English page ends up with German dates.
74
+ */
75
+ type FormatDateStyle = "short" | "medium" | "long" | "full" | "dateTime" | "time" | "monthYear";
76
+ interface FormatDateOptions extends BaseOptions {
77
+ /** An IANA zone ("Europe/Zurich") for a timestamp. Left out: the device's. */
78
+ timeZone?: string;
79
+ }
80
+ /** What {@link formatDate} and {@link formatRelativeTime} accept. */
81
+ type DateInput = string | number | Date | null | undefined;
82
+ /**
83
+ * `value` as a Date, or null. A bare "YYYY-MM-DD" is a CALENDAR day and is read on the
84
+ * local calendar (`parseIsoDate`) — `new Date("2026-07-08")` is UTC midnight, which is
85
+ * the 7th anywhere west of Greenwich. Anything else (a full ISO timestamp, epoch
86
+ * milliseconds, a Date) is an instant and goes through `Date` as is.
87
+ */
88
+ declare function toDate(value: DateInput): Date | null;
89
+ /**
90
+ * A date or timestamp in `locale`. Takes an ISO date ("2026-07-08", read on the local
91
+ * calendar), an ISO timestamp ("2026-07-08T14:05:00Z"), epoch milliseconds or a Date.
92
+ * `style` defaults to `medium`; see {@link FormatDateStyle}.
93
+ *
94
+ * `formatIsoDate` (`@eifi1/ui-kit/dates`) remains for a plain "YYYY-MM-DD" with an
95
+ * explicit locale; this is its superset for timestamps and the named styles.
96
+ */
97
+ declare function formatDate(value: DateInput, style?: FormatDateStyle | Intl.DateTimeFormatOptions, options?: FormatDateOptions): string;
98
+ interface FormatRelativeTimeOptions extends BaseOptions {
99
+ /** The moment it is measured from. Default: now. */
100
+ now?: DateInput;
101
+ /** `"auto"` (default): "yesterday", "now", "next week". `"always"`: "1 day ago". */
102
+ numeric?: Intl.RelativeTimeFormatNumeric;
103
+ /** `"long"` (default) "3 hours ago", `"short"` "3 hr. ago", `"narrow"` "3h ago". */
104
+ style?: Intl.RelativeTimeFormatStyle;
105
+ /**
106
+ * Past this many days either way, print the date instead ({@link formatDate} in
107
+ * `absoluteStyle`) — kastlan's feed switches to the date after a week, because "5
108
+ * weeks ago" makes a reader do sums. Default: never.
109
+ */
110
+ absoluteAfterDays?: number;
111
+ /** The style of that date. Default `medium`. */
112
+ absoluteStyle?: FormatDateStyle | Intl.DateTimeFormatOptions;
113
+ }
114
+ /**
115
+ * How long ago (or until) `value` is, in `locale`'s words, through
116
+ * `Intl.RelativeTimeFormat`: "3 hours ago", "vor 3 Stunden", "il y a 3 heures",
117
+ * "in 2 days", and under 45 seconds "now". The unit is picked by size — seconds,
118
+ * minutes, hours, days, weeks, months, years — and the count is rounded.
119
+ */
120
+ declare function formatRelativeTime(value: DateInput, options?: FormatRelativeTimeOptions): string;
121
+ type WithoutLocale<T> = Omit<T, "locale"> & {
122
+ locale?: string;
123
+ };
124
+ /** What {@link useKitFormat} returns: the formatters above with the provider's locale
125
+ * as their default (an explicit `locale` in the options still wins). */
126
+ interface KitFormat {
127
+ /** The locale they format in — the provider's, or `undefined` (runtime default). */
128
+ locale: string | undefined;
129
+ formatNumber: (value: number | null | undefined, options?: WithoutLocale<FormatNumberOptions>) => string;
130
+ formatMoney: (value: number | null | undefined, currency: string, options?: WithoutLocale<FormatMoneyOptions>) => string;
131
+ formatPercent: (value: number | null | undefined, options?: WithoutLocale<FormatPercentOptions>) => string;
132
+ formatDate: (value: DateInput, style?: FormatDateStyle | Intl.DateTimeFormatOptions, options?: WithoutLocale<FormatDateOptions>) => string;
133
+ formatRelativeTime: (value: DateInput, options?: WithoutLocale<FormatRelativeTimeOptions>) => string;
134
+ }
135
+ /**
136
+ * The formatters, bound to the `<UiKitProvider locale>` (or to `locale`, which wins
137
+ * over it — the same precedence as a component's `locale` prop):
138
+ *
139
+ * const { formatMoney, formatRelativeTime } = useKitFormat();
140
+ * formatMoney(row.balance, "CHF");
141
+ */
142
+ declare function useKitFormat(locale?: string): KitFormat;
143
+
144
+ export { type DateInput, EMPTY_FORMATTED_VALUE, type FormatDateOptions, type FormatDateStyle, type FormatDigits, type FormatMoneyOptions, type FormatNumberOptions, type FormatPercentOptions, type FormatRelativeTimeOptions, type KitFormat, formatDate, formatMoney, formatNumber, formatPercent, formatRelativeTime, toDate, useKitFormat };
@@ -0,0 +1,149 @@
1
+ "use client";
2
+ import { useMemo } from "react";
3
+ import { useKitLocale } from "../i18n/kit-labels.js";
4
+ import { parseIsoDate } from "./dates.js";
5
+ const EMPTY_FORMATTED_VALUE = "\u2014";
6
+ const numberFormatters = /* @__PURE__ */ new Map();
7
+ function numberFormatter(locale, options) {
8
+ const key = `${locale ?? ""}|${JSON.stringify(options)}`;
9
+ let f = numberFormatters.get(key);
10
+ if (!f) {
11
+ f = new Intl.NumberFormat(locale, options);
12
+ numberFormatters.set(key, f);
13
+ }
14
+ return f;
15
+ }
16
+ function digitOptions(digits) {
17
+ if (digits === void 0) return {};
18
+ if (typeof digits === "number") return { minimumFractionDigits: digits, maximumFractionDigits: digits };
19
+ return {
20
+ ...digits.min !== void 0 ? { minimumFractionDigits: digits.min } : null,
21
+ ...digits.max !== void 0 ? { maximumFractionDigits: digits.max } : null
22
+ };
23
+ }
24
+ function numberOptions(o) {
25
+ return {
26
+ ...digitOptions(o.digits),
27
+ ...o.compact ? { notation: "compact" } : null,
28
+ ...o.signDisplay ? { signDisplay: o.signDisplay } : null,
29
+ ...o.options
30
+ };
31
+ }
32
+ function isMissing(value) {
33
+ return value === null || value === void 0 || Number.isNaN(value);
34
+ }
35
+ function formatNumber(value, options = {}) {
36
+ if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;
37
+ return numberFormatter(options.locale, numberOptions(options)).format(value);
38
+ }
39
+ function formatMoney(value, currency, options = {}) {
40
+ if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;
41
+ return numberFormatter(options.locale, {
42
+ style: "currency",
43
+ currency,
44
+ ...options.currencyDisplay ? { currencyDisplay: options.currencyDisplay } : null,
45
+ ...numberOptions(options)
46
+ }).format(value);
47
+ }
48
+ function formatPercent(value, options = {}) {
49
+ if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;
50
+ const ratio = options.ratio === false ? value / 100 : value;
51
+ return numberFormatter(options.locale, {
52
+ style: "percent",
53
+ ...options.digits === void 0 ? { maximumFractionDigits: 1 } : null,
54
+ ...numberOptions(options)
55
+ }).format(ratio);
56
+ }
57
+ const DATE_STYLES = {
58
+ short: { dateStyle: "short" },
59
+ medium: { dateStyle: "medium" },
60
+ long: { dateStyle: "long" },
61
+ full: { dateStyle: "full" },
62
+ dateTime: { dateStyle: "medium", timeStyle: "short" },
63
+ time: { timeStyle: "short" },
64
+ monthYear: { month: "long", year: "numeric" }
65
+ };
66
+ function toDate(value) {
67
+ if (value === null || value === void 0 || value === "") return null;
68
+ if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value)) return parseIsoDate(value);
69
+ const d = value instanceof Date ? value : new Date(value);
70
+ return Number.isNaN(d.getTime()) ? null : d;
71
+ }
72
+ const dateFormatters = /* @__PURE__ */ new Map();
73
+ function dateFormatter(locale, options) {
74
+ const key = `${locale ?? ""}|${JSON.stringify(options)}`;
75
+ let f = dateFormatters.get(key);
76
+ if (!f) {
77
+ f = new Intl.DateTimeFormat(locale, options);
78
+ dateFormatters.set(key, f);
79
+ }
80
+ return f;
81
+ }
82
+ function formatDate(value, style = "medium", options = {}) {
83
+ const d = toDate(value);
84
+ if (!d) return options.empty ?? EMPTY_FORMATTED_VALUE;
85
+ const base = typeof style === "string" ? DATE_STYLES[style] : style;
86
+ return dateFormatter(options.locale, options.timeZone ? { ...base, timeZone: options.timeZone } : base).format(d);
87
+ }
88
+ const SECOND = 1e3;
89
+ const MINUTE = 60 * SECOND;
90
+ const HOUR = 60 * MINUTE;
91
+ const DAY = 24 * HOUR;
92
+ const RELATIVE_STEPS = [
93
+ [45 * SECOND, "second", SECOND],
94
+ [45 * MINUTE, "minute", MINUTE],
95
+ [22 * HOUR, "hour", HOUR],
96
+ [6.5 * DAY, "day", DAY],
97
+ [26 * DAY, "week", 7 * DAY],
98
+ [320 * DAY, "month", 30.44 * DAY],
99
+ [Infinity, "year", 365.25 * DAY]
100
+ ];
101
+ const relativeFormatters = /* @__PURE__ */ new Map();
102
+ function relativeFormatter(locale, numeric, style) {
103
+ const key = `${locale ?? ""}|${numeric}|${style}`;
104
+ let f = relativeFormatters.get(key);
105
+ if (!f) {
106
+ f = new Intl.RelativeTimeFormat(locale, { numeric, style });
107
+ relativeFormatters.set(key, f);
108
+ }
109
+ return f;
110
+ }
111
+ function formatRelativeTime(value, options = {}) {
112
+ const d = toDate(value);
113
+ if (!d) return options.empty ?? EMPTY_FORMATTED_VALUE;
114
+ const now = toDate(options.now ?? /* @__PURE__ */ new Date()) ?? /* @__PURE__ */ new Date();
115
+ const diff = d.getTime() - now.getTime();
116
+ const abs = Math.abs(diff);
117
+ if (options.absoluteAfterDays !== void 0 && abs > options.absoluteAfterDays * DAY) {
118
+ return formatDate(d, options.absoluteStyle ?? "medium", options);
119
+ }
120
+ const rtf = relativeFormatter(options.locale, options.numeric ?? "auto", options.style ?? "long");
121
+ const [, unit, size] = RELATIVE_STEPS.find(([limit]) => abs < limit);
122
+ const count = unit === "second" ? 0 : Math.round(diff / size);
123
+ return rtf.format(count + 0, unit);
124
+ }
125
+ function useKitFormat(locale) {
126
+ const kitLocale = useKitLocale(locale);
127
+ return useMemo(
128
+ () => ({
129
+ locale: kitLocale,
130
+ formatNumber: (value, options) => formatNumber(value, { locale: kitLocale, ...options }),
131
+ formatMoney: (value, currency, options) => formatMoney(value, currency, { locale: kitLocale, ...options }),
132
+ formatPercent: (value, options) => formatPercent(value, { locale: kitLocale, ...options }),
133
+ formatDate: (value, style, options) => formatDate(value, style, { locale: kitLocale, ...options }),
134
+ formatRelativeTime: (value, options) => formatRelativeTime(value, { locale: kitLocale, ...options })
135
+ }),
136
+ [kitLocale]
137
+ );
138
+ }
139
+ export {
140
+ EMPTY_FORMATTED_VALUE,
141
+ formatDate,
142
+ formatMoney,
143
+ formatNumber,
144
+ formatPercent,
145
+ formatRelativeTime,
146
+ toDate,
147
+ useKitFormat
148
+ };
149
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/format.ts"],"sourcesContent":["import { useMemo } from \"react\";\nimport { useKitLocale } from \"../i18n/kit-labels\";\nimport { parseIsoDate } from \"./dates\";\n\n/**\n * Number, money, percentage, date and relative-time formatting — `Intl`, in one place,\n * for the figures an app prints OUTSIDE a kit component.\n *\n * Each app grew its own: kastlan's `utils/formatters.ts` (a Swiss-pinned\n * `formatNumber`/`formatCurrency`, a date-fns `formatDate`, and a `formatRelativeTime`\n * whose \"5m ago\" / \"3h ago\" / \"2d ago\" stayed English in every language), its tenancy\n * `format.ts` (`formatChf`, a `formatPct` gluing a \"+\" onto `toFixed(2)`), keksdose's\n * own set. Every one of them is `Intl` with a locale bolted on, so the only thing\n * worth sharing is the locale — and the kit already has one, the provider's.\n *\n * The plain functions take a `locale` (none: the runtime default, as every `Intl` API\n * reads `undefined`). Inside a component, {@link useKitFormat} hands back the same\n * functions bound to the `<UiKitProvider locale>`, so a page formats exactly like the\n * kit components on it.\n *\n * A missing value — `null`, `undefined`, `NaN`, an unparseable date — formats as\n * `empty` (default \"—\", the dash `StatTile` shows for no value), never as \"NaN\" or\n * \"Invalid Date\".\n */\n\n/** What every formatter here prints for a missing value, unless told otherwise. */\nexport const EMPTY_FORMATTED_VALUE = \"—\";\n\n/** Fraction digits: a number pins both the minimum and the maximum (`2` → \"3.10\");\n * an object sets them apart (a meter reading, 1–3). */\nexport type FormatDigits = number | { min?: number; max?: number };\n\ninterface BaseOptions {\n /** BCP 47 tag. Left out: the runtime default (the hook: the provider's). */\n locale?: string;\n /** Printed for a missing value. Default {@link EMPTY_FORMATTED_VALUE}. */\n empty?: string;\n}\n\nexport interface FormatNumberOptions extends BaseOptions {\n digits?: FormatDigits;\n /** Compact notation: 12 400 → \"12K\". */\n compact?: boolean;\n /** `\"exceptZero\"` for a change (\"+3\", \"−3\", \"0\"). Default: the minus only. */\n signDisplay?: Intl.NumberFormatOptions[\"signDisplay\"];\n /** Any further `Intl.NumberFormat` options, merged over the ones above. */\n options?: Intl.NumberFormatOptions;\n}\n\nexport interface FormatMoneyOptions extends FormatNumberOptions {\n /** `\"symbol\"` (default), `\"narrowSymbol\"`, `\"code\"` (\"CHF 12.00\") or `\"name\"`. */\n currencyDisplay?: Intl.NumberFormatOptions[\"currencyDisplay\"];\n}\n\nexport interface FormatPercentOptions extends FormatNumberOptions {\n /**\n * `true` (default): `value` is a RATIO, as `Intl` and `StatTile`'s percent delta read\n * it — `0.12` is 12 %. `false`: it is already a percentage — `12` is 12 %\n * (kastlan's rent `change_pct`).\n */\n ratio?: boolean;\n}\n\n// One formatter per option set: a table formats the same way thousands of times, and\n// constructing an Intl formatter is the expensive part.\nconst numberFormatters = new Map<string, Intl.NumberFormat>();\nfunction numberFormatter(locale: string | undefined, options: Intl.NumberFormatOptions): Intl.NumberFormat {\n const key = `${locale ?? \"\"}|${JSON.stringify(options)}`;\n let f = numberFormatters.get(key);\n if (!f) {\n f = new Intl.NumberFormat(locale, options);\n numberFormatters.set(key, f);\n }\n return f;\n}\n\nfunction digitOptions(digits: FormatDigits | undefined): Intl.NumberFormatOptions {\n if (digits === undefined) return {};\n if (typeof digits === \"number\") return { minimumFractionDigits: digits, maximumFractionDigits: digits };\n return {\n ...(digits.min !== undefined ? { minimumFractionDigits: digits.min } : null),\n ...(digits.max !== undefined ? { maximumFractionDigits: digits.max } : null),\n };\n}\n\nfunction numberOptions(o: FormatNumberOptions): Intl.NumberFormatOptions {\n return {\n ...digitOptions(o.digits),\n ...(o.compact ? { notation: \"compact\" } : null),\n ...(o.signDisplay ? { signDisplay: o.signDisplay } : null),\n ...o.options,\n };\n}\n\nfunction isMissing(value: number | null | undefined): value is null | undefined {\n return value === null || value === undefined || Number.isNaN(value);\n}\n\n/** A number in `locale`: \"1,234.5\", \"1.234,5\", \"1’234.5\". */\nexport function formatNumber(value: number | null | undefined, options: FormatNumberOptions = {}): string {\n if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;\n return numberFormatter(options.locale, numberOptions(options)).format(value);\n}\n\n/** An amount of `currency` (ISO 4217) in `locale`: \"CHF 1’234.50\", \"1.234,50 €\". */\nexport function formatMoney(\n value: number | null | undefined,\n currency: string,\n options: FormatMoneyOptions = {},\n): string {\n if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;\n return numberFormatter(options.locale, {\n style: \"currency\",\n currency,\n ...(options.currencyDisplay ? { currencyDisplay: options.currencyDisplay } : null),\n ...numberOptions(options),\n }).format(value);\n}\n\n/** A percentage in `locale` (\"12%\", \"12 %\", \"+1.25%\"). See {@link FormatPercentOptions.ratio}\n * for what `value` means. Default digits: at most one. */\nexport function formatPercent(value: number | null | undefined, options: FormatPercentOptions = {}): string {\n if (isMissing(value)) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const ratio = options.ratio === false ? value / 100 : value;\n return numberFormatter(options.locale, {\n style: \"percent\",\n ...(options.digits === undefined ? { maximumFractionDigits: 1 } : null),\n ...numberOptions(options),\n }).format(ratio);\n}\n\n/* ── Dates ─────────────────────────────────────────────────────────────────── */\n\n/**\n * The shapes a date is printed in, each a set of `Intl.DateTimeFormat` options:\n * - `short` / `medium` / `long` / `full` — the date alone, at that `dateStyle`\n * (\"08/07/2026\", \"8 Jul 2026\", \"8 July 2026\", \"Wednesday, 8 July 2026\" in en-GB).\n * - `dateTime` — medium date and short time: a timestamp (\"8 Jul 2026, 14:05\").\n * - `time` — the short time alone (\"14:05\").\n * - `monthYear` — \"July 2026\".\n *\n * Or pass `Intl.DateTimeFormatOptions` for anything else. A date-fns PATTERN\n * (\"dd.MM.yyyy\") is deliberately not accepted: a pattern is one locale's order baked\n * into a string, which is how an English page ends up with German dates.\n */\nexport type FormatDateStyle = \"short\" | \"medium\" | \"long\" | \"full\" | \"dateTime\" | \"time\" | \"monthYear\";\n\nconst DATE_STYLES: Record<FormatDateStyle, Intl.DateTimeFormatOptions> = {\n short: { dateStyle: \"short\" },\n medium: { dateStyle: \"medium\" },\n long: { dateStyle: \"long\" },\n full: { dateStyle: \"full\" },\n dateTime: { dateStyle: \"medium\", timeStyle: \"short\" },\n time: { timeStyle: \"short\" },\n monthYear: { month: \"long\", year: \"numeric\" },\n};\n\nexport interface FormatDateOptions extends BaseOptions {\n /** An IANA zone (\"Europe/Zurich\") for a timestamp. Left out: the device's. */\n timeZone?: string;\n}\n\n/** What {@link formatDate} and {@link formatRelativeTime} accept. */\nexport type DateInput = string | number | Date | null | undefined;\n\n/**\n * `value` as a Date, or null. A bare \"YYYY-MM-DD\" is a CALENDAR day and is read on the\n * local calendar (`parseIsoDate`) — `new Date(\"2026-07-08\")` is UTC midnight, which is\n * the 7th anywhere west of Greenwich. Anything else (a full ISO timestamp, epoch\n * milliseconds, a Date) is an instant and goes through `Date` as is.\n */\nexport function toDate(value: DateInput): Date | null {\n if (value === null || value === undefined || value === \"\") return null;\n if (typeof value === \"string\" && /^\\d{4}-\\d{2}-\\d{2}$/.test(value)) return parseIsoDate(value);\n const d = value instanceof Date ? value : new Date(value);\n return Number.isNaN(d.getTime()) ? null : d;\n}\n\nconst dateFormatters = new Map<string, Intl.DateTimeFormat>();\nfunction dateFormatter(locale: string | undefined, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {\n const key = `${locale ?? \"\"}|${JSON.stringify(options)}`;\n let f = dateFormatters.get(key);\n if (!f) {\n f = new Intl.DateTimeFormat(locale, options);\n dateFormatters.set(key, f);\n }\n return f;\n}\n\n/**\n * A date or timestamp in `locale`. Takes an ISO date (\"2026-07-08\", read on the local\n * calendar), an ISO timestamp (\"2026-07-08T14:05:00Z\"), epoch milliseconds or a Date.\n * `style` defaults to `medium`; see {@link FormatDateStyle}.\n *\n * `formatIsoDate` (`@eifi1/ui-kit/dates`) remains for a plain \"YYYY-MM-DD\" with an\n * explicit locale; this is its superset for timestamps and the named styles.\n */\nexport function formatDate(\n value: DateInput,\n style: FormatDateStyle | Intl.DateTimeFormatOptions = \"medium\",\n options: FormatDateOptions = {},\n): string {\n const d = toDate(value);\n if (!d) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const base = typeof style === \"string\" ? DATE_STYLES[style] : style;\n return dateFormatter(options.locale, options.timeZone ? { ...base, timeZone: options.timeZone } : base).format(d);\n}\n\nexport interface FormatRelativeTimeOptions extends BaseOptions {\n /** The moment it is measured from. Default: now. */\n now?: DateInput;\n /** `\"auto\"` (default): \"yesterday\", \"now\", \"next week\". `\"always\"`: \"1 day ago\". */\n numeric?: Intl.RelativeTimeFormatNumeric;\n /** `\"long\"` (default) \"3 hours ago\", `\"short\"` \"3 hr. ago\", `\"narrow\"` \"3h ago\". */\n style?: Intl.RelativeTimeFormatStyle;\n /**\n * Past this many days either way, print the date instead ({@link formatDate} in\n * `absoluteStyle`) — kastlan's feed switches to the date after a week, because \"5\n * weeks ago\" makes a reader do sums. Default: never.\n */\n absoluteAfterDays?: number;\n /** The style of that date. Default `medium`. */\n absoluteStyle?: FormatDateStyle | Intl.DateTimeFormatOptions;\n}\n\nconst SECOND = 1000;\nconst MINUTE = 60 * SECOND;\nconst HOUR = 60 * MINUTE;\nconst DAY = 24 * HOUR;\n\n/** [the largest |difference| a unit is used for, the unit, its length]. */\nconst RELATIVE_STEPS: Array<[number, Intl.RelativeTimeFormatUnit, number]> = [\n [45 * SECOND, \"second\", SECOND],\n [45 * MINUTE, \"minute\", MINUTE],\n [22 * HOUR, \"hour\", HOUR],\n [6.5 * DAY, \"day\", DAY],\n [26 * DAY, \"week\", 7 * DAY],\n [320 * DAY, \"month\", 30.44 * DAY],\n [Infinity, \"year\", 365.25 * DAY],\n];\n\nconst relativeFormatters = new Map<string, Intl.RelativeTimeFormat>();\nfunction relativeFormatter(\n locale: string | undefined,\n numeric: Intl.RelativeTimeFormatNumeric,\n style: Intl.RelativeTimeFormatStyle,\n): Intl.RelativeTimeFormat {\n const key = `${locale ?? \"\"}|${numeric}|${style}`;\n let f = relativeFormatters.get(key);\n if (!f) {\n f = new Intl.RelativeTimeFormat(locale, { numeric, style });\n relativeFormatters.set(key, f);\n }\n return f;\n}\n\n/**\n * How long ago (or until) `value` is, in `locale`'s words, through\n * `Intl.RelativeTimeFormat`: \"3 hours ago\", \"vor 3 Stunden\", \"il y a 3 heures\",\n * \"in 2 days\", and under 45 seconds \"now\". The unit is picked by size — seconds,\n * minutes, hours, days, weeks, months, years — and the count is rounded.\n */\nexport function formatRelativeTime(value: DateInput, options: FormatRelativeTimeOptions = {}): string {\n const d = toDate(value);\n if (!d) return options.empty ?? EMPTY_FORMATTED_VALUE;\n const now = toDate(options.now ?? new Date()) ?? new Date();\n const diff = d.getTime() - now.getTime();\n const abs = Math.abs(diff);\n if (options.absoluteAfterDays !== undefined && abs > options.absoluteAfterDays * DAY) {\n return formatDate(d, options.absoluteStyle ?? \"medium\", options);\n }\n const rtf = relativeFormatter(options.locale, options.numeric ?? \"auto\", options.style ?? \"long\");\n const [, unit, size] = RELATIVE_STEPS.find(([limit]) => abs < limit)!;\n // Under 45 seconds is \"now\" rather than \"12 seconds ago\": a feed that ticks through\n // the seconds is noise, and nothing reads a timestamp to the second.\n const count = unit === \"second\" ? 0 : Math.round(diff / size);\n // `+ 0` folds -0 into 0: `format(-0, …)` says \"0 seconds ago\" instead of \"now\".\n return rtf.format(count + 0, unit);\n}\n\n/* ── Bound to the provider ─────────────────────────────────────────────────── */\n\ntype WithoutLocale<T> = Omit<T, \"locale\"> & { locale?: string };\n\n/** What {@link useKitFormat} returns: the formatters above with the provider's locale\n * as their default (an explicit `locale` in the options still wins). */\nexport interface KitFormat {\n /** The locale they format in — the provider's, or `undefined` (runtime default). */\n locale: string | undefined;\n formatNumber: (value: number | null | undefined, options?: WithoutLocale<FormatNumberOptions>) => string;\n formatMoney: (\n value: number | null | undefined,\n currency: string,\n options?: WithoutLocale<FormatMoneyOptions>,\n ) => string;\n formatPercent: (value: number | null | undefined, options?: WithoutLocale<FormatPercentOptions>) => string;\n formatDate: (\n value: DateInput,\n style?: FormatDateStyle | Intl.DateTimeFormatOptions,\n options?: WithoutLocale<FormatDateOptions>,\n ) => string;\n formatRelativeTime: (value: DateInput, options?: WithoutLocale<FormatRelativeTimeOptions>) => string;\n}\n\n/**\n * The formatters, bound to the `<UiKitProvider locale>` (or to `locale`, which wins\n * over it — the same precedence as a component's `locale` prop):\n *\n * const { formatMoney, formatRelativeTime } = useKitFormat();\n * formatMoney(row.balance, \"CHF\");\n */\nexport function useKitFormat(locale?: string): KitFormat {\n const kitLocale = useKitLocale(locale);\n return useMemo<KitFormat>(\n () => ({\n locale: kitLocale,\n formatNumber: (value, options) => formatNumber(value, { locale: kitLocale, ...options }),\n formatMoney: (value, currency, options) => formatMoney(value, currency, { locale: kitLocale, ...options }),\n formatPercent: (value, options) => formatPercent(value, { locale: kitLocale, ...options }),\n formatDate: (value, style, options) => formatDate(value, style, { locale: kitLocale, ...options }),\n formatRelativeTime: (value, options) => formatRelativeTime(value, { locale: kitLocale, ...options }),\n }),\n [kitLocale],\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * A QR code encoder: text in, a square of dark / light modules out (ISO/IEC 18004).
3
+ *
4
+ * WHY IT IS IN THE KIT rather than a dependency. `TwoFactorSetting` needs one QR code —
5
+ * an `otpauth://` URI, around 100–200 bytes — and every app that shows it was pulling
6
+ * in `qrcode.react` plus `react-dom/server` to render that one code to a static SVG
7
+ * string, then base64-encoding it so the kit could draw it back as an `<img>`.
8
+ * A dependency here would land in all three apps whether they show 2FA or not; an
9
+ * optional peer would mean a lazy import and a loading state for something that is
10
+ * a few hundred lines of arithmetic. So it is those lines: pure, no imports, no module
11
+ * state beyond two constant tables, safe under `sideEffects: false`, and
12
+ * tree-shaken out of every app that never renders a {@link QrCode}.
13
+ *
14
+ * Scope, chosen for what the kit renders: BYTE mode only (UTF-8), all 40 versions,
15
+ * all four error-correction levels, automatic version and mask selection. Numeric,
16
+ * alphanumeric and Kanji modes are denser for their alphabets but would not shrink an
17
+ * otpauth URI (lower-case letters are not in the alphanumeric set). The algorithm and
18
+ * its penalty rules follow the reference implementation by Project Nayuki (MIT),
19
+ * which is also what `qrcode.react` wraps — so for the same input, level and boost
20
+ * the modules match that library's output exactly (the test suite pins a matrix).
21
+ */
22
+ type QrErrorCorrection = "L" | "M" | "Q" | "H";
23
+ interface QrEncodeOptions {
24
+ /** How much of the code may be damaged and still read. Default `"M"` (~15 %). */
25
+ errorCorrection?: QrErrorCorrection;
26
+ /** Raise the level as far as the chosen version still fits, for free. Default true. */
27
+ boostErrorCorrection?: boolean;
28
+ }
29
+ interface QrMatrix {
30
+ /** 1–40. */
31
+ version: number;
32
+ /** Modules per side: `version * 4 + 17`. */
33
+ size: number;
34
+ /** The level actually used — higher than asked when boosted. */
35
+ errorCorrection: QrErrorCorrection;
36
+ /** 0–7. */
37
+ mask: number;
38
+ /** `modules[y][x]`, true = dark. No quiet zone: the renderer adds its own. */
39
+ modules: boolean[][];
40
+ }
41
+ /** Encode `text` (as UTF-8 bytes). Throws a `RangeError` past version 40's capacity. */
42
+ declare function encodeQr(text: string, options?: QrEncodeOptions): QrMatrix;
43
+
44
+ export { type QrEncodeOptions, type QrErrorCorrection, type QrMatrix, encodeQr };