@eifi1/ui-kit 0.51.6 → 0.52.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 (805) hide show
  1. package/README.md +146 -26
  2. package/dist/account/account-parts.d.ts +10 -8
  3. package/dist/account/account-parts.js +1 -18
  4. package/dist/account/account-parts.js.map +1 -1
  5. package/dist/account/data-export-setting-labels.js +1 -1
  6. package/dist/account/data-export-setting-labels.js.map +1 -1
  7. package/dist/account/delete-account-setting-labels.js +1 -1
  8. package/dist/account/delete-account-setting-labels.js.map +1 -1
  9. package/dist/account/email-change-setting-labels.js +1 -1
  10. package/dist/account/email-change-setting-labels.js.map +1 -1
  11. package/dist/account/passkeys-setting.d.ts +9 -2
  12. package/dist/account/passkeys-setting.js +41 -29
  13. package/dist/account/passkeys-setting.js.map +1 -1
  14. package/dist/account/sessions-setting.d.ts +3 -2
  15. package/dist/account/sessions-setting.js +10 -8
  16. package/dist/account/sessions-setting.js.map +1 -1
  17. package/dist/admin/admin-action-confirm.js +3 -7
  18. package/dist/admin/admin-action-confirm.js.map +1 -1
  19. package/dist/admin/admin-parts.d.ts +6 -3
  20. package/dist/admin/admin-parts.js +6 -21
  21. package/dist/admin/admin-parts.js.map +1 -1
  22. package/dist/admin/mail-test-card-labels.js +1 -1
  23. package/dist/admin/mail-test-card-labels.js.map +1 -1
  24. package/dist/admin/plan-change-labels.d.ts +2 -2
  25. package/dist/admin/plan-change-labels.js +1 -17
  26. package/dist/admin/plan-change-labels.js.map +1 -1
  27. package/dist/admin/plan-change.d.ts +3 -42
  28. package/dist/admin/plan-change.js +1 -1
  29. package/dist/admin/plan-change.js.map +1 -1
  30. package/dist/admin/reviewer-scope-editor.js +3 -3
  31. package/dist/admin/reviewer-scope-editor.js.map +1 -1
  32. package/dist/auth/auth-errors-labels.d.ts +11 -0
  33. package/dist/auth/auth-errors-labels.js +13 -0
  34. package/dist/auth/auth-errors-labels.js.map +1 -0
  35. package/dist/auth/auth-errors.d.ts +7 -23
  36. package/dist/auth/auth-errors.js +2 -15
  37. package/dist/auth/auth-errors.js.map +1 -1
  38. package/dist/auth/complete-name-dialog.d.ts +3 -2
  39. package/dist/auth/complete-name-dialog.js +3 -5
  40. package/dist/auth/complete-name-dialog.js.map +1 -1
  41. package/dist/auth/forgot-password-form-labels.js +1 -1
  42. package/dist/auth/forgot-password-form-labels.js.map +1 -1
  43. package/dist/auth/forgot-password-form.js +7 -2
  44. package/dist/auth/forgot-password-form.js.map +1 -1
  45. package/dist/auth/register-form-labels.js +1 -1
  46. package/dist/auth/register-form-labels.js.map +1 -1
  47. package/dist/auth/reset-password-form-labels.js +1 -1
  48. package/dist/auth/reset-password-form-labels.js.map +1 -1
  49. package/dist/auth/reset-password-form.js +5 -1
  50. package/dist/auth/reset-password-form.js.map +1 -1
  51. package/dist/auth/sign-in-form-labels.js +1 -1
  52. package/dist/auth/sign-in-form-labels.js.map +1 -1
  53. package/dist/auth/status-parts.d.ts +0 -3
  54. package/dist/auth/status-parts.js +0 -4
  55. package/dist/auth/status-parts.js.map +1 -1
  56. package/dist/auth/verify-email-labels.js +1 -1
  57. package/dist/auth/verify-email-labels.js.map +1 -1
  58. package/dist/auth/verify-email.js +7 -2
  59. package/dist/auth/verify-email.js.map +1 -1
  60. package/dist/billing/cancel-request.d.ts +11 -2
  61. package/dist/billing/cancel-request.js +15 -3
  62. package/dist/billing/cancel-request.js.map +1 -1
  63. package/dist/billing/cancel-subscription.d.ts +22 -10
  64. package/dist/billing/cancel-subscription.js +8 -11
  65. package/dist/billing/cancel-subscription.js.map +1 -1
  66. package/dist/billing/dialog-close.d.ts +29 -0
  67. package/dist/billing/dialog-close.js +8 -0
  68. package/dist/billing/dialog-close.js.map +1 -0
  69. package/dist/billing/plan-card.d.ts +14 -4
  70. package/dist/billing/plan-card.js +15 -8
  71. package/dist/billing/plan-card.js.map +1 -1
  72. package/dist/billing/plan-change-labels.d.ts +42 -0
  73. package/dist/billing/plan-change-labels.js +22 -0
  74. package/dist/billing/plan-change-labels.js.map +1 -0
  75. package/dist/billing/plan-switch.d.ts +21 -9
  76. package/dist/billing/plan-switch.js +10 -8
  77. package/dist/billing/plan-switch.js.map +1 -1
  78. package/dist/chart.d.ts +12 -0
  79. package/dist/chart.js +16 -1
  80. package/dist/chart.js.map +1 -1
  81. package/dist/components/account-chips.d.ts +5 -5
  82. package/dist/components/account-chips.js +11 -13
  83. package/dist/components/account-chips.js.map +1 -1
  84. package/dist/components/alert-banner.d.ts +16 -5
  85. package/dist/components/alert-banner.js +46 -1
  86. package/dist/components/alert-banner.js.map +1 -1
  87. package/dist/components/amount-input.d.ts +22 -4
  88. package/dist/components/amount-input.js +3 -3
  89. package/dist/components/amount-input.js.map +1 -1
  90. package/dist/components/autocomplete.js +57 -65
  91. package/dist/components/autocomplete.js.map +1 -1
  92. package/dist/components/bulk-action-bar.d.ts +12 -2
  93. package/dist/components/bulk-action-bar.js +65 -21
  94. package/dist/components/bulk-action-bar.js.map +1 -1
  95. package/dist/components/button-look.d.ts +5 -5
  96. package/dist/components/button-look.js.map +1 -1
  97. package/dist/components/button.d.ts +2 -2
  98. package/dist/components/button.js.map +1 -1
  99. package/dist/components/calculator.d.ts +21 -2
  100. package/dist/components/calculator.js +53 -14
  101. package/dist/components/calculator.js.map +1 -1
  102. package/dist/components/chart-edge.d.ts +16 -0
  103. package/dist/components/chart-edge.js +14 -1
  104. package/dist/components/chart-edge.js.map +1 -1
  105. package/dist/components/chart-figure.d.ts +13 -0
  106. package/dist/components/chart-figure.js +15 -0
  107. package/dist/components/chart-figure.js.map +1 -0
  108. package/dist/components/chart-zoom-core.js +1 -4
  109. package/dist/components/chart-zoom-core.js.map +1 -1
  110. package/dist/components/chart.d.ts +8 -1
  111. package/dist/components/chart.js +19 -10
  112. package/dist/components/chart.js.map +1 -1
  113. package/dist/components/combobox-core.d.ts +119 -7
  114. package/dist/components/combobox-core.js +151 -46
  115. package/dist/components/combobox-core.js.map +1 -1
  116. package/dist/components/combobox.d.ts +1 -1
  117. package/dist/components/combobox.js +142 -131
  118. package/dist/components/combobox.js.map +1 -1
  119. package/dist/components/copy-button.d.ts +6 -5
  120. package/dist/components/copy-button.js +5 -3
  121. package/dist/components/copy-button.js.map +1 -1
  122. package/dist/components/country-select.js +2 -2
  123. package/dist/components/country-select.js.map +1 -1
  124. package/dist/components/data-table-column-settings.d.ts +1 -1
  125. package/dist/components/data-table-column-settings.js +33 -7
  126. package/dist/components/data-table-column-settings.js.map +1 -1
  127. package/dist/components/data-table-filters.d.ts +4 -2
  128. package/dist/components/data-table-filters.js +2 -2
  129. package/dist/components/data-table-filters.js.map +1 -1
  130. package/dist/components/data-table-fit.d.ts +6 -4
  131. package/dist/components/data-table-fit.js +4 -1
  132. package/dist/components/data-table-fit.js.map +1 -1
  133. package/dist/components/data-table-groups.d.ts +1 -1
  134. package/dist/components/data-table-groups.js.map +1 -1
  135. package/dist/components/data-table-head.d.ts +1 -1
  136. package/dist/components/data-table-head.js +15 -12
  137. package/dist/components/data-table-head.js.map +1 -1
  138. package/dist/components/data-table-mobile.d.ts +36 -7
  139. package/dist/components/data-table-mobile.js +63 -31
  140. package/dist/components/data-table-mobile.js.map +1 -1
  141. package/dist/components/data-table-pagination.js +11 -6
  142. package/dist/components/data-table-pagination.js.map +1 -1
  143. package/dist/components/data-table-resize.d.ts +1 -1
  144. package/dist/components/data-table-resize.js.map +1 -1
  145. package/dist/components/data-table-row-actions.d.ts +2 -2
  146. package/dist/components/data-table-row-actions.js.map +1 -1
  147. package/dist/components/data-table-row.d.ts +6 -2
  148. package/dist/components/data-table-row.js +17 -10
  149. package/dist/components/data-table-row.js.map +1 -1
  150. package/dist/components/data-table-selection.d.ts +1 -1
  151. package/dist/components/data-table-selection.js.map +1 -1
  152. package/dist/components/data-table-shared.d.ts +10 -2
  153. package/dist/components/data-table-shared.js +18 -0
  154. package/dist/components/data-table-shared.js.map +1 -1
  155. package/dist/components/data-table-sort.d.ts +21 -2
  156. package/dist/components/data-table-sort.js +24 -0
  157. package/dist/components/data-table-sort.js.map +1 -1
  158. package/dist/components/data-table-virtual.d.ts +1 -1
  159. package/dist/components/data-table-virtual.js +18 -6
  160. package/dist/components/data-table-virtual.js.map +1 -1
  161. package/dist/components/data-table.d.ts +25 -8
  162. package/dist/components/data-table.js +154 -25
  163. package/dist/components/data-table.js.map +1 -1
  164. package/dist/components/date-field.d.ts +24 -2
  165. package/dist/components/date-field.js +33 -8
  166. package/dist/components/date-field.js.map +1 -1
  167. package/dist/components/date-picker.d.ts +15 -2
  168. package/dist/components/date-picker.js +55 -26
  169. package/dist/components/date-picker.js.map +1 -1
  170. package/dist/components/date-range-picker.d.ts +47 -20
  171. package/dist/components/date-range-picker.js +23 -7
  172. package/dist/components/date-range-picker.js.map +1 -1
  173. package/dist/components/dialog-frame.d.ts +2 -1
  174. package/dist/components/dialog-frame.js.map +1 -1
  175. package/dist/components/entity-combobox.js +4 -2
  176. package/dist/components/entity-combobox.js.map +1 -1
  177. package/dist/components/error-boundary.js +37 -30
  178. package/dist/components/error-boundary.js.map +1 -1
  179. package/dist/components/{ui-labels.d.ts → field-base-labels.d.ts} +0 -2
  180. package/dist/components/{ui-labels.js → field-base-labels.js} +2 -9
  181. package/dist/components/field-base-labels.js.map +1 -0
  182. package/dist/components/field-base.d.ts +4 -3
  183. package/dist/components/field-base.js +3 -3
  184. package/dist/components/field-base.js.map +1 -1
  185. package/dist/components/field-sync-labels.d.ts +1 -0
  186. package/dist/components/field-sync-labels.js.map +1 -1
  187. package/dist/components/field-sync.d.ts +23 -1
  188. package/dist/components/field-sync.js.map +1 -1
  189. package/dist/components/form-actions.js +11 -5
  190. package/dist/components/form-actions.js.map +1 -1
  191. package/dist/components/hover-menu.d.ts +3 -0
  192. package/dist/components/hover-menu.js +5 -3
  193. package/dist/components/hover-menu.js.map +1 -1
  194. package/dist/components/icon-button.js +14 -9
  195. package/dist/components/icon-button.js.map +1 -1
  196. package/dist/components/inline-confirm.d.ts +43 -0
  197. package/dist/components/inline-confirm.js +67 -0
  198. package/dist/components/inline-confirm.js.map +1 -0
  199. package/dist/components/inline-entity-combobox.d.ts +4 -3
  200. package/dist/components/inline-entity-combobox.js +150 -141
  201. package/dist/components/inline-entity-combobox.js.map +1 -1
  202. package/dist/components/input.js +1 -1
  203. package/dist/components/input.js.map +1 -1
  204. package/dist/components/jump-calendar.js +28 -112
  205. package/dist/components/jump-calendar.js.map +1 -1
  206. package/dist/components/legal-page.d.ts +3 -1
  207. package/dist/components/legal-page.js +3 -2
  208. package/dist/components/legal-page.js.map +1 -1
  209. package/dist/components/legal.d.ts +4 -2
  210. package/dist/components/legal.js.map +1 -1
  211. package/dist/components/lightbox.d.ts +2 -2
  212. package/dist/components/lightbox.js +17 -6
  213. package/dist/components/lightbox.js.map +1 -1
  214. package/dist/components/list.d.ts +6 -2
  215. package/dist/components/list.js +4 -3
  216. package/dist/components/list.js.map +1 -1
  217. package/dist/components/measured-grid.d.ts +1 -1
  218. package/dist/components/measured-grid.js +2 -1
  219. package/dist/components/measured-grid.js.map +1 -1
  220. package/dist/components/mini-calendar.js +3 -5
  221. package/dist/components/mini-calendar.js.map +1 -1
  222. package/dist/components/modal.d.ts +24 -0
  223. package/dist/components/modal.js +12 -4
  224. package/dist/components/modal.js.map +1 -1
  225. package/dist/components/money-draft.js +7 -5
  226. package/dist/components/money-draft.js.map +1 -1
  227. package/dist/components/money-field.d.ts +2 -2
  228. package/dist/components/money-field.js.map +1 -1
  229. package/dist/components/month-grid.d.ts +55 -0
  230. package/dist/components/month-grid.js +148 -0
  231. package/dist/components/month-grid.js.map +1 -0
  232. package/dist/components/month-picker.d.ts +36 -4
  233. package/dist/components/month-picker.js +123 -146
  234. package/dist/components/month-picker.js.map +1 -1
  235. package/dist/components/multi-entity-combobox.js +2 -1
  236. package/dist/components/multi-entity-combobox.js.map +1 -1
  237. package/dist/components/multi-select.d.ts +2 -2
  238. package/dist/components/multi-select.js +11 -6
  239. package/dist/components/multi-select.js.map +1 -1
  240. package/dist/components/number-field.d.ts +6 -4
  241. package/dist/components/number-field.js +3 -2
  242. package/dist/components/number-field.js.map +1 -1
  243. package/dist/components/number-input.d.ts +4 -1
  244. package/dist/components/number-input.js +3 -2
  245. package/dist/components/number-input.js.map +1 -1
  246. package/dist/components/numpad-sheet.js.map +1 -1
  247. package/dist/components/picker-sheet.js +2 -1
  248. package/dist/components/picker-sheet.js.map +1 -1
  249. package/dist/components/pie-chart-labels.d.ts +7 -0
  250. package/dist/components/pie-chart-labels.js +1 -0
  251. package/dist/components/pie-chart-labels.js.map +1 -1
  252. package/dist/components/pie-chart.d.ts +5 -3
  253. package/dist/components/pie-chart.js +7 -22
  254. package/dist/components/pie-chart.js.map +1 -1
  255. package/dist/components/popover.d.ts +3 -0
  256. package/dist/components/popover.js +7 -1
  257. package/dist/components/popover.js.map +1 -1
  258. package/dist/components/reauth-dialog.d.ts +4 -5
  259. package/dist/components/reauth-dialog.js +1 -4
  260. package/dist/components/reauth-dialog.js.map +1 -1
  261. package/dist/components/row-actions.js +2 -1
  262. package/dist/components/row-actions.js.map +1 -1
  263. package/dist/components/section-nav.d.ts +7 -0
  264. package/dist/components/section-nav.js.map +1 -1
  265. package/dist/components/series-chart-axes.d.ts +10 -3
  266. package/dist/components/series-chart-axes.js +1 -0
  267. package/dist/components/series-chart-axes.js.map +1 -1
  268. package/dist/components/series-chart-labels.d.ts +7 -4
  269. package/dist/components/series-chart-labels.js.map +1 -1
  270. package/dist/components/series-chart.d.ts +31 -7
  271. package/dist/components/series-chart.js +72 -29
  272. package/dist/components/series-chart.js.map +1 -1
  273. package/dist/components/share-card.js +3 -3
  274. package/dist/components/share-card.js.map +1 -1
  275. package/dist/components/sign-chip-labels.d.ts +1 -0
  276. package/dist/components/sign-chip-labels.js.map +1 -1
  277. package/dist/components/sign-chip.d.ts +10 -1
  278. package/dist/components/sign-chip.js.map +1 -1
  279. package/dist/components/tabs-labels.d.ts +2 -0
  280. package/dist/components/tabs-labels.js +11 -0
  281. package/dist/components/tabs-labels.js.map +1 -0
  282. package/dist/components/tabs.d.ts +1 -1
  283. package/dist/components/tabs.js +13 -11
  284. package/dist/components/tabs.js.map +1 -1
  285. package/dist/components/toggle-legend.d.ts +2 -2
  286. package/dist/components/toggle-legend.js +6 -6
  287. package/dist/components/toggle-legend.js.map +1 -1
  288. package/dist/components/translation-review-editor.d.ts +4 -2
  289. package/dist/components/translation-review-editor.js +2 -3
  290. package/dist/components/translation-review-editor.js.map +1 -1
  291. package/dist/components/translation-review.d.ts +2 -1
  292. package/dist/components/translation-review.js +2 -2
  293. package/dist/components/translation-review.js.map +1 -1
  294. package/dist/components/treemap-labels.d.ts +26 -0
  295. package/dist/components/treemap-labels.js +10 -0
  296. package/dist/components/treemap-labels.js.map +1 -0
  297. package/dist/components/treemap-layout.d.ts +62 -0
  298. package/dist/components/treemap-layout.js +126 -0
  299. package/dist/components/treemap-layout.js.map +1 -0
  300. package/dist/components/treemap.d.ts +140 -17
  301. package/dist/components/treemap.js +324 -97
  302. package/dist/components/treemap.js.map +1 -1
  303. package/dist/components/use-table-state.d.ts +5 -5
  304. package/dist/components/use-table-state.js +4 -1
  305. package/dist/components/use-table-state.js.map +1 -1
  306. package/dist/data-table.d.ts +6 -2
  307. package/dist/data-table.js +7 -2
  308. package/dist/data-table.js.map +1 -1
  309. package/dist/eslint/index.js +1 -1
  310. package/dist/eslint/role-utilities.d.ts +36 -7
  311. package/dist/eslint/role-utilities.js +42 -9
  312. package/dist/eslint/role-utilities.js.map +1 -1
  313. package/dist/feedback/feedback-dialog.d.ts +25 -16
  314. package/dist/feedback/feedback-dialog.js +6 -2
  315. package/dist/feedback/feedback-dialog.js.map +1 -1
  316. package/dist/feedback/feedback-mutation.d.ts +30 -0
  317. package/dist/feedback/feedback-mutation.js +16 -0
  318. package/dist/feedback/feedback-mutation.js.map +1 -0
  319. package/dist/feedback/feedback-page-link.d.ts +53 -0
  320. package/dist/feedback/feedback-page-link.js +37 -0
  321. package/dist/feedback/feedback-page-link.js.map +1 -0
  322. package/dist/feedback/feedback-public-list.d.ts +12 -3
  323. package/dist/feedback/feedback-public-list.js +2 -1
  324. package/dist/feedback/feedback-public-list.js.map +1 -1
  325. package/dist/feedback/feedback-replies.d.ts +3 -1
  326. package/dist/feedback/feedback-replies.js +5 -4
  327. package/dist/feedback/feedback-replies.js.map +1 -1
  328. package/dist/feedback/feedback-row-detail-labels.js +5 -0
  329. package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
  330. package/dist/feedback/feedback-row-detail.d.ts +64 -28
  331. package/dist/feedback/feedback-row-detail.js +30 -28
  332. package/dist/feedback/feedback-row-detail.js.map +1 -1
  333. package/dist/feedback/feedback-status-undo.d.ts +30 -11
  334. package/dist/feedback/feedback-status-undo.js +10 -15
  335. package/dist/feedback/feedback-status-undo.js.map +1 -1
  336. package/dist/feedback/feedback-submit-dialog.d.ts +1 -1
  337. package/dist/feedback/feedback-submit-dialog.js +1 -1
  338. package/dist/feedback/feedback-submit-dialog.js.map +1 -1
  339. package/dist/feedback/feedback-submit.js +9 -3
  340. package/dist/feedback/feedback-submit.js.map +1 -1
  341. package/dist/feedback/feedback-swipe.d.ts +1 -1
  342. package/dist/feedback/feedback-swipe.js.map +1 -1
  343. package/dist/feedback/feedback-table.d.ts +21 -9
  344. package/dist/feedback/feedback-table.js +45 -25
  345. package/dist/feedback/feedback-table.js.map +1 -1
  346. package/dist/feedback/feedback-thread-labels.d.ts +2 -2
  347. package/dist/feedback/feedback-thread-labels.js.map +1 -1
  348. package/dist/feedback/feedback-thread.d.ts +24 -23
  349. package/dist/feedback/feedback-thread.js +2 -1
  350. package/dist/feedback/feedback-thread.js.map +1 -1
  351. package/dist/feedback.d.ts +34 -0
  352. package/dist/feedback.js +42 -0
  353. package/dist/feedback.js.map +1 -1
  354. package/dist/hooks/use-dismiss.js.map +1 -1
  355. package/dist/hooks/use-focus-trap.d.ts +35 -0
  356. package/dist/hooks/use-focus-trap.js +6 -4
  357. package/dist/hooks/use-focus-trap.js.map +1 -1
  358. package/dist/hooks/use-hotkey.d.ts +5 -2
  359. package/dist/hooks/use-hotkey.js +5 -4
  360. package/dist/hooks/use-hotkey.js.map +1 -1
  361. package/dist/hooks/use-long-press.d.ts +7 -4
  362. package/dist/hooks/use-long-press.js +3 -0
  363. package/dist/hooks/use-long-press.js.map +1 -1
  364. package/dist/hooks/use-media-query.js +12 -4
  365. package/dist/hooks/use-media-query.js.map +1 -1
  366. package/dist/hooks/use-mounted.d.ts +22 -0
  367. package/dist/hooks/use-mounted.js +24 -0
  368. package/dist/hooks/use-mounted.js.map +1 -0
  369. package/dist/hooks/use-overlay-history.d.ts +7 -1
  370. package/dist/hooks/use-overlay-history.js +35 -5
  371. package/dist/hooks/use-overlay-history.js.map +1 -1
  372. package/dist/i18n/defaults.js +4 -5
  373. package/dist/i18n/defaults.js.map +1 -1
  374. package/dist/i18n/german.js +7 -1
  375. package/dist/i18n/german.js.map +1 -1
  376. package/dist/i18n/kit-labels.d.ts +9 -2
  377. package/dist/i18n/kit-labels.js.map +1 -1
  378. package/dist/i18n/locales/en.js +20 -41
  379. package/dist/i18n/locales/en.js.map +3 -3
  380. package/dist/i18n/locales/es.js +7 -1
  381. package/dist/i18n/locales/es.js.map +1 -1
  382. package/dist/i18n/locales/fr.js +20 -10
  383. package/dist/i18n/locales/fr.js.map +1 -1
  384. package/dist/i18n/locales/hu.js +9 -1
  385. package/dist/i18n/locales/hu.js.map +1 -1
  386. package/dist/i18n/locales/it.js +7 -1
  387. package/dist/i18n/locales/it.js.map +1 -1
  388. package/dist/i18n/locales/zh.js +8 -1
  389. package/dist/i18n/locales/zh.js.map +1 -1
  390. package/dist/i18n/review.js +5 -0
  391. package/dist/i18n/review.js.map +1 -1
  392. package/dist/index.d.ts +64 -17
  393. package/dist/index.js +77 -12
  394. package/dist/index.js.map +1 -1
  395. package/dist/landing/routing.d.ts +3 -1
  396. package/dist/landing/routing.js +4 -7
  397. package/dist/landing/routing.js.map +1 -1
  398. package/dist/lib/broadcast.d.ts +17 -0
  399. package/dist/lib/broadcast.js +30 -0
  400. package/dist/lib/broadcast.js.map +1 -0
  401. package/dist/lib/calc.d.ts +7 -1
  402. package/dist/lib/calc.js +3 -1
  403. package/dist/lib/calc.js.map +1 -1
  404. package/dist/lib/clipboard-minus.d.ts +2 -2
  405. package/dist/lib/clipboard-minus.js +1 -1
  406. package/dist/lib/clipboard-minus.js.map +1 -1
  407. package/dist/lib/date-format.d.ts +37 -2
  408. package/dist/lib/date-format.js +34 -8
  409. package/dist/lib/date-format.js.map +1 -1
  410. package/dist/lib/day-format.d.ts +46 -0
  411. package/dist/lib/day-format.js +46 -0
  412. package/dist/lib/day-format.js.map +1 -0
  413. package/dist/lib/decimal-marks.d.ts +7 -1
  414. package/dist/lib/decimal-marks.js +5 -2
  415. package/dist/lib/decimal-marks.js.map +1 -1
  416. package/dist/lib/describe-failure.d.ts +14 -0
  417. package/dist/lib/describe-failure.js +9 -0
  418. package/dist/lib/describe-failure.js.map +1 -0
  419. package/dist/lib/direction.d.ts +16 -0
  420. package/dist/lib/direction.js +9 -1
  421. package/dist/lib/direction.js.map +1 -1
  422. package/dist/lib/focus-left.d.ts +63 -0
  423. package/dist/lib/focus-left.js +67 -0
  424. package/dist/lib/focus-left.js.map +1 -0
  425. package/dist/lib/format.d.ts +5 -3
  426. package/dist/lib/format.js +16 -13
  427. package/dist/lib/format.js.map +1 -1
  428. package/dist/lib/http-status.d.ts +24 -0
  429. package/dist/lib/http-status.js +14 -0
  430. package/dist/lib/http-status.js.map +1 -0
  431. package/dist/lib/intl-cache.d.ts +14 -0
  432. package/dist/lib/intl-cache.js +15 -0
  433. package/dist/lib/intl-cache.js.map +1 -0
  434. package/dist/lib/isin.d.ts +10 -0
  435. package/dist/lib/isin.js.map +1 -1
  436. package/dist/lib/iso-day.d.ts +11 -0
  437. package/dist/lib/iso-day.js +8 -0
  438. package/dist/lib/iso-day.js.map +1 -0
  439. package/dist/lib/lazy-with-fallback.d.ts +31 -0
  440. package/dist/lib/lazy-with-fallback.js +40 -0
  441. package/dist/lib/lazy-with-fallback.js.map +1 -0
  442. package/dist/lib/path-pattern.d.ts +34 -0
  443. package/dist/lib/path-pattern.js +24 -0
  444. package/dist/lib/path-pattern.js.map +1 -0
  445. package/dist/lib/reduced-motion.d.ts +2 -1
  446. package/dist/lib/reduced-motion.js.map +1 -1
  447. package/dist/lib/round.d.ts +2 -1
  448. package/dist/lib/round.js +2 -1
  449. package/dist/lib/round.js.map +1 -1
  450. package/dist/lib/safe-storage.d.ts +24 -5
  451. package/dist/lib/safe-storage.js +27 -2
  452. package/dist/lib/safe-storage.js.map +1 -1
  453. package/dist/lib/session-refresh.js +1 -1
  454. package/dist/lib/session-refresh.js.map +1 -1
  455. package/dist/lib/share-format.d.ts +13 -0
  456. package/dist/lib/share-format.js +10 -0
  457. package/dist/lib/share-format.js.map +1 -0
  458. package/dist/lib/text-case.d.ts +17 -0
  459. package/dist/lib/text-case.js +8 -0
  460. package/dist/lib/text-case.js.map +1 -0
  461. package/dist/lib/text-match.d.ts +38 -0
  462. package/dist/lib/text-match.js +24 -2
  463. package/dist/lib/text-match.js.map +1 -1
  464. package/dist/lib/thenable.d.ts +13 -0
  465. package/dist/lib/thenable.js +8 -0
  466. package/dist/lib/thenable.js.map +1 -0
  467. package/dist/lib/translation-review.d.ts +7 -2
  468. package/dist/lib/translation-review.js +4 -4
  469. package/dist/lib/translation-review.js.map +1 -1
  470. package/dist/notifications/device-zone-report.js +1 -1
  471. package/dist/notifications/device-zone-report.js.map +1 -1
  472. package/dist/notifications/notification-bell.d.ts +2 -1
  473. package/dist/notifications/notification-bell.js +3 -3
  474. package/dist/notifications/notification-bell.js.map +1 -1
  475. package/dist/notifications/notification-inbox.d.ts +10 -6
  476. package/dist/notifications/notification-inbox.js +26 -48
  477. package/dist/notifications/notification-inbox.js.map +1 -1
  478. package/dist/notifications/notification-parts.d.ts +3 -2
  479. package/dist/notifications/notification-parts.js +1 -3
  480. package/dist/notifications/notification-parts.js.map +1 -1
  481. package/dist/notifications/notification-types-setting.js +1 -1
  482. package/dist/notifications/notification-types-setting.js.map +1 -1
  483. package/dist/notifications/notifications-labels.d.ts +6 -0
  484. package/dist/notifications/notifications-labels.js +1 -0
  485. package/dist/notifications/notifications-labels.js.map +1 -1
  486. package/dist/notifications/push-device-setting.js +1 -1
  487. package/dist/notifications/push-device-setting.js.map +1 -1
  488. package/dist/notifications/push-devices-setting.d.ts +2 -1
  489. package/dist/notifications/push-devices-setting.js +27 -49
  490. package/dist/notifications/push-devices-setting.js.map +1 -1
  491. package/dist/notifications/quiet-hours-setting.d.ts +10 -3
  492. package/dist/notifications/quiet-hours-setting.js +65 -26
  493. package/dist/notifications/quiet-hours-setting.js.map +1 -1
  494. package/dist/notifications/unsubscribe-panel.js +4 -7
  495. package/dist/notifications/unsubscribe-panel.js.map +1 -1
  496. package/dist/notifications/use-push-device.d.ts +39 -1
  497. package/dist/notifications/use-push-device.js +28 -7
  498. package/dist/notifications/use-push-device.js.map +1 -1
  499. package/dist/offline/query-persistence.d.ts +3 -1
  500. package/dist/offline/query-persistence.js +19 -2
  501. package/dist/offline/query-persistence.js.map +1 -1
  502. package/dist/rhf/fields.d.ts +11 -4
  503. package/dist/rhf/fields.js.map +1 -1
  504. package/dist/search/command-palette.js +1 -1
  505. package/dist/search/command-palette.js.map +1 -1
  506. package/dist/search/global-search.js +19 -9
  507. package/dist/search/global-search.js.map +1 -1
  508. package/dist/search/use-command-key.d.ts +12 -1
  509. package/dist/search/use-command-key.js +3 -12
  510. package/dist/search/use-command-key.js.map +1 -1
  511. package/dist/search.d.ts +7 -0
  512. package/dist/search.js +10 -0
  513. package/dist/search.js.map +1 -1
  514. package/dist/settings/use-settings-focus.js +1 -0
  515. package/dist/settings/use-settings-focus.js.map +1 -1
  516. package/dist/shell/account-menu-language.d.ts +21 -5
  517. package/dist/shell/account-menu-language.js +3 -1
  518. package/dist/shell/account-menu-language.js.map +1 -1
  519. package/dist/shell/account-menu-text-size.d.ts +26 -9
  520. package/dist/shell/account-menu-text-size.js +3 -2
  521. package/dist/shell/account-menu-text-size.js.map +1 -1
  522. package/dist/shell/dev-menu.js +5 -18
  523. package/dist/shell/dev-menu.js.map +1 -1
  524. package/dist/shell/role-switcher.d.ts +7 -0
  525. package/dist/shell/role-switcher.js.map +1 -1
  526. package/dist/shell/top-bar-actions.d.ts +1 -1
  527. package/dist/shell/top-bar-actions.js.map +1 -1
  528. package/dist/shell/top-bar-parts-labels.d.ts +0 -2
  529. package/dist/shell/top-bar-parts-labels.js +0 -1
  530. package/dist/shell/top-bar-parts-labels.js.map +1 -1
  531. package/dist/shell.d.ts +27 -8
  532. package/dist/shell.js +28 -7
  533. package/dist/shell.js.map +1 -1
  534. package/dist/sync/engine.d.ts +30 -4
  535. package/dist/sync/engine.js +172 -52
  536. package/dist/sync/engine.js.map +1 -1
  537. package/dist/sync.d.ts +6 -2
  538. package/dist/sync.js +6 -1
  539. package/dist/sync.js.map +1 -1
  540. package/dist/testing/css-variables.js +3 -8
  541. package/dist/testing/css-variables.js.map +1 -1
  542. package/dist/theme/contrast.d.ts +2 -1
  543. package/dist/theme/contrast.js +3 -8
  544. package/dist/theme/contrast.js.map +1 -1
  545. package/dist/theme/palette-store.js +6 -6
  546. package/dist/theme/palette-store.js.map +1 -1
  547. package/dist/theme/text-size.js +3 -8
  548. package/dist/theme/text-size.js.map +1 -1
  549. package/dist/theme/theme-store.js +3 -6
  550. package/dist/theme/theme-store.js.map +1 -1
  551. package/dist/tour/tour-catalogue.d.ts +7 -4
  552. package/dist/tour/tour-catalogue.js +12 -10
  553. package/dist/tour/tour-catalogue.js.map +1 -1
  554. package/dist/tour/tour-dock.d.ts +0 -2
  555. package/dist/tour/tour-dock.js +0 -2
  556. package/dist/tour/tour-dock.js.map +1 -1
  557. package/dist/tour/tour-gates.d.ts +4 -0
  558. package/dist/tour/tour-gates.js +14 -0
  559. package/dist/tour/tour-gates.js.map +1 -1
  560. package/dist/tour/tour-overlay.d.ts +27 -0
  561. package/dist/tour/tour-overlay.js +741 -0
  562. package/dist/tour/tour-overlay.js.map +1 -0
  563. package/dist/tour/tour.d.ts +16 -4
  564. package/dist/tour/tour.js +62 -778
  565. package/dist/tour/tour.js.map +1 -1
  566. package/dist/tour/use-tour-progress.js +1 -1
  567. package/dist/tour/use-tour-progress.js.map +1 -1
  568. package/dist/tour/use-tour-url-sync.js +2 -2
  569. package/dist/tour/use-tour-url-sync.js.map +1 -1
  570. package/dist/wizard/stepper-nav.d.ts +5 -1
  571. package/dist/wizard/stepper-nav.js.map +1 -1
  572. package/dist/wizard/types.d.ts +6 -2
  573. package/dist/wizard/types.js.map +1 -1
  574. package/dist/wizard/use-wizard.d.ts +3 -1
  575. package/dist/wizard/use-wizard.js.map +1 -1
  576. package/dist/wizard/validation.d.ts +3 -1
  577. package/dist/wizard/validation.js.map +1 -1
  578. package/package.json +12 -8
  579. package/src/account/account-parts.ts +5 -25
  580. package/src/account/data-export-setting-labels.ts +1 -1
  581. package/src/account/delete-account-setting-labels.ts +1 -1
  582. package/src/account/email-change-setting-labels.ts +1 -1
  583. package/src/account/passkeys-setting.tsx +60 -38
  584. package/src/account/sessions-setting.tsx +18 -12
  585. package/src/admin/admin-action-confirm.tsx +5 -10
  586. package/src/admin/admin-parts.ts +11 -25
  587. package/src/admin/mail-test-card-labels.ts +1 -1
  588. package/src/admin/plan-change-labels.ts +5 -23
  589. package/src/admin/plan-change.tsx +7 -44
  590. package/src/admin/reviewer-scope-editor.tsx +3 -3
  591. package/src/auth/auth-errors-labels.ts +23 -0
  592. package/src/auth/auth-errors.ts +11 -38
  593. package/src/auth/complete-name-dialog.tsx +9 -8
  594. package/src/auth/forgot-password-form-labels.ts +1 -1
  595. package/src/auth/forgot-password-form.tsx +11 -6
  596. package/src/auth/register-form-labels.ts +1 -1
  597. package/src/auth/reset-password-form-labels.ts +1 -1
  598. package/src/auth/reset-password-form.tsx +8 -4
  599. package/src/auth/sign-in-form-labels.ts +1 -1
  600. package/src/auth/status-parts.tsx +0 -6
  601. package/src/auth/verify-email-labels.ts +1 -1
  602. package/src/auth/verify-email.tsx +11 -6
  603. package/src/billing/cancel-request.ts +28 -6
  604. package/src/billing/cancel-subscription.tsx +29 -22
  605. package/src/billing/dialog-close.ts +34 -0
  606. package/src/billing/plan-card.tsx +36 -11
  607. package/src/billing/plan-change-labels.ts +66 -0
  608. package/src/billing/plan-switch.tsx +31 -20
  609. package/src/chart.ts +21 -3
  610. package/src/components/account-chips.tsx +20 -23
  611. package/src/components/alert-banner.tsx +82 -5
  612. package/src/components/amount-input.tsx +32 -14
  613. package/src/components/autocomplete.tsx +64 -70
  614. package/src/components/bulk-action-bar.tsx +70 -2
  615. package/src/components/button-look.ts +5 -5
  616. package/src/components/button.tsx +2 -2
  617. package/src/components/calculator.tsx +98 -17
  618. package/src/components/chart-edge.ts +34 -1
  619. package/src/components/chart-figure.tsx +35 -0
  620. package/src/components/chart-zoom-core.tsx +6 -5
  621. package/src/components/chart.tsx +26 -5
  622. package/src/components/combobox-core.tsx +303 -69
  623. package/src/components/combobox.tsx +184 -155
  624. package/src/components/copy-button.tsx +34 -23
  625. package/src/components/country-select.tsx +16 -11
  626. package/src/components/data-table-column-settings.tsx +52 -10
  627. package/src/components/data-table-filters.ts +6 -4
  628. package/src/components/data-table-fit.ts +19 -5
  629. package/src/components/data-table-groups.tsx +1 -1
  630. package/src/components/data-table-head.tsx +32 -22
  631. package/src/components/data-table-mobile.tsx +110 -27
  632. package/src/components/data-table-pagination.tsx +21 -6
  633. package/src/components/data-table-resize.ts +1 -1
  634. package/src/components/data-table-row-actions.tsx +3 -3
  635. package/src/components/data-table-row.tsx +25 -14
  636. package/src/components/data-table-selection.ts +1 -1
  637. package/src/components/data-table-shared.tsx +33 -1
  638. package/src/components/data-table-sort.ts +57 -2
  639. package/src/components/data-table-virtual.ts +32 -8
  640. package/src/components/data-table.tsx +267 -41
  641. package/src/components/date-field.tsx +65 -8
  642. package/src/components/date-picker.tsx +125 -48
  643. package/src/components/date-range-picker.tsx +82 -31
  644. package/src/components/dialog-frame.tsx +2 -1
  645. package/src/components/entity-combobox.tsx +4 -1
  646. package/src/components/error-boundary.tsx +53 -39
  647. package/src/components/field-base-labels.ts +20 -0
  648. package/src/components/field-base.tsx +13 -7
  649. package/src/components/field-sync-labels.ts +1 -0
  650. package/src/components/field-sync.tsx +23 -1
  651. package/src/components/form-actions.tsx +20 -5
  652. package/src/components/hover-menu.tsx +18 -8
  653. package/src/components/icon-button.tsx +14 -9
  654. package/src/components/inline-confirm.tsx +116 -0
  655. package/src/components/inline-entity-combobox.tsx +198 -152
  656. package/src/components/input.tsx +1 -1
  657. package/src/components/jump-calendar.tsx +30 -146
  658. package/src/components/legal-page.tsx +10 -7
  659. package/src/components/legal.tsx +4 -2
  660. package/src/components/lightbox.tsx +21 -8
  661. package/src/components/list.tsx +14 -5
  662. package/src/components/measured-grid.tsx +8 -4
  663. package/src/components/mini-calendar.tsx +6 -5
  664. package/src/components/modal.tsx +45 -4
  665. package/src/components/money-draft.ts +16 -5
  666. package/src/components/money-field.tsx +2 -2
  667. package/src/components/month-grid.tsx +229 -0
  668. package/src/components/month-picker.tsx +192 -200
  669. package/src/components/multi-entity-combobox.tsx +1 -0
  670. package/src/components/multi-select.tsx +21 -9
  671. package/src/components/number-field.tsx +14 -7
  672. package/src/components/number-input.tsx +9 -3
  673. package/src/components/numpad-sheet.tsx +4 -3
  674. package/src/components/picker-sheet.tsx +12 -9
  675. package/src/components/pie-chart-labels.ts +8 -0
  676. package/src/components/pie-chart.tsx +22 -28
  677. package/src/components/popover.tsx +18 -1
  678. package/src/components/reauth-dialog.tsx +6 -10
  679. package/src/components/row-actions.tsx +2 -1
  680. package/src/components/section-nav.tsx +7 -0
  681. package/src/components/series-chart-axes.ts +11 -3
  682. package/src/components/series-chart-labels.ts +7 -4
  683. package/src/components/series-chart.tsx +178 -69
  684. package/src/components/share-card.tsx +6 -6
  685. package/src/components/sign-chip-labels.ts +1 -0
  686. package/src/components/sign-chip.tsx +10 -1
  687. package/src/components/tabs-labels.ts +13 -0
  688. package/src/components/tabs.tsx +35 -19
  689. package/src/components/toggle-legend.tsx +16 -11
  690. package/src/components/translation-review-editor.tsx +6 -5
  691. package/src/components/translation-review.tsx +4 -3
  692. package/src/components/treemap-labels.ts +39 -0
  693. package/src/components/treemap-layout.ts +210 -0
  694. package/src/components/treemap.tsx +722 -133
  695. package/src/components/use-table-state.ts +12 -9
  696. package/src/data-table.ts +9 -2
  697. package/src/eslint/role-utilities.ts +97 -19
  698. package/src/feedback/feedback-dialog.tsx +30 -19
  699. package/src/feedback/feedback-mutation.ts +41 -0
  700. package/src/feedback/feedback-page-link.ts +90 -0
  701. package/src/feedback/feedback-public-list.tsx +17 -5
  702. package/src/feedback/feedback-replies.tsx +13 -6
  703. package/src/feedback/feedback-row-detail-labels.ts +5 -0
  704. package/src/feedback/feedback-row-detail.tsx +103 -67
  705. package/src/feedback/feedback-status-undo.ts +41 -27
  706. package/src/feedback/feedback-submit-dialog.tsx +2 -2
  707. package/src/feedback/feedback-submit.tsx +21 -7
  708. package/src/feedback/feedback-swipe.tsx +1 -1
  709. package/src/feedback/feedback-table.tsx +86 -38
  710. package/src/feedback/feedback-thread-labels.ts +2 -2
  711. package/src/feedback/feedback-thread.tsx +27 -25
  712. package/src/feedback.ts +42 -0
  713. package/src/hooks/use-dismiss.ts +3 -2
  714. package/src/hooks/use-focus-trap.ts +8 -24
  715. package/src/hooks/use-hotkey.ts +24 -7
  716. package/src/hooks/use-long-press.ts +16 -2
  717. package/src/hooks/use-media-query.ts +20 -4
  718. package/src/hooks/use-mounted.ts +39 -0
  719. package/src/hooks/use-overlay-history.ts +70 -5
  720. package/src/i18n/defaults.ts +4 -5
  721. package/src/i18n/german.ts +8 -2
  722. package/src/i18n/kit-labels.tsx +9 -2
  723. package/src/i18n/locales/es.ts +8 -2
  724. package/src/i18n/locales/fr.ts +21 -13
  725. package/src/i18n/locales/hu.ts +10 -2
  726. package/src/i18n/locales/it.ts +8 -2
  727. package/src/i18n/locales/zh.ts +9 -2
  728. package/src/i18n/review.ts +5 -0
  729. package/src/index.ts +103 -13
  730. package/src/landing/routing.tsx +14 -10
  731. package/src/lib/broadcast.ts +52 -0
  732. package/src/lib/calc.ts +13 -2
  733. package/src/lib/clipboard-minus.ts +14 -4
  734. package/src/lib/date-format.ts +89 -13
  735. package/src/lib/day-format.ts +96 -0
  736. package/src/lib/decimal-marks.ts +12 -3
  737. package/src/lib/describe-failure.ts +18 -0
  738. package/src/lib/direction.ts +39 -0
  739. package/src/lib/focus-left.ts +156 -0
  740. package/src/lib/format.ts +34 -19
  741. package/src/lib/http-status.ts +38 -0
  742. package/src/lib/intl-cache.ts +25 -0
  743. package/src/lib/isin.ts +10 -0
  744. package/src/lib/iso-day.ts +13 -0
  745. package/src/lib/lazy-with-fallback.ts +82 -0
  746. package/src/lib/path-pattern.ts +55 -0
  747. package/src/lib/reduced-motion.ts +2 -1
  748. package/src/lib/round.ts +7 -2
  749. package/src/lib/safe-storage.ts +48 -6
  750. package/src/lib/session-refresh.ts +1 -1
  751. package/src/lib/share-format.ts +18 -0
  752. package/src/lib/text-case.ts +19 -0
  753. package/src/lib/text-match.ts +69 -2
  754. package/src/lib/thenable.ts +19 -0
  755. package/src/lib/translation-review.ts +13 -5
  756. package/src/notifications/device-zone-report.ts +1 -1
  757. package/src/notifications/notification-bell.tsx +7 -4
  758. package/src/notifications/notification-inbox.tsx +40 -58
  759. package/src/notifications/notification-parts.tsx +3 -4
  760. package/src/notifications/notification-types-setting.tsx +1 -1
  761. package/src/notifications/notifications-labels.ts +7 -0
  762. package/src/notifications/push-device-setting.tsx +1 -1
  763. package/src/notifications/push-devices-setting.tsx +30 -55
  764. package/src/notifications/quiet-hours-setting.tsx +93 -31
  765. package/src/notifications/unsubscribe-panel.tsx +4 -7
  766. package/src/notifications/use-push-device.ts +76 -10
  767. package/src/offline/query-persistence.ts +35 -3
  768. package/src/rhf/fields.tsx +11 -4
  769. package/src/search/command-palette.tsx +2 -4
  770. package/src/search/global-search.tsx +36 -15
  771. package/src/search/use-command-key.ts +13 -12
  772. package/src/search.ts +9 -0
  773. package/src/settings/use-settings-focus.ts +3 -0
  774. package/src/shell/account-menu-language.tsx +27 -5
  775. package/src/shell/account-menu-text-size.tsx +32 -10
  776. package/src/shell/dev-menu.tsx +7 -21
  777. package/src/shell/role-switcher.tsx +7 -0
  778. package/src/shell/top-bar-actions.tsx +1 -1
  779. package/src/shell/top-bar-parts-labels.ts +0 -3
  780. package/src/shell.ts +40 -9
  781. package/src/sync/engine.ts +339 -61
  782. package/src/sync.ts +8 -1
  783. package/src/testing/css-variables.ts +8 -9
  784. package/src/theme/contrast.ts +8 -9
  785. package/src/theme/palette-store.ts +9 -9
  786. package/src/theme/text-size.ts +6 -8
  787. package/src/theme/theme-store.ts +7 -9
  788. package/src/tour/tour-catalogue.tsx +26 -15
  789. package/src/tour/tour-dock.ts +0 -2
  790. package/src/tour/tour-gates.ts +23 -0
  791. package/src/tour/tour-overlay.tsx +1170 -0
  792. package/src/tour/tour.tsx +116 -1193
  793. package/src/tour/use-tour-progress.ts +1 -1
  794. package/src/tour/use-tour-url-sync.ts +7 -3
  795. package/src/wizard/stepper-nav.tsx +3 -1
  796. package/src/wizard/types.ts +11 -2
  797. package/src/wizard/use-wizard.ts +2 -1
  798. package/src/wizard/validation.ts +8 -2
  799. package/tokens.css +2 -1
  800. package/dist/components/ui-labels.js.map +0 -1
  801. package/dist/shell/dev-switchers.d.ts +0 -6
  802. package/dist/shell/dev-switchers.js +0 -12
  803. package/dist/shell/dev-switchers.js.map +0 -1
  804. package/src/components/ui-labels.ts +0 -24
  805. package/src/shell/dev-switchers.tsx +0 -18
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/calc.ts"],"sourcesContent":["/**\n * A tiny, dependency-free arithmetic evaluator for the numeric input fields.\n * It lets users type a calculation (e.g. \"12.50+3.20\" or \"100/3\") straight into\n * an amount field, or build one in the calculator popover, and have it resolve\n * to a number. Deliberately NOT backed by `eval`/`Function` — it's a hand-rolled\n * tokenizer + recursive-descent parser over a closed grammar (digits, decimals,\n * `+ - * /`, unary minus, parentheses), so no arbitrary code can run.\n */\n\ntype Token =\n | { kind: \"num\"; value: number }\n | { kind: \"op\"; value: \"+\" | \"-\" | \"*\" | \"/\" }\n | { kind: \"lparen\" }\n | { kind: \"rparen\" };\n\nfunction tokenize(input: string): Token[] | null {\n const tokens: Token[] = [];\n let i = 0;\n while (i < input.length) {\n const ch = input[i];\n if (ch === \" \") {\n i++;\n continue;\n }\n if (ch === \"+\" || ch === \"-\" || ch === \"*\" || ch === \"/\") {\n tokens.push({ kind: \"op\", value: ch });\n i++;\n continue;\n }\n if (ch === \"(\") {\n tokens.push({ kind: \"lparen\" });\n i++;\n continue;\n }\n if (ch === \")\") {\n tokens.push({ kind: \"rparen\" });\n i++;\n continue;\n }\n if ((ch >= \"0\" && ch <= \"9\") || ch === \".\") {\n let j = i;\n let dotSeen = false;\n while (j < input.length && ((input[j] >= \"0\" && input[j] <= \"9\") || input[j] === \".\")) {\n if (input[j] === \".\") {\n if (dotSeen) return null; // two dots in one number → invalid\n dotSeen = true;\n }\n j++;\n }\n const num = Number(input.slice(i, j));\n if (!Number.isFinite(num)) return null;\n tokens.push({ kind: \"num\", value: num });\n i = j;\n continue;\n }\n return null; // unknown character\n }\n return tokens;\n}\n\n/**\n * Recursive-descent parse honouring `* /` over `+ -` and left-associativity.\n * Returns `null` for any malformed input (the `null` sentinel is safe because a\n * valid result of 0 is distinct from it). Division by zero is treated as invalid.\n *\n * expr := term (('+' | '-') term)*\n * term := factor (('*' | '/') factor)*\n * factor := ('+' | '-') factor | number | '(' expr ')'\n */\nfunction parse(tokens: Token[]): number | null {\n let pos = 0;\n\n function parseExpr(): number | null {\n let value = parseTerm();\n if (value === null) return null;\n while (pos < tokens.length) {\n const tok = tokens[pos];\n if (tok.kind === \"op\" && (tok.value === \"+\" || tok.value === \"-\")) {\n pos++;\n const rhs = parseTerm();\n if (rhs === null) return null;\n value = tok.value === \"+\" ? value + rhs : value - rhs;\n } else break;\n }\n return value;\n }\n\n function parseTerm(): number | null {\n let value = parseFactor();\n if (value === null) return null;\n while (pos < tokens.length) {\n const tok = tokens[pos];\n if (tok.kind === \"op\" && (tok.value === \"*\" || tok.value === \"/\")) {\n pos++;\n const rhs = parseFactor();\n if (rhs === null) return null;\n if (tok.value === \"/\") {\n if (rhs === 0) return null;\n value = value / rhs;\n } else {\n value = value * rhs;\n }\n } else break;\n }\n return value;\n }\n\n function parseFactor(): number | null {\n const tok = tokens[pos];\n if (!tok) return null;\n if (tok.kind === \"op\" && (tok.value === \"+\" || tok.value === \"-\")) {\n pos++;\n const operand = parseFactor();\n if (operand === null) return null;\n return tok.value === \"-\" ? -operand : operand;\n }\n if (tok.kind === \"lparen\") {\n pos++;\n const inner = parseExpr();\n if (inner === null) return null;\n if (tokens[pos]?.kind !== \"rparen\") return null;\n pos++;\n return inner;\n }\n if (tok.kind === \"num\") {\n pos++;\n return tok.value;\n }\n return null;\n }\n\n const result = parseExpr();\n if (result === null) return null;\n if (pos !== tokens.length) return null; // leftover tokens → invalid\n return result;\n}\n\n/** Evaluate an arithmetic string, returning `null` if it isn't a valid sum.\n * Accepts the on-screen operator glyphs (`× ÷ −`) and comma decimals too. */\nexport function evaluateExpression(expr: string): number | null {\n const normalized = expr\n .replace(/×/g, \"*\")\n .replace(/÷/g, \"/\")\n .replace(/[−–—]/g, \"-\") // unicode minus / en / em dash → ascii\n .replace(/,/g, \".\")\n .trim();\n if (normalized === \"\") return null;\n const tokens = tokenize(normalized);\n if (tokens === null || tokens.length === 0) return null;\n const result = parse(tokens);\n if (result === null || !Number.isFinite(result)) return null;\n return result;\n}\n\n/** True when `s` carries arithmetic beyond a single leading minus — i.e. a plain\n * number (or a lone negative like \"-5\") is NOT an expression, but \"12+5\" is.\n * Used to decide whether a field's text should be evaluated on commit.\n *\n * The operator set is the one {@link evaluateExpression} accepts, on-screen glyphs\n * included: the calculator keypad inserts `× ÷ −`, and a `−` that this said was not\n * arithmetic went on to be read as a plain number — where the digits-only fallback\n * in {@link commitExpression} silently ate it, turning \"-42−−50\" (= 8) into a bare\n * 8 with no record that a sum had happened at all. */\nexport function looksLikeExpression(s: string): boolean {\n const t = s.trim();\n if (t === \"\") return false;\n if (/^-?\\d*[.,]?\\d*$/.test(t)) return false; // plain (optionally negative) decimal\n return /[+\\-*/×÷−–—()]/.test(t.replace(/^-/, \"\"));\n}\n\n/** Render an evaluated number back to a field-friendly string, trimming binary\n * float noise (so 0.1 + 0.2 reads \"0.3\", not \"0.30000000000000004\").\n *\n * FIXED notation, always, and that is the whole point of the function rather than a\n * detail of it. This used to end in `parseFloat(n.toFixed(10)).toString()`, and\n * `Number.prototype.toString` switches to EXPONENT notation below 1e-6 and at or\n * above 1e21 — an alphabet containing `e`, `+` and `-`. Neither function that reads a\n * field's text back accepts it: {@link tokenize} rejects `e` as an unknown character\n * and {@link sanitizeLive} deletes it. So {@link commitExpression} fell through to\n * its digits-only cleanup, which stripped the `e` and CONCATENATED the mantissa to\n * the exponent. A field committed its own output as a different number:\n *\n * \"1/10000000\" -> \"1e-7\" -> \"17\" (AmountInput: \"6\")\n * \"999999999999*999999999999\" -> \"9.99999999998e+23\" -> \"9.9999999999823\"\n *\n * Both controls commit on blur AND on Enter, so Enter-then-click-away was enough on\n * its own, it was silent, and it was in a money field.\n *\n * The invariant, which the test asserts directly rather than by example:\n * **`commitExpression(formatResult(n))` equals `formatResult(n)` for every finite\n * `n`.** Anything rendered here has to survive being read back. */\nexport function formatResult(n: number): string {\n if (!Number.isFinite(n)) return \"\";\n // At or above 1e21 `toFixed` gives up and hands back `toString`'s exponent form —\n // but every double that large is an integer, so BigInt renders it exactly.\n if (Math.abs(n) >= 1e21) return BigInt(n).toString();\n return trimTrailingZeros(n.toFixed(10));\n}\n\n/** \"15.7000000000\" -> \"15.7\", \"0.0000000000\" -> \"0\". The ten decimals are where the\n * binary float noise gets rounded off; this is only about not showing the padding. */\nfunction trimTrailingZeros(fixed: string): string {\n if (!fixed.includes(\".\")) return fixed;\n const trimmed = fixed.replace(/0+$/, \"\").replace(/\\.$/, \"\");\n // `toFixed` keeps the sign on a magnitude that rounded away to nothing\n // (\"-0.0000000000\"); `toString` did not, and \"-0\" in an amount field is noise.\n return trimmed === \"-0\" || trimmed === \"\" ? \"0\" : trimmed;\n}\n\n/** The single \"on blur / Enter\" transform a numeric field applies: evaluate the\n * text if it's an expression, otherwise normalise it to a bare number string\n * (comma → dot, dropping stray characters, non-leading minuses and anything after\n * a second decimal point).\n *\n * The last clause is the backstop for Keksdose live #201. A field the user typed in\n * cannot reach here malformed any more — {@link sanitizeLive} runs on every change,\n * including a paste — but a value handed in from elsewhere (a recurring rule, an\n * import, a test) still can, and \"1.2.3\" is not an expression (no operator), so it\n * used to fall straight through unchanged and be read as NaN. Here the text is CUT\n * at the second dot (\"1.2\") rather than joined (\"1.23\"): with no keystroke to\n * ignore there is nothing to say which reading was meant, and the shorter one\n * cannot silently invent a bigger number. */\nexport function commitExpression(raw: string): string {\n const s = raw.trim();\n if (looksLikeExpression(s)) {\n const n = evaluateExpression(s);\n if (n !== null) return formatResult(n);\n // Unparseable (e.g. a half-typed \"12+\"): fall through to a digits-only clean.\n }\n // Global, like sanitizeLive's: these two are the two halves of one rule and must\n // not appear to state it differently. (The `[^0-9.-]` strip on the same line\n // already ate whatever commas a non-global replace missed, so this changes no\n // behaviour — it removes the need for the next reader to run it to find that out.)\n const cleaned = s\n .replace(/,/g, \".\")\n .replace(/[^0-9.-]/g, \"\")\n .replace(/(?!^)-/g, \"\");\n return cleaned.replace(/^(-?\\d*\\.?\\d*).*$/, \"$1\");\n}\n\n/** The live, per-keystroke transform a numeric field applies: comma → dot and\n * drop anything that isn't a digit, decimal point, arithmetic operator (incl.\n * the on-screen `× ÷` glyphs) or paren — so a typed calculation survives\n * keystroke-by-keystroke but stray characters can't be entered. A lone leading\n * `-` is kept — in a plain field it is simply a negative amount; in a field whose\n * caller owns the sign it is read off again by {@link splitLeadingSign}. The text\n * is resolved to a plain number on blur/Enter via {@link commitExpression}. */\nexport function sanitizeLive(raw: string): string {\n return dropExtraDots(raw.replace(/,/g, \".\").replace(/[^0-9.+\\-*/×÷()]/g, \"\"));\n}\n\n/**\n * At most ONE decimal point per number (Keksdose live #201).\n *\n * Per NUMBER, not per string: \"1.5+2.5\" is a legitimate calculation with two dots\n * in two operands, so the counter resets at every operator and parenthesis.\n *\n * A second dot inside one operand is a **no-op keystroke** — the digits after it\n * keep landing, so \"1.2\" + \".\" + \"3\" is \"1.23\". That is what every calculator does\n * with a repeated decimal point, and it is the only option that does not make the\n * field feel broken (\"my 3 disappeared\"). This runs on paste as well as on typing,\n * so the malformed value never reaches the rest of the app: everything downstream\n * reads the field with `Number()`, NaN became a silent 0, and on the create form\n * that 0 was painted with the income green — a figure that was not a number at all\n * asserting money was coming in.\n */\nfunction dropExtraDots(s: string): string {\n let out = \"\";\n let dotInNumber = false;\n for (const ch of s) {\n if (ch === \".\") {\n if (dotInNumber) continue;\n dotInNumber = true;\n } else if (ch < \"0\" || ch > \"9\") {\n // An operator or a paren ends the current operand.\n dotInNumber = false;\n }\n out += ch;\n }\n return out;\n}\n\n/** A bare, unsigned amount: digits with at most one decimal separator and nothing\n * else — \"42\", \"42.\", \".5\", \"0,5\". NOT \"\" (an empty field has no figure to put a\n * sign in front of), not \"-42\", not \"12+5\". It is the only shape a DISPLAYED sign\n * prefix makes sense in front of; an expression carries its own arithmetic and has\n * to survive keystroke-for-keystroke. */\nexport function isBareAmount(s: string): boolean {\n const t = s.trim();\n return t !== \"\" && /^\\d*[.,]?\\d*$/.test(t);\n}\n\n/**\n * Split a LEADING sign off a plain signed number (Keksdose live #201 rework):\n * `\"-42\"` → `{ sign: \"-\", rest: \"42\" }`, everything else → `{ sign: \"\", rest: text }`.\n *\n * Only in front of a bare amount, because a minus with arithmetic after it IS\n * arithmetic: \"12-30\" is a subtraction, and \"-12+5\" is -7, not -(12+5). Those come\n * back untouched and are resolved by {@link commitExpression} on blur — at which\n * point the RESULT is a plain signed number and this function does get to speak.\n *\n * A lone \"-\" splits (rest \"\"): the sign is a statement about the direction the\n * moment it is typed, before any digit exists.\n */\nexport function splitLeadingSign(text: string): { sign: \"\" | \"-\" | \"+\"; rest: string } {\n const trimmed = text.trimStart();\n const sign = trimmed.charAt(0);\n if (sign !== \"-\" && sign !== \"+\") return { sign: \"\", rest: text };\n const rest = trimmed.slice(1);\n if (rest !== \"\" && !isBareAmount(rest)) return { sign: \"\", rest: text };\n return { sign, rest };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/lib/calc.ts"],"sourcesContent":["/**\n * A tiny, dependency-free arithmetic evaluator for the numeric input fields.\n * It lets users type a calculation (e.g. \"12.50+3.20\" or \"100/3\") straight into\n * an amount field, or build one in the calculator popover, and have it resolve\n * to a number. Deliberately NOT backed by `eval`/`Function` — it's a hand-rolled\n * tokenizer + recursive-descent parser over a closed grammar (digits, decimals,\n * `+ - * /`, unary minus, parentheses), so no arbitrary code can run.\n */\n\ntype Token =\n | { kind: \"num\"; value: number }\n | { kind: \"op\"; value: \"+\" | \"-\" | \"*\" | \"/\" }\n | { kind: \"lparen\" }\n | { kind: \"rparen\" };\n\nfunction tokenize(input: string): Token[] | null {\n const tokens: Token[] = [];\n let i = 0;\n while (i < input.length) {\n const ch = input[i];\n if (ch === \" \") {\n i++;\n continue;\n }\n if (ch === \"+\" || ch === \"-\" || ch === \"*\" || ch === \"/\") {\n tokens.push({ kind: \"op\", value: ch });\n i++;\n continue;\n }\n if (ch === \"(\") {\n tokens.push({ kind: \"lparen\" });\n i++;\n continue;\n }\n if (ch === \")\") {\n tokens.push({ kind: \"rparen\" });\n i++;\n continue;\n }\n if ((ch >= \"0\" && ch <= \"9\") || ch === \".\") {\n let j = i;\n let dotSeen = false;\n while (j < input.length && ((input[j] >= \"0\" && input[j] <= \"9\") || input[j] === \".\")) {\n if (input[j] === \".\") {\n if (dotSeen) return null; // two dots in one number → invalid\n dotSeen = true;\n }\n j++;\n }\n const num = Number(input.slice(i, j));\n if (!Number.isFinite(num)) return null;\n tokens.push({ kind: \"num\", value: num });\n i = j;\n continue;\n }\n return null; // unknown character\n }\n return tokens;\n}\n\n/**\n * Recursive-descent parse honouring `* /` over `+ -` and left-associativity.\n * Returns `null` for any malformed input (the `null` sentinel is safe because a\n * valid result of 0 is distinct from it). Division by zero is treated as invalid.\n *\n * expr := term (('+' | '-') term)*\n * term := factor (('*' | '/') factor)*\n * factor := ('+' | '-') factor | number | '(' expr ')'\n */\nfunction parse(tokens: Token[]): number | null {\n let pos = 0;\n\n function parseExpr(): number | null {\n let value = parseTerm();\n if (value === null) return null;\n while (pos < tokens.length) {\n const tok = tokens[pos];\n if (tok.kind === \"op\" && (tok.value === \"+\" || tok.value === \"-\")) {\n pos++;\n const rhs = parseTerm();\n if (rhs === null) return null;\n value = tok.value === \"+\" ? value + rhs : value - rhs;\n } else break;\n }\n return value;\n }\n\n function parseTerm(): number | null {\n let value = parseFactor();\n if (value === null) return null;\n while (pos < tokens.length) {\n const tok = tokens[pos];\n if (tok.kind === \"op\" && (tok.value === \"*\" || tok.value === \"/\")) {\n pos++;\n const rhs = parseFactor();\n if (rhs === null) return null;\n if (tok.value === \"/\") {\n if (rhs === 0) return null;\n value = value / rhs;\n } else {\n value = value * rhs;\n }\n } else break;\n }\n return value;\n }\n\n function parseFactor(): number | null {\n const tok = tokens[pos];\n if (!tok) return null;\n if (tok.kind === \"op\" && (tok.value === \"+\" || tok.value === \"-\")) {\n pos++;\n const operand = parseFactor();\n if (operand === null) return null;\n return tok.value === \"-\" ? -operand : operand;\n }\n if (tok.kind === \"lparen\") {\n pos++;\n const inner = parseExpr();\n if (inner === null) return null;\n if (tokens[pos]?.kind !== \"rparen\") return null;\n pos++;\n return inner;\n }\n if (tok.kind === \"num\") {\n pos++;\n return tok.value;\n }\n return null;\n }\n\n const result = parseExpr();\n if (result === null) return null;\n if (pos !== tokens.length) return null; // leftover tokens → invalid\n return result;\n}\n\n/** Evaluate an arithmetic string, returning `null` if it isn't a valid sum.\n * Accepts the on-screen operator glyphs (`× ÷ −`) and comma decimals too. */\nexport function evaluateExpression(expr: string): number | null {\n const normalized = expr\n .replace(/×/g, \"*\")\n .replace(/÷/g, \"/\")\n .replace(/[−–—]/g, \"-\") // unicode minus / en / em dash → ascii\n .replace(/,/g, \".\")\n .trim();\n if (normalized === \"\") return null;\n const tokens = tokenize(normalized);\n if (tokens === null || tokens.length === 0) return null;\n const result = parse(tokens);\n if (result === null || !Number.isFinite(result)) return null;\n return result;\n}\n\n/** True when `s` carries arithmetic beyond a single leading minus — i.e. a plain\n * number (or a lone negative like \"-5\") is NOT an expression, but \"12+5\" is.\n * Used to decide whether a field's text should be evaluated on commit.\n *\n * The operator set is the one {@link evaluateExpression} accepts, on-screen glyphs\n * included: the calculator keypad inserts `× ÷ −`, and a `−` that this said was not\n * arithmetic went on to be read as a plain number — where the digits-only fallback\n * in {@link commitExpression} silently ate it, turning \"-42−−50\" (= 8) into a bare\n * 8 with no record that a sum had happened at all. */\nexport function looksLikeExpression(s: string): boolean {\n const t = s.trim();\n if (t === \"\") return false;\n if (/^-?\\d*[.,]?\\d*$/.test(t)) return false; // plain (optionally negative) decimal\n return /[+\\-*/×÷−–—()]/.test(t.replace(/^-/, \"\"));\n}\n\n/** Render an evaluated number back to a field-friendly string, trimming binary\n * float noise (so 0.1 + 0.2 reads \"0.3\", not \"0.30000000000000004\").\n *\n * FIXED notation, always, and that is the whole point of the function rather than a\n * detail of it. This used to end in `parseFloat(n.toFixed(10)).toString()`, and\n * `Number.prototype.toString` switches to EXPONENT notation below 1e-6 and at or\n * above 1e21 — an alphabet containing `e`, `+` and `-`. Neither function that reads a\n * field's text back accepts it: {@link tokenize} rejects `e` as an unknown character\n * and {@link sanitizeLive} deletes it. So {@link commitExpression} fell through to\n * its digits-only cleanup, which stripped the `e` and CONCATENATED the mantissa to\n * the exponent. A field committed its own output as a different number:\n *\n * \"1/10000000\" -> \"1e-7\" -> \"17\" (AmountInput: \"6\")\n * \"999999999999*999999999999\" -> \"9.99999999998e+23\" -> \"9.9999999999823\"\n *\n * Both controls commit on blur AND on Enter, so Enter-then-click-away was enough on\n * its own, it was silent, and it was in a money field.\n *\n * The invariant, which the test asserts directly rather than by example:\n * **`commitExpression(formatResult(n))` equals `formatResult(n)` for every finite\n * `n`.** Anything rendered here has to survive being read back. */\nexport function formatResult(n: number): string {\n if (!Number.isFinite(n)) return \"\";\n // At or above 1e21 `toFixed` gives up and hands back `toString`'s exponent form —\n // but every double that large is an integer, so BigInt renders it exactly.\n if (Math.abs(n) >= 1e21) return BigInt(n).toString();\n return trimTrailingZeros(n.toFixed(10));\n}\n\n/** \"15.7000000000\" -> \"15.7\", \"0.0000000000\" -> \"0\". The ten decimals are where the\n * binary float noise gets rounded off; this is only about not showing the padding. */\nfunction trimTrailingZeros(fixed: string): string {\n if (!fixed.includes(\".\")) return fixed;\n const trimmed = fixed.replace(/0+$/, \"\").replace(/\\.$/, \"\");\n // `toFixed` keeps the sign on a magnitude that rounded away to nothing\n // (\"-0.0000000000\"); `toString` did not, and \"-0\" in an amount field is noise.\n return trimmed === \"-0\" || trimmed === \"\" ? \"0\" : trimmed;\n}\n\n/** The single \"on blur / Enter\" transform a numeric field applies: evaluate the\n * text if it's an expression, otherwise normalise it to a bare number string\n * (comma → dot, dropping stray characters, non-leading minuses and anything after\n * a second decimal point).\n *\n * The last clause is the backstop for Keksdose live #201. A field the user typed in\n * cannot reach here malformed any more — {@link sanitizeLive} runs on every change,\n * including a paste — but a value handed in from elsewhere (a recurring rule, an\n * import, a test) still can, and \"1.2.3\" is not an expression (no operator), so it\n * used to fall straight through unchanged and be read as NaN. Here the text is CUT\n * at the second dot (\"1.2\") rather than joined (\"1.23\"): with no keystroke to\n * ignore there is nothing to say which reading was meant, and the shorter one\n * cannot silently invent a bigger number. */\nexport function commitExpression(raw: string): string {\n const s = raw.trim();\n if (looksLikeExpression(s)) {\n const n = evaluateExpression(s);\n if (n !== null) return formatResult(n);\n // Unparseable (e.g. a half-typed \"12+\"): fall through to a digits-only clean.\n }\n // Global, like sanitizeLive's: these two are the two halves of one rule and must\n // not appear to state it differently. (The `[^0-9.-]` strip on the same line\n // already ate whatever commas a non-global replace missed, so this changes no\n // behaviour — it removes the need for the next reader to run it to find that out.)\n const cleaned = s\n .replace(/,/g, \".\")\n .replace(/[^0-9.-]/g, \"\")\n .replace(/(?!^)-/g, \"\");\n return cleaned.replace(/^(-?\\d*\\.?\\d*).*$/, \"$1\");\n}\n\n/** The live, per-keystroke transform a numeric field applies: comma → dot and\n * drop anything that isn't a digit, decimal point, arithmetic operator (incl.\n * the on-screen `× ÷` glyphs) or paren — so a typed calculation survives\n * keystroke-by-keystroke but stray characters can't be entered. A lone leading\n * `-` is kept — in a plain field it is simply a negative amount; in a field whose\n * caller owns the sign it is read off again by {@link splitLeadingSign}. The text\n * is resolved to a plain number on blur/Enter via {@link commitExpression}.\n *\n * The typographic minus and the two dashes become \"-\" first, as in\n * {@link evaluateExpression}: U+2212 is what the kit's own `formatMoney` and\n * `formatNumber` print for every negative since 0.49, and stripping it with the\n * stray characters turned a pasted \"−1.234,50 €\" into a positive amount (refactor\n * audit 2026-10-30, INP-3). */\nexport function sanitizeLive(raw: string): string {\n return dropExtraDots(\n raw\n .replace(/[−–—]/g, \"-\")\n .replace(/,/g, \".\")\n .replace(/[^0-9.+\\-*/×÷()]/g, \"\"),\n );\n}\n\n/**\n * At most ONE decimal point per number (Keksdose live #201).\n *\n * Per NUMBER, not per string: \"1.5+2.5\" is a legitimate calculation with two dots\n * in two operands, so the counter resets at every operator and parenthesis.\n *\n * A second dot inside one operand is a **no-op keystroke** — the digits after it\n * keep landing, so \"1.2\" + \".\" + \"3\" is \"1.23\". That is what every calculator does\n * with a repeated decimal point, and it is the only option that does not make the\n * field feel broken (\"my 3 disappeared\"). This runs on paste as well as on typing,\n * so the malformed value never reaches the rest of the app: everything downstream\n * reads the field with `Number()`, NaN became a silent 0, and on the create form\n * that 0 was painted with the income green — a figure that was not a number at all\n * asserting money was coming in.\n */\nfunction dropExtraDots(s: string): string {\n let out = \"\";\n let dotInNumber = false;\n for (const ch of s) {\n if (ch === \".\") {\n if (dotInNumber) continue;\n dotInNumber = true;\n } else if (ch < \"0\" || ch > \"9\") {\n // An operator or a paren ends the current operand.\n dotInNumber = false;\n }\n out += ch;\n }\n return out;\n}\n\n/** A bare, unsigned amount: digits with at most one decimal separator and nothing\n * else — \"42\", \"42.\", \".5\", \"0,5\". NOT \"\" (an empty field has no figure to put a\n * sign in front of), not \"-42\", not \"12+5\". It is the only shape a DISPLAYED sign\n * prefix makes sense in front of; an expression carries its own arithmetic and has\n * to survive keystroke-for-keystroke. */\nexport function isBareAmount(s: string): boolean {\n const t = s.trim();\n return t !== \"\" && /^\\d*[.,]?\\d*$/.test(t);\n}\n\n/**\n * Split a LEADING sign off a plain signed number (Keksdose live #201 rework):\n * `\"-42\"` → `{ sign: \"-\", rest: \"42\" }`, everything else → `{ sign: \"\", rest: text }`.\n *\n * Only in front of a bare amount, because a minus with arithmetic after it IS\n * arithmetic: \"12-30\" is a subtraction, and \"-12+5\" is -7, not -(12+5). Those come\n * back untouched and are resolved by {@link commitExpression} on blur — at which\n * point the RESULT is a plain signed number and this function does get to speak.\n *\n * A lone \"-\" splits (rest \"\"): the sign is a statement about the direction the\n * moment it is typed, before any digit exists.\n */\nexport function splitLeadingSign(text: string): { sign: \"\" | \"-\" | \"+\"; rest: string } {\n const trimmed = text.trimStart();\n const sign = trimmed.charAt(0);\n if (sign !== \"-\" && sign !== \"+\") return { sign: \"\", rest: text };\n const rest = trimmed.slice(1);\n if (rest !== \"\" && !isBareAmount(rest)) return { sign: \"\", rest: text };\n return { sign, rest };\n}\n"],"mappings":"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","names":[]}
@@ -5,8 +5,8 @@
5
5
  * as a number: a figure copied out of a kit table and pasted into Excel landed as text.
6
6
  * So while a selection inside the page is copied, every U+2212 standing directly before a
7
7
  * number — optionally after a currency code or symbol and spaces: "−CHF 1,250.50",
8
- * "−€2,000", "−12%" — becomes the ASCII "-" in the clipboard, in `text/plain` AND in
9
- * `text/html` (Excel pastes the HTML flavour, as cells). The page keeps "−".
8
+ * "−€2,000", "−US$12", "−12%" — becomes the ASCII "-" in the clipboard, in `text/plain` AND
9
+ * in `text/html` (Excel pastes the HTML flavour, as cells). The page keeps "−".
10
10
  *
11
11
  * A copy with no such minus is left to the browser untouched, and so is any copy out of an
12
12
  * input, a textarea or an editable element: what the person typed is theirs.
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- const MINUS_BEFORE_NUMBER = /\u2212(?=(?:[A-Z]{3}|\p{Sc})?[\s\u00a0\u202f]*\d)/gu;
2
+ const MINUS_BEFORE_NUMBER = /\u2212(?=[\p{L}\p{Sc}.]{0,5}[\s\u00a0\u202f]*\d)/gu;
3
3
  function asciiMinusInNumbers(text) {
4
4
  return text.replace(MINUS_BEFORE_NUMBER, "-");
5
5
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/clipboard-minus.ts"],"sourcesContent":["/**\n * The copy handler for the one minus sign (0.49 KIT-1b, Marcel's follow-up to KIT-1).\n *\n * The kit prints a negative with U+2212 (lib/minus.ts). A spreadsheet does not read that\n * as a number: a figure copied out of a kit table and pasted into Excel landed as text.\n * So while a selection inside the page is copied, every U+2212 standing directly before a\n * number — optionally after a currency code or symbol and spaces: \"−CHF 1,250.50\",\n * \"−€2,000\", \"−12%\" — becomes the ASCII \"-\" in the clipboard, in `text/plain` AND in\n * `text/html` (Excel pastes the HTML flavour, as cells). The page keeps \"−\".\n *\n * A copy with no such minus is left to the browser untouched, and so is any copy out of an\n * input, a textarea or an editable element: what the person typed is theirs.\n *\n * Installed once per document by `UiKitProvider` (`copyAsciiMinus`, default `true`) —\n * the one part every app mounts, the signed-out pages included.\n */\n\n/** U+2212, then (optionally) a currency code or symbol, then spaces, then a digit. */\nconst MINUS_BEFORE_NUMBER = /\\u2212(?=(?:[A-Z]{3}|\\p{Sc})?[\\s\\u00a0\\u202f]*\\d)/gu;\n\n/**\n * `text` with each U+2212 that stands before a number (in the sense above) as the ASCII\n * hyphen-minus, every other character as it was — for text an app hands a machine that\n * did not come from a formatter's `minus: \"ascii\"`.\n */\nexport function asciiMinusInNumbers(text: string): string {\n return text.replace(MINUS_BEFORE_NUMBER, \"-\");\n}\n\nfunction hasMinusBeforeNumber(text: string): boolean {\n MINUS_BEFORE_NUMBER.lastIndex = 0;\n const found = MINUS_BEFORE_NUMBER.test(text);\n MINUS_BEFORE_NUMBER.lastIndex = 0;\n return found;\n}\n\n/** An input, a textarea or an editable element: a copy there is the person's own text. */\nfunction isEditable(node: Node | null | undefined): boolean {\n const el = node instanceof Element ? node : node?.parentElement;\n if (!el) return false;\n if (el.closest(\"input, textarea, select\")) return true;\n return (\n (el as HTMLElement).isContentEditable === true ||\n el.closest('[contenteditable=\"\"], [contenteditable=\"true\"]') !== null\n );\n}\n\n/** Table parts copied alone are wrapped back into a table, which Excel reads as cells. */\nfunction wrapTableParts(container: HTMLElement): string {\n const first = container.firstElementChild?.tagName;\n const html = container.innerHTML;\n if (first === \"TD\" || first === \"TH\") return `<table><tbody><tr>${html}</tr></tbody></table>`;\n if (first === \"TR\") return `<table><tbody>${html}</tbody></table>`;\n if (first === \"TBODY\" || first === \"THEAD\" || first === \"TFOOT\") return `<table>${html}</table>`;\n return html;\n}\n\nfunction onCopy(event: ClipboardEvent): void {\n if (event.defaultPrevented || !event.clipboardData) return;\n const doc = (event.target as Node | null)?.ownerDocument ?? document;\n if (isEditable(event.target as Node | null) || isEditable(doc.activeElement)) return;\n const selection = doc.getSelection();\n if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return;\n if (isEditable(selection.anchorNode) || isEditable(selection.focusNode)) return;\n const text = selection.toString();\n if (!hasMinusBeforeNumber(text)) return;\n\n const container = doc.createElement(\"div\");\n for (let i = 0; i < selection.rangeCount; i++) container.appendChild(selection.getRangeAt(i).cloneContents());\n const walker = doc.createTreeWalker(container, NodeFilter.SHOW_TEXT);\n for (let node = walker.nextNode(); node; node = walker.nextNode()) {\n node.nodeValue = asciiMinusInNumbers(node.nodeValue ?? \"\");\n }\n event.clipboardData.setData(\"text/plain\", asciiMinusInNumbers(text));\n event.clipboardData.setData(\"text/html\", wrapTableParts(container));\n event.preventDefault();\n}\n\nconst installs = new WeakMap<Document, number>();\n\n/**\n * Listen for copies on `doc` until the returned function is called. Counted: nested\n * providers install it once, and the last one to go removes it.\n */\nexport function installClipboardMinus(doc: Document = document): () => void {\n const count = installs.get(doc) ?? 0;\n if (count === 0) doc.addEventListener(\"copy\", onCopy);\n installs.set(doc, count + 1);\n let done = false;\n return () => {\n if (done) return;\n done = true;\n const left = (installs.get(doc) ?? 1) - 1;\n installs.set(doc, left);\n if (left === 0) doc.removeEventListener(\"copy\", onCopy);\n };\n}\n"],"mappings":";AAkBA,MAAM,sBAAsB;AAOrB,SAAS,oBAAoB,MAAsB;AACxD,SAAO,KAAK,QAAQ,qBAAqB,GAAG;AAC9C;AAEA,SAAS,qBAAqB,MAAuB;AACnD,sBAAoB,YAAY;AAChC,QAAM,QAAQ,oBAAoB,KAAK,IAAI;AAC3C,sBAAoB,YAAY;AAChC,SAAO;AACT;AAGA,SAAS,WAAW,MAAwC;AAC1D,QAAM,KAAK,gBAAgB,UAAU,OAAO,MAAM;AAClD,MAAI,CAAC,GAAI,QAAO;AAChB,MAAI,GAAG,QAAQ,yBAAyB,EAAG,QAAO;AAClD,SACG,GAAmB,sBAAsB,QAC1C,GAAG,QAAQ,gDAAgD,MAAM;AAErE;AAGA,SAAS,eAAe,WAAgC;AACtD,QAAM,QAAQ,UAAU,mBAAmB;AAC3C,QAAM,OAAO,UAAU;AACvB,MAAI,UAAU,QAAQ,UAAU,KAAM,QAAO,qBAAqB,IAAI;AACtE,MAAI,UAAU,KAAM,QAAO,iBAAiB,IAAI;AAChD,MAAI,UAAU,WAAW,UAAU,WAAW,UAAU,QAAS,QAAO,UAAU,IAAI;AACtF,SAAO;AACT;AAEA,SAAS,OAAO,OAA6B;AAC3C,MAAI,MAAM,oBAAoB,CAAC,MAAM,cAAe;AACpD,QAAM,MAAO,MAAM,QAAwB,iBAAiB;AAC5D,MAAI,WAAW,MAAM,MAAqB,KAAK,WAAW,IAAI,aAAa,EAAG;AAC9E,QAAM,YAAY,IAAI,aAAa;AACnC,MAAI,CAAC,aAAa,UAAU,eAAe,KAAK,UAAU,YAAa;AACvE,MAAI,WAAW,UAAU,UAAU,KAAK,WAAW,UAAU,SAAS,EAAG;AACzE,QAAM,OAAO,UAAU,SAAS;AAChC,MAAI,CAAC,qBAAqB,IAAI,EAAG;AAEjC,QAAM,YAAY,IAAI,cAAc,KAAK;AACzC,WAAS,IAAI,GAAG,IAAI,UAAU,YAAY,IAAK,WAAU,YAAY,UAAU,WAAW,CAAC,EAAE,cAAc,CAAC;AAC5G,QAAM,SAAS,IAAI,iBAAiB,WAAW,WAAW,SAAS;AACnE,WAAS,OAAO,OAAO,SAAS,GAAG,MAAM,OAAO,OAAO,SAAS,GAAG;AACjE,SAAK,YAAY,oBAAoB,KAAK,aAAa,EAAE;AAAA,EAC3D;AACA,QAAM,cAAc,QAAQ,cAAc,oBAAoB,IAAI,CAAC;AACnE,QAAM,cAAc,QAAQ,aAAa,eAAe,SAAS,CAAC;AAClE,QAAM,eAAe;AACvB;AAEA,MAAM,WAAW,oBAAI,QAA0B;AAMxC,SAAS,sBAAsB,MAAgB,UAAsB;AAC1E,QAAM,QAAQ,SAAS,IAAI,GAAG,KAAK;AACnC,MAAI,UAAU,EAAG,KAAI,iBAAiB,QAAQ,MAAM;AACpD,WAAS,IAAI,KAAK,QAAQ,CAAC;AAC3B,MAAI,OAAO;AACX,SAAO,MAAM;AACX,QAAI,KAAM;AACV,WAAO;AACP,UAAM,QAAQ,SAAS,IAAI,GAAG,KAAK,KAAK;AACxC,aAAS,IAAI,KAAK,IAAI;AACtB,QAAI,SAAS,EAAG,KAAI,oBAAoB,QAAQ,MAAM;AAAA,EACxD;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/lib/clipboard-minus.ts"],"sourcesContent":["/**\n * The copy handler for the one minus sign (0.49 KIT-1b, Marcel's follow-up to KIT-1).\n *\n * The kit prints a negative with U+2212 (lib/minus.ts). A spreadsheet does not read that\n * as a number: a figure copied out of a kit table and pasted into Excel landed as text.\n * So while a selection inside the page is copied, every U+2212 standing directly before a\n * number — optionally after a currency code or symbol and spaces: \"−CHF 1,250.50\",\n * \"−€2,000\", \"−US$12\", \"−12%\" — becomes the ASCII \"-\" in the clipboard, in `text/plain` AND\n * in `text/html` (Excel pastes the HTML flavour, as cells). The page keeps \"−\".\n *\n * A copy with no such minus is left to the browser untouched, and so is any copy out of an\n * input, a textarea or an editable element: what the person typed is theirs.\n *\n * Installed once per document by `UiKitProvider` (`copyAsciiMinus`, default `true`) —\n * the one part every app mounts, the signed-out pages included.\n */\n\n/**\n * U+2212, then up to five letters, currency symbols or dots, then spaces, then a digit.\n *\n * Five of them, not \"a code or one symbol\": English and Chinese write a currency with\n * letters before the symbol — \"−US$\", \"−CA$\", \"−A$\", \"−NZ$\", \"−HK$\", \"−R$\", \"−MX$\",\n * \"−JP¥\", \"−CN¥\" — and `narrowSymbol` with letters alone (\"−kr\", \"−zł\", \"−Kč\"). Those kept\n * U+2212 in the clipboard, and Excel pasted them as text: 35 of 351 locale × currency\n * pairs at the default display, every one English or Chinese, and 136 of 1428 with\n * `narrowSymbol` (refactor audit 2026-10-30, DSP-11). A minus with no digit after it\n * (\"x−y\", \"a − b\") is still left alone.\n */\nconst MINUS_BEFORE_NUMBER = /\\u2212(?=[\\p{L}\\p{Sc}.]{0,5}[\\s\\u00a0\\u202f]*\\d)/gu;\n\n/**\n * `text` with each U+2212 that stands before a number (in the sense above) as the ASCII\n * hyphen-minus, every other character as it was — for text an app hands a machine that\n * did not come from a formatter's `minus: \"ascii\"`.\n */\nexport function asciiMinusInNumbers(text: string): string {\n return text.replace(MINUS_BEFORE_NUMBER, \"-\");\n}\n\nfunction hasMinusBeforeNumber(text: string): boolean {\n MINUS_BEFORE_NUMBER.lastIndex = 0;\n const found = MINUS_BEFORE_NUMBER.test(text);\n MINUS_BEFORE_NUMBER.lastIndex = 0;\n return found;\n}\n\n/** An input, a textarea or an editable element: a copy there is the person's own text. */\nfunction isEditable(node: Node | null | undefined): boolean {\n const el = node instanceof Element ? node : node?.parentElement;\n if (!el) return false;\n if (el.closest(\"input, textarea, select\")) return true;\n return (\n (el as HTMLElement).isContentEditable === true ||\n el.closest('[contenteditable=\"\"], [contenteditable=\"true\"]') !== null\n );\n}\n\n/** Table parts copied alone are wrapped back into a table, which Excel reads as cells. */\nfunction wrapTableParts(container: HTMLElement): string {\n const first = container.firstElementChild?.tagName;\n const html = container.innerHTML;\n if (first === \"TD\" || first === \"TH\") return `<table><tbody><tr>${html}</tr></tbody></table>`;\n if (first === \"TR\") return `<table><tbody>${html}</tbody></table>`;\n if (first === \"TBODY\" || first === \"THEAD\" || first === \"TFOOT\") return `<table>${html}</table>`;\n return html;\n}\n\nfunction onCopy(event: ClipboardEvent): void {\n if (event.defaultPrevented || !event.clipboardData) return;\n const doc = (event.target as Node | null)?.ownerDocument ?? document;\n if (isEditable(event.target as Node | null) || isEditable(doc.activeElement)) return;\n const selection = doc.getSelection();\n if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return;\n if (isEditable(selection.anchorNode) || isEditable(selection.focusNode)) return;\n const text = selection.toString();\n if (!hasMinusBeforeNumber(text)) return;\n\n const container = doc.createElement(\"div\");\n for (let i = 0; i < selection.rangeCount; i++) container.appendChild(selection.getRangeAt(i).cloneContents());\n const walker = doc.createTreeWalker(container, NodeFilter.SHOW_TEXT);\n for (let node = walker.nextNode(); node; node = walker.nextNode()) {\n node.nodeValue = asciiMinusInNumbers(node.nodeValue ?? \"\");\n }\n event.clipboardData.setData(\"text/plain\", asciiMinusInNumbers(text));\n event.clipboardData.setData(\"text/html\", wrapTableParts(container));\n event.preventDefault();\n}\n\nconst installs = new WeakMap<Document, number>();\n\n/**\n * Listen for copies on `doc` until the returned function is called. Counted: nested\n * providers install it once, and the last one to go removes it.\n */\nexport function installClipboardMinus(doc: Document = document): () => void {\n const count = installs.get(doc) ?? 0;\n if (count === 0) doc.addEventListener(\"copy\", onCopy);\n installs.set(doc, count + 1);\n let done = false;\n return () => {\n if (done) return;\n done = true;\n const left = (installs.get(doc) ?? 1) - 1;\n installs.set(doc, left);\n if (left === 0) doc.removeEventListener(\"copy\", onCopy);\n };\n}\n"],"mappings":";AA4BA,MAAM,sBAAsB;AAOrB,SAAS,oBAAoB,MAAsB;AACxD,SAAO,KAAK,QAAQ,qBAAqB,GAAG;AAC9C;AAEA,SAAS,qBAAqB,MAAuB;AACnD,sBAAoB,YAAY;AAChC,QAAM,QAAQ,oBAAoB,KAAK,IAAI;AAC3C,sBAAoB,YAAY;AAChC,SAAO;AACT;AAGA,SAAS,WAAW,MAAwC;AAC1D,QAAM,KAAK,gBAAgB,UAAU,OAAO,MAAM;AAClD,MAAI,CAAC,GAAI,QAAO;AAChB,MAAI,GAAG,QAAQ,yBAAyB,EAAG,QAAO;AAClD,SACG,GAAmB,sBAAsB,QAC1C,GAAG,QAAQ,gDAAgD,MAAM;AAErE;AAGA,SAAS,eAAe,WAAgC;AACtD,QAAM,QAAQ,UAAU,mBAAmB;AAC3C,QAAM,OAAO,UAAU;AACvB,MAAI,UAAU,QAAQ,UAAU,KAAM,QAAO,qBAAqB,IAAI;AACtE,MAAI,UAAU,KAAM,QAAO,iBAAiB,IAAI;AAChD,MAAI,UAAU,WAAW,UAAU,WAAW,UAAU,QAAS,QAAO,UAAU,IAAI;AACtF,SAAO;AACT;AAEA,SAAS,OAAO,OAA6B;AAC3C,MAAI,MAAM,oBAAoB,CAAC,MAAM,cAAe;AACpD,QAAM,MAAO,MAAM,QAAwB,iBAAiB;AAC5D,MAAI,WAAW,MAAM,MAAqB,KAAK,WAAW,IAAI,aAAa,EAAG;AAC9E,QAAM,YAAY,IAAI,aAAa;AACnC,MAAI,CAAC,aAAa,UAAU,eAAe,KAAK,UAAU,YAAa;AACvE,MAAI,WAAW,UAAU,UAAU,KAAK,WAAW,UAAU,SAAS,EAAG;AACzE,QAAM,OAAO,UAAU,SAAS;AAChC,MAAI,CAAC,qBAAqB,IAAI,EAAG;AAEjC,QAAM,YAAY,IAAI,cAAc,KAAK;AACzC,WAAS,IAAI,GAAG,IAAI,UAAU,YAAY,IAAK,WAAU,YAAY,UAAU,WAAW,CAAC,EAAE,cAAc,CAAC;AAC5G,QAAM,SAAS,IAAI,iBAAiB,WAAW,WAAW,SAAS;AACnE,WAAS,OAAO,OAAO,SAAS,GAAG,MAAM,OAAO,OAAO,SAAS,GAAG;AACjE,SAAK,YAAY,oBAAoB,KAAK,aAAa,EAAE;AAAA,EAC3D;AACA,QAAM,cAAc,QAAQ,cAAc,oBAAoB,IAAI,CAAC;AACnE,QAAM,cAAc,QAAQ,aAAa,eAAe,SAAS,CAAC;AAClE,QAAM,eAAe;AACvB;AAEA,MAAM,WAAW,oBAAI,QAA0B;AAMxC,SAAS,sBAAsB,MAAgB,UAAsB;AAC1E,QAAM,QAAQ,SAAS,IAAI,GAAG,KAAK;AACnC,MAAI,UAAU,EAAG,KAAI,iBAAiB,QAAQ,MAAM;AACpD,WAAS,IAAI,KAAK,QAAQ,CAAC;AAC3B,MAAI,OAAO;AACX,SAAO,MAAM;AACX,QAAI,KAAM;AACV,WAAO;AACP,UAAM,QAAQ,SAAS,IAAI,GAAG,KAAK,KAAK;AACxC,aAAS,IAAI,KAAK,IAAI;AACtB,QAAI,SAAS,EAAG,KAAI,oBAAoB,QAAQ,MAAM;AAAA,EACxD;AACF;","names":[]}
@@ -49,6 +49,15 @@ export interface DateFormatOptions {
49
49
  /** Name the day of the week too, in the interface language ("Mo., 01.08.2026"). A date
50
50
  * in a COLUMN carries it; a date in a SENTENCE does not (keksdose dev#546). */
51
51
  weekday?: boolean;
52
+ /**
53
+ * 0.52: the IANA zone a TIMESTAMP's day and clock are read in ("Europe/Zurich") — pass
54
+ * the `<UiKitProvider timeZone>` (`useKitTimeZone()`), so an app's own column says what
55
+ * the kit's components say for the same instant. Left out: the device's. A bare
56
+ * "YYYY-MM-DD" ignores it: a calendar day is the day it names in every zone. A zone
57
+ * `Intl` refuses reads as the device's. The renderers read it; {@link resolveDateFormat}
58
+ * does not.
59
+ */
60
+ timeZone?: string;
52
61
  }
53
62
  /**
54
63
  * What a date is rendered with, given the preference and the interface language
@@ -64,10 +73,36 @@ export declare function resolveDateFormat(preference: DateFormatPreference, lang
64
73
  * A date under the preference: `""` for no value, the input itself when it does not
65
74
  * parse. A bare "YYYY-MM-DD" is the calendar day it names in every zone (formatted in
66
75
  * UTC, undoing `new Date`'s UTC-midnight parse — west of Greenwich it read a day early);
67
- * a timestamp is an instant and stays in the reader's zone. The weekday, when asked for,
68
- * is rendered in the same zone, so a Sunday is never captioned Saturday.
76
+ * a timestamp is an instant, read in `options.timeZone`, else the reader's zone. The
77
+ * weekday, when asked for, is rendered in the same zone, so a Sunday is never captioned
78
+ * Saturday.
69
79
  */
70
80
  export declare function formatDateWithPreference(value: string | Date | null | undefined, preference: DateFormatPreference, language: string | undefined, options?: DateFormatOptions): string;
81
+ /**
82
+ * The clock time of `date` as the language writes it ("14:05"), two digits each: rule 3,
83
+ * a clock follows the language, never the preference — `sv-SE` is a statement about the
84
+ * order of a date's digits, not about a 12- or 24-hour clock. `language` is read through
85
+ * `formatLocaleOf`, so English is the British 24-hour clock, not "02:05 PM" in a
86
+ * German household's app. In `timeZone` when given, else the reader's.
87
+ *
88
+ * @internal Shared with the kit's own day formatter; not part of the barrel.
89
+ */
90
+ export declare function clockTime(date: Date, language: string | undefined, timeZone?: string): string;
91
+ /**
92
+ * A timestamp under the preference (0.52, keksdose's `formatDateTimeFor`, refactor audit
93
+ * 2026-10-30, DUP-2): the date as {@link formatDateWithPreference} writes it, weekday
94
+ * when asked for, then the clock time in the LANGUAGE ({@link clockTime}): "01.08.2026,
95
+ * 14:05", "Sa., 01.08.2026, 14:05". keksdose's notification inbox and admin scheduler
96
+ * were where an explicit ISO choice did nothing until it wrote this. `""` for no value,
97
+ * the input itself when it does not parse; a bare "YYYY-MM-DD" names no moment, so it
98
+ * prints as the date alone.
99
+ *
100
+ * Day and clock in `options.timeZone` — pass the provider's (`useKitTimeZone()`): without
101
+ * it they were the device's, and an app on Zurich time read from New York printed 09:05
102
+ * in its own column where the kit's inbox printed 14:05 for the same instant (0.52 code
103
+ * review, this file).
104
+ */
105
+ export declare function formatDateTimeWithPreference(value: string | Date | null | undefined, preference: DateFormatPreference, language: string | undefined, options?: DateFormatOptions): string;
71
106
  /** The sample an option is labelled with — 1 August 2026 in its shape, the shape being
72
107
  * the whole choice (a country name would say nothing about the digit order). */
73
108
  export declare function dateFormatSample(preference: DateFormatPreference, language: string | undefined): string;
@@ -1,5 +1,8 @@
1
1
  "use client";
2
2
  import { formatLocaleOf } from "../i18n/languages.js";
3
+ import { pad, toLocalIso } from "./dates.js";
4
+ import { dateFormatter } from "./intl-cache.js";
5
+ import { isBareIsoDay } from "./iso-day.js";
3
6
  const DATE_FORMAT_PREFERENCES = ["auto", "de-DE", "en-GB", "en-US", "sv-SE"];
4
7
  function isDateFormatPreference(value) {
5
8
  return typeof value === "string" && DATE_FORMAT_PREFERENCES.includes(value);
@@ -12,10 +15,9 @@ function resolveDateFormat(preference, language, options = {}) {
12
15
  }
13
16
  return { locale: preference, options: { year: "numeric", month: "2-digit", day: "2-digit" }, ...weekday };
14
17
  }
15
- const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
16
18
  function withWeekday(date, dateText, locale, options) {
17
19
  const isDatePart = (p) => p.type === "day" || p.type === "month" || p.type === "year";
18
- const parts = new Intl.DateTimeFormat(locale, { weekday: "short", ...options }).formatToParts(date);
20
+ const parts = dateFormatter(locale, { weekday: "short", ...options }).formatToParts(date);
19
21
  const first = parts.findIndex(isDatePart);
20
22
  if (first === -1) return dateText;
21
23
  let last = first;
@@ -28,16 +30,38 @@ function withWeekday(date, dateText, locale, options) {
28
30
  function formatDateWithPreference(value, preference, language, options = {}) {
29
31
  if (!value) return "";
30
32
  const resolved = resolveDateFormat(preference, language, options);
31
- const dateOnly = typeof value === "string" && DATE_ONLY.test(value);
33
+ const dateOnly = isBareIsoDay(value);
32
34
  const date = typeof value === "string" ? new Date(value) : value;
33
35
  if (Number.isNaN(date.getTime())) return String(value);
34
- const intl = dateOnly ? { ...resolved.options, timeZone: "UTC" } : resolved.options;
35
- try {
36
- const text = date.toLocaleDateString(resolved.locale, intl);
36
+ const render = (timeZone) => {
37
+ const intl = timeZone ? { ...resolved.options, timeZone } : resolved.options;
38
+ const text = dateFormatter(resolved.locale, intl).format(date);
37
39
  return resolved.weekday ? withWeekday(date, text, resolved.weekday.locale, intl) : text;
38
- } catch {
39
- return date.toISOString().slice(0, 10);
40
+ };
41
+ if (dateOnly) return firstAccepted([() => render("UTC")], () => value);
42
+ return firstAccepted([() => render(options.timeZone), () => render(void 0)], () => toLocalIso(date));
43
+ }
44
+ function firstAccepted(renders, last) {
45
+ for (const render of renders) {
46
+ try {
47
+ return render();
48
+ } catch {
49
+ }
40
50
  }
51
+ return last();
52
+ }
53
+ function clockTime(date, language, timeZone) {
54
+ const locale = language ? formatLocaleOf(language) : void 0;
55
+ const clock = (zone) => dateFormatter(locale, { hour: "2-digit", minute: "2-digit", ...zone ? { timeZone: zone } : null }).format(date);
56
+ const reader = () => `${pad(date.getHours())}:${pad(date.getMinutes())}`;
57
+ return firstAccepted([() => clock(timeZone), () => clock(void 0)], reader);
58
+ }
59
+ function formatDateTimeWithPreference(value, preference, language, options = {}) {
60
+ if (!value) return "";
61
+ if (isBareIsoDay(value)) return formatDateWithPreference(value, preference, language, options);
62
+ const date = typeof value === "string" ? new Date(value) : value;
63
+ if (Number.isNaN(date.getTime())) return String(value);
64
+ return `${formatDateWithPreference(date, preference, language, options)}, ${clockTime(date, language, options.timeZone)}`;
41
65
  }
42
66
  function dateFormatSample(preference, language) {
43
67
  return formatDateWithPreference("2026-08-01", preference, language);
@@ -47,8 +71,10 @@ function dateFormatterFor(preference, language) {
47
71
  }
48
72
  export {
49
73
  DATE_FORMAT_PREFERENCES,
74
+ clockTime,
50
75
  dateFormatSample,
51
76
  dateFormatterFor,
77
+ formatDateTimeWithPreference,
52
78
  formatDateWithPreference,
53
79
  isDateFormatPreference,
54
80
  resolveDateFormat
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/date-format.ts"],"sourcesContent":["import { formatLocaleOf } from \"../i18n/languages\";\nimport type { KitDateFormatter } from \"../i18n/kit-labels\";\n\n/**\n * The date-format preference (0.46, lifted from keksdose's `date-format-store.ts`,\n * feedback #180): how a DAY is written — the order of day, month and year and the\n * separator between them — as a choice of its own, apart from the interface language.\n * \"Read the app in English and still see German dates\", or ISO everywhere.\n *\n * keksdose's rules, which the kit keeps:\n *\n * 1. **A label that identifies a day follows the preference** — anything that prints\n * day, month and year goes through {@link formatDateWithPreference} (or the\n * provider's `formatDate`, {@link dateFormatterFor}, for the kit's own date fields).\n * 2. **A label without a day follows the language**: \"August 2026\", a month or weekday\n * NAME has no order to arrange. {@link dateFormatterFor} answers `\"\"` for a month or a\n * year, so the kit keeps its own rendering in the language.\n * 3. **A clock time follows the language**: `sv-SE` is a statement about digit order in\n * a date, not about a 12- or 24-hour clock.\n * 4. **A weekday's NAME follows the language** (dev#546): `sv-SE` means \"year first,\n * with dashes\", not \"the reader speaks Swedish\" — so with `weekday` the day's name is\n * the language's, joined to the date the preference renders.\n *\n * The preference is the APP's to keep — keksdose holds it per device (a persisted store,\n * not an account column: nothing server-written formats a date for the reader). The kit\n * owns the vocabulary, the resolver, the renderer and the settings field\n * ({@link DateFormatSetting}).\n */\n\n/** `\"auto\"` follows the interface language (the default — nobody's dates move until\n * they ask); the rest are BCP 47 tags chosen for the SHAPE they produce: German\n * 01.08.2026, British 01/08/2026, US 08/01/2026, ISO 2026-08-01. */\nexport type DateFormatPreference = \"auto\" | \"de-DE\" | \"en-GB\" | \"en-US\" | \"sv-SE\";\n\n/** The options in the order the setting lists them: `auto` first (the default), then\n * German → UK → US → ISO. */\nexport const DATE_FORMAT_PREFERENCES: readonly DateFormatPreference[] = [\"auto\", \"de-DE\", \"en-GB\", \"en-US\", \"sv-SE\"];\n\n/** Whether `value` is one of {@link DATE_FORMAT_PREFERENCES} — for a value read back from\n * storage, where anything else falls back to `\"auto\"`. */\nexport function isDateFormatPreference(value: unknown): value is DateFormatPreference {\n return typeof value === \"string\" && (DATE_FORMAT_PREFERENCES as readonly string[]).includes(value);\n}\n\n/** A resolved preference: the locale and `Intl` options the date itself is rendered\n * with, and — only when asked for — the locale the weekday's NAME comes from. */\nexport interface ResolvedDateFormat {\n locale: string | undefined;\n options: Intl.DateTimeFormatOptions;\n /** The language's format locale, present when `weekday` was asked for. */\n weekday?: { locale: string | undefined };\n}\n\n/** What {@link resolveDateFormat} and the renderers take beyond the preference. */\nexport interface DateFormatOptions {\n /** Name the day of the week too, in the interface language (\"Mo., 01.08.2026\"). A date\n * in a COLUMN carries it; a date in a SENTENCE does not (keksdose dev#546). */\n weekday?: boolean;\n}\n\n/**\n * What a date is rendered with, given the preference and the interface language\n * (`language`: a code or tag — `\"de\"`, `\"en\"`, i18next's `resolvedLanguage`; it is read\n * through `formatLocaleOf`, so English is British and an unshipped tag stays itself).\n *\n * `\"auto\"` keeps the short-month shape (\"01 Aug 2026\", \"01. Aug. 2026\"); every explicit\n * choice is all-numeric, the only way it produces the shape its option promises —\n * `month: \"short\"` under `sv-SE` would print \"2026 aug. 01\", not ISO.\n */\nexport function resolveDateFormat(\n preference: DateFormatPreference,\n language: string | undefined,\n options: DateFormatOptions = {},\n): ResolvedDateFormat {\n const languageLocale = language ? formatLocaleOf(language) : undefined;\n const weekday = options.weekday ? { weekday: { locale: languageLocale } } : {};\n if (preference === \"auto\") {\n return { locale: languageLocale, options: { year: \"numeric\", month: \"short\", day: \"2-digit\" }, ...weekday };\n }\n return { locale: preference, options: { year: \"numeric\", month: \"2-digit\", day: \"2-digit\" }, ...weekday };\n}\n\n/** A bare calendar day: no time, no zone. */\nconst DATE_ONLY = /^\\d{4}-\\d{2}-\\d{2}$/;\n\n/**\n * `dateText`, already rendered under the preference, with the day's NAME around it in\n * `locale` (the language's). Not \"weekday alone + comma\": German says \"So\" alone and\n * \"So., 06.09.2026\" in company. The full date is formatted in the language and split\n * into parts; what that language puts before the first and after the last day/month/\n * year part (the weekday, its comma) frames the preference's digits — so where the two\n * locales coincide the output is the native string, character for character.\n */\nfunction withWeekday(date: Date, dateText: string, locale: string | undefined, options: Intl.DateTimeFormatOptions) {\n const isDatePart = (p: Intl.DateTimeFormatPart) => p.type === \"day\" || p.type === \"month\" || p.type === \"year\";\n const parts = new Intl.DateTimeFormat(locale, { weekday: \"short\", ...options }).formatToParts(date);\n const first = parts.findIndex(isDatePart);\n if (first === -1) return dateText;\n let last = first;\n parts.forEach((p, i) => {\n if (isDatePart(p)) last = i;\n });\n const join = (ps: Intl.DateTimeFormatPart[]) => ps.map((p) => p.value).join(\"\");\n return join(parts.slice(0, first)) + dateText + join(parts.slice(last + 1));\n}\n\n/**\n * A date under the preference: `\"\"` for no value, the input itself when it does not\n * parse. A bare \"YYYY-MM-DD\" is the calendar day it names in every zone (formatted in\n * UTC, undoing `new Date`'s UTC-midnight parse — west of Greenwich it read a day early);\n * a timestamp is an instant and stays in the reader's zone. The weekday, when asked for,\n * is rendered in the same zone, so a Sunday is never captioned Saturday.\n */\nexport function formatDateWithPreference(\n value: string | Date | null | undefined,\n preference: DateFormatPreference,\n language: string | undefined,\n options: DateFormatOptions = {},\n): string {\n if (!value) return \"\";\n const resolved = resolveDateFormat(preference, language, options);\n const dateOnly = typeof value === \"string\" && DATE_ONLY.test(value);\n const date = typeof value === \"string\" ? new Date(value) : value;\n if (Number.isNaN(date.getTime())) return String(value);\n const intl: Intl.DateTimeFormatOptions = dateOnly ? { ...resolved.options, timeZone: \"UTC\" } : resolved.options;\n try {\n const text = date.toLocaleDateString(resolved.locale, intl);\n return resolved.weekday ? withWeekday(date, text, resolved.weekday.locale, intl) : text;\n } catch {\n // A language tag `Intl` refuses: the ISO day rather than nothing (keksdose's fallback).\n return date.toISOString().slice(0, 10);\n }\n}\n\n/** The sample an option is labelled with — 1 August 2026 in its shape, the shape being\n * the whole choice (a country name would say nothing about the digit order). */\nexport function dateFormatSample(preference: DateFormatPreference, language: string | undefined): string {\n return formatDateWithPreference(\"2026-08-01\", preference, language);\n}\n\n/**\n * The `<UiKitProvider formatDate>` for a preference — so the kit's own date fields\n * (DatePicker, DateRangePicker, DateMark) write a day as the app's dates do. A day\n * follows the preference, with the weekday where the kit suggests it; a month or a year\n * answers `\"\"` and keeps the kit's rendering in the language (rule 2). Rebuild it when\n * the preference or the language changes (a `useMemo` on both).\n */\nexport function dateFormatterFor(preference: DateFormatPreference, language: string | undefined): KitDateFormatter {\n return (iso, { unit, weekday, locale }) =>\n unit === \"day\" ? formatDateWithPreference(iso, preference, locale ?? language, { weekday }) : \"\";\n}\n"],"mappings":";AAAA,SAAS,sBAAsB;AAoCxB,MAAM,0BAA2D,CAAC,QAAQ,SAAS,SAAS,SAAS,OAAO;AAI5G,SAAS,uBAAuB,OAA+C;AACpF,SAAO,OAAO,UAAU,YAAa,wBAA8C,SAAS,KAAK;AACnG;AA2BO,SAAS,kBACd,YACA,UACA,UAA6B,CAAC,GACV;AACpB,QAAM,iBAAiB,WAAW,eAAe,QAAQ,IAAI;AAC7D,QAAM,UAAU,QAAQ,UAAU,EAAE,SAAS,EAAE,QAAQ,eAAe,EAAE,IAAI,CAAC;AAC7E,MAAI,eAAe,QAAQ;AACzB,WAAO,EAAE,QAAQ,gBAAgB,SAAS,EAAE,MAAM,WAAW,OAAO,SAAS,KAAK,UAAU,GAAG,GAAG,QAAQ;AAAA,EAC5G;AACA,SAAO,EAAE,QAAQ,YAAY,SAAS,EAAE,MAAM,WAAW,OAAO,WAAW,KAAK,UAAU,GAAG,GAAG,QAAQ;AAC1G;AAGA,MAAM,YAAY;AAUlB,SAAS,YAAY,MAAY,UAAkB,QAA4B,SAAqC;AAClH,QAAM,aAAa,CAAC,MAA+B,EAAE,SAAS,SAAS,EAAE,SAAS,WAAW,EAAE,SAAS;AACxG,QAAM,QAAQ,IAAI,KAAK,eAAe,QAAQ,EAAE,SAAS,SAAS,GAAG,QAAQ,CAAC,EAAE,cAAc,IAAI;AAClG,QAAM,QAAQ,MAAM,UAAU,UAAU;AACxC,MAAI,UAAU,GAAI,QAAO;AACzB,MAAI,OAAO;AACX,QAAM,QAAQ,CAAC,GAAG,MAAM;AACtB,QAAI,WAAW,CAAC,EAAG,QAAO;AAAA,EAC5B,CAAC;AACD,QAAM,OAAO,CAAC,OAAkC,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE;AAC9E,SAAO,KAAK,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,WAAW,KAAK,MAAM,MAAM,OAAO,CAAC,CAAC;AAC5E;AASO,SAAS,yBACd,OACA,YACA,UACA,UAA6B,CAAC,GACtB;AACR,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,WAAW,kBAAkB,YAAY,UAAU,OAAO;AAChE,QAAM,WAAW,OAAO,UAAU,YAAY,UAAU,KAAK,KAAK;AAClE,QAAM,OAAO,OAAO,UAAU,WAAW,IAAI,KAAK,KAAK,IAAI;AAC3D,MAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,EAAG,QAAO,OAAO,KAAK;AACrD,QAAM,OAAmC,WAAW,EAAE,GAAG,SAAS,SAAS,UAAU,MAAM,IAAI,SAAS;AACxG,MAAI;AACF,UAAM,OAAO,KAAK,mBAAmB,SAAS,QAAQ,IAAI;AAC1D,WAAO,SAAS,UAAU,YAAY,MAAM,MAAM,SAAS,QAAQ,QAAQ,IAAI,IAAI;AAAA,EACrF,QAAQ;AAEN,WAAO,KAAK,YAAY,EAAE,MAAM,GAAG,EAAE;AAAA,EACvC;AACF;AAIO,SAAS,iBAAiB,YAAkC,UAAsC;AACvG,SAAO,yBAAyB,cAAc,YAAY,QAAQ;AACpE;AASO,SAAS,iBAAiB,YAAkC,UAAgD;AACjH,SAAO,CAAC,KAAK,EAAE,MAAM,SAAS,OAAO,MACnC,SAAS,QAAQ,yBAAyB,KAAK,YAAY,UAAU,UAAU,EAAE,QAAQ,CAAC,IAAI;AAClG;","names":[]}
1
+ {"version":3,"sources":["../../src/lib/date-format.ts"],"sourcesContent":["import { formatLocaleOf } from \"../i18n/languages\";\nimport type { KitDateFormatter } from \"../i18n/kit-labels\";\nimport { pad, toLocalIso } from \"./dates\";\nimport { dateFormatter } from \"./intl-cache\";\nimport { isBareIsoDay } from \"./iso-day\";\n\n/**\n * The date-format preference (0.46, lifted from keksdose's `date-format-store.ts`,\n * feedback #180): how a DAY is written — the order of day, month and year and the\n * separator between them — as a choice of its own, apart from the interface language.\n * \"Read the app in English and still see German dates\", or ISO everywhere.\n *\n * keksdose's rules, which the kit keeps:\n *\n * 1. **A label that identifies a day follows the preference** — anything that prints\n * day, month and year goes through {@link formatDateWithPreference} (or the\n * provider's `formatDate`, {@link dateFormatterFor}, for the kit's own date fields).\n * 2. **A label without a day follows the language**: \"August 2026\", a month or weekday\n * NAME has no order to arrange. {@link dateFormatterFor} answers `\"\"` for a month or a\n * year, so the kit keeps its own rendering in the language.\n * 3. **A clock time follows the language**: `sv-SE` is a statement about digit order in\n * a date, not about a 12- or 24-hour clock.\n * 4. **A weekday's NAME follows the language** (dev#546): `sv-SE` means \"year first,\n * with dashes\", not \"the reader speaks Swedish\" — so with `weekday` the day's name is\n * the language's, joined to the date the preference renders.\n *\n * The preference is the APP's to keep — keksdose holds it per device (a persisted store,\n * not an account column: nothing server-written formats a date for the reader). The kit\n * owns the vocabulary, the resolver, the renderer and the settings field\n * ({@link DateFormatSetting}).\n */\n\n/** `\"auto\"` follows the interface language (the default — nobody's dates move until\n * they ask); the rest are BCP 47 tags chosen for the SHAPE they produce: German\n * 01.08.2026, British 01/08/2026, US 08/01/2026, ISO 2026-08-01. */\nexport type DateFormatPreference = \"auto\" | \"de-DE\" | \"en-GB\" | \"en-US\" | \"sv-SE\";\n\n/** The options in the order the setting lists them: `auto` first (the default), then\n * German → UK → US → ISO. */\nexport const DATE_FORMAT_PREFERENCES: readonly DateFormatPreference[] = [\"auto\", \"de-DE\", \"en-GB\", \"en-US\", \"sv-SE\"];\n\n/** Whether `value` is one of {@link DATE_FORMAT_PREFERENCES} — for a value read back from\n * storage, where anything else falls back to `\"auto\"`. */\nexport function isDateFormatPreference(value: unknown): value is DateFormatPreference {\n return typeof value === \"string\" && (DATE_FORMAT_PREFERENCES as readonly string[]).includes(value);\n}\n\n/** A resolved preference: the locale and `Intl` options the date itself is rendered\n * with, and — only when asked for — the locale the weekday's NAME comes from. */\nexport interface ResolvedDateFormat {\n locale: string | undefined;\n options: Intl.DateTimeFormatOptions;\n /** The language's format locale, present when `weekday` was asked for. */\n weekday?: { locale: string | undefined };\n}\n\n/** What {@link resolveDateFormat} and the renderers take beyond the preference. */\nexport interface DateFormatOptions {\n /** Name the day of the week too, in the interface language (\"Mo., 01.08.2026\"). A date\n * in a COLUMN carries it; a date in a SENTENCE does not (keksdose dev#546). */\n weekday?: boolean;\n /**\n * 0.52: the IANA zone a TIMESTAMP's day and clock are read in (\"Europe/Zurich\") — pass\n * the `<UiKitProvider timeZone>` (`useKitTimeZone()`), so an app's own column says what\n * the kit's components say for the same instant. Left out: the device's. A bare\n * \"YYYY-MM-DD\" ignores it: a calendar day is the day it names in every zone. A zone\n * `Intl` refuses reads as the device's. The renderers read it; {@link resolveDateFormat}\n * does not.\n */\n timeZone?: string;\n}\n\n/**\n * What a date is rendered with, given the preference and the interface language\n * (`language`: a code or tag — `\"de\"`, `\"en\"`, i18next's `resolvedLanguage`; it is read\n * through `formatLocaleOf`, so English is British and an unshipped tag stays itself).\n *\n * `\"auto\"` keeps the short-month shape (\"01 Aug 2026\", \"01. Aug. 2026\"); every explicit\n * choice is all-numeric, the only way it produces the shape its option promises —\n * `month: \"short\"` under `sv-SE` would print \"2026 aug. 01\", not ISO.\n */\nexport function resolveDateFormat(\n preference: DateFormatPreference,\n language: string | undefined,\n options: DateFormatOptions = {},\n): ResolvedDateFormat {\n const languageLocale = language ? formatLocaleOf(language) : undefined;\n const weekday = options.weekday ? { weekday: { locale: languageLocale } } : {};\n if (preference === \"auto\") {\n return { locale: languageLocale, options: { year: \"numeric\", month: \"short\", day: \"2-digit\" }, ...weekday };\n }\n return { locale: preference, options: { year: \"numeric\", month: \"2-digit\", day: \"2-digit\" }, ...weekday };\n}\n\n/**\n * `dateText`, already rendered under the preference, with the day's NAME around it in\n * `locale` (the language's). Not \"weekday alone + comma\": German says \"So\" alone and\n * \"So., 06.09.2026\" in company. The full date is formatted in the language and split\n * into parts; what that language puts before the first and after the last day/month/\n * year part (the weekday, its comma) frames the preference's digits — so where the two\n * locales coincide the output is the native string, character for character.\n */\nfunction withWeekday(date: Date, dateText: string, locale: string | undefined, options: Intl.DateTimeFormatOptions) {\n const isDatePart = (p: Intl.DateTimeFormatPart) => p.type === \"day\" || p.type === \"month\" || p.type === \"year\";\n const parts = dateFormatter(locale, { weekday: \"short\", ...options }).formatToParts(date);\n const first = parts.findIndex(isDatePart);\n if (first === -1) return dateText;\n let last = first;\n parts.forEach((p, i) => {\n if (isDatePart(p)) last = i;\n });\n const join = (ps: Intl.DateTimeFormatPart[]) => ps.map((p) => p.value).join(\"\");\n return join(parts.slice(0, first)) + dateText + join(parts.slice(last + 1));\n}\n\n/**\n * A date under the preference: `\"\"` for no value, the input itself when it does not\n * parse. A bare \"YYYY-MM-DD\" is the calendar day it names in every zone (formatted in\n * UTC, undoing `new Date`'s UTC-midnight parse — west of Greenwich it read a day early);\n * a timestamp is an instant, read in `options.timeZone`, else the reader's zone. The\n * weekday, when asked for, is rendered in the same zone, so a Sunday is never captioned\n * Saturday.\n */\nexport function formatDateWithPreference(\n value: string | Date | null | undefined,\n preference: DateFormatPreference,\n language: string | undefined,\n options: DateFormatOptions = {},\n): string {\n if (!value) return \"\";\n const resolved = resolveDateFormat(preference, language, options);\n const dateOnly = isBareIsoDay(value);\n const date = typeof value === \"string\" ? new Date(value) : value;\n if (Number.isNaN(date.getTime())) return String(value);\n const render = (timeZone: string | undefined) => {\n const intl: Intl.DateTimeFormatOptions = timeZone ? { ...resolved.options, timeZone } : resolved.options;\n // Through the shared cache (refactor audit 2026-10-30, DUP-2): a `toLocaleDateString`\n // builds a formatter per call, which is why keksdose kept its own copy of this\n // renderer — 392 ms per click on a 270-row register (dev#632).\n const text = dateFormatter(resolved.locale, intl).format(date);\n return resolved.weekday ? withWeekday(date, text, resolved.weekday.locale, intl) : text;\n };\n if (dateOnly) return firstAccepted([() => render(\"UTC\")], () => value);\n // A zone `Intl` refuses: the reader's own day, as `clockTime` reads the reader's own\n // clock — never the UTC day `toISOString` gave, a day off around midnight (0.52 code\n // review, this file).\n return firstAccepted([() => render(options.timeZone), () => render(undefined)], () => toLocalIso(date));\n}\n\n/** The first of `renders` that `Intl` accepts — a language tag or a zone it refuses\n * throws — else `last()`: the ISO day or the clock rather than nothing (keksdose's\n * fallback). */\nfunction firstAccepted(renders: readonly (() => string)[], last: () => string): string {\n for (const render of renders) {\n try {\n return render();\n } catch {\n // Refused: the next.\n }\n }\n return last();\n}\n\n/**\n * The clock time of `date` as the language writes it (\"14:05\"), two digits each: rule 3,\n * a clock follows the language, never the preference — `sv-SE` is a statement about the\n * order of a date's digits, not about a 12- or 24-hour clock. `language` is read through\n * `formatLocaleOf`, so English is the British 24-hour clock, not \"02:05 PM\" in a\n * German household's app. In `timeZone` when given, else the reader's.\n *\n * @internal Shared with the kit's own day formatter; not part of the barrel.\n */\nexport function clockTime(date: Date, language: string | undefined, timeZone?: string): string {\n const locale = language ? formatLocaleOf(language) : undefined;\n const clock = (zone: string | undefined) =>\n dateFormatter(locale, { hour: \"2-digit\", minute: \"2-digit\", ...(zone ? { timeZone: zone } : null) }).format(date);\n // A zone `Intl` refuses: the reader's own clock, as `isoDayIn` reads the reader's own\n // day — the UTC clock `toISOString` gave printed 13:05 for 14:05 in Zurich, an hour or\n // more off with no error (0.52 code review, this file).\n const reader = () => `${pad(date.getHours())}:${pad(date.getMinutes())}`;\n return firstAccepted([() => clock(timeZone), () => clock(undefined)], reader);\n}\n\n/**\n * A timestamp under the preference (0.52, keksdose's `formatDateTimeFor`, refactor audit\n * 2026-10-30, DUP-2): the date as {@link formatDateWithPreference} writes it, weekday\n * when asked for, then the clock time in the LANGUAGE ({@link clockTime}): \"01.08.2026,\n * 14:05\", \"Sa., 01.08.2026, 14:05\". keksdose's notification inbox and admin scheduler\n * were where an explicit ISO choice did nothing until it wrote this. `\"\"` for no value,\n * the input itself when it does not parse; a bare \"YYYY-MM-DD\" names no moment, so it\n * prints as the date alone.\n *\n * Day and clock in `options.timeZone` — pass the provider's (`useKitTimeZone()`): without\n * it they were the device's, and an app on Zurich time read from New York printed 09:05\n * in its own column where the kit's inbox printed 14:05 for the same instant (0.52 code\n * review, this file).\n */\nexport function formatDateTimeWithPreference(\n value: string | Date | null | undefined,\n preference: DateFormatPreference,\n language: string | undefined,\n options: DateFormatOptions = {},\n): string {\n if (!value) return \"\";\n if (isBareIsoDay(value)) return formatDateWithPreference(value, preference, language, options);\n const date = typeof value === \"string\" ? new Date(value) : value;\n if (Number.isNaN(date.getTime())) return String(value);\n return `${formatDateWithPreference(date, preference, language, options)}, ${clockTime(date, language, options.timeZone)}`;\n}\n\n/** The sample an option is labelled with — 1 August 2026 in its shape, the shape being\n * the whole choice (a country name would say nothing about the digit order). */\nexport function dateFormatSample(preference: DateFormatPreference, language: string | undefined): string {\n return formatDateWithPreference(\"2026-08-01\", preference, language);\n}\n\n/**\n * The `<UiKitProvider formatDate>` for a preference — so the kit's own date fields\n * (DatePicker, DateRangePicker, DateMark) write a day as the app's dates do. A day\n * follows the preference, with the weekday where the kit suggests it; a month or a year\n * answers `\"\"` and keeps the kit's rendering in the language (rule 2). Rebuild it when\n * the preference or the language changes (a `useMemo` on both).\n */\nexport function dateFormatterFor(preference: DateFormatPreference, language: string | undefined): KitDateFormatter {\n return (iso, { unit, weekday, locale }) =>\n unit === \"day\" ? formatDateWithPreference(iso, preference, locale ?? language, { weekday }) : \"\";\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,46 @@
1
+ import { type DateInput, type FormatDateStyle } from "./format.js";
2
+ /**
3
+ * How a kit component prints a day — ONE ladder (refactor audit 2026-10-30, DUP-2):
4
+ *
5
+ * 1. the component's own prop (`formatDate`, `renderDate`), when the app gave one and it
6
+ * answers;
7
+ * 2. else the `<UiKitProvider formatDate>` (keksdose K12: the app's date-format
8
+ * preference), asked for the calendar day with `source: "dateMark"`;
9
+ * 3. else the kit's `formatDate` in the provider's locale AND `timeZone` (0.46).
10
+ *
11
+ * 0.46's rule — "a label that identifies a day follows the preference" — held only where
12
+ * a component wrote that ladder itself: four copies (admin's `useDayText`, AccountStateChip,
13
+ * DateMark, the feedback table), while about twelve components took their own prop or
14
+ * nothing and went straight to `lib/format`. keksdose set the provider's `formatDate` and
15
+ * still passed it into six components by hand; and the 10 that imported the raw
16
+ * `formatDate` ignored the provider's `timeZone`.
17
+ *
18
+ * @internal Not part of the barrel: the components' props are the public way in.
19
+ */
20
+ /** What a {@link DayFormatter} call may ask for. */
21
+ export interface DayTextOptions {
22
+ /**
23
+ * Name the day of the week — what the provider's formatter is told (its `weekday`).
24
+ * keksdose's rule: a date in a COLUMN carries it, a date in a SENTENCE does not
25
+ * (dev#546). Default `false`. The kit's own rendering follows `style`, not this.
26
+ */
27
+ weekday?: boolean;
28
+ /** Add the clock time, in the language (date-format rule 3): a timestamp. Default false. */
29
+ time?: boolean;
30
+ /** The kit's own rendering (step 3). Default `"medium"`, `"dateTime"` with `time`. */
31
+ style?: FormatDateStyle | Intl.DateTimeFormatOptions;
32
+ /** `false`: skip the provider (step 2) — a component's explicit style wins over it,
33
+ * as `DateMark`'s `dateStyle` does. Default `true`. */
34
+ preference?: boolean;
35
+ /** Printed for no value. Default "—". */
36
+ empty?: string;
37
+ }
38
+ /** A component's day printer: a value in, its text out. */
39
+ export type DayFormatter<V extends DateInput = DateInput> = (value: V, options?: DayTextOptions) => string;
40
+ /**
41
+ * The component's day printer, bound to its `prop`, the provider's `formatDate`, locale
42
+ * and `timeZone`. `prop` is called with the value as the component passed it, and wins
43
+ * whenever it answers non-empty. A value that does not parse is printed as it came
44
+ * (a bug upstream is better seen than hidden behind a dash).
45
+ */
46
+ export declare function useDayFormatter<V extends DateInput = DateInput>(prop?: ((value: V) => string) | null, localeProp?: string): DayFormatter<V>;
@@ -0,0 +1,46 @@
1
+ "use client";
2
+ import { useCallback } from "react";
3
+ import { useKitDateFormatter, useKitLocale, useKitTimeZone } from "../i18n/kit-labels.js";
4
+ import { toLocalIso } from "./dates.js";
5
+ import { clockTime } from "./date-format.js";
6
+ import { EMPTY_FORMATTED_VALUE, formatDate, toDate } from "./format.js";
7
+ import { dateFormatter } from "./intl-cache.js";
8
+ import { isBareIsoDay } from "./iso-day.js";
9
+ function isoDayIn(date, timeZone) {
10
+ if (!timeZone) return toLocalIso(date);
11
+ try {
12
+ const parts = dateFormatter("en-CA", { timeZone, year: "numeric", month: "2-digit", day: "2-digit" }).formatToParts(
13
+ date
14
+ );
15
+ const part = (type) => parts.find((p) => p.type === type)?.value ?? "";
16
+ return `${part("year")}-${part("month")}-${part("day")}`;
17
+ } catch {
18
+ return toLocalIso(date);
19
+ }
20
+ }
21
+ function useDayFormatter(prop, localeProp) {
22
+ const locale = useKitLocale(localeProp);
23
+ const timeZone = useKitTimeZone();
24
+ const fromProvider = useKitDateFormatter();
25
+ return useCallback(
26
+ (value, options = {}) => {
27
+ const own = prop?.(value);
28
+ if (own) return own;
29
+ if (value === null || value === void 0 || value === "") return options.empty ?? EMPTY_FORMATTED_VALUE;
30
+ const date = toDate(value);
31
+ if (!date) return String(value);
32
+ const bareDay = isBareIsoDay(value);
33
+ if (fromProvider && options.preference !== false) {
34
+ const iso = bareDay ? value : isoDayIn(date, timeZone);
35
+ const day = fromProvider(iso, { unit: "day", source: "dateMark", locale, weekday: options.weekday ?? false });
36
+ if (day) return options.time && !bareDay ? `${day}, ${clockTime(date, locale, timeZone)}` : day;
37
+ }
38
+ return formatDate(value, options.style ?? (options.time ? "dateTime" : "medium"), { locale, timeZone });
39
+ },
40
+ [prop, fromProvider, locale, timeZone]
41
+ );
42
+ }
43
+ export {
44
+ useDayFormatter
45
+ };
46
+ //# sourceMappingURL=day-format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/day-format.ts"],"sourcesContent":["import { useCallback } from \"react\";\nimport { useKitDateFormatter, useKitLocale, useKitTimeZone } from \"../i18n/kit-labels\";\nimport { toLocalIso } from \"./dates\";\nimport { clockTime } from \"./date-format\";\nimport { EMPTY_FORMATTED_VALUE, formatDate, toDate, type DateInput, type FormatDateStyle } from \"./format\";\nimport { dateFormatter } from \"./intl-cache\";\nimport { isBareIsoDay } from \"./iso-day\";\n\n/**\n * How a kit component prints a day — ONE ladder (refactor audit 2026-10-30, DUP-2):\n *\n * 1. the component's own prop (`formatDate`, `renderDate`), when the app gave one and it\n * answers;\n * 2. else the `<UiKitProvider formatDate>` (keksdose K12: the app's date-format\n * preference), asked for the calendar day with `source: \"dateMark\"`;\n * 3. else the kit's `formatDate` in the provider's locale AND `timeZone` (0.46).\n *\n * 0.46's rule — \"a label that identifies a day follows the preference\" — held only where\n * a component wrote that ladder itself: four copies (admin's `useDayText`, AccountStateChip,\n * DateMark, the feedback table), while about twelve components took their own prop or\n * nothing and went straight to `lib/format`. keksdose set the provider's `formatDate` and\n * still passed it into six components by hand; and the 10 that imported the raw\n * `formatDate` ignored the provider's `timeZone`.\n *\n * @internal Not part of the barrel: the components' props are the public way in.\n */\n\n/** What a {@link DayFormatter} call may ask for. */\nexport interface DayTextOptions {\n /**\n * Name the day of the week — what the provider's formatter is told (its `weekday`).\n * keksdose's rule: a date in a COLUMN carries it, a date in a SENTENCE does not\n * (dev#546). Default `false`. The kit's own rendering follows `style`, not this.\n */\n weekday?: boolean;\n /** Add the clock time, in the language (date-format rule 3): a timestamp. Default false. */\n time?: boolean;\n /** The kit's own rendering (step 3). Default `\"medium\"`, `\"dateTime\"` with `time`. */\n style?: FormatDateStyle | Intl.DateTimeFormatOptions;\n /** `false`: skip the provider (step 2) — a component's explicit style wins over it,\n * as `DateMark`'s `dateStyle` does. Default `true`. */\n preference?: boolean;\n /** Printed for no value. Default \"—\". */\n empty?: string;\n}\n\n/** A component's day printer: a value in, its text out. */\nexport type DayFormatter<V extends DateInput = DateInput> = (value: V, options?: DayTextOptions) => string;\n\n/** The calendar day `date` falls on in `timeZone`, as \"YYYY-MM-DD\" — so the day the\n * provider is asked for is the day the kit's own rendering would print. */\nfunction isoDayIn(date: Date, timeZone: string | undefined): string {\n if (!timeZone) return toLocalIso(date);\n try {\n // `en-CA` writes YYYY-MM-DD with Latin digits.\n const parts = dateFormatter(\"en-CA\", { timeZone, year: \"numeric\", month: \"2-digit\", day: \"2-digit\" }).formatToParts(\n date,\n );\n const part = (type: Intl.DateTimeFormatPartTypes) => parts.find((p) => p.type === type)?.value ?? \"\";\n return `${part(\"year\")}-${part(\"month\")}-${part(\"day\")}`;\n } catch {\n return toLocalIso(date);\n }\n}\n\n/**\n * The component's day printer, bound to its `prop`, the provider's `formatDate`, locale\n * and `timeZone`. `prop` is called with the value as the component passed it, and wins\n * whenever it answers non-empty. A value that does not parse is printed as it came\n * (a bug upstream is better seen than hidden behind a dash).\n */\nexport function useDayFormatter<V extends DateInput = DateInput>(\n prop?: ((value: V) => string) | null,\n localeProp?: string,\n): DayFormatter<V> {\n const locale = useKitLocale(localeProp);\n const timeZone = useKitTimeZone();\n const fromProvider = useKitDateFormatter();\n return useCallback<DayFormatter<V>>(\n (value, options = {}) => {\n const own = prop?.(value);\n if (own) return own;\n if (value === null || value === undefined || value === \"\") return options.empty ?? EMPTY_FORMATTED_VALUE;\n const date = toDate(value);\n if (!date) return String(value);\n const bareDay = isBareIsoDay(value);\n if (fromProvider && options.preference !== false) {\n const iso = bareDay ? value : isoDayIn(date, timeZone);\n const day = fromProvider(iso, { unit: \"day\", source: \"dateMark\", locale, weekday: options.weekday ?? false });\n if (day) return options.time && !bareDay ? `${day}, ${clockTime(date, locale, timeZone)}` : day;\n }\n return formatDate(value, options.style ?? (options.time ? \"dateTime\" : \"medium\"), { locale, timeZone });\n },\n [prop, fromProvider, locale, timeZone],\n );\n}\n"],"mappings":";AAAA,SAAS,mBAAmB;AAC5B,SAAS,qBAAqB,cAAc,sBAAsB;AAClE,SAAS,kBAAkB;AAC3B,SAAS,iBAAiB;AAC1B,SAAS,uBAAuB,YAAY,cAAoD;AAChG,SAAS,qBAAqB;AAC9B,SAAS,oBAAoB;AA6C7B,SAAS,SAAS,MAAY,UAAsC;AAClE,MAAI,CAAC,SAAU,QAAO,WAAW,IAAI;AACrC,MAAI;AAEF,UAAM,QAAQ,cAAc,SAAS,EAAE,UAAU,MAAM,WAAW,OAAO,WAAW,KAAK,UAAU,CAAC,EAAE;AAAA,MACpG;AAAA,IACF;AACA,UAAM,OAAO,CAAC,SAAuC,MAAM,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI,GAAG,SAAS;AAClG,WAAO,GAAG,KAAK,MAAM,CAAC,IAAI,KAAK,OAAO,CAAC,IAAI,KAAK,KAAK,CAAC;AAAA,EACxD,QAAQ;AACN,WAAO,WAAW,IAAI;AAAA,EACxB;AACF;AAQO,SAAS,gBACd,MACA,YACiB;AACjB,QAAM,SAAS,aAAa,UAAU;AACtC,QAAM,WAAW,eAAe;AAChC,QAAM,eAAe,oBAAoB;AACzC,SAAO;AAAA,IACL,CAAC,OAAO,UAAU,CAAC,MAAM;AACvB,YAAM,MAAM,OAAO,KAAK;AACxB,UAAI,IAAK,QAAO;AAChB,UAAI,UAAU,QAAQ,UAAU,UAAa,UAAU,GAAI,QAAO,QAAQ,SAAS;AACnF,YAAM,OAAO,OAAO,KAAK;AACzB,UAAI,CAAC,KAAM,QAAO,OAAO,KAAK;AAC9B,YAAM,UAAU,aAAa,KAAK;AAClC,UAAI,gBAAgB,QAAQ,eAAe,OAAO;AAChD,cAAM,MAAM,UAAU,QAAQ,SAAS,MAAM,QAAQ;AACrD,cAAM,MAAM,aAAa,KAAK,EAAE,MAAM,OAAO,QAAQ,YAAY,QAAQ,SAAS,QAAQ,WAAW,MAAM,CAAC;AAC5G,YAAI,IAAK,QAAO,QAAQ,QAAQ,CAAC,UAAU,GAAG,GAAG,KAAK,UAAU,MAAM,QAAQ,QAAQ,CAAC,KAAK;AAAA,MAC9F;AACA,aAAO,WAAW,OAAO,QAAQ,UAAU,QAAQ,OAAO,aAAa,WAAW,EAAE,QAAQ,SAAS,CAAC;AAAA,IACxG;AAAA,IACA,CAAC,MAAM,cAAc,QAAQ,QAAQ;AAAA,EACvC;AACF;","names":[]}
@@ -11,7 +11,13 @@ export declare function decimalMark(locale: string | undefined): "," | ".";
11
11
  * field's settle then made that 1.23, final. This decides per operand, from the
12
12
  * typist's own text, before that fold:
13
13
  *
14
- * - both marks present: the one that is not the locale's is grouping, and goes;
14
+ * - both marks present: the LAST one is the decimal mark, whichever the locale's is, and
15
+ * the other is grouping and goes. The order says the convention the figure was written
16
+ * in — "1.234,56" German, "1,234.56" English — and a figure pasted from another app or
17
+ * locale keeps it. Taking the non-locale mark as grouping wherever it stood read
18
+ * "1.234,56" in a "." locale (en, de-CH) as 1.23456, which a money field then settled
19
+ * to 1.23 (refactor audit 2026-10-30, INP-3). Typed in the locale's own convention, its
20
+ * mark comes last and nothing changes;
15
21
  * - in a "," locale, only "." present: grouping when it cannot be a decimal as a
16
22
  * German reader writes it — twice ("1.234.567"), or in thousands shape ("1.234",
17
23
  * "12.500") — otherwise a decimal ("1.5", typed by a dot-decimal habit). A first
@@ -11,10 +11,13 @@ function decimalMark(locale) {
11
11
  function normalizeTypedMarks(raw, mark) {
12
12
  const other = mark === "," ? "." : ",";
13
13
  return raw.replace(/[0-9.,]+/g, (operand) => {
14
- const hasMark = operand.includes(mark);
15
14
  const count = operand.split(other).length - 1;
16
15
  if (count === 0) return operand;
17
- const grouping = hasMark ? true : count > 1 || mark === "," && /^[1-9]\d{0,2}(\.\d{3})+$/.test(operand);
16
+ if (operand.includes(mark)) {
17
+ const grouping2 = operand.lastIndexOf(mark) > operand.lastIndexOf(other) ? other : mark;
18
+ return operand.split(grouping2).join("");
19
+ }
20
+ const grouping = count > 1 || mark === "," && /^[1-9]\d{0,2}(\.\d{3})+$/.test(operand);
18
21
  return grouping ? operand.split(other).join("") : operand;
19
22
  });
20
23
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/decimal-marks.ts"],"sourcesContent":["/**\n * The locale's decimal mark and the reading of typed grouping marks, shared by the\n * kit's numeric text fields: {@link AmountInput}, {@link NumberInput} and, through\n * NumberInput, {@link NumberField} and {@link MoneyField}.\n *\n * Package-private — not re-exported from the barrel. It lives here rather than in\n * `calc.ts` because that module is re-exported wholesale (`export * from\n * \"./lib/calc\"`), and these are the fields' plumbing, not API.\n *\n * WHY ONE MODULE. AmountInput learned the locale's mark (kastlan 40) and the grouping\n * parse (keksdose G1); NumberInput learned neither, so the same \"1.234,56\" a German\n * user typed into a money field read as 1234.56 and, typed into the budget-goal field\n * next to it, as 1.23456 (keksdose K5). Two private copies of the same rule are how\n * that happened, so the rule now has one home.\n */\nimport { sanitizeLive } from \"./calc\";\n\n/**\n * The locale's decimal mark, as far as a field can type it: \",\" or \".\" (kastlan 40).\n * A mark outside those two (Arabic's \"٫\") is not one {@link sanitizeLive} lets\n * through, so those locales keep the dot rather than get a field that eats the key it\n * shows. `undefined` is the runtime's own locale, which is what `Intl` reads it as.\n */\nexport function decimalMark(locale: string | undefined): \",\" | \".\" {\n try {\n const part = new Intl.NumberFormat(locale).formatToParts(1.5).find((p) => p.type === \"decimal\");\n return part?.value === \",\" ? \",\" : \".\";\n } catch {\n // An invalid tag throws a RangeError; a typo in a locale must not take the form down.\n return \".\";\n }\n}\n\n/**\n * Which typed mark is the decimal one (keksdose G1). {@link sanitizeLive} folds every\n * \",\" into \".\", so on its own it read the German \"1.234,56\" as 1.23456 — and a money\n * field's settle then made that 1.23, final. This decides per operand, from the\n * typist's own text, before that fold:\n *\n * - both marks present: the one that is not the locale's is grouping, and goes;\n * - in a \",\" locale, only \".\" present: grouping when it cannot be a decimal as a\n * German reader writes it — twice (\"1.234.567\"), or in thousands shape (\"1.234\",\n * \"12.500\") — otherwise a decimal (\"1.5\", typed by a dot-decimal habit). A first\n * group that starts with 0 is never thousands: \"0.500\" is a half, not five hundred\n * (BUG-6, refactor audit 2026-10);\n * - in a \".\" locale, only \",\" present: grouping when it appears twice (\"1,234,567\");\n * once it stays a decimal, because Swiss and German typists write \"1,5\" in a\n * de-CH form (kastlan 40) and a unit price \"1,789\" is a decimal.\n *\n * Only a keystroke's text comes through here. A calculator result is dot-decimal\n * already, and \"1.234\" from `100/81.03…` is never a thousand.\n */\nexport function normalizeTypedMarks(raw: string, mark: \",\" | \".\"): string {\n const other = mark === \",\" ? \".\" : \",\";\n return raw.replace(/[0-9.,]+/g, (operand) => {\n const hasMark = operand.includes(mark);\n const count = operand.split(other).length - 1;\n if (count === 0) return operand;\n const grouping = hasMark ? true : count > 1 || (mark === \",\" && /^[1-9]\\d{0,2}(\\.\\d{3})+$/.test(operand));\n return grouping ? operand.split(other).join(\"\") : operand;\n });\n}\n\n/** What a keystroke's raw text reports: grouping read off, then the field's live\n * sanitising (comma → dot, stray characters out, one decimal point per operand). */\nexport function readTyped(raw: string, mark: \",\" | \".\"): string {\n return sanitizeLive(normalizeTypedMarks(raw, mark));\n}\n\n// Spacing a typist uses to group digits — a space, the no-break and narrow no-break\n// spaces `Intl` itself writes for fr-CH (\"1 234,5\" with U+202F), and the Swiss\n// apostrophes (\"1'234.50\"). The live sanitising drops them, so they never reach the\n// value; the draft may still show them.\nconst GROUP_SPACING = /[\\s'’\\u00a0\\u202f]/g;\n\n/**\n * Whether the field may keep SHOWING `raw` — the typist's own text — while it holds\n * `canonical` (dot-decimal, as `onChange` reported it).\n *\n * Showing the typed text rather than re-spelling the value is what keeps the caret\n * where the typist put it, and what keeps a grouping \".\" from being turned into a \",\"\n * under their fingers — after which no rule could tell the two apart (keksdose G1).\n * But only text that says the same as the value, and says it with nothing the field\n * would have DROPPED: a letter, or a second decimal point in one operand (\"1.2.3\"),\n * must not stay on screen while the value says \"1.23\" (Keksdose live #201 is the bug\n * that showing it would bring back). Grouping marks and grouping spaces may stay.\n */\nexport function showsTyped(raw: string, mark: \",\" | \".\", canonical: string): boolean {\n const read = normalizeTypedMarks(raw, mark).replace(GROUP_SPACING, \"\");\n const sanitized = sanitizeLive(read);\n // Nothing dropped: the read text is the sanitised one, but for which mark it uses.\n return sanitized === read.replace(/,/g, \".\") && sanitized === sanitizeLive(canonical);\n}\n\n/** A dot-decimal text spelt in `mark` — display only, one character for one, so a\n * re-spelling never moves a caret. */\nexport function localizeMark(text: string, mark: \",\" | \".\"): string {\n return mark === \",\" ? text.replace(/\\./g, \",\") : text;\n}\n"],"mappings":";AAeA,SAAS,oBAAoB;AAQtB,SAAS,YAAY,QAAuC;AACjE,MAAI;AACF,UAAM,OAAO,IAAI,KAAK,aAAa,MAAM,EAAE,cAAc,GAAG,EAAE,KAAK,CAAC,MAAM,EAAE,SAAS,SAAS;AAC9F,WAAO,MAAM,UAAU,MAAM,MAAM;AAAA,EACrC,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AAqBO,SAAS,oBAAoB,KAAa,MAAyB;AACxE,QAAM,QAAQ,SAAS,MAAM,MAAM;AACnC,SAAO,IAAI,QAAQ,aAAa,CAAC,YAAY;AAC3C,UAAM,UAAU,QAAQ,SAAS,IAAI;AACrC,UAAM,QAAQ,QAAQ,MAAM,KAAK,EAAE,SAAS;AAC5C,QAAI,UAAU,EAAG,QAAO;AACxB,UAAM,WAAW,UAAU,OAAO,QAAQ,KAAM,SAAS,OAAO,2BAA2B,KAAK,OAAO;AACvG,WAAO,WAAW,QAAQ,MAAM,KAAK,EAAE,KAAK,EAAE,IAAI;AAAA,EACpD,CAAC;AACH;AAIO,SAAS,UAAU,KAAa,MAAyB;AAC9D,SAAO,aAAa,oBAAoB,KAAK,IAAI,CAAC;AACpD;AAMA,MAAM,gBAAgB;AAcf,SAAS,WAAW,KAAa,MAAiB,WAA4B;AACnF,QAAM,OAAO,oBAAoB,KAAK,IAAI,EAAE,QAAQ,eAAe,EAAE;AACrE,QAAM,YAAY,aAAa,IAAI;AAEnC,SAAO,cAAc,KAAK,QAAQ,MAAM,GAAG,KAAK,cAAc,aAAa,SAAS;AACtF;AAIO,SAAS,aAAa,MAAc,MAAyB;AAClE,SAAO,SAAS,MAAM,KAAK,QAAQ,OAAO,GAAG,IAAI;AACnD;","names":[]}
1
+ {"version":3,"sources":["../../src/lib/decimal-marks.ts"],"sourcesContent":["/**\n * The locale's decimal mark and the reading of typed grouping marks, shared by the\n * kit's numeric text fields: {@link AmountInput}, {@link NumberInput} and, through\n * NumberInput, {@link NumberField} and {@link MoneyField}.\n *\n * Package-private — not re-exported from the barrel. It lives here rather than in\n * `calc.ts` because that module is re-exported wholesale (`export * from\n * \"./lib/calc\"`), and these are the fields' plumbing, not API.\n *\n * WHY ONE MODULE. AmountInput learned the locale's mark (kastlan 40) and the grouping\n * parse (keksdose G1); NumberInput learned neither, so the same \"1.234,56\" a German\n * user typed into a money field read as 1234.56 and, typed into the budget-goal field\n * next to it, as 1.23456 (keksdose K5). Two private copies of the same rule are how\n * that happened, so the rule now has one home.\n */\nimport { sanitizeLive } from \"./calc\";\n\n/**\n * The locale's decimal mark, as far as a field can type it: \",\" or \".\" (kastlan 40).\n * A mark outside those two (Arabic's \"٫\") is not one {@link sanitizeLive} lets\n * through, so those locales keep the dot rather than get a field that eats the key it\n * shows. `undefined` is the runtime's own locale, which is what `Intl` reads it as.\n */\nexport function decimalMark(locale: string | undefined): \",\" | \".\" {\n try {\n const part = new Intl.NumberFormat(locale).formatToParts(1.5).find((p) => p.type === \"decimal\");\n return part?.value === \",\" ? \",\" : \".\";\n } catch {\n // An invalid tag throws a RangeError; a typo in a locale must not take the form down.\n return \".\";\n }\n}\n\n/**\n * Which typed mark is the decimal one (keksdose G1). {@link sanitizeLive} folds every\n * \",\" into \".\", so on its own it read the German \"1.234,56\" as 1.23456 — and a money\n * field's settle then made that 1.23, final. This decides per operand, from the\n * typist's own text, before that fold:\n *\n * - both marks present: the LAST one is the decimal mark, whichever the locale's is, and\n * the other is grouping and goes. The order says the convention the figure was written\n * in — \"1.234,56\" German, \"1,234.56\" English — and a figure pasted from another app or\n * locale keeps it. Taking the non-locale mark as grouping wherever it stood read\n * \"1.234,56\" in a \".\" locale (en, de-CH) as 1.23456, which a money field then settled\n * to 1.23 (refactor audit 2026-10-30, INP-3). Typed in the locale's own convention, its\n * mark comes last and nothing changes;\n * - in a \",\" locale, only \".\" present: grouping when it cannot be a decimal as a\n * German reader writes it — twice (\"1.234.567\"), or in thousands shape (\"1.234\",\n * \"12.500\") — otherwise a decimal (\"1.5\", typed by a dot-decimal habit). A first\n * group that starts with 0 is never thousands: \"0.500\" is a half, not five hundred\n * (BUG-6, refactor audit 2026-10);\n * - in a \".\" locale, only \",\" present: grouping when it appears twice (\"1,234,567\");\n * once it stays a decimal, because Swiss and German typists write \"1,5\" in a\n * de-CH form (kastlan 40) and a unit price \"1,789\" is a decimal.\n *\n * Only a keystroke's text comes through here. A calculator result is dot-decimal\n * already, and \"1.234\" from `100/81.03…` is never a thousand.\n */\nexport function normalizeTypedMarks(raw: string, mark: \",\" | \".\"): string {\n const other = mark === \",\" ? \".\" : \",\";\n return raw.replace(/[0-9.,]+/g, (operand) => {\n const count = operand.split(other).length - 1;\n if (count === 0) return operand;\n if (operand.includes(mark)) {\n const grouping = operand.lastIndexOf(mark) > operand.lastIndexOf(other) ? other : mark;\n return operand.split(grouping).join(\"\");\n }\n const grouping = count > 1 || (mark === \",\" && /^[1-9]\\d{0,2}(\\.\\d{3})+$/.test(operand));\n return grouping ? operand.split(other).join(\"\") : operand;\n });\n}\n\n/** What a keystroke's raw text reports: grouping read off, then the field's live\n * sanitising (comma → dot, stray characters out, one decimal point per operand). */\nexport function readTyped(raw: string, mark: \",\" | \".\"): string {\n return sanitizeLive(normalizeTypedMarks(raw, mark));\n}\n\n// Spacing a typist uses to group digits — a space, the no-break and narrow no-break\n// spaces `Intl` itself writes for fr-CH (\"1 234,5\" with U+202F), and the Swiss\n// apostrophes (\"1'234.50\"). The live sanitising drops them, so they never reach the\n// value; the draft may still show them.\nconst GROUP_SPACING = /[\\s'’\\u00a0\\u202f]/g;\n\n/**\n * Whether the field may keep SHOWING `raw` — the typist's own text — while it holds\n * `canonical` (dot-decimal, as `onChange` reported it).\n *\n * Showing the typed text rather than re-spelling the value is what keeps the caret\n * where the typist put it, and what keeps a grouping \".\" from being turned into a \",\"\n * under their fingers — after which no rule could tell the two apart (keksdose G1).\n * But only text that says the same as the value, and says it with nothing the field\n * would have DROPPED: a letter, or a second decimal point in one operand (\"1.2.3\"),\n * must not stay on screen while the value says \"1.23\" (Keksdose live #201 is the bug\n * that showing it would bring back). Grouping marks and grouping spaces may stay.\n */\nexport function showsTyped(raw: string, mark: \",\" | \".\", canonical: string): boolean {\n const read = normalizeTypedMarks(raw, mark).replace(GROUP_SPACING, \"\");\n const sanitized = sanitizeLive(read);\n // Nothing dropped: the read text is the sanitised one, but for which mark it uses.\n return sanitized === read.replace(/,/g, \".\") && sanitized === sanitizeLive(canonical);\n}\n\n/** A dot-decimal text spelt in `mark` — display only, one character for one, so a\n * re-spelling never moves a caret. */\nexport function localizeMark(text: string, mark: \",\" | \".\"): string {\n return mark === \",\" ? text.replace(/\\./g, \",\") : text;\n}\n"],"mappings":";AAeA,SAAS,oBAAoB;AAQtB,SAAS,YAAY,QAAuC;AACjE,MAAI;AACF,UAAM,OAAO,IAAI,KAAK,aAAa,MAAM,EAAE,cAAc,GAAG,EAAE,KAAK,CAAC,MAAM,EAAE,SAAS,SAAS;AAC9F,WAAO,MAAM,UAAU,MAAM,MAAM;AAAA,EACrC,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AA2BO,SAAS,oBAAoB,KAAa,MAAyB;AACxE,QAAM,QAAQ,SAAS,MAAM,MAAM;AACnC,SAAO,IAAI,QAAQ,aAAa,CAAC,YAAY;AAC3C,UAAM,QAAQ,QAAQ,MAAM,KAAK,EAAE,SAAS;AAC5C,QAAI,UAAU,EAAG,QAAO;AACxB,QAAI,QAAQ,SAAS,IAAI,GAAG;AAC1B,YAAMA,YAAW,QAAQ,YAAY,IAAI,IAAI,QAAQ,YAAY,KAAK,IAAI,QAAQ;AAClF,aAAO,QAAQ,MAAMA,SAAQ,EAAE,KAAK,EAAE;AAAA,IACxC;AACA,UAAM,WAAW,QAAQ,KAAM,SAAS,OAAO,2BAA2B,KAAK,OAAO;AACtF,WAAO,WAAW,QAAQ,MAAM,KAAK,EAAE,KAAK,EAAE,IAAI;AAAA,EACpD,CAAC;AACH;AAIO,SAAS,UAAU,KAAa,MAAyB;AAC9D,SAAO,aAAa,oBAAoB,KAAK,IAAI,CAAC;AACpD;AAMA,MAAM,gBAAgB;AAcf,SAAS,WAAW,KAAa,MAAiB,WAA4B;AACnF,QAAM,OAAO,oBAAoB,KAAK,IAAI,EAAE,QAAQ,eAAe,EAAE;AACrE,QAAM,YAAY,aAAa,IAAI;AAEnC,SAAO,cAAc,KAAK,QAAQ,MAAM,GAAG,KAAK,cAAc,aAAa,SAAS;AACtF;AAIO,SAAS,aAAa,MAAc,MAAyB;AAClE,SAAO,SAAS,MAAM,KAAK,QAAQ,OAAO,GAAG,IAAI;AACnD;","names":["grouping"]}
@@ -0,0 +1,14 @@
1
+ import type { ReactNode } from "react";
2
+ /**
3
+ * The failure ladder, once: the app's words for a failure when it has any, else the
4
+ * kit's `fallback`.
5
+ *
6
+ * "Any" is {@link hasContent}, so an app's `describeError` answering `""` (nothing to say
7
+ * about this one) gets the kit's sentence. Written as `own ?? fallback` in the auth forms,
8
+ * where `""` printed an empty alert, and both ways within one file in the reviewer scope
9
+ * editor; it lived in the notification cards' parts only (refactor audit 2026-10-30,
10
+ * DUP-14).
11
+ *
12
+ * @internal Package-private — not re-exported from the barrel.
13
+ */
14
+ export declare function describeFailure(own: ReactNode | undefined, fallback: ReactNode): ReactNode;
@@ -0,0 +1,9 @@
1
+ "use client";
2
+ import { hasContent } from "./presence.js";
3
+ function describeFailure(own, fallback) {
4
+ return hasContent(own) ? own : fallback;
5
+ }
6
+ export {
7
+ describeFailure
8
+ };
9
+ //# sourceMappingURL=describe-failure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/describe-failure.ts"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { hasContent } from \"./presence\";\n\n/**\n * The failure ladder, once: the app's words for a failure when it has any, else the\n * kit's `fallback`.\n *\n * \"Any\" is {@link hasContent}, so an app's `describeError` answering `\"\"` (nothing to say\n * about this one) gets the kit's sentence. Written as `own ?? fallback` in the auth forms,\n * where `\"\"` printed an empty alert, and both ways within one file in the reviewer scope\n * editor; it lived in the notification cards' parts only (refactor audit 2026-10-30,\n * DUP-14).\n *\n * @internal Package-private — not re-exported from the barrel.\n */\nexport function describeFailure(own: ReactNode | undefined, fallback: ReactNode): ReactNode {\n return hasContent(own) ? own : fallback;\n}\n"],"mappings":";AACA,SAAS,kBAAkB;AAcpB,SAAS,gBAAgB,KAA4B,UAAgC;AAC1F,SAAO,WAAW,GAAG,IAAI,MAAM;AACjC;","names":[]}
@@ -20,3 +20,19 @@ export declare function isRtl(el: Element | null | undefined): boolean;
20
20
  * any other key. ArrowRight is forward in LTR and backward in RTL.
21
21
  */
22
22
  export declare function horizontalStep(key: string, el: Element | null | undefined): 1 | -1 | 0;
23
+ /**
24
+ * Where a key moves the focus in a list with ONE tab stop (a roving tabindex): the index
25
+ * to focus from `at` among `count` items, or `undefined` for a key that moves nothing.
26
+ * ←/→ along `dir` — → is next in `ltr` and previous in `rtl` — ↓/↑ as next and previous,
27
+ * Home and End to the ends. With `wrap`, past the last is the first and before the first
28
+ * the last (a ring: PieChart's slices); without, the ends hold (a plane: Treemap's tiles).
29
+ *
30
+ * The charts each mapped the keys themselves, and differently at the ends (0.52 code
31
+ * review of treemap.tsx, finding 9); the end rule is now a word at the call.
32
+ *
33
+ * `dir` and not an element to read it off (0.52 code review, deep, charts, findings 1 and
34
+ * 14): a list in the page's flow takes the page's (`dirOf(el)`), but a plot is laid out
35
+ * left to right in any page (see `ChartContainer`) and passes `"ltr"` — read off the
36
+ * element, Treemap's → walked an RTL page's tiles backwards, against the picture.
37
+ */
38
+ export declare function rovingTarget(key: string, dir: Direction, at: number, count: number, wrap: boolean): number | undefined;
@@ -11,9 +11,17 @@ function horizontalStep(key, el) {
11
11
  const forward = key === "ArrowRight" !== isRtl(el);
12
12
  return forward ? 1 : -1;
13
13
  }
14
+ function rovingTarget(key, dir, at, count, wrap) {
15
+ const physical = key === "ArrowRight" ? 1 : key === "ArrowLeft" ? -1 : 0;
16
+ const step = dir === "rtl" ? -physical : physical;
17
+ const to = step ? at + step : key === "ArrowDown" ? at + 1 : key === "ArrowUp" ? at - 1 : key === "Home" ? 0 : key === "End" ? count - 1 : void 0;
18
+ if (to === void 0 || count <= 0) return void 0;
19
+ return wrap ? (to % count + count) % count : Math.max(0, Math.min(count - 1, to));
20
+ }
14
21
  export {
15
22
  dirOf,
16
23
  horizontalStep,
17
- isRtl
24
+ isRtl,
25
+ rovingTarget
18
26
  };
19
27
  //# sourceMappingURL=direction.js.map