@eifi1/ui-kit 0.51.7 → 0.53.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 (775) hide show
  1. package/README.md +178 -67
  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 +1 -3
  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.js +2 -1
  39. package/dist/auth/complete-name-dialog.js.map +1 -1
  40. package/dist/auth/forgot-password-form-labels.js +1 -1
  41. package/dist/auth/forgot-password-form-labels.js.map +1 -1
  42. package/dist/auth/forgot-password-form.js +7 -2
  43. package/dist/auth/forgot-password-form.js.map +1 -1
  44. package/dist/auth/register-form-labels.js +1 -1
  45. package/dist/auth/register-form-labels.js.map +1 -1
  46. package/dist/auth/reset-password-form-labels.js +1 -1
  47. package/dist/auth/reset-password-form-labels.js.map +1 -1
  48. package/dist/auth/reset-password-form.js +5 -1
  49. package/dist/auth/reset-password-form.js.map +1 -1
  50. package/dist/auth/sign-in-form-labels.js +1 -1
  51. package/dist/auth/sign-in-form-labels.js.map +1 -1
  52. package/dist/auth/status-parts.d.ts +0 -3
  53. package/dist/auth/status-parts.js +0 -4
  54. package/dist/auth/status-parts.js.map +1 -1
  55. package/dist/auth/verify-email-labels.js +1 -1
  56. package/dist/auth/verify-email-labels.js.map +1 -1
  57. package/dist/auth/verify-email.js +7 -2
  58. package/dist/auth/verify-email.js.map +1 -1
  59. package/dist/billing/cancel-subscription.d.ts +10 -7
  60. package/dist/billing/cancel-subscription.js +6 -6
  61. package/dist/billing/cancel-subscription.js.map +1 -1
  62. package/dist/billing/plan-card.d.ts +5 -3
  63. package/dist/billing/plan-card.js +7 -8
  64. package/dist/billing/plan-card.js.map +1 -1
  65. package/dist/billing/plan-change-labels.d.ts +42 -0
  66. package/dist/billing/plan-change-labels.js +22 -0
  67. package/dist/billing/plan-change-labels.js.map +1 -0
  68. package/dist/billing/plan-switch.d.ts +9 -6
  69. package/dist/billing/plan-switch.js +7 -5
  70. package/dist/billing/plan-switch.js.map +1 -1
  71. package/dist/chart.d.ts +4 -3
  72. package/dist/chart.js +72 -4
  73. package/dist/chart.js.map +1 -1
  74. package/dist/components/account-chips.d.ts +5 -5
  75. package/dist/components/account-chips.js +11 -13
  76. package/dist/components/account-chips.js.map +1 -1
  77. package/dist/components/alert-banner.d.ts +16 -5
  78. package/dist/components/alert-banner.js +46 -1
  79. package/dist/components/alert-banner.js.map +1 -1
  80. package/dist/components/amount-input.d.ts +13 -0
  81. package/dist/components/amount-input.js +1 -0
  82. package/dist/components/amount-input.js.map +1 -1
  83. package/dist/components/autocomplete.js +57 -65
  84. package/dist/components/autocomplete.js.map +1 -1
  85. package/dist/components/bulk-action-bar.d.ts +12 -2
  86. package/dist/components/bulk-action-bar.js +65 -21
  87. package/dist/components/bulk-action-bar.js.map +1 -1
  88. package/dist/components/button-look.d.ts +5 -5
  89. package/dist/components/button-look.js.map +1 -1
  90. package/dist/components/button.d.ts +2 -2
  91. package/dist/components/button.js.map +1 -1
  92. package/dist/components/chart-edge.d.ts +16 -0
  93. package/dist/components/chart-edge.js +14 -1
  94. package/dist/components/chart-edge.js.map +1 -1
  95. package/dist/components/chart-figure.d.ts +13 -0
  96. package/dist/components/chart-figure.js +15 -0
  97. package/dist/components/chart-figure.js.map +1 -0
  98. package/dist/components/chart-zoom-core.js +1 -4
  99. package/dist/components/chart-zoom-core.js.map +1 -1
  100. package/dist/components/chart.d.ts +8 -1
  101. package/dist/components/chart.js +19 -10
  102. package/dist/components/chart.js.map +1 -1
  103. package/dist/components/combobox-core.d.ts +111 -2
  104. package/dist/components/combobox-core.js +148 -42
  105. package/dist/components/combobox-core.js.map +1 -1
  106. package/dist/components/combobox.d.ts +1 -1
  107. package/dist/components/combobox.js +141 -130
  108. package/dist/components/combobox.js.map +1 -1
  109. package/dist/components/copy-button.d.ts +6 -5
  110. package/dist/components/copy-button.js +5 -3
  111. package/dist/components/copy-button.js.map +1 -1
  112. package/dist/components/country-select.js +2 -2
  113. package/dist/components/country-select.js.map +1 -1
  114. package/dist/components/data-table-column-settings.d.ts +1 -1
  115. package/dist/components/data-table-column-settings.js +33 -7
  116. package/dist/components/data-table-column-settings.js.map +1 -1
  117. package/dist/components/data-table-filters.d.ts +4 -2
  118. package/dist/components/data-table-filters.js +2 -2
  119. package/dist/components/data-table-filters.js.map +1 -1
  120. package/dist/components/data-table-fit.d.ts +6 -4
  121. package/dist/components/data-table-fit.js +4 -1
  122. package/dist/components/data-table-fit.js.map +1 -1
  123. package/dist/components/data-table-groups.d.ts +1 -1
  124. package/dist/components/data-table-groups.js.map +1 -1
  125. package/dist/components/data-table-head.d.ts +1 -1
  126. package/dist/components/data-table-head.js +15 -12
  127. package/dist/components/data-table-head.js.map +1 -1
  128. package/dist/components/data-table-mobile.d.ts +12 -7
  129. package/dist/components/data-table-mobile.js +43 -30
  130. package/dist/components/data-table-mobile.js.map +1 -1
  131. package/dist/components/data-table-pagination.js +11 -6
  132. package/dist/components/data-table-pagination.js.map +1 -1
  133. package/dist/components/data-table-resize.d.ts +1 -1
  134. package/dist/components/data-table-resize.js.map +1 -1
  135. package/dist/components/data-table-row-actions.d.ts +2 -2
  136. package/dist/components/data-table-row-actions.js.map +1 -1
  137. package/dist/components/data-table-row.d.ts +6 -2
  138. package/dist/components/data-table-row.js +17 -10
  139. package/dist/components/data-table-row.js.map +1 -1
  140. package/dist/components/data-table-selection.d.ts +1 -1
  141. package/dist/components/data-table-selection.js.map +1 -1
  142. package/dist/components/data-table-shared.d.ts +10 -2
  143. package/dist/components/data-table-shared.js +18 -0
  144. package/dist/components/data-table-shared.js.map +1 -1
  145. package/dist/components/data-table-sort.d.ts +21 -2
  146. package/dist/components/data-table-sort.js +24 -0
  147. package/dist/components/data-table-sort.js.map +1 -1
  148. package/dist/components/data-table-virtual.d.ts +1 -1
  149. package/dist/components/data-table-virtual.js +18 -6
  150. package/dist/components/data-table-virtual.js.map +1 -1
  151. package/dist/components/data-table.d.ts +13 -4
  152. package/dist/components/data-table.js +139 -36
  153. package/dist/components/data-table.js.map +1 -1
  154. package/dist/components/date-field.d.ts +25 -3
  155. package/dist/components/date-field.js +33 -8
  156. package/dist/components/date-field.js.map +1 -1
  157. package/dist/components/date-picker.d.ts +16 -3
  158. package/dist/components/date-picker.js +55 -26
  159. package/dist/components/date-picker.js.map +1 -1
  160. package/dist/components/date-range-picker.d.ts +38 -24
  161. package/dist/components/date-range-picker.js +23 -7
  162. package/dist/components/date-range-picker.js.map +1 -1
  163. package/dist/components/error-boundary.js +37 -30
  164. package/dist/components/error-boundary.js.map +1 -1
  165. package/dist/components/{ui-labels.d.ts → field-base-labels.d.ts} +0 -2
  166. package/dist/components/{ui-labels.js → field-base-labels.js} +2 -9
  167. package/dist/components/field-base-labels.js.map +1 -0
  168. package/dist/components/field-base.d.ts +4 -3
  169. package/dist/components/field-base.js +3 -3
  170. package/dist/components/field-base.js.map +1 -1
  171. package/dist/components/form-actions.js +11 -5
  172. package/dist/components/form-actions.js.map +1 -1
  173. package/dist/components/hover-menu.d.ts +3 -0
  174. package/dist/components/hover-menu.js +5 -3
  175. package/dist/components/hover-menu.js.map +1 -1
  176. package/dist/components/icon-button.js +14 -9
  177. package/dist/components/icon-button.js.map +1 -1
  178. package/dist/components/inline-confirm.d.ts +43 -0
  179. package/dist/components/inline-confirm.js +67 -0
  180. package/dist/components/inline-confirm.js.map +1 -0
  181. package/dist/components/inline-entity-combobox.d.ts +4 -3
  182. package/dist/components/inline-entity-combobox.js +148 -140
  183. package/dist/components/inline-entity-combobox.js.map +1 -1
  184. package/dist/components/input.js +1 -1
  185. package/dist/components/input.js.map +1 -1
  186. package/dist/components/jump-calendar.js +28 -112
  187. package/dist/components/jump-calendar.js.map +1 -1
  188. package/dist/components/legal-page.d.ts +3 -1
  189. package/dist/components/legal-page.js +3 -2
  190. package/dist/components/legal-page.js.map +1 -1
  191. package/dist/components/legal.d.ts +4 -2
  192. package/dist/components/legal.js.map +1 -1
  193. package/dist/components/lightbox.d.ts +2 -2
  194. package/dist/components/lightbox.js +17 -6
  195. package/dist/components/lightbox.js.map +1 -1
  196. package/dist/components/list.d.ts +6 -2
  197. package/dist/components/list.js +3 -2
  198. package/dist/components/list.js.map +1 -1
  199. package/dist/components/measured-grid.d.ts +1 -1
  200. package/dist/components/measured-grid.js +2 -1
  201. package/dist/components/measured-grid.js.map +1 -1
  202. package/dist/components/mini-calendar.js +3 -5
  203. package/dist/components/mini-calendar.js.map +1 -1
  204. package/dist/components/modal.js +3 -1
  205. package/dist/components/modal.js.map +1 -1
  206. package/dist/components/month-grid.d.ts +55 -0
  207. package/dist/components/month-grid.js +148 -0
  208. package/dist/components/month-grid.js.map +1 -0
  209. package/dist/components/month-picker.d.ts +36 -4
  210. package/dist/components/month-picker.js +123 -146
  211. package/dist/components/month-picker.js.map +1 -1
  212. package/dist/components/multi-select.d.ts +2 -2
  213. package/dist/components/multi-select.js +11 -6
  214. package/dist/components/multi-select.js.map +1 -1
  215. package/dist/components/nav-pills.d.ts +1 -1
  216. package/dist/components/nav-pills.js.map +1 -1
  217. package/dist/components/picker-sheet.js +2 -1
  218. package/dist/components/picker-sheet.js.map +1 -1
  219. package/dist/components/pie-chart-labels.d.ts +7 -0
  220. package/dist/components/pie-chart-labels.js +1 -0
  221. package/dist/components/pie-chart-labels.js.map +1 -1
  222. package/dist/components/pie-chart.d.ts +5 -3
  223. package/dist/components/pie-chart.js +7 -22
  224. package/dist/components/pie-chart.js.map +1 -1
  225. package/dist/components/popover.d.ts +3 -0
  226. package/dist/components/popover.js +7 -1
  227. package/dist/components/popover.js.map +1 -1
  228. package/dist/components/row-actions.js +2 -1
  229. package/dist/components/row-actions.js.map +1 -1
  230. package/dist/components/series-chart-axes.d.ts +4 -4
  231. package/dist/components/series-chart-axes.js.map +1 -1
  232. package/dist/components/series-chart-labels.d.ts +7 -4
  233. package/dist/components/series-chart-labels.js.map +1 -1
  234. package/dist/components/series-chart.d.ts +92 -23
  235. package/dist/components/series-chart.js +74 -30
  236. package/dist/components/series-chart.js.map +1 -1
  237. package/dist/components/share-card.js +3 -3
  238. package/dist/components/share-card.js.map +1 -1
  239. package/dist/components/tabs-labels.d.ts +2 -0
  240. package/dist/components/tabs-labels.js +11 -0
  241. package/dist/components/tabs-labels.js.map +1 -0
  242. package/dist/components/tabs.d.ts +1 -1
  243. package/dist/components/tabs.js +1 -1
  244. package/dist/components/tabs.js.map +1 -1
  245. package/dist/components/toggle-legend.d.ts +2 -2
  246. package/dist/components/toggle-legend.js +6 -6
  247. package/dist/components/toggle-legend.js.map +1 -1
  248. package/dist/components/translation-review-editor.d.ts +4 -2
  249. package/dist/components/translation-review-editor.js +2 -3
  250. package/dist/components/translation-review-editor.js.map +1 -1
  251. package/dist/components/translation-review.d.ts +2 -1
  252. package/dist/components/translation-review.js +2 -2
  253. package/dist/components/translation-review.js.map +1 -1
  254. package/dist/components/treemap-labels.d.ts +31 -0
  255. package/dist/components/treemap-labels.js +10 -0
  256. package/dist/components/treemap-labels.js.map +1 -0
  257. package/dist/components/treemap-layout.d.ts +62 -0
  258. package/dist/components/treemap-layout.js +126 -0
  259. package/dist/components/treemap-layout.js.map +1 -0
  260. package/dist/components/treemap.d.ts +167 -19
  261. package/dist/components/treemap.js +340 -100
  262. package/dist/components/treemap.js.map +1 -1
  263. package/dist/components/use-table-state.d.ts +5 -5
  264. package/dist/components/use-table-state.js +4 -1
  265. package/dist/components/use-table-state.js.map +1 -1
  266. package/dist/data-table.d.ts +1 -2
  267. package/dist/data-table.js +1 -4
  268. package/dist/data-table.js.map +1 -1
  269. package/dist/demo/demo-countdown.d.ts +0 -5
  270. package/dist/demo/demo-countdown.js +0 -4
  271. package/dist/demo/demo-countdown.js.map +1 -1
  272. package/dist/eslint/index.js +1 -1
  273. package/dist/eslint/role-utilities.d.ts +36 -7
  274. package/dist/eslint/role-utilities.js +42 -9
  275. package/dist/eslint/role-utilities.js.map +1 -1
  276. package/dist/feedback/feedback-dialog.d.ts +25 -16
  277. package/dist/feedback/feedback-dialog.js +6 -2
  278. package/dist/feedback/feedback-dialog.js.map +1 -1
  279. package/dist/feedback/feedback-mutation.d.ts +18 -0
  280. package/dist/feedback/feedback-mutation.js +8 -0
  281. package/dist/feedback/feedback-mutation.js.map +1 -0
  282. package/dist/feedback/feedback-page-link.d.ts +53 -0
  283. package/dist/feedback/feedback-page-link.js +37 -0
  284. package/dist/feedback/feedback-page-link.js.map +1 -0
  285. package/dist/feedback/feedback-public-list.d.ts +8 -4
  286. package/dist/feedback/feedback-public-list.js +2 -2
  287. package/dist/feedback/feedback-public-list.js.map +1 -1
  288. package/dist/feedback/feedback-replies.d.ts +3 -1
  289. package/dist/feedback/feedback-replies.js +5 -4
  290. package/dist/feedback/feedback-replies.js.map +1 -1
  291. package/dist/feedback/feedback-row-detail-labels.js +0 -3
  292. package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
  293. package/dist/feedback/feedback-row-detail.d.ts +33 -44
  294. package/dist/feedback/feedback-row-detail.js +28 -34
  295. package/dist/feedback/feedback-row-detail.js.map +1 -1
  296. package/dist/feedback/feedback-status-undo.d.ts +17 -14
  297. package/dist/feedback/feedback-status-undo.js +10 -15
  298. package/dist/feedback/feedback-status-undo.js.map +1 -1
  299. package/dist/feedback/feedback-submit-dialog.d.ts +1 -1
  300. package/dist/feedback/feedback-submit-dialog.js +1 -1
  301. package/dist/feedback/feedback-submit-dialog.js.map +1 -1
  302. package/dist/feedback/feedback-submit.js +9 -3
  303. package/dist/feedback/feedback-submit.js.map +1 -1
  304. package/dist/feedback/feedback-swipe.d.ts +1 -1
  305. package/dist/feedback/feedback-swipe.js.map +1 -1
  306. package/dist/feedback/feedback-table.d.ts +21 -9
  307. package/dist/feedback/feedback-table.js +45 -25
  308. package/dist/feedback/feedback-table.js.map +1 -1
  309. package/dist/feedback/feedback-thread-labels.d.ts +2 -2
  310. package/dist/feedback/feedback-thread-labels.js.map +1 -1
  311. package/dist/feedback/feedback-thread.d.ts +15 -23
  312. package/dist/feedback/feedback-thread.js +2 -1
  313. package/dist/feedback/feedback-thread.js.map +1 -1
  314. package/dist/feedback.d.ts +6 -6
  315. package/dist/feedback.js +90 -6
  316. package/dist/feedback.js.map +1 -1
  317. package/dist/hooks/use-focus-trap.d.ts +35 -0
  318. package/dist/hooks/use-focus-trap.js +6 -4
  319. package/dist/hooks/use-focus-trap.js.map +1 -1
  320. package/dist/hooks/use-hotkey.d.ts +5 -2
  321. package/dist/hooks/use-hotkey.js +5 -4
  322. package/dist/hooks/use-hotkey.js.map +1 -1
  323. package/dist/hooks/use-long-press.d.ts +7 -4
  324. package/dist/hooks/use-long-press.js +3 -0
  325. package/dist/hooks/use-long-press.js.map +1 -1
  326. package/dist/hooks/use-media-query.js +12 -4
  327. package/dist/hooks/use-media-query.js.map +1 -1
  328. package/dist/hooks/use-mounted.d.ts +22 -0
  329. package/dist/hooks/use-mounted.js +24 -0
  330. package/dist/hooks/use-mounted.js.map +1 -0
  331. package/dist/hooks/use-overlay-history.js +19 -3
  332. package/dist/hooks/use-overlay-history.js.map +1 -1
  333. package/dist/i18n/defaults.js +4 -9
  334. package/dist/i18n/defaults.js.map +1 -1
  335. package/dist/i18n/german.js +7 -18
  336. package/dist/i18n/german.js.map +1 -1
  337. package/dist/i18n/kit-labels.d.ts +9 -11
  338. package/dist/i18n/kit-labels.js.map +1 -1
  339. package/dist/i18n/locales/en.js +15 -64
  340. package/dist/i18n/locales/en.js.map +3 -3
  341. package/dist/i18n/locales/es.js +7 -18
  342. package/dist/i18n/locales/es.js.map +1 -1
  343. package/dist/i18n/locales/fr.js +20 -27
  344. package/dist/i18n/locales/fr.js.map +1 -1
  345. package/dist/i18n/locales/hu.js +9 -18
  346. package/dist/i18n/locales/hu.js.map +1 -1
  347. package/dist/i18n/locales/it.js +7 -19
  348. package/dist/i18n/locales/it.js.map +1 -1
  349. package/dist/i18n/locales/zh.js +8 -18
  350. package/dist/i18n/locales/zh.js.map +1 -1
  351. package/dist/i18n/review.js +12 -2
  352. package/dist/i18n/review.js.map +1 -1
  353. package/dist/i18n/shared-labels.js +0 -1
  354. package/dist/i18n/shared-labels.js.map +1 -1
  355. package/dist/index.d.ts +21 -23
  356. package/dist/index.js +50 -65
  357. package/dist/index.js.map +1 -1
  358. package/dist/landing/routing.d.ts +3 -1
  359. package/dist/landing/routing.js +4 -7
  360. package/dist/landing/routing.js.map +1 -1
  361. package/dist/lib/broadcast.d.ts +17 -0
  362. package/dist/lib/broadcast.js +30 -0
  363. package/dist/lib/broadcast.js.map +1 -0
  364. package/dist/lib/date-format.d.ts +37 -2
  365. package/dist/lib/date-format.js +34 -8
  366. package/dist/lib/date-format.js.map +1 -1
  367. package/dist/lib/day-format.d.ts +46 -0
  368. package/dist/lib/day-format.js +46 -0
  369. package/dist/lib/day-format.js.map +1 -0
  370. package/dist/lib/describe-failure.d.ts +14 -0
  371. package/dist/lib/describe-failure.js +9 -0
  372. package/dist/lib/describe-failure.js.map +1 -0
  373. package/dist/lib/direction.d.ts +16 -0
  374. package/dist/lib/direction.js +9 -1
  375. package/dist/lib/direction.js.map +1 -1
  376. package/dist/lib/focus-left.d.ts +63 -0
  377. package/dist/lib/focus-left.js +67 -0
  378. package/dist/lib/focus-left.js.map +1 -0
  379. package/dist/lib/format.js +5 -14
  380. package/dist/lib/format.js.map +1 -1
  381. package/dist/lib/http-status.d.ts +24 -0
  382. package/dist/lib/http-status.js +14 -0
  383. package/dist/lib/http-status.js.map +1 -0
  384. package/dist/lib/intl-cache.d.ts +14 -0
  385. package/dist/lib/intl-cache.js +15 -0
  386. package/dist/lib/intl-cache.js.map +1 -0
  387. package/dist/lib/iso-day.d.ts +11 -0
  388. package/dist/lib/iso-day.js +8 -0
  389. package/dist/lib/iso-day.js.map +1 -0
  390. package/dist/lib/lazy-with-fallback.d.ts +31 -0
  391. package/dist/lib/lazy-with-fallback.js +40 -0
  392. package/dist/lib/lazy-with-fallback.js.map +1 -0
  393. package/dist/lib/path-pattern.d.ts +34 -0
  394. package/dist/lib/path-pattern.js +24 -0
  395. package/dist/lib/path-pattern.js.map +1 -0
  396. package/dist/lib/reduced-motion.d.ts +2 -1
  397. package/dist/lib/reduced-motion.js.map +1 -1
  398. package/dist/lib/round.d.ts +2 -1
  399. package/dist/lib/round.js +2 -1
  400. package/dist/lib/round.js.map +1 -1
  401. package/dist/lib/safe-storage.d.ts +24 -5
  402. package/dist/lib/safe-storage.js +27 -2
  403. package/dist/lib/safe-storage.js.map +1 -1
  404. package/dist/lib/session-refresh.js +1 -1
  405. package/dist/lib/session-refresh.js.map +1 -1
  406. package/dist/lib/share-format.d.ts +13 -0
  407. package/dist/lib/share-format.js +10 -0
  408. package/dist/lib/share-format.js.map +1 -0
  409. package/dist/lib/text-case.d.ts +17 -0
  410. package/dist/lib/text-case.js +8 -0
  411. package/dist/lib/text-case.js.map +1 -0
  412. package/dist/lib/text-match.d.ts +29 -0
  413. package/dist/lib/text-match.js +20 -2
  414. package/dist/lib/text-match.js.map +1 -1
  415. package/dist/lib/thenable.d.ts +13 -0
  416. package/dist/lib/thenable.js +8 -0
  417. package/dist/lib/thenable.js.map +1 -0
  418. package/dist/lib/translation-review.d.ts +7 -2
  419. package/dist/lib/translation-review.js +4 -4
  420. package/dist/lib/translation-review.js.map +1 -1
  421. package/dist/notifications/device-zone-report.js +1 -1
  422. package/dist/notifications/device-zone-report.js.map +1 -1
  423. package/dist/notifications/notification-bell.d.ts +2 -1
  424. package/dist/notifications/notification-bell.js +3 -3
  425. package/dist/notifications/notification-bell.js.map +1 -1
  426. package/dist/notifications/notification-inbox.d.ts +10 -6
  427. package/dist/notifications/notification-inbox.js +26 -48
  428. package/dist/notifications/notification-inbox.js.map +1 -1
  429. package/dist/notifications/notification-parts.d.ts +3 -2
  430. package/dist/notifications/notification-parts.js +1 -3
  431. package/dist/notifications/notification-parts.js.map +1 -1
  432. package/dist/notifications/notification-types-setting.js +1 -1
  433. package/dist/notifications/notification-types-setting.js.map +1 -1
  434. package/dist/notifications/notifications-labels.d.ts +6 -0
  435. package/dist/notifications/notifications-labels.js +1 -0
  436. package/dist/notifications/notifications-labels.js.map +1 -1
  437. package/dist/notifications/push-device-setting.js +1 -1
  438. package/dist/notifications/push-device-setting.js.map +1 -1
  439. package/dist/notifications/push-devices-setting.d.ts +2 -1
  440. package/dist/notifications/push-devices-setting.js +27 -49
  441. package/dist/notifications/push-devices-setting.js.map +1 -1
  442. package/dist/notifications/quiet-hours-setting.d.ts +10 -3
  443. package/dist/notifications/quiet-hours-setting.js +65 -26
  444. package/dist/notifications/quiet-hours-setting.js.map +1 -1
  445. package/dist/notifications/unsubscribe-panel.js +4 -7
  446. package/dist/notifications/unsubscribe-panel.js.map +1 -1
  447. package/dist/notifications/use-push-device.d.ts +39 -1
  448. package/dist/notifications/use-push-device.js +28 -7
  449. package/dist/notifications/use-push-device.js.map +1 -1
  450. package/dist/offline/query-persistence.d.ts +3 -1
  451. package/dist/offline/query-persistence.js +19 -2
  452. package/dist/offline/query-persistence.js.map +1 -1
  453. package/dist/rhf/fields.d.ts +11 -4
  454. package/dist/rhf/fields.js.map +1 -1
  455. package/dist/search/command-palette.js +1 -1
  456. package/dist/search/command-palette.js.map +1 -1
  457. package/dist/search/global-search.js +19 -9
  458. package/dist/search/global-search.js.map +1 -1
  459. package/dist/search/search-index.d.ts +0 -6
  460. package/dist/search/search-index.js +0 -4
  461. package/dist/search/search-index.js.map +1 -1
  462. package/dist/search/use-command-key.d.ts +12 -1
  463. package/dist/search/use-command-key.js +3 -12
  464. package/dist/search/use-command-key.js.map +1 -1
  465. package/dist/search.d.ts +1 -1
  466. package/dist/search.js +6 -1
  467. package/dist/search.js.map +1 -1
  468. package/dist/settings/use-settings-focus.js +1 -0
  469. package/dist/settings/use-settings-focus.js.map +1 -1
  470. package/dist/shell/account-menu-language.d.ts +5 -3
  471. package/dist/shell/account-menu-language.js +2 -2
  472. package/dist/shell/account-menu-language.js.map +1 -1
  473. package/dist/shell/account-menu-text-size.d.ts +9 -6
  474. package/dist/shell/account-menu-text-size.js +2 -2
  475. package/dist/shell/account-menu-text-size.js.map +1 -1
  476. package/dist/shell/app-shell.d.ts +0 -10
  477. package/dist/shell/app-shell.js +1 -7
  478. package/dist/shell/app-shell.js.map +1 -1
  479. package/dist/shell/company-switcher.d.ts +1 -1
  480. package/dist/shell/company-switcher.js.map +1 -1
  481. package/dist/shell/dev-menu.d.ts +1 -1
  482. package/dist/shell/dev-menu.js +5 -18
  483. package/dist/shell/dev-menu.js.map +1 -1
  484. package/dist/shell/top-bar-actions.d.ts +1 -1
  485. package/dist/shell/top-bar-actions.js.map +1 -1
  486. package/dist/shell/top-bar-parts-labels.d.ts +1 -3
  487. package/dist/shell/top-bar-parts-labels.js +0 -1
  488. package/dist/shell/top-bar-parts-labels.js.map +1 -1
  489. package/dist/shell.d.ts +7 -10
  490. package/dist/shell.js +14 -23
  491. package/dist/shell.js.map +1 -1
  492. package/dist/sync/engine.d.ts +30 -4
  493. package/dist/sync/engine.js +172 -52
  494. package/dist/sync/engine.js.map +1 -1
  495. package/dist/sync.d.ts +2 -2
  496. package/dist/sync.js +2 -10
  497. package/dist/sync.js.map +1 -1
  498. package/dist/testing/css-variables.js +3 -8
  499. package/dist/testing/css-variables.js.map +1 -1
  500. package/dist/theme/chart-palette.d.ts +0 -5
  501. package/dist/theme/chart-palette.js +0 -2
  502. package/dist/theme/chart-palette.js.map +1 -1
  503. package/dist/theme/contrast.d.ts +2 -1
  504. package/dist/theme/contrast.js +3 -8
  505. package/dist/theme/contrast.js.map +1 -1
  506. package/dist/theme/palette-store.js +6 -6
  507. package/dist/theme/palette-store.js.map +1 -1
  508. package/dist/theme/text-size.js +3 -8
  509. package/dist/theme/text-size.js.map +1 -1
  510. package/dist/theme/theme-store.js +3 -6
  511. package/dist/theme/theme-store.js.map +1 -1
  512. package/dist/tour/tour-catalogue.d.ts +7 -4
  513. package/dist/tour/tour-catalogue.js +12 -10
  514. package/dist/tour/tour-catalogue.js.map +1 -1
  515. package/dist/tour/tour-dock.d.ts +0 -2
  516. package/dist/tour/tour-dock.js +0 -2
  517. package/dist/tour/tour-dock.js.map +1 -1
  518. package/dist/tour/tour-gates.d.ts +4 -0
  519. package/dist/tour/tour-gates.js +14 -0
  520. package/dist/tour/tour-gates.js.map +1 -1
  521. package/dist/tour/tour-overlay.d.ts +27 -0
  522. package/dist/tour/tour-overlay.js +741 -0
  523. package/dist/tour/tour-overlay.js.map +1 -0
  524. package/dist/tour/tour.d.ts +13 -3
  525. package/dist/tour/tour.js +59 -781
  526. package/dist/tour/tour.js.map +1 -1
  527. package/dist/tour/use-tour-progress.js +1 -1
  528. package/dist/tour/use-tour-progress.js.map +1 -1
  529. package/dist/wizard/stepper-nav.d.ts +5 -1
  530. package/dist/wizard/stepper-nav.js.map +1 -1
  531. package/dist/wizard/types.d.ts +6 -2
  532. package/dist/wizard/types.js.map +1 -1
  533. package/dist/wizard/use-wizard.d.ts +3 -1
  534. package/dist/wizard/use-wizard.js.map +1 -1
  535. package/dist/wizard/validation.d.ts +3 -1
  536. package/dist/wizard/validation.js.map +1 -1
  537. package/package.json +10 -6
  538. package/src/account/account-parts.ts +5 -25
  539. package/src/account/data-export-setting-labels.ts +1 -1
  540. package/src/account/delete-account-setting-labels.ts +1 -1
  541. package/src/account/email-change-setting-labels.ts +1 -1
  542. package/src/account/passkeys-setting.tsx +60 -38
  543. package/src/account/sessions-setting.tsx +18 -12
  544. package/src/admin/admin-action-confirm.tsx +1 -5
  545. package/src/admin/admin-parts.ts +11 -25
  546. package/src/admin/mail-test-card-labels.ts +1 -1
  547. package/src/admin/plan-change-labels.ts +5 -23
  548. package/src/admin/plan-change.tsx +7 -44
  549. package/src/admin/reviewer-scope-editor.tsx +3 -3
  550. package/src/auth/auth-errors-labels.ts +23 -0
  551. package/src/auth/auth-errors.ts +11 -38
  552. package/src/auth/complete-name-dialog.tsx +4 -1
  553. package/src/auth/forgot-password-form-labels.ts +1 -1
  554. package/src/auth/forgot-password-form.tsx +11 -6
  555. package/src/auth/register-form-labels.ts +1 -1
  556. package/src/auth/reset-password-form-labels.ts +1 -1
  557. package/src/auth/reset-password-form.tsx +8 -4
  558. package/src/auth/sign-in-form-labels.ts +1 -1
  559. package/src/auth/status-parts.tsx +0 -6
  560. package/src/auth/verify-email-labels.ts +1 -1
  561. package/src/auth/verify-email.tsx +11 -6
  562. package/src/billing/cancel-subscription.tsx +15 -12
  563. package/src/billing/plan-card.tsx +18 -10
  564. package/src/billing/plan-change-labels.ts +66 -0
  565. package/src/billing/plan-switch.tsx +15 -12
  566. package/src/chart.ts +73 -6
  567. package/src/components/account-chips.tsx +20 -23
  568. package/src/components/alert-banner.tsx +82 -5
  569. package/src/components/amount-input.tsx +18 -7
  570. package/src/components/autocomplete.tsx +64 -70
  571. package/src/components/bulk-action-bar.tsx +70 -2
  572. package/src/components/button-look.ts +5 -5
  573. package/src/components/button.tsx +2 -2
  574. package/src/components/chart-edge.ts +34 -1
  575. package/src/components/chart-figure.tsx +35 -0
  576. package/src/components/chart-zoom-core.tsx +6 -5
  577. package/src/components/chart.tsx +26 -5
  578. package/src/components/combobox-core.tsx +293 -62
  579. package/src/components/combobox.tsx +183 -153
  580. package/src/components/copy-button.tsx +34 -23
  581. package/src/components/country-select.tsx +16 -11
  582. package/src/components/data-table-column-settings.tsx +52 -10
  583. package/src/components/data-table-filters.ts +6 -4
  584. package/src/components/data-table-fit.ts +19 -5
  585. package/src/components/data-table-groups.tsx +1 -1
  586. package/src/components/data-table-head.tsx +32 -22
  587. package/src/components/data-table-mobile.tsx +61 -26
  588. package/src/components/data-table-pagination.tsx +21 -6
  589. package/src/components/data-table-resize.ts +1 -1
  590. package/src/components/data-table-row-actions.tsx +3 -3
  591. package/src/components/data-table-row.tsx +25 -14
  592. package/src/components/data-table-selection.ts +1 -1
  593. package/src/components/data-table-shared.tsx +33 -1
  594. package/src/components/data-table-sort.ts +57 -2
  595. package/src/components/data-table-virtual.ts +32 -8
  596. package/src/components/data-table.tsx +239 -68
  597. package/src/components/date-field.tsx +66 -9
  598. package/src/components/date-picker.tsx +125 -49
  599. package/src/components/date-range-picker.tsx +71 -36
  600. package/src/components/error-boundary.tsx +53 -39
  601. package/src/components/field-base-labels.ts +20 -0
  602. package/src/components/field-base.tsx +13 -7
  603. package/src/components/form-actions.tsx +20 -5
  604. package/src/components/hover-menu.tsx +18 -8
  605. package/src/components/icon-button.tsx +14 -9
  606. package/src/components/inline-confirm.tsx +116 -0
  607. package/src/components/inline-entity-combobox.tsx +197 -153
  608. package/src/components/input.tsx +1 -1
  609. package/src/components/jump-calendar.tsx +30 -146
  610. package/src/components/legal-page.tsx +10 -7
  611. package/src/components/legal.tsx +4 -2
  612. package/src/components/lightbox.tsx +21 -8
  613. package/src/components/list.tsx +10 -4
  614. package/src/components/measured-grid.tsx +8 -4
  615. package/src/components/mini-calendar.tsx +6 -5
  616. package/src/components/modal.tsx +6 -1
  617. package/src/components/month-grid.tsx +229 -0
  618. package/src/components/month-picker.tsx +192 -200
  619. package/src/components/multi-select.tsx +21 -9
  620. package/src/components/nav-pills.tsx +1 -1
  621. package/src/components/picker-sheet.tsx +12 -9
  622. package/src/components/pie-chart-labels.ts +8 -0
  623. package/src/components/pie-chart.tsx +22 -28
  624. package/src/components/popover.tsx +18 -1
  625. package/src/components/row-actions.tsx +2 -1
  626. package/src/components/series-chart-axes.ts +9 -4
  627. package/src/components/series-chart-labels.ts +7 -4
  628. package/src/components/series-chart.tsx +253 -87
  629. package/src/components/share-card.tsx +6 -6
  630. package/src/components/tabs-labels.ts +13 -0
  631. package/src/components/tabs.tsx +2 -2
  632. package/src/components/toggle-legend.tsx +16 -11
  633. package/src/components/translation-review-editor.tsx +6 -5
  634. package/src/components/translation-review.tsx +4 -3
  635. package/src/components/treemap-labels.ts +47 -0
  636. package/src/components/treemap-layout.ts +210 -0
  637. package/src/components/treemap.tsx +780 -138
  638. package/src/components/use-table-state.ts +12 -9
  639. package/src/data-table.ts +1 -4
  640. package/src/demo/demo-countdown.ts +3 -8
  641. package/src/eslint/role-utilities.ts +97 -19
  642. package/src/feedback/feedback-dialog.tsx +30 -19
  643. package/src/feedback/feedback-mutation.ts +20 -0
  644. package/src/feedback/feedback-page-link.ts +90 -0
  645. package/src/feedback/feedback-public-list.tsx +13 -7
  646. package/src/feedback/feedback-replies.tsx +13 -6
  647. package/src/feedback/feedback-row-detail-labels.ts +0 -3
  648. package/src/feedback/feedback-row-detail.tsx +70 -84
  649. package/src/feedback/feedback-status-undo.ts +28 -28
  650. package/src/feedback/feedback-submit-dialog.tsx +2 -2
  651. package/src/feedback/feedback-submit.tsx +21 -7
  652. package/src/feedback/feedback-swipe.tsx +1 -1
  653. package/src/feedback/feedback-table.tsx +86 -38
  654. package/src/feedback/feedback-thread-labels.ts +2 -2
  655. package/src/feedback/feedback-thread.tsx +18 -26
  656. package/src/feedback.ts +89 -6
  657. package/src/hooks/use-focus-trap.ts +8 -24
  658. package/src/hooks/use-hotkey.ts +24 -7
  659. package/src/hooks/use-long-press.ts +16 -2
  660. package/src/hooks/use-media-query.ts +20 -4
  661. package/src/hooks/use-mounted.ts +39 -0
  662. package/src/hooks/use-overlay-history.ts +32 -3
  663. package/src/i18n/defaults.ts +4 -9
  664. package/src/i18n/german.ts +8 -19
  665. package/src/i18n/kit-labels.tsx +9 -11
  666. package/src/i18n/locales/es.ts +8 -19
  667. package/src/i18n/locales/fr.ts +21 -30
  668. package/src/i18n/locales/hu.ts +10 -19
  669. package/src/i18n/locales/it.ts +8 -20
  670. package/src/i18n/locales/zh.ts +9 -19
  671. package/src/i18n/review.ts +12 -2
  672. package/src/i18n/shared-labels.ts +0 -1
  673. package/src/index.ts +57 -49
  674. package/src/landing/routing.tsx +14 -10
  675. package/src/lib/broadcast.ts +52 -0
  676. package/src/lib/date-format.ts +89 -13
  677. package/src/lib/day-format.ts +96 -0
  678. package/src/lib/describe-failure.ts +18 -0
  679. package/src/lib/direction.ts +39 -0
  680. package/src/lib/focus-left.ts +156 -0
  681. package/src/lib/format.ts +5 -17
  682. package/src/lib/http-status.ts +38 -0
  683. package/src/lib/intl-cache.ts +25 -0
  684. package/src/lib/iso-day.ts +13 -0
  685. package/src/lib/lazy-with-fallback.ts +82 -0
  686. package/src/lib/path-pattern.ts +55 -0
  687. package/src/lib/reduced-motion.ts +2 -1
  688. package/src/lib/round.ts +7 -2
  689. package/src/lib/safe-storage.ts +48 -6
  690. package/src/lib/session-refresh.ts +1 -1
  691. package/src/lib/share-format.ts +18 -0
  692. package/src/lib/text-case.ts +19 -0
  693. package/src/lib/text-match.ts +57 -2
  694. package/src/lib/thenable.ts +19 -0
  695. package/src/lib/translation-review.ts +13 -5
  696. package/src/notifications/device-zone-report.ts +1 -1
  697. package/src/notifications/notification-bell.tsx +7 -4
  698. package/src/notifications/notification-inbox.tsx +40 -58
  699. package/src/notifications/notification-parts.tsx +3 -4
  700. package/src/notifications/notification-types-setting.tsx +1 -1
  701. package/src/notifications/notifications-labels.ts +7 -0
  702. package/src/notifications/push-device-setting.tsx +1 -1
  703. package/src/notifications/push-devices-setting.tsx +30 -55
  704. package/src/notifications/quiet-hours-setting.tsx +93 -31
  705. package/src/notifications/unsubscribe-panel.tsx +4 -7
  706. package/src/notifications/use-push-device.ts +76 -10
  707. package/src/offline/query-persistence.ts +35 -3
  708. package/src/rhf/fields.tsx +11 -4
  709. package/src/search/command-palette.tsx +2 -4
  710. package/src/search/global-search.tsx +36 -15
  711. package/src/search/search-index.ts +0 -15
  712. package/src/search/use-command-key.ts +13 -12
  713. package/src/search.ts +10 -1
  714. package/src/settings/use-settings-focus.ts +3 -0
  715. package/src/shell/account-menu-language.tsx +6 -4
  716. package/src/shell/account-menu-text-size.tsx +10 -7
  717. package/src/shell/app-shell.tsx +0 -13
  718. package/src/shell/company-switcher.tsx +1 -1
  719. package/src/shell/dev-menu.tsx +8 -22
  720. package/src/shell/top-bar-actions.tsx +1 -1
  721. package/src/shell/top-bar-parts-labels.ts +1 -4
  722. package/src/shell.ts +21 -17
  723. package/src/sync/engine.ts +339 -61
  724. package/src/sync.ts +2 -8
  725. package/src/testing/css-variables.ts +8 -9
  726. package/src/theme/chart-palette.ts +0 -2
  727. package/src/theme/contrast.ts +8 -9
  728. package/src/theme/palette-store.ts +9 -9
  729. package/src/theme/text-size.ts +6 -8
  730. package/src/theme/theme-store.ts +7 -9
  731. package/src/tour/tour-catalogue.tsx +26 -15
  732. package/src/tour/tour-dock.ts +0 -2
  733. package/src/tour/tour-gates.ts +23 -0
  734. package/src/tour/tour-overlay.tsx +1170 -0
  735. package/src/tour/tour.tsx +108 -1202
  736. package/src/tour/use-tour-progress.ts +1 -1
  737. package/src/wizard/stepper-nav.tsx +3 -1
  738. package/src/wizard/types.ts +11 -2
  739. package/src/wizard/use-wizard.ts +2 -1
  740. package/src/wizard/validation.ts +8 -2
  741. package/tokens.css +6 -5
  742. package/dist/components/field-sync-labels.d.ts +0 -2
  743. package/dist/components/field-sync-labels.js +0 -12
  744. package/dist/components/field-sync-labels.js.map +0 -1
  745. package/dist/components/field-sync.d.ts +0 -164
  746. package/dist/components/field-sync.js +0 -269
  747. package/dist/components/field-sync.js.map +0 -1
  748. package/dist/components/section-nav.d.ts +0 -37
  749. package/dist/components/section-nav.js +0 -33
  750. package/dist/components/section-nav.js.map +0 -1
  751. package/dist/components/sign-chip-labels.d.ts +0 -2
  752. package/dist/components/sign-chip-labels.js +0 -11
  753. package/dist/components/sign-chip-labels.js.map +0 -1
  754. package/dist/components/sign-chip.d.ts +0 -68
  755. package/dist/components/sign-chip.js +0 -47
  756. package/dist/components/sign-chip.js.map +0 -1
  757. package/dist/components/ui-labels.js.map +0 -1
  758. package/dist/lib/isin.d.ts +0 -28
  759. package/dist/lib/isin.js +0 -36
  760. package/dist/lib/isin.js.map +0 -1
  761. package/dist/shell/dev-switchers.d.ts +0 -6
  762. package/dist/shell/dev-switchers.js +0 -12
  763. package/dist/shell/dev-switchers.js.map +0 -1
  764. package/dist/shell/role-switcher.d.ts +0 -21
  765. package/dist/shell/role-switcher.js +0 -31
  766. package/dist/shell/role-switcher.js.map +0 -1
  767. package/src/components/field-sync-labels.ts +0 -13
  768. package/src/components/field-sync.tsx +0 -482
  769. package/src/components/section-nav.tsx +0 -65
  770. package/src/components/sign-chip-labels.ts +0 -12
  771. package/src/components/sign-chip.tsx +0 -124
  772. package/src/components/ui-labels.ts +0 -24
  773. package/src/lib/isin.ts +0 -62
  774. package/src/shell/dev-switchers.tsx +0 -18
  775. package/src/shell/role-switcher.tsx +0 -46
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/safe-storage.ts"],"sourcesContent":["/**\n * `localStorage` that cannot take the page down with it.\n *\n * Accessing `window.localStorage` **throws** — rather than returning null — when a\n * browser has site data blocked: Safari private browsing, Chrome with third-party\n * site data disabled, a partitioned webview. Three places in this package already\n * knew that and wrapped their calls in try/catch (`logger.ts`, and the DataTable's\n * `loadPersisted`/`savePersisted`). One did not: `AppShell` read the sidebar's\n * collapse flag straight out of a `useState` initialiser, so the throw happened\n * during render of the application's top-level shell. That is not a lost preference\n * — nothing mounts, in every consumer of this package at once.\n *\n * So the guard lives here, once, and the next storage-backed preference cannot\n * forget it. A read that cannot happen is indistinguishable from a key that was\n * never set, which is the right answer for every caller: the default.\n *\n * Since 0.34 (DUP-11, refactor audit 2026-10) it also removes, and guards\n * `sessionStorage` the same way: `landing/routing`, `billing/checkout-processing` and\n * the demo banner each wrote that guard again by hand. `removeStored` and the session\n * pair are package-private — the barrel exports `readStored` / `writeStored` only.\n */\n\n/** `access` on the storage area, or `fallback` where there is none (SSR) or it throws. */\nfunction guarded<T>(area: \"localStorage\" | \"sessionStorage\", fallback: T, access: (storage: Storage) => T): T {\n if (typeof window === \"undefined\") return fallback;\n try {\n return access(window[area]);\n } catch {\n // private mode, quota, partitioned or blocked storage\n return fallback;\n }\n}\n\n/** The stored string for `key`, or null when absent, unreadable, or blocked. */\nexport function readStored(key: string): string | null {\n return guarded(\"localStorage\", null, (storage) => storage.getItem(key));\n}\n\n/** Store `value` under `key`, silently doing nothing when storage is unavailable\n * or full. A preference that cannot be persisted is not worth an exception. */\nexport function writeStored(key: string, value: string): void {\n guarded(\"localStorage\", undefined, (storage) => storage.setItem(key, value));\n}\n\n/** Forget `key`, silently doing nothing when storage is blocked — there is nothing to\n * forget then. */\nexport function removeStored(key: string): void {\n guarded(\"localStorage\", undefined, (storage) => storage.removeItem(key));\n}\n\n/** {@link readStored} for `sessionStorage`: this tab's value, or null. */\nexport function readSessionStored(key: string): string | null {\n return guarded(\"sessionStorage\", null, (storage) => storage.getItem(key));\n}\n\n/** {@link writeStored} for `sessionStorage`; `null` removes the key. */\nexport function writeSessionStored(key: string, value: string | null): void {\n guarded(\"sessionStorage\", undefined, (storage) =>\n value === null ? storage.removeItem(key) : storage.setItem(key, value),\n );\n}\n\n/**\n * Every key in `area` that starts with `prefix`, or `[]` where storage refuses — for the\n * reset of this device's settings, which removes a table's views by their prefix\n * (docs/settings-harmonization.md §11.4). Package-private, like {@link removeStored}.\n */\nexport function storedKeys(prefix: string, area: \"local\" | \"session\" = \"local\"): string[] {\n return guarded(area === \"session\" ? \"sessionStorage\" : \"localStorage\", [], (storage) => {\n const keys: string[] = [];\n for (let i = 0; i < storage.length; i++) {\n const key = storage.key(i);\n if (key !== null && key.startsWith(prefix)) keys.push(key);\n }\n return keys;\n });\n}\n"],"mappings":";AAuBA,SAAS,QAAW,MAAyC,UAAa,QAAoC;AAC5G,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,MAAI;AACF,WAAO,OAAO,OAAO,IAAI,CAAC;AAAA,EAC5B,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,WAAW,KAA4B;AACrD,SAAO,QAAQ,gBAAgB,MAAM,CAAC,YAAY,QAAQ,QAAQ,GAAG,CAAC;AACxE;AAIO,SAAS,YAAY,KAAa,OAAqB;AAC5D,UAAQ,gBAAgB,QAAW,CAAC,YAAY,QAAQ,QAAQ,KAAK,KAAK,CAAC;AAC7E;AAIO,SAAS,aAAa,KAAmB;AAC9C,UAAQ,gBAAgB,QAAW,CAAC,YAAY,QAAQ,WAAW,GAAG,CAAC;AACzE;AAGO,SAAS,kBAAkB,KAA4B;AAC5D,SAAO,QAAQ,kBAAkB,MAAM,CAAC,YAAY,QAAQ,QAAQ,GAAG,CAAC;AAC1E;AAGO,SAAS,mBAAmB,KAAa,OAA4B;AAC1E;AAAA,IAAQ;AAAA,IAAkB;AAAA,IAAW,CAAC,YACpC,UAAU,OAAO,QAAQ,WAAW,GAAG,IAAI,QAAQ,QAAQ,KAAK,KAAK;AAAA,EACvE;AACF;AAOO,SAAS,WAAW,QAAgB,OAA4B,SAAmB;AACxF,SAAO,QAAQ,SAAS,YAAY,mBAAmB,gBAAgB,CAAC,GAAG,CAAC,YAAY;AACtF,UAAM,OAAiB,CAAC;AACxB,aAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,YAAM,MAAM,QAAQ,IAAI,CAAC;AACzB,UAAI,QAAQ,QAAQ,IAAI,WAAW,MAAM,EAAG,MAAK,KAAK,GAAG;AAAA,IAC3D;AACA,WAAO;AAAA,EACT,CAAC;AACH;","names":[]}
1
+ {"version":3,"sources":["../../src/lib/safe-storage.ts"],"sourcesContent":["/**\n * `localStorage` that cannot take the page down with it.\n *\n * Accessing `window.localStorage` **throws** — rather than returning null — when a\n * browser has site data blocked: Safari private browsing, Chrome with third-party\n * site data disabled, a partitioned webview. Three places in this package already\n * knew that and wrapped their calls in try/catch (`logger.ts`, and the DataTable's\n * `loadPersisted`/`savePersisted`). One did not: `AppShell` read the sidebar's\n * collapse flag straight out of a `useState` initialiser, so the throw happened\n * during render of the application's top-level shell. That is not a lost preference\n * — nothing mounts, in every consumer of this package at once.\n *\n * So the guard lives here, once, and the next storage-backed preference cannot\n * forget it. A read that cannot happen is indistinguishable from a key that was\n * never set, which is the right answer for every caller: the default.\n *\n * Since 0.34 (DUP-11, refactor audit 2026-10) it also removes, and guards\n * `sessionStorage` the same way: `landing/routing`, `billing/checkout-processing` and\n * the demo banner each wrote that guard again by hand.\n *\n * Since 0.52 all of it but {@link storedKeys} is public, with a JSON pair beside it\n * (DUP-8, refactor audit 2026-10-30). Kept package-private, the guard was written again\n * four times in the kit's showcase and in 16 files across the three apps, some of them\n * without it: a token read that throws where storage is blocked takes every request\n * down with it. (The kit's own DevMenu, 0.51, wrote the session pair a second time too.)\n */\n\n/** `access` on the storage area, or `fallback` where there is none (SSR) or it throws. */\nfunction guarded<T>(area: \"localStorage\" | \"sessionStorage\", fallback: T, access: (storage: Storage) => T): T {\n if (typeof window === \"undefined\") return fallback;\n try {\n return access(window[area]);\n } catch {\n // private mode, quota, partitioned or blocked storage\n return fallback;\n }\n}\n\n/** The stored string for `key`, or null when absent, unreadable, or blocked. */\nexport function readStored(key: string): string | null {\n return guarded(\"localStorage\", null, (storage) => storage.getItem(key));\n}\n\n/** Store `value` under `key`, silently doing nothing when storage is unavailable\n * or full. A preference that cannot be persisted is not worth an exception. `null`\n * removes the key, as {@link writeSessionStored}'s does (0.52; the two pairs wrote\n * differently, and `null` past the types stored the string \"null\" — 0.52 code review). */\nexport function writeStored(key: string, value: string | null): void {\n guarded(\"localStorage\", undefined, (storage) =>\n value === null ? storage.removeItem(key) : storage.setItem(key, value),\n );\n}\n\n/** Forget `key`, silently doing nothing when storage is blocked — there is nothing to\n * forget then. */\nexport function removeStored(key: string): void {\n guarded(\"localStorage\", undefined, (storage) => storage.removeItem(key));\n}\n\n/** {@link readStored} for `sessionStorage`: this tab's value, or null. */\nexport function readSessionStored(key: string): string | null {\n return guarded(\"sessionStorage\", null, (storage) => storage.getItem(key));\n}\n\n/** {@link writeStored} for `sessionStorage`; `null` removes the key. */\nexport function writeSessionStored(key: string, value: string | null): void {\n guarded(\"sessionStorage\", undefined, (storage) =>\n value === null ? storage.removeItem(key) : storage.setItem(key, value),\n );\n}\n\n/**\n * The JSON stored under `key`, when it parses and `isShape` accepts it; else `null`. Absent,\n * blocked, malformed and another shape all read as \"never set\" — the caller's default —\n * which is what the six hand-written `JSON.parse(readStored(key))` reads in the kit each\n * answered in their own try/catch (DUP-8). `isShape` gets the parsed value as `unknown`:\n * the shape check is the one part that is the caller's. A shape check that throws reads\n * as another shape: null.\n */\nexport function readStoredJson<T>(key: string, isShape: (value: unknown) => value is T): T | null {\n const text = readStored(key);\n if (text === null) return null;\n try {\n const value: unknown = JSON.parse(text);\n return isShape(value) ? value : null;\n } catch {\n // not JSON, or a shape check that read past what is there\n return null;\n }\n}\n\n/** Store `value` as JSON under `key`, silently doing nothing when storage is unavailable\n * or full, or the value has no JSON form (a cycle, a BigInt) — {@link writeStored}'s rule. */\nexport function writeStoredJson(key: string, value: unknown): void {\n let text: string | undefined;\n try {\n text = JSON.stringify(value);\n } catch {\n return;\n }\n // `undefined` (and a function) has no JSON form either: stringify answers undefined.\n if (text !== undefined) writeStored(key, text);\n}\n\n/**\n * Every key in `area` that starts with `prefix`, or `[]` where storage refuses — for the\n * reset of this device's settings, which removes a table's views by their prefix\n * (docs/settings-harmonization.md §11.4). Package-private: the reset's alone.\n */\nexport function storedKeys(prefix: string, area: \"local\" | \"session\" = \"local\"): string[] {\n return guarded(area === \"session\" ? \"sessionStorage\" : \"localStorage\", [], (storage) => {\n const keys: string[] = [];\n for (let i = 0; i < storage.length; i++) {\n const key = storage.key(i);\n if (key !== null && key.startsWith(prefix)) keys.push(key);\n }\n return keys;\n });\n}\n"],"mappings":";AA4BA,SAAS,QAAW,MAAyC,UAAa,QAAoC;AAC5G,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,MAAI;AACF,WAAO,OAAO,OAAO,IAAI,CAAC;AAAA,EAC5B,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,WAAW,KAA4B;AACrD,SAAO,QAAQ,gBAAgB,MAAM,CAAC,YAAY,QAAQ,QAAQ,GAAG,CAAC;AACxE;AAMO,SAAS,YAAY,KAAa,OAA4B;AACnE;AAAA,IAAQ;AAAA,IAAgB;AAAA,IAAW,CAAC,YAClC,UAAU,OAAO,QAAQ,WAAW,GAAG,IAAI,QAAQ,QAAQ,KAAK,KAAK;AAAA,EACvE;AACF;AAIO,SAAS,aAAa,KAAmB;AAC9C,UAAQ,gBAAgB,QAAW,CAAC,YAAY,QAAQ,WAAW,GAAG,CAAC;AACzE;AAGO,SAAS,kBAAkB,KAA4B;AAC5D,SAAO,QAAQ,kBAAkB,MAAM,CAAC,YAAY,QAAQ,QAAQ,GAAG,CAAC;AAC1E;AAGO,SAAS,mBAAmB,KAAa,OAA4B;AAC1E;AAAA,IAAQ;AAAA,IAAkB;AAAA,IAAW,CAAC,YACpC,UAAU,OAAO,QAAQ,WAAW,GAAG,IAAI,QAAQ,QAAQ,KAAK,KAAK;AAAA,EACvE;AACF;AAUO,SAAS,eAAkB,KAAa,SAAmD;AAChG,QAAM,OAAO,WAAW,GAAG;AAC3B,MAAI,SAAS,KAAM,QAAO;AAC1B,MAAI;AACF,UAAM,QAAiB,KAAK,MAAM,IAAI;AACtC,WAAO,QAAQ,KAAK,IAAI,QAAQ;AAAA,EAClC,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AAIO,SAAS,gBAAgB,KAAa,OAAsB;AACjE,MAAI;AACJ,MAAI;AACF,WAAO,KAAK,UAAU,KAAK;AAAA,EAC7B,QAAQ;AACN;AAAA,EACF;AAEA,MAAI,SAAS,OAAW,aAAY,KAAK,IAAI;AAC/C;AAOO,SAAS,WAAW,QAAgB,OAA4B,SAAmB;AACxF,SAAO,QAAQ,SAAS,YAAY,mBAAmB,gBAAgB,CAAC,GAAG,CAAC,YAAY;AACtF,UAAM,OAAiB,CAAC;AACxB,aAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,YAAM,MAAM,QAAQ,IAAI,CAAC;AACzB,UAAI,QAAQ,QAAQ,IAAI,WAAW,MAAM,EAAG,MAAK,KAAK,GAAG;AAAA,IAC3D;AACA,WAAO;AAAA,EACT,CAAC;AACH;","names":[]}
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { httpStatusOf } from "../auth/auth-errors.js";
2
+ import { httpStatusOf } from "./http-status.js";
3
3
  const SIGNED_OUT_STEPS = [
4
4
  "login",
5
5
  "passkey/login",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/session-refresh.ts"],"sourcesContent":["/**\n * Renewing a session when the access token runs out: the client half of\n * docs/auth-harmonization.md §11 (round 0.34 C7, KS U6), lifted from keksdose's\n * `refreshOnce` (Kurvenschmiede's is the same, line for line) and corrected where the\n * three apps' reviews found it wrong. It ends kastlan's sign-out over a network blip (B3).\n *\n * THE KIT OWNS THE RULE, THE APP OWNS THE CALL (§11.2, decision 1). The kit decides when\n * to refresh, who waits, what a failure means, when the session is over and whether a\n * renewal may still be stored. The app keeps its endpoint, its token storage, its\n * sign-out and where it navigates, and hands them in as {@link SessionRefreshOptions}.\n *\n * - **One flight per tab** (decision 2): every 401 that arrives while a refresh runs\n * waits for it, and so does `session.refresh()` (kastlan's boot).\n * - **Definitive** (decision 3): the device holds no refresh token (`refresh` resolves\n * `null`), the refresh answers 401 or 403, or the request replayed with the fresh\n * token gets a 401 again. Only then does the app sign out (`onEnded`), and only while\n * that session is still the app's.\n * - **Transient** (decision 4): everything else. The session stays: nothing is signed\n * out, stored or redirected, and the kit shows nothing. The request rejects with its\n * OWN 401, marked ({@link isTransientRefreshFailure}), so the app's message can say\n * \"can't reach the server\" instead of \"not signed in\".\n * - **Retries only where waiting helps** (decision 5, KS 7): no answer, the kit's own\n * timeout, 408, 429 and 5xx get up to three attempts, 1 s and then 3 s apart, each cut\n * off after 15 s; none while the browser reports itself offline. Any other answer, and\n * anything that is not an HTTP error, ends the flight at the first attempt.\n * - **A stale session is never written back** (§11.7, ka 3, KS 3, KS 4): every request\n * is stamped with `currentToken()` when it goes out, and every flight notes it when it\n * starts. A 401 to a request sent with an older token replays it with the app's token\n * now, without a refresh; a flight whose session changed while it ran stores nothing\n * and ends nothing. The access token is the session's identity because every change\n * the apps make replaces it — a refresh, another tab, a company switch, a password\n * change, a sign-in, a demo start, a sign-out.\n *\n * Framework-free like `server-wake.ts`: {@link attachSessionRefresh} takes the app's\n * axios instance as an ARGUMENT, typed structurally, so the kit never imports axios.\n */\n\nimport type { AxiosLikeInstance, ServerWakeAxiosConfig } from \"./server-wake\";\nimport { httpStatusOf } from \"../auth/auth-errors\";\n\n/** What the app's `refresh` resolves: the new access token, plus whatever its `store`\n * needs (its refresh token, the user). */\nexport interface SessionRenewal {\n /** The new access token; the waiting requests are replayed with it. */\n token: string;\n /**\n * `false`: store the renewal, but replay nothing — each waiting request rejects with\n * its 401, unmarked. kastlan's refresh can land in another company (§11.5, ka 3): a\n * waiting write, such as the handover's `POST /sync`, must not be replayed into it.\n */\n replay?: false;\n}\n\n/** What the app hands {@link createSessionRefresh} (§11.4). */\nexport interface SessionRefreshOptions<R extends SessionRenewal> {\n /**\n * The app's own `POST /auth/refresh`, on a BARE client — never the instance the kit is\n * attached to, so the refresh cannot re-enter the interceptor. It **writes nothing**:\n * the kit hands the answer to `store`, and only while the session is unchanged.\n *\n * Resolves the answer as a renewal, or `null` when the device holds no refresh token.\n * Throws on failure, and the kit classifies the throw: a 401 or 403 is definitive; no\n * answer (an axios error without a `response`), the kit's own timeout, 408, 429 and\n * 5xx are transient and retried; any other answer, and anything that is not an HTTP\n * error (a bug in this function), is transient and not retried. The status is read\n * from `err.response.status` (axios), `err.status` or `err.statusCode`.\n *\n * `signal` aborts when the attempt runs past `timeoutMs`; pass it to the request.\n */\n refresh: (signal: AbortSignal) => Promise<R | null>;\n /** Writes the renewal: the tokens and the user (a missing refresh token keeps the\n * stored one). Called only while the session the flight renews is still the app's,\n * and before the waiting requests are replayed — so it writes the token SYNCHRONOUSLY\n * where `currentToken` reads it: the replay goes through the app's own request\n * interceptor again, and is stamped then. A throw is the app's bug: the waiting\n * requests and `session.refresh()` reject with it. */\n store: (renewal: R) => void;\n /** The access token the app sends NOW, read where its request interceptor reads it;\n * `null` when there is none. */\n currentToken: () => string | null;\n /** Whether the session is a demo: a demo's 401 ends it, with no refresh tried\n * (landing-demo §5.5). */\n isDemo: () => boolean;\n /**\n * The session is over: drop the tokens and the user, then navigate — `/demo/ended` for\n * a demo, else the sign-in with the page it came from (auth §5.2). Called once per\n * session, however many requests found out (KS 8), and never by `session.refresh()`.\n */\n onEnded: (ended: { demo: boolean }) => void;\n /** Whether a URL is a signed-out step, whose 401 judges the form and never the\n * session. Replaces the default list, {@link isSignedOutStep}. */\n isSignedOutStep?: (url: string) => boolean;\n /** Attempts for a refresh that got no answer, timed out, or got a 408, 429 or 5xx.\n * Default 3. */\n attempts?: number;\n /** The waits before the second and later attempts; the last one repeats. Default\n * `[1000, 3000]`. */\n backoffMs?: readonly number[];\n /** Each attempt is cut off after this, and its `signal` aborted. Default 15 000. */\n timeoutMs?: number;\n}\n\n/** What `session.refresh()` resolves (§11.4), by `result`:\n *\n * | `result` | When | What the kit has done |\n * |---|---|---|\n * | `\"renewed\"` | a renewal while the session was unchanged | `store(renewal)` |\n * | `\"ended\"` | definitive | nothing; `onEnded` only for requests that waited on the same flight |\n * | `\"transient\"` | transient | nothing |\n * | `\"changed\"` | the session changed while it ran | nothing; read the app's own state |\n */\nexport type SessionRefreshOutcome<R extends SessionRenewal = SessionRenewal> =\n { result: \"renewed\"; renewal: R } | { result: \"ended\" } | { result: \"transient\" } | { result: \"changed\" };\n\n/** The flight {@link createSessionRefresh} makes; {@link attachSessionRefresh} hooks it\n * to an axios instance. */\nexport interface SessionRefresh<R extends SessionRenewal = SessionRenewal> {\n /**\n * The same flight, for a caller outside the interceptor — kastlan's boot. Joins a\n * running flight, or starts one. Never calls `onEnded`: the caller decides what an\n * `\"ended\"` means for it.\n */\n refresh(): Promise<SessionRefreshOutcome<R>>;\n}\n\n// ── The signed-out steps ──────────────────────────────────────────────────────\n\n/**\n * Every `/auth/` call made without a session, the union of the three apps' lists\n * (§11.3): signing in (with its `2fa` and `set-password` steps, and a passkey's), up,\n * into a demo, a password reset, the verification link, the invitation check, the email\n * change's confirmation link, and the refresh itself. NOT `resend-verification` (KS 1):\n * in keksdose and Kurvenschmiede it needs a session; in kastlan it never answers 401.\n */\nconst SIGNED_OUT_STEPS = [\n \"login\",\n \"passkey/login\",\n \"register\",\n \"demo-session\",\n \"password-reset\",\n \"verify-email\",\n \"invitations/check\",\n \"me/email/confirm\",\n \"refresh\",\n] as const;\n\n/**\n * Whether a request's URL is a signed-out step — the kit's default list (§11.3), exported\n * for an app's tests and for an app's own `isSignedOutStep` that adds to it.\n *\n * **Whole steps only** (KS 2): the part of the path after its `auth/` segment, without\n * the query string, is the entry itself or starts with the entry and a `/`. So\n * `login/2fa` is `login`'s, but `register` never catches `/auth/passkey/register/begin`\n * (adding a passkey, which needs a session), `invitations/check` never\n * `/auth/invitations/accept`, and `me/email/confirm` never `/auth/me/email`. Relative and\n * absolute URLs alike; a URL with no `auth` segment is no step.\n */\nexport function isSignedOutStep(url: string): boolean {\n const path = url.replace(/[?#].*$/s, \"\").replace(/^([a-z][a-z\\d+.-]*:)?\\/\\/[^/]*/i, \"\");\n const segments = path.split(\"/\");\n const at = segments.indexOf(\"auth\");\n if (at < 0) return false;\n const step = segments.slice(at + 1).join(\"/\");\n return SIGNED_OUT_STEPS.some((entry) => step === entry || step.startsWith(`${entry}/`));\n}\n\n// ── The marker ────────────────────────────────────────────────────────────────\n\n/** On the request's own axios error, after a transient failure (decision 8). A\n * `Symbol.for` key: shared by every copy of the kit in a page, and left out of\n * `JSON.stringify`, so a log that serialises the error doesn't print it (kk 2). */\nconst TRANSIENT = Symbol.for(\"@eifi1/ui-kit:session-refresh.transient\");\n\n/**\n * Whether a request was refused because the refresh behind it failed TRANSIENTLY —\n * offline, a timeout, a 5xx — and not because the session is over (decision 8). The\n * error is the request's own 401, `err.response?.status` and every check an app already\n * has keep working; this reads the mark the kit put on it, so the app's message can say\n * \"can't reach the server\" instead of \"not signed in\". Read it FIRST: keksdose's sync\n * chip, kastlan's handover sync and Kurvenschmiede's `apiErrorMessage` count it as\n * offline (§11.5).\n *\n * Not the refresh's own error: its config carries the refresh token in its body, into any\n * log or crash report that keeps the error.\n */\nexport function isTransientRefreshFailure(error: unknown): boolean {\n return typeof error === \"object\" && error !== null && (error as Record<symbol, unknown>)[TRANSIENT] === true;\n}\n\n// ── The flight ────────────────────────────────────────────────────────────────\n\n/** A flight's result, as the interceptor needs it: `\"ended\"` keeps the session it ended. */\ntype Flight<R extends SessionRenewal> =\n | { result: \"renewed\"; renewal: R }\n | { result: \"ended\"; token: string | null }\n | { result: \"transient\" }\n | { result: \"changed\" };\n\n/** What one attempt came to. */\ntype Attempt<R> = { renewal: R } | \"definitive\" | \"retry\" | \"transient\";\n\n/** What {@link attachSessionRefresh} needs of a session. Kept off the public interface:\n * only what {@link createSessionRefresh} made can be attached. */\ninterface Engine {\n token(): string | null;\n isDemo(): boolean;\n isSignedOutStep(url: string): boolean;\n join(): Promise<Flight<SessionRenewal>>;\n end(token: string | null, demo: boolean): void;\n}\n\nconst engines = new WeakMap<SessionRefresh<SessionRenewal>, Engine>();\n\n/** The kit's own timeout, told apart from anything the app's `refresh` throws. */\nconst TIMED_OUT = Symbol(\"timed out\");\n\nfunction isOffline(): boolean {\n return typeof navigator !== \"undefined\" && navigator.onLine === false;\n}\n\n/** A refresh's throw, classified (§11.4). The status is read where `isRateLimited` reads\n * it — `httpStatusOf`, one probe for axios, ofetch and Node errors (DUP-11). */\nfunction classify(error: unknown): \"definitive\" | \"retry\" | \"transient\" {\n if (error === TIMED_OUT) return \"retry\";\n if (typeof error !== \"object\" || error === null) return \"transient\";\n const status = httpStatusOf(error);\n if (status === undefined) {\n // No answer: axios marks its own errors, and one without a response never reached a\n // server (offline, DNS, CORS, its own timeout). Anything else is a bug in `refresh`.\n return (error as { isAxiosError?: unknown }).isAxiosError === true ? \"retry\" : \"transient\";\n }\n if (status === 401 || status === 403) return \"definitive\";\n if (status === 408 || status === 429 || status >= 500) return \"retry\";\n return \"transient\";\n}\n\nconst sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));\n\n/**\n * Make the flight: `const session = createSessionRefresh({ refresh, store, currentToken,\n * isDemo, onEnded })`, then {@link attachSessionRefresh}`(api, session)`. One per tab.\n */\nexport function createSessionRefresh<R extends SessionRenewal>(options: SessionRefreshOptions<R>): SessionRefresh<R> {\n const { attempts = 3, backoffMs = [1000, 3000], timeoutMs = 15_000 } = options;\n // An empty string is no token: the stamp and the flight compare tokens, and \"\" against\n // `null` must not read as a change of session.\n const token = () => options.currentToken() || null;\n\n /** One call of the app's `refresh`, cut off after `timeoutMs` even if it ignores the\n * signal. */\n const attempt = async (): Promise<Attempt<R>> => {\n const controller = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n reject(TIMED_OUT);\n controller.abort();\n }, timeoutMs);\n });\n try {\n // Through `then`, so a `refresh` that throws before it returns a promise is\n // classified like one that rejects.\n const renewal = await Promise.race([Promise.resolve().then(() => options.refresh(controller.signal)), timeout]);\n return renewal ? { renewal } : \"definitive\";\n } catch (error) {\n return classify(error);\n } finally {\n clearTimeout(timer);\n }\n };\n\n const run = async (): Promise<Flight<R>> => {\n // The session this flight renews (§11.3 step 5).\n const noted = token();\n const changed = () => token() !== noted;\n let outcome = await attempt();\n for (let made = 1; outcome === \"retry\"; made += 1) {\n // No further attempt while the browser says it is offline, nor for a session that\n // is no longer the app's: whatever the answer, it would be \"changed\".\n if (made >= attempts || isOffline() || changed()) break;\n await sleep(backoffMs[made - 1] ?? backoffMs[backoffMs.length - 1] ?? 0);\n if (isOffline() || changed()) break;\n outcome = await attempt();\n }\n // A late answer never writes the old session back over a sign-out, a demo start or a\n // password change, and the old session's refusal never signs anyone out (KS 3).\n if (changed()) return { result: \"changed\" };\n if (outcome === \"definitive\") return { result: \"ended\", token: noted };\n if (outcome === \"retry\" || outcome === \"transient\") return { result: \"transient\" };\n options.store(outcome.renewal);\n return { result: \"renewed\", renewal: outcome.renewal };\n };\n\n let flight: Promise<Flight<R>> | null = null;\n const join = () => {\n // Cleared before any waiter resumes (`finally` settles first), so a waiter that\n // needs another flight starts a new one.\n flight ??= run().finally(() => {\n flight = null;\n });\n return flight;\n };\n\n /** The session `onEnded` was last called for: once per session (KS 8). */\n let ended: { token: string | null } | undefined;\n\n const session: SessionRefresh<R> = {\n async refresh() {\n const outcome = await join();\n return outcome.result === \"ended\" ? { result: \"ended\" } : outcome;\n },\n };\n engines.set(session, {\n token,\n isDemo: () => options.isDemo(),\n isSignedOutStep: options.isSignedOutStep ?? isSignedOutStep,\n join,\n end(endedToken, demo) {\n if (ended && ended.token === endedToken) return;\n ended = { token: endedToken };\n options.onEnded({ demo });\n },\n });\n return session;\n}\n\n// ── axios ─────────────────────────────────────────────────────────────────────\n\n/** The config fields the kit reads and writes — what axios's request config has. */\ninterface SessionRefreshAxiosConfig extends ServerWakeAxiosConfig {\n /** axios's `AxiosHeaders` (written through its `set`), or a plain record. */\n headers?: unknown;\n}\n\n/** An axios instance, structurally: {@link AxiosLikeInstance}, plus `request(config)` for\n * the replay. `axios.create()`'s result fits it as is. `NoInfer`, because axios's\n * `request` takes the WIDER `AxiosRequestConfig`: inferred from there, the config type\n * no longer fits the interceptors, which hand out `InternalAxiosRequestConfig`. */\ninterface SessionRefreshInstance<\n C extends SessionRefreshAxiosConfig,\n R extends { config: C },\n> extends AxiosLikeInstance<C, R> {\n request(config: NoInfer<C>): Promise<unknown>;\n}\n\n/** The keys the kit keeps on a request's config, as `attachServerWake` keeps\n * `_serverWake`: never a header. Plain string keys holding primitives, because axios\n * copies the config on a replay (`mergeConfig`) and a symbol key would be dropped. */\nconst STAMP = \"_sessionStamp\";\nconst REFRESHED = \"_sessionRefreshed\";\nconst RESTAMPED = \"_sessionRestamped\";\ntype Marked = {\n /** `currentToken()` when the request went out. */\n [STAMP]?: string | null;\n /** Replayed after a refresh: a 401 now is definitive (decision 3). */\n [REFRESHED]?: boolean;\n /** Replayed because it was sent with an older token: once for this reason. */\n [RESTAMPED]?: boolean;\n};\n\n/** `Authorization: Bearer <token>` on a config about to be replayed. The app's own\n * request interceptor runs again on the replay and writes the same, from the same\n * source. */\nfunction authorize(config: SessionRefreshAxiosConfig, token: string): void {\n const value = `Bearer ${token}`;\n const headers = config.headers;\n if (typeof headers !== \"object\" || headers === null) {\n config.headers = { Authorization: value };\n } else if (typeof (headers as { set?: unknown }).set === \"function\") {\n (headers as { set(name: string, value: string): unknown }).set(\"Authorization\", value);\n } else {\n (headers as Record<string, unknown>).Authorization = value;\n }\n}\n\n/**\n * Hook a session to an axios instance: `attachSessionRefresh(api, session)`. Adds a\n * request interceptor (the stamp) and a response interceptor (the 401), and returns a\n * function that ejects both.\n *\n * Attach it LAST (§11.3, kk 4): after `attachServerWake`, which asks to be first, and\n * after the app's own response interceptors (logging, a watchdog of its own). axios runs\n * response interceptors in the order they were added, and each of those must see the 401\n * settle before the refresh is awaited. The request side's order does not matter.\n *\n * On a 401, in this order (§11.3):\n *\n * 1. a signed-out step → reject, nothing else;\n * 2. sent with an older token → replayed with the app's token now, with no refresh, once\n * for this reason; with no token now, reject;\n * 3. a demo → `onEnded({ demo: true })`, reject;\n * 4. already replayed after a refresh → definitive: `onEnded({ demo: false })`, reject;\n * 5. otherwise it joins the flight, or starts it. A renewal replays it once with the\n * renewal's token (`replay: false`: rejects, unmarked); definitive ends the session;\n * transient rejects, marked; a session that changed meanwhile sends it through step\n * 2 again.\n *\n * Every rejection is the request's OWN error. Any other status passes through untouched.\n */\nexport function attachSessionRefresh<C extends SessionRefreshAxiosConfig, R extends { config: C }>(\n instance: SessionRefreshInstance<C, R>,\n session: SessionRefresh<SessionRenewal>,\n): () => void {\n const engine = engines.get(session);\n if (!engine) throw new TypeError(\"attachSessionRefresh takes the session createSessionRefresh made\");\n\n const onUnauthorized = async (error: object, config: C & Marked): Promise<unknown> => {\n if (engine.isSignedOutStep(config.url ?? \"\")) throw error;\n // A config the kit never stamped (sent before it was attached) counts as current.\n const stamp = config[STAMP] === undefined ? engine.token() : config[STAMP];\n const replay = (token: string) => {\n authorize(config, token);\n return instance.request(config);\n };\n for (;;) {\n const now = engine.token();\n // 2. The session it was sent for is gone, and whatever ended it has already done\n // what follows (ka 3, KS 3, KS 4).\n if (stamp !== now) {\n if (now === null || config[RESTAMPED]) throw error;\n config[RESTAMPED] = true;\n return replay(now);\n }\n // 3. A demo has nothing to refresh: its token lives as long as its account\n // (landing-demo §5.5).\n if (engine.isDemo()) {\n engine.end(now, true);\n throw error;\n }\n // 4. A replay for an older token doesn't count here: the app's current token may\n // simply have expired too, which needs a refresh, not a sign-out.\n if (config[REFRESHED]) {\n engine.end(now, false);\n throw error;\n }\n // 5. The flight.\n const flight = await engine.join();\n if (flight.result === \"changed\") continue;\n if (flight.result === \"ended\") {\n engine.end(flight.token, false);\n throw error;\n }\n if (flight.result === \"transient\") {\n (error as Record<symbol, unknown>)[TRANSIENT] = true;\n throw error;\n }\n if (flight.renewal.replay === false) throw error;\n config[REFRESHED] = true;\n return replay(flight.renewal.token);\n }\n };\n\n const request = instance.interceptors.request.use(\n (config) => {\n (config as C & Marked)[STAMP] = engine.token();\n return config;\n },\n (error) => Promise.reject(error),\n );\n const response = instance.interceptors.response.use(\n (res) => res,\n (error) => {\n const failed = error as { config?: C & Marked; response?: { status?: unknown } } | null;\n if (failed?.response?.status !== 401 || !failed.config) return Promise.reject(error);\n return onUnauthorized(failed, failed.config);\n },\n );\n return () => {\n instance.interceptors.request.eject(request);\n instance.interceptors.response.eject(response);\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/lib/session-refresh.ts"],"sourcesContent":["/**\n * Renewing a session when the access token runs out: the client half of\n * docs/auth-harmonization.md §11 (round 0.34 C7, KS U6), lifted from keksdose's\n * `refreshOnce` (Kurvenschmiede's is the same, line for line) and corrected where the\n * three apps' reviews found it wrong. It ends kastlan's sign-out over a network blip (B3).\n *\n * THE KIT OWNS THE RULE, THE APP OWNS THE CALL (§11.2, decision 1). The kit decides when\n * to refresh, who waits, what a failure means, when the session is over and whether a\n * renewal may still be stored. The app keeps its endpoint, its token storage, its\n * sign-out and where it navigates, and hands them in as {@link SessionRefreshOptions}.\n *\n * - **One flight per tab** (decision 2): every 401 that arrives while a refresh runs\n * waits for it, and so does `session.refresh()` (kastlan's boot).\n * - **Definitive** (decision 3): the device holds no refresh token (`refresh` resolves\n * `null`), the refresh answers 401 or 403, or the request replayed with the fresh\n * token gets a 401 again. Only then does the app sign out (`onEnded`), and only while\n * that session is still the app's.\n * - **Transient** (decision 4): everything else. The session stays: nothing is signed\n * out, stored or redirected, and the kit shows nothing. The request rejects with its\n * OWN 401, marked ({@link isTransientRefreshFailure}), so the app's message can say\n * \"can't reach the server\" instead of \"not signed in\".\n * - **Retries only where waiting helps** (decision 5, KS 7): no answer, the kit's own\n * timeout, 408, 429 and 5xx get up to three attempts, 1 s and then 3 s apart, each cut\n * off after 15 s; none while the browser reports itself offline. Any other answer, and\n * anything that is not an HTTP error, ends the flight at the first attempt.\n * - **A stale session is never written back** (§11.7, ka 3, KS 3, KS 4): every request\n * is stamped with `currentToken()` when it goes out, and every flight notes it when it\n * starts. A 401 to a request sent with an older token replays it with the app's token\n * now, without a refresh; a flight whose session changed while it ran stores nothing\n * and ends nothing. The access token is the session's identity because every change\n * the apps make replaces it — a refresh, another tab, a company switch, a password\n * change, a sign-in, a demo start, a sign-out.\n *\n * Framework-free like `server-wake.ts`: {@link attachSessionRefresh} takes the app's\n * axios instance as an ARGUMENT, typed structurally, so the kit never imports axios.\n */\n\nimport type { AxiosLikeInstance, ServerWakeAxiosConfig } from \"./server-wake\";\nimport { httpStatusOf } from \"./http-status\";\n\n/** What the app's `refresh` resolves: the new access token, plus whatever its `store`\n * needs (its refresh token, the user). */\nexport interface SessionRenewal {\n /** The new access token; the waiting requests are replayed with it. */\n token: string;\n /**\n * `false`: store the renewal, but replay nothing — each waiting request rejects with\n * its 401, unmarked. kastlan's refresh can land in another company (§11.5, ka 3): a\n * waiting write, such as the handover's `POST /sync`, must not be replayed into it.\n */\n replay?: false;\n}\n\n/** What the app hands {@link createSessionRefresh} (§11.4). */\nexport interface SessionRefreshOptions<R extends SessionRenewal> {\n /**\n * The app's own `POST /auth/refresh`, on a BARE client — never the instance the kit is\n * attached to, so the refresh cannot re-enter the interceptor. It **writes nothing**:\n * the kit hands the answer to `store`, and only while the session is unchanged.\n *\n * Resolves the answer as a renewal, or `null` when the device holds no refresh token.\n * Throws on failure, and the kit classifies the throw: a 401 or 403 is definitive; no\n * answer (an axios error without a `response`), the kit's own timeout, 408, 429 and\n * 5xx are transient and retried; any other answer, and anything that is not an HTTP\n * error (a bug in this function), is transient and not retried. The status is read\n * from `err.response.status` (axios), `err.status` or `err.statusCode`.\n *\n * `signal` aborts when the attempt runs past `timeoutMs`; pass it to the request.\n */\n refresh: (signal: AbortSignal) => Promise<R | null>;\n /** Writes the renewal: the tokens and the user (a missing refresh token keeps the\n * stored one). Called only while the session the flight renews is still the app's,\n * and before the waiting requests are replayed — so it writes the token SYNCHRONOUSLY\n * where `currentToken` reads it: the replay goes through the app's own request\n * interceptor again, and is stamped then. A throw is the app's bug: the waiting\n * requests and `session.refresh()` reject with it. */\n store: (renewal: R) => void;\n /** The access token the app sends NOW, read where its request interceptor reads it;\n * `null` when there is none. */\n currentToken: () => string | null;\n /** Whether the session is a demo: a demo's 401 ends it, with no refresh tried\n * (landing-demo §5.5). */\n isDemo: () => boolean;\n /**\n * The session is over: drop the tokens and the user, then navigate — `/demo/ended` for\n * a demo, else the sign-in with the page it came from (auth §5.2). Called once per\n * session, however many requests found out (KS 8), and never by `session.refresh()`.\n */\n onEnded: (ended: { demo: boolean }) => void;\n /** Whether a URL is a signed-out step, whose 401 judges the form and never the\n * session. Replaces the default list, {@link isSignedOutStep}. */\n isSignedOutStep?: (url: string) => boolean;\n /** Attempts for a refresh that got no answer, timed out, or got a 408, 429 or 5xx.\n * Default 3. */\n attempts?: number;\n /** The waits before the second and later attempts; the last one repeats. Default\n * `[1000, 3000]`. */\n backoffMs?: readonly number[];\n /** Each attempt is cut off after this, and its `signal` aborted. Default 15 000. */\n timeoutMs?: number;\n}\n\n/** What `session.refresh()` resolves (§11.4), by `result`:\n *\n * | `result` | When | What the kit has done |\n * |---|---|---|\n * | `\"renewed\"` | a renewal while the session was unchanged | `store(renewal)` |\n * | `\"ended\"` | definitive | nothing; `onEnded` only for requests that waited on the same flight |\n * | `\"transient\"` | transient | nothing |\n * | `\"changed\"` | the session changed while it ran | nothing; read the app's own state |\n */\nexport type SessionRefreshOutcome<R extends SessionRenewal = SessionRenewal> =\n { result: \"renewed\"; renewal: R } | { result: \"ended\" } | { result: \"transient\" } | { result: \"changed\" };\n\n/** The flight {@link createSessionRefresh} makes; {@link attachSessionRefresh} hooks it\n * to an axios instance. */\nexport interface SessionRefresh<R extends SessionRenewal = SessionRenewal> {\n /**\n * The same flight, for a caller outside the interceptor — kastlan's boot. Joins a\n * running flight, or starts one. Never calls `onEnded`: the caller decides what an\n * `\"ended\"` means for it.\n */\n refresh(): Promise<SessionRefreshOutcome<R>>;\n}\n\n// ── The signed-out steps ──────────────────────────────────────────────────────\n\n/**\n * Every `/auth/` call made without a session, the union of the three apps' lists\n * (§11.3): signing in (with its `2fa` and `set-password` steps, and a passkey's), up,\n * into a demo, a password reset, the verification link, the invitation check, the email\n * change's confirmation link, and the refresh itself. NOT `resend-verification` (KS 1):\n * in keksdose and Kurvenschmiede it needs a session; in kastlan it never answers 401.\n */\nconst SIGNED_OUT_STEPS = [\n \"login\",\n \"passkey/login\",\n \"register\",\n \"demo-session\",\n \"password-reset\",\n \"verify-email\",\n \"invitations/check\",\n \"me/email/confirm\",\n \"refresh\",\n] as const;\n\n/**\n * Whether a request's URL is a signed-out step — the kit's default list (§11.3), exported\n * for an app's tests and for an app's own `isSignedOutStep` that adds to it.\n *\n * **Whole steps only** (KS 2): the part of the path after its `auth/` segment, without\n * the query string, is the entry itself or starts with the entry and a `/`. So\n * `login/2fa` is `login`'s, but `register` never catches `/auth/passkey/register/begin`\n * (adding a passkey, which needs a session), `invitations/check` never\n * `/auth/invitations/accept`, and `me/email/confirm` never `/auth/me/email`. Relative and\n * absolute URLs alike; a URL with no `auth` segment is no step.\n */\nexport function isSignedOutStep(url: string): boolean {\n const path = url.replace(/[?#].*$/s, \"\").replace(/^([a-z][a-z\\d+.-]*:)?\\/\\/[^/]*/i, \"\");\n const segments = path.split(\"/\");\n const at = segments.indexOf(\"auth\");\n if (at < 0) return false;\n const step = segments.slice(at + 1).join(\"/\");\n return SIGNED_OUT_STEPS.some((entry) => step === entry || step.startsWith(`${entry}/`));\n}\n\n// ── The marker ────────────────────────────────────────────────────────────────\n\n/** On the request's own axios error, after a transient failure (decision 8). A\n * `Symbol.for` key: shared by every copy of the kit in a page, and left out of\n * `JSON.stringify`, so a log that serialises the error doesn't print it (kk 2). */\nconst TRANSIENT = Symbol.for(\"@eifi1/ui-kit:session-refresh.transient\");\n\n/**\n * Whether a request was refused because the refresh behind it failed TRANSIENTLY —\n * offline, a timeout, a 5xx — and not because the session is over (decision 8). The\n * error is the request's own 401, `err.response?.status` and every check an app already\n * has keep working; this reads the mark the kit put on it, so the app's message can say\n * \"can't reach the server\" instead of \"not signed in\". Read it FIRST: keksdose's sync\n * chip, kastlan's handover sync and Kurvenschmiede's `apiErrorMessage` count it as\n * offline (§11.5).\n *\n * Not the refresh's own error: its config carries the refresh token in its body, into any\n * log or crash report that keeps the error.\n */\nexport function isTransientRefreshFailure(error: unknown): boolean {\n return typeof error === \"object\" && error !== null && (error as Record<symbol, unknown>)[TRANSIENT] === true;\n}\n\n// ── The flight ────────────────────────────────────────────────────────────────\n\n/** A flight's result, as the interceptor needs it: `\"ended\"` keeps the session it ended. */\ntype Flight<R extends SessionRenewal> =\n | { result: \"renewed\"; renewal: R }\n | { result: \"ended\"; token: string | null }\n | { result: \"transient\" }\n | { result: \"changed\" };\n\n/** What one attempt came to. */\ntype Attempt<R> = { renewal: R } | \"definitive\" | \"retry\" | \"transient\";\n\n/** What {@link attachSessionRefresh} needs of a session. Kept off the public interface:\n * only what {@link createSessionRefresh} made can be attached. */\ninterface Engine {\n token(): string | null;\n isDemo(): boolean;\n isSignedOutStep(url: string): boolean;\n join(): Promise<Flight<SessionRenewal>>;\n end(token: string | null, demo: boolean): void;\n}\n\nconst engines = new WeakMap<SessionRefresh<SessionRenewal>, Engine>();\n\n/** The kit's own timeout, told apart from anything the app's `refresh` throws. */\nconst TIMED_OUT = Symbol(\"timed out\");\n\nfunction isOffline(): boolean {\n return typeof navigator !== \"undefined\" && navigator.onLine === false;\n}\n\n/** A refresh's throw, classified (§11.4). The status is read where `isRateLimited` reads\n * it — `httpStatusOf`, one probe for axios, ofetch and Node errors (DUP-11). */\nfunction classify(error: unknown): \"definitive\" | \"retry\" | \"transient\" {\n if (error === TIMED_OUT) return \"retry\";\n if (typeof error !== \"object\" || error === null) return \"transient\";\n const status = httpStatusOf(error);\n if (status === undefined) {\n // No answer: axios marks its own errors, and one without a response never reached a\n // server (offline, DNS, CORS, its own timeout). Anything else is a bug in `refresh`.\n return (error as { isAxiosError?: unknown }).isAxiosError === true ? \"retry\" : \"transient\";\n }\n if (status === 401 || status === 403) return \"definitive\";\n if (status === 408 || status === 429 || status >= 500) return \"retry\";\n return \"transient\";\n}\n\nconst sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));\n\n/**\n * Make the flight: `const session = createSessionRefresh({ refresh, store, currentToken,\n * isDemo, onEnded })`, then {@link attachSessionRefresh}`(api, session)`. One per tab.\n */\nexport function createSessionRefresh<R extends SessionRenewal>(options: SessionRefreshOptions<R>): SessionRefresh<R> {\n const { attempts = 3, backoffMs = [1000, 3000], timeoutMs = 15_000 } = options;\n // An empty string is no token: the stamp and the flight compare tokens, and \"\" against\n // `null` must not read as a change of session.\n const token = () => options.currentToken() || null;\n\n /** One call of the app's `refresh`, cut off after `timeoutMs` even if it ignores the\n * signal. */\n const attempt = async (): Promise<Attempt<R>> => {\n const controller = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n reject(TIMED_OUT);\n controller.abort();\n }, timeoutMs);\n });\n try {\n // Through `then`, so a `refresh` that throws before it returns a promise is\n // classified like one that rejects.\n const renewal = await Promise.race([Promise.resolve().then(() => options.refresh(controller.signal)), timeout]);\n return renewal ? { renewal } : \"definitive\";\n } catch (error) {\n return classify(error);\n } finally {\n clearTimeout(timer);\n }\n };\n\n const run = async (): Promise<Flight<R>> => {\n // The session this flight renews (§11.3 step 5).\n const noted = token();\n const changed = () => token() !== noted;\n let outcome = await attempt();\n for (let made = 1; outcome === \"retry\"; made += 1) {\n // No further attempt while the browser says it is offline, nor for a session that\n // is no longer the app's: whatever the answer, it would be \"changed\".\n if (made >= attempts || isOffline() || changed()) break;\n await sleep(backoffMs[made - 1] ?? backoffMs[backoffMs.length - 1] ?? 0);\n if (isOffline() || changed()) break;\n outcome = await attempt();\n }\n // A late answer never writes the old session back over a sign-out, a demo start or a\n // password change, and the old session's refusal never signs anyone out (KS 3).\n if (changed()) return { result: \"changed\" };\n if (outcome === \"definitive\") return { result: \"ended\", token: noted };\n if (outcome === \"retry\" || outcome === \"transient\") return { result: \"transient\" };\n options.store(outcome.renewal);\n return { result: \"renewed\", renewal: outcome.renewal };\n };\n\n let flight: Promise<Flight<R>> | null = null;\n const join = () => {\n // Cleared before any waiter resumes (`finally` settles first), so a waiter that\n // needs another flight starts a new one.\n flight ??= run().finally(() => {\n flight = null;\n });\n return flight;\n };\n\n /** The session `onEnded` was last called for: once per session (KS 8). */\n let ended: { token: string | null } | undefined;\n\n const session: SessionRefresh<R> = {\n async refresh() {\n const outcome = await join();\n return outcome.result === \"ended\" ? { result: \"ended\" } : outcome;\n },\n };\n engines.set(session, {\n token,\n isDemo: () => options.isDemo(),\n isSignedOutStep: options.isSignedOutStep ?? isSignedOutStep,\n join,\n end(endedToken, demo) {\n if (ended && ended.token === endedToken) return;\n ended = { token: endedToken };\n options.onEnded({ demo });\n },\n });\n return session;\n}\n\n// ── axios ─────────────────────────────────────────────────────────────────────\n\n/** The config fields the kit reads and writes — what axios's request config has. */\ninterface SessionRefreshAxiosConfig extends ServerWakeAxiosConfig {\n /** axios's `AxiosHeaders` (written through its `set`), or a plain record. */\n headers?: unknown;\n}\n\n/** An axios instance, structurally: {@link AxiosLikeInstance}, plus `request(config)` for\n * the replay. `axios.create()`'s result fits it as is. `NoInfer`, because axios's\n * `request` takes the WIDER `AxiosRequestConfig`: inferred from there, the config type\n * no longer fits the interceptors, which hand out `InternalAxiosRequestConfig`. */\ninterface SessionRefreshInstance<\n C extends SessionRefreshAxiosConfig,\n R extends { config: C },\n> extends AxiosLikeInstance<C, R> {\n request(config: NoInfer<C>): Promise<unknown>;\n}\n\n/** The keys the kit keeps on a request's config, as `attachServerWake` keeps\n * `_serverWake`: never a header. Plain string keys holding primitives, because axios\n * copies the config on a replay (`mergeConfig`) and a symbol key would be dropped. */\nconst STAMP = \"_sessionStamp\";\nconst REFRESHED = \"_sessionRefreshed\";\nconst RESTAMPED = \"_sessionRestamped\";\ntype Marked = {\n /** `currentToken()` when the request went out. */\n [STAMP]?: string | null;\n /** Replayed after a refresh: a 401 now is definitive (decision 3). */\n [REFRESHED]?: boolean;\n /** Replayed because it was sent with an older token: once for this reason. */\n [RESTAMPED]?: boolean;\n};\n\n/** `Authorization: Bearer <token>` on a config about to be replayed. The app's own\n * request interceptor runs again on the replay and writes the same, from the same\n * source. */\nfunction authorize(config: SessionRefreshAxiosConfig, token: string): void {\n const value = `Bearer ${token}`;\n const headers = config.headers;\n if (typeof headers !== \"object\" || headers === null) {\n config.headers = { Authorization: value };\n } else if (typeof (headers as { set?: unknown }).set === \"function\") {\n (headers as { set(name: string, value: string): unknown }).set(\"Authorization\", value);\n } else {\n (headers as Record<string, unknown>).Authorization = value;\n }\n}\n\n/**\n * Hook a session to an axios instance: `attachSessionRefresh(api, session)`. Adds a\n * request interceptor (the stamp) and a response interceptor (the 401), and returns a\n * function that ejects both.\n *\n * Attach it LAST (§11.3, kk 4): after `attachServerWake`, which asks to be first, and\n * after the app's own response interceptors (logging, a watchdog of its own). axios runs\n * response interceptors in the order they were added, and each of those must see the 401\n * settle before the refresh is awaited. The request side's order does not matter.\n *\n * On a 401, in this order (§11.3):\n *\n * 1. a signed-out step → reject, nothing else;\n * 2. sent with an older token → replayed with the app's token now, with no refresh, once\n * for this reason; with no token now, reject;\n * 3. a demo → `onEnded({ demo: true })`, reject;\n * 4. already replayed after a refresh → definitive: `onEnded({ demo: false })`, reject;\n * 5. otherwise it joins the flight, or starts it. A renewal replays it once with the\n * renewal's token (`replay: false`: rejects, unmarked); definitive ends the session;\n * transient rejects, marked; a session that changed meanwhile sends it through step\n * 2 again.\n *\n * Every rejection is the request's OWN error. Any other status passes through untouched.\n */\nexport function attachSessionRefresh<C extends SessionRefreshAxiosConfig, R extends { config: C }>(\n instance: SessionRefreshInstance<C, R>,\n session: SessionRefresh<SessionRenewal>,\n): () => void {\n const engine = engines.get(session);\n if (!engine) throw new TypeError(\"attachSessionRefresh takes the session createSessionRefresh made\");\n\n const onUnauthorized = async (error: object, config: C & Marked): Promise<unknown> => {\n if (engine.isSignedOutStep(config.url ?? \"\")) throw error;\n // A config the kit never stamped (sent before it was attached) counts as current.\n const stamp = config[STAMP] === undefined ? engine.token() : config[STAMP];\n const replay = (token: string) => {\n authorize(config, token);\n return instance.request(config);\n };\n for (;;) {\n const now = engine.token();\n // 2. The session it was sent for is gone, and whatever ended it has already done\n // what follows (ka 3, KS 3, KS 4).\n if (stamp !== now) {\n if (now === null || config[RESTAMPED]) throw error;\n config[RESTAMPED] = true;\n return replay(now);\n }\n // 3. A demo has nothing to refresh: its token lives as long as its account\n // (landing-demo §5.5).\n if (engine.isDemo()) {\n engine.end(now, true);\n throw error;\n }\n // 4. A replay for an older token doesn't count here: the app's current token may\n // simply have expired too, which needs a refresh, not a sign-out.\n if (config[REFRESHED]) {\n engine.end(now, false);\n throw error;\n }\n // 5. The flight.\n const flight = await engine.join();\n if (flight.result === \"changed\") continue;\n if (flight.result === \"ended\") {\n engine.end(flight.token, false);\n throw error;\n }\n if (flight.result === \"transient\") {\n (error as Record<symbol, unknown>)[TRANSIENT] = true;\n throw error;\n }\n if (flight.renewal.replay === false) throw error;\n config[REFRESHED] = true;\n return replay(flight.renewal.token);\n }\n };\n\n const request = instance.interceptors.request.use(\n (config) => {\n (config as C & Marked)[STAMP] = engine.token();\n return config;\n },\n (error) => Promise.reject(error),\n );\n const response = instance.interceptors.response.use(\n (res) => res,\n (error) => {\n const failed = error as { config?: C & Marked; response?: { status?: unknown } } | null;\n if (failed?.response?.status !== 401 || !failed.config) return Promise.reject(error);\n return onUnauthorized(failed, failed.config);\n },\n );\n return () => {\n instance.interceptors.request.eject(request);\n instance.interceptors.response.eject(response);\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * How a chart says a share of its whole — PieChart's slices and centre, Treemap's tiles
3
+ * and groups. Internal, not exported from the barrel.
4
+ *
5
+ * Each chart held a copy of the same two `Intl` formats and the same switch between them
6
+ * (0.52 code review of treemap.tsx, finding 9); one copy is the rule both say.
7
+ */
8
+ /**
9
+ * A formatter for shares (0–1) in `locale`, as a percent: one decimal under 10 %, where
10
+ * rounding to whole percents starts to lie — a slice of 0.4 % said as "0%" says it is not
11
+ * there — and whole percents above. Make it once per locale (it builds two `Intl` formats).
12
+ */
13
+ export declare function shareFormat(locale: string | undefined): (share: number) => string;
@@ -0,0 +1,10 @@
1
+ "use client";
2
+ function shareFormat(locale) {
3
+ const whole = new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 0 });
4
+ const fine = new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 1 });
5
+ return (share) => (share < 0.1 ? fine : whole).format(share);
6
+ }
7
+ export {
8
+ shareFormat
9
+ };
10
+ //# sourceMappingURL=share-format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/share-format.ts"],"sourcesContent":["/**\n * How a chart says a share of its whole — PieChart's slices and centre, Treemap's tiles\n * and groups. Internal, not exported from the barrel.\n *\n * Each chart held a copy of the same two `Intl` formats and the same switch between them\n * (0.52 code review of treemap.tsx, finding 9); one copy is the rule both say.\n */\n\n/**\n * A formatter for shares (0–1) in `locale`, as a percent: one decimal under 10 %, where\n * rounding to whole percents starts to lie — a slice of 0.4 % said as \"0%\" says it is not\n * there — and whole percents above. Make it once per locale (it builds two `Intl` formats).\n */\nexport function shareFormat(locale: string | undefined): (share: number) => string {\n const whole = new Intl.NumberFormat(locale, { style: \"percent\", maximumFractionDigits: 0 });\n const fine = new Intl.NumberFormat(locale, { style: \"percent\", maximumFractionDigits: 1 });\n return (share) => (share < 0.1 ? fine : whole).format(share);\n}\n"],"mappings":";AAaO,SAAS,YAAY,QAAuD;AACjF,QAAM,QAAQ,IAAI,KAAK,aAAa,QAAQ,EAAE,OAAO,WAAW,uBAAuB,EAAE,CAAC;AAC1F,QAAM,OAAO,IAAI,KAAK,aAAa,QAAQ,EAAE,OAAO,WAAW,uBAAuB,EAAE,CAAC;AACzF,SAAO,CAAC,WAAW,QAAQ,MAAM,OAAO,OAAO,OAAO,KAAK;AAC7D;","names":[]}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Upper-case the first letter only, in `locale` — the one copy of the rule (refactor audit
3
+ * 2026-10-30, DUP-11: MonthPicker and JumpCalendar each had it, and MiniCalendar's caption
4
+ * still used the CSS `capitalize` it was written against).
5
+ *
6
+ * Several locales (fr, es, it, pt, …) write month names in lower case, which at the head
7
+ * of a field or a panel reads as a typo. But only the FIRST letter is ours to change:
8
+ * `capitalize` upper-cases every word, so Spanish read "Septiembre De 2026", and a
9
+ * caption that does not start with the month kept its wrong capital — Hungarian read
10
+ * "2026. Szeptember". This gives "Septiembre de 2026" and "2026. szeptember".
11
+ *
12
+ * In `locale` because the case mapping is the language's: Turkish upper-cases "i" to "İ".
13
+ * `undefined` is the runtime's default, as everywhere `Intl` takes one.
14
+ *
15
+ * Package-private — not re-exported from the barrel.
16
+ */
17
+ export declare function upperFirst(text: string, locale: string | undefined): string;
@@ -0,0 +1,8 @@
1
+ "use client";
2
+ function upperFirst(text, locale) {
3
+ return text.charAt(0).toLocaleUpperCase(locale) + text.slice(1);
4
+ }
5
+ export {
6
+ upperFirst
7
+ };
8
+ //# sourceMappingURL=text-case.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/text-case.ts"],"sourcesContent":["/**\n * Upper-case the first letter only, in `locale` — the one copy of the rule (refactor audit\n * 2026-10-30, DUP-11: MonthPicker and JumpCalendar each had it, and MiniCalendar's caption\n * still used the CSS `capitalize` it was written against).\n *\n * Several locales (fr, es, it, pt, …) write month names in lower case, which at the head\n * of a field or a panel reads as a typo. But only the FIRST letter is ours to change:\n * `capitalize` upper-cases every word, so Spanish read \"Septiembre De 2026\", and a\n * caption that does not start with the month kept its wrong capital — Hungarian read\n * \"2026. Szeptember\". This gives \"Septiembre de 2026\" and \"2026. szeptember\".\n *\n * In `locale` because the case mapping is the language's: Turkish upper-cases \"i\" to \"İ\".\n * `undefined` is the runtime's default, as everywhere `Intl` takes one.\n *\n * Package-private — not re-exported from the barrel.\n */\nexport function upperFirst(text: string, locale: string | undefined): string {\n return text.charAt(0).toLocaleUpperCase(locale) + text.slice(1);\n}\n"],"mappings":";AAgBO,SAAS,WAAW,MAAc,QAAoC;AAC3E,SAAO,KAAK,OAAO,CAAC,EAAE,kBAAkB,MAAM,IAAI,KAAK,MAAM,CAAC;AAChE;","names":[]}
@@ -68,3 +68,32 @@ export declare function rankMatch(label: string, query: string, { code, locale }
68
68
  code?: string;
69
69
  locale?: string;
70
70
  }): MatchRank;
71
+ /** A row {@link searchRanked} kept, and how well it answers the WHOLE query:
72
+ * {@link rankMatch}'s rank, or `3` for a row that holds the query's words apart
73
+ * ("Zürich Migros" for "migros zürich"). */
74
+ export interface RankedHit<T> {
75
+ row: T;
76
+ rank: Exclude<MatchRank, -1> | 3;
77
+ }
78
+ /**
79
+ * The rows that hold every word of `query`, each ranked for the WHOLE query — for the
80
+ * lists that FILTER by words ({@link matchesSearch}) and RANK by the whole query
81
+ * ({@link rankMatch}): Combobox, CountrySelect and InlineEntityCombobox (refactor audit
82
+ * 2026-10-30, DUP-4). In the order given; sort by `rank` with a stable sort, and the rows
83
+ * of one rank keep it. No words keep every row, at `0`.
84
+ *
85
+ * Each row's texts are folded ONCE, and the query once. The lists asked `matchesSearch`
86
+ * and then `rankMatch` of each row they kept, which folded the row three times and the
87
+ * query once per row, per keystroke — InlineEntityCombobox asked `rankMatch` twice of
88
+ * most rows (0.52 code review, combobox.tsx).
89
+ *
90
+ * `texts(row)`: where a word may stand; the FIRST is the label the rank is for.
91
+ * `code(row)`: a key that answers only WHOLE — a word that IS the code matches the row,
92
+ * and the code as the whole query ranks `0` ("ch" is Switzerland before it is Chad) —
93
+ * never a letter of it ("w" is not Aruba, AW: 0.52 code review, country-select.tsx).
94
+ * Internal: not in the barrel.
95
+ */
96
+ export declare function searchRanked<T>(rows: readonly T[], query: string, texts: (row: T) => readonly (string | undefined)[], { code, locale }?: {
97
+ code?: (row: T) => string;
98
+ locale?: string;
99
+ }): RankedHit<T>[];
@@ -22,16 +22,34 @@ function matchesSearch(words, texts, locale) {
22
22
  function rankMatch(label, query, { code, locale } = {}) {
23
23
  const q = foldForSearch(query.trim(), locale);
24
24
  if (!q) return 0;
25
- const text = foldForSearch(label, locale);
26
- if (text === q || code !== void 0 && foldForSearch(code, locale) === q) return 0;
25
+ return rankFolded(foldForSearch(label, locale), q, code === void 0 ? void 0 : foldForSearch(code, locale));
26
+ }
27
+ function rankFolded(text, q, code) {
28
+ if (text === q || code === q) return 0;
27
29
  if (text.startsWith(q)) return 1;
28
30
  return text.includes(q) ? 2 : -1;
29
31
  }
32
+ function searchRanked(rows, query, texts, { code, locale } = {}) {
33
+ const q = foldForSearch(query.trim(), locale);
34
+ const words = q.split(/\s+/).filter((word) => word !== "");
35
+ if (words.length === 0) return rows.map((row) => ({ row, rank: 0 }));
36
+ const hits = [];
37
+ for (const row of rows) {
38
+ const folded = texts(row).filter((t) => t !== void 0).map((t) => foldForSearch(t, locale));
39
+ const key = code === void 0 ? void 0 : foldForSearch(code(row), locale);
40
+ const hay = folded.join("\n");
41
+ if (!words.every((word) => word === key || hay.includes(word))) continue;
42
+ const rank = rankFolded(folded[0] ?? "", q, key);
43
+ hits.push({ row, rank: rank === -1 ? 3 : rank });
44
+ }
45
+ return hits;
46
+ }
30
47
  export {
31
48
  foldForSearch,
32
49
  matchesSearch,
33
50
  rankMatch,
34
51
  sameName,
52
+ searchRanked,
35
53
  searchWords
36
54
  };
37
55
  //# sourceMappingURL=text-match.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/lib/text-match.ts"],"sourcesContent":["/**\n * How typing into a kit list matches its rows — one rule for every list (DUP-5, refactor\n * audit 2026-10).\n *\n * There were six. \"zurich\" found Zürich in CountrySelect and the command palette and in no\n * EntityCombobox, Combobox, MultiSelect or DataTable text filter, where keksdose's\n * Swiss-German payees and categories and kastlan's eighteen entity pickers live; \"İ\"\n * folded by the locale in GroupedPicker and nowhere in the combobox family. The folding\n * only ever widens a match, so moving every list onto it took nothing away.\n */\n\n/**\n * A text as search compares it: the locale's lower case, accents off, ß spelt \"ss\" —\n * \"Zürich\" and \"ZURICH\" both read \"zurich\", \"Straße\" reads \"strasse\". Punctuation and\n * symbols stay: a currency is found by its \"€\", and \"c++\" is not the empty query.\n *\n * Lower-cased by the locale FIRST, then decomposed: the Turkish \"İ\" lower-cases to \"i\"\n * and the dotted capital's mark is then dropped like any other, so \"istanbul\" finds\n * \"İSTANBUL\" in every locale, while \"I\" folds to the dotless \"ı\" in Turkish alone.\n * `locale` left out is the runtime's default; a malformed tag folds as if left out.\n */\nexport function foldForSearch(text: string, locale?: string): string {\n let lower: string;\n try {\n lower = text.toLocaleLowerCase(locale);\n } catch {\n lower = text.toLocaleLowerCase();\n }\n return lower\n .normalize(\"NFD\")\n .replace(/\\p{M}+/gu, \"\")\n .replace(/ß/g, \"ss\");\n}\n\n/**\n * Whether two names are one name as a picker's search reads them: folded\n * ({@link foldForSearch}) and trimmed — \"cafe\" is Café, \" MIGROS \" is Migros. For \"does\n * the typed text name a record that exists\" (a create row, InlineEntityCombobox's blur\n * commit), which compared with plain `toLowerCase` while the list beside it folded, so\n * \"cafe\" listed Café as a hit and offered \"Create “cafe”\" too — a duplicate payee or shop\n * chain one Enter away (refactor audit 2026-10-30, DUP-5). Internal: not in the barrel.\n */\nexport function sameName(a: string, b: string, locale?: string): boolean {\n return foldForSearch(a.trim(), locale) === foldForSearch(b.trim(), locale);\n}\n\n/**\n * A picker's query as its words, each folded (0.51.4, keksdose's /prices shop picker):\n * \"Migros Zürich\" → `[\"migros\", \"zurich\"]`. An empty or blank query has no words.\n */\nexport function searchWords(query: string, locale?: string): string[] {\n return foldForSearch(query, locale)\n .split(/\\s+/)\n .filter((word) => word !== \"\");\n}\n\n/**\n * Whether a row matches a picker's query: EVERY word appears in at least one of the row's\n * texts — \"migros 8004\" finds a row named \"Migros · 8004 Zürich\", and a row whose chain is\n * its group and whose place is its label (pass both). Until 0.51.4 the whole query had to\n * be ONE substring of one text, so two words found nothing unless they stood side by side\n * in that order. One word matches exactly as before, and so does the old query when it\n * was a substring. No words match everything.\n *\n * `words` from {@link searchWords}; `texts` as they are (folded here), `undefined` skipped.\n *\n * Exported since 0.51.5 for an app's own async source: a picker shows `loadOptions`'\n * rows as they come, so a source that narrows on the client filters with this, the same\n * rule the kit applies to `options` (keksdose's shop picker past its first hundred shops).\n */\nexport function matchesSearch(\n words: readonly string[],\n texts: readonly (string | undefined)[],\n locale?: string,\n): boolean {\n if (words.length === 0) return true;\n // One haystack per row; a word holds no whitespace, so it cannot match across the\n // newline between two texts.\n const hay = texts\n .filter((t): t is string => t !== undefined)\n .map((t) => foldForSearch(t, locale))\n .join(\"\\n\");\n return words.every((word) => hay.includes(word));\n}\n\n/** {@link rankMatch}'s answer: lower is better, `-1` is no match. */\nexport type MatchRank = -1 | 0 | 1 | 2;\n\n/**\n * How well `label` answers `query` — CountrySelect's ranking, for every list that orders\n * its hits:\n *\n * - `0`: the query IS the row — its `code` (\"ch\" means Switzerland before it means Chad)\n * or the whole label;\n * - `1`: the label starts with it;\n * - `2`: the label contains it;\n * - `-1`: no match.\n *\n * Both sides are folded here ({@link foldForSearch}, by `locale`), so pass them as typed\n * and as shown. An empty query matches every row alike (`0`). Sort by the rank with a\n * stable sort, and rows of one rank keep the order they came in.\n */\nexport function rankMatch(\n label: string,\n query: string,\n { code, locale }: { code?: string; locale?: string } = {},\n): MatchRank {\n const q = foldForSearch(query.trim(), locale);\n if (!q) return 0;\n const text = foldForSearch(label, locale);\n if (text === q || (code !== undefined && foldForSearch(code, locale) === q)) return 0;\n if (text.startsWith(q)) return 1;\n return text.includes(q) ? 2 : -1;\n}\n"],"mappings":";AAqBO,SAAS,cAAc,MAAc,QAAyB;AACnE,MAAI;AACJ,MAAI;AACF,YAAQ,KAAK,kBAAkB,MAAM;AAAA,EACvC,QAAQ;AACN,YAAQ,KAAK,kBAAkB;AAAA,EACjC;AACA,SAAO,MACJ,UAAU,KAAK,EACf,QAAQ,YAAY,EAAE,EACtB,QAAQ,MAAM,IAAI;AACvB;AAUO,SAAS,SAAS,GAAW,GAAW,QAA0B;AACvE,SAAO,cAAc,EAAE,KAAK,GAAG,MAAM,MAAM,cAAc,EAAE,KAAK,GAAG,MAAM;AAC3E;AAMO,SAAS,YAAY,OAAe,QAA2B;AACpE,SAAO,cAAc,OAAO,MAAM,EAC/B,MAAM,KAAK,EACX,OAAO,CAAC,SAAS,SAAS,EAAE;AACjC;AAgBO,SAAS,cACd,OACA,OACA,QACS;AACT,MAAI,MAAM,WAAW,EAAG,QAAO;AAG/B,QAAM,MAAM,MACT,OAAO,CAAC,MAAmB,MAAM,MAAS,EAC1C,IAAI,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,EACnC,KAAK,IAAI;AACZ,SAAO,MAAM,MAAM,CAAC,SAAS,IAAI,SAAS,IAAI,CAAC;AACjD;AAmBO,SAAS,UACd,OACA,OACA,EAAE,MAAM,OAAO,IAAwC,CAAC,GAC7C;AACX,QAAM,IAAI,cAAc,MAAM,KAAK,GAAG,MAAM;AAC5C,MAAI,CAAC,EAAG,QAAO;AACf,QAAM,OAAO,cAAc,OAAO,MAAM;AACxC,MAAI,SAAS,KAAM,SAAS,UAAa,cAAc,MAAM,MAAM,MAAM,EAAI,QAAO;AACpF,MAAI,KAAK,WAAW,CAAC,EAAG,QAAO;AAC/B,SAAO,KAAK,SAAS,CAAC,IAAI,IAAI;AAChC;","names":[]}
1
+ {"version":3,"sources":["../../src/lib/text-match.ts"],"sourcesContent":["/**\n * How typing into a kit list matches its rows — one rule for every list (DUP-5, refactor\n * audit 2026-10).\n *\n * There were six. \"zurich\" found Zürich in CountrySelect and the command palette and in no\n * EntityCombobox, Combobox, MultiSelect or DataTable text filter, where keksdose's\n * Swiss-German payees and categories and kastlan's eighteen entity pickers live; \"İ\"\n * folded by the locale in GroupedPicker and nowhere in the combobox family. The folding\n * only ever widens a match, so moving every list onto it took nothing away.\n */\n\n/**\n * A text as search compares it: the locale's lower case, accents off, ß spelt \"ss\" —\n * \"Zürich\" and \"ZURICH\" both read \"zurich\", \"Straße\" reads \"strasse\". Punctuation and\n * symbols stay: a currency is found by its \"€\", and \"c++\" is not the empty query.\n *\n * Lower-cased by the locale FIRST, then decomposed: the Turkish \"İ\" lower-cases to \"i\"\n * and the dotted capital's mark is then dropped like any other, so \"istanbul\" finds\n * \"İSTANBUL\" in every locale, while \"I\" folds to the dotless \"ı\" in Turkish alone.\n * `locale` left out is the runtime's default; a malformed tag folds as if left out.\n */\nexport function foldForSearch(text: string, locale?: string): string {\n let lower: string;\n try {\n lower = text.toLocaleLowerCase(locale);\n } catch {\n lower = text.toLocaleLowerCase();\n }\n return lower\n .normalize(\"NFD\")\n .replace(/\\p{M}+/gu, \"\")\n .replace(/ß/g, \"ss\");\n}\n\n/**\n * Whether two names are one name as a picker's search reads them: folded\n * ({@link foldForSearch}) and trimmed — \"cafe\" is Café, \" MIGROS \" is Migros. For \"does\n * the typed text name a record that exists\" (a create row, InlineEntityCombobox's blur\n * commit), which compared with plain `toLowerCase` while the list beside it folded, so\n * \"cafe\" listed Café as a hit and offered \"Create “cafe”\" too — a duplicate payee or shop\n * chain one Enter away (refactor audit 2026-10-30, DUP-5). Internal: not in the barrel.\n */\nexport function sameName(a: string, b: string, locale?: string): boolean {\n return foldForSearch(a.trim(), locale) === foldForSearch(b.trim(), locale);\n}\n\n/**\n * A picker's query as its words, each folded (0.51.4, keksdose's /prices shop picker):\n * \"Migros Zürich\" → `[\"migros\", \"zurich\"]`. An empty or blank query has no words.\n */\nexport function searchWords(query: string, locale?: string): string[] {\n return foldForSearch(query, locale)\n .split(/\\s+/)\n .filter((word) => word !== \"\");\n}\n\n/**\n * Whether a row matches a picker's query: EVERY word appears in at least one of the row's\n * texts — \"migros 8004\" finds a row named \"Migros · 8004 Zürich\", and a row whose chain is\n * its group and whose place is its label (pass both). Until 0.51.4 the whole query had to\n * be ONE substring of one text, so two words found nothing unless they stood side by side\n * in that order. One word matches exactly as before, and so does the old query when it\n * was a substring. No words match everything.\n *\n * `words` from {@link searchWords}; `texts` as they are (folded here), `undefined` skipped.\n *\n * Exported since 0.51.5 for an app's own async source: a picker shows `loadOptions`'\n * rows as they come, so a source that narrows on the client filters with this, the same\n * rule the kit applies to `options` (keksdose's shop picker past its first hundred shops).\n */\nexport function matchesSearch(\n words: readonly string[],\n texts: readonly (string | undefined)[],\n locale?: string,\n): boolean {\n if (words.length === 0) return true;\n // One haystack per row; a word holds no whitespace, so it cannot match across the\n // newline between two texts.\n const hay = texts\n .filter((t): t is string => t !== undefined)\n .map((t) => foldForSearch(t, locale))\n .join(\"\\n\");\n return words.every((word) => hay.includes(word));\n}\n\n/** {@link rankMatch}'s answer: lower is better, `-1` is no match. */\nexport type MatchRank = -1 | 0 | 1 | 2;\n\n/**\n * How well `label` answers `query` — CountrySelect's ranking, for every list that orders\n * its hits:\n *\n * - `0`: the query IS the row — its `code` (\"ch\" means Switzerland before it means Chad)\n * or the whole label;\n * - `1`: the label starts with it;\n * - `2`: the label contains it;\n * - `-1`: no match.\n *\n * Both sides are folded here ({@link foldForSearch}, by `locale`), so pass them as typed\n * and as shown. An empty query matches every row alike (`0`). Sort by the rank with a\n * stable sort, and rows of one rank keep the order they came in.\n */\nexport function rankMatch(\n label: string,\n query: string,\n { code, locale }: { code?: string; locale?: string } = {},\n): MatchRank {\n const q = foldForSearch(query.trim(), locale);\n if (!q) return 0;\n return rankFolded(foldForSearch(label, locale), q, code === undefined ? undefined : foldForSearch(code, locale));\n}\n\n/** {@link rankMatch} on a text, a non-empty query and a code all folded already. */\nfunction rankFolded(text: string, q: string, code: string | undefined): MatchRank {\n if (text === q || code === q) return 0;\n if (text.startsWith(q)) return 1;\n return text.includes(q) ? 2 : -1;\n}\n\n/** A row {@link searchRanked} kept, and how well it answers the WHOLE query:\n * {@link rankMatch}'s rank, or `3` for a row that holds the query's words apart\n * (\"Zürich Migros\" for \"migros zürich\"). */\nexport interface RankedHit<T> {\n row: T;\n rank: Exclude<MatchRank, -1> | 3;\n}\n\n/**\n * The rows that hold every word of `query`, each ranked for the WHOLE query — for the\n * lists that FILTER by words ({@link matchesSearch}) and RANK by the whole query\n * ({@link rankMatch}): Combobox, CountrySelect and InlineEntityCombobox (refactor audit\n * 2026-10-30, DUP-4). In the order given; sort by `rank` with a stable sort, and the rows\n * of one rank keep it. No words keep every row, at `0`.\n *\n * Each row's texts are folded ONCE, and the query once. The lists asked `matchesSearch`\n * and then `rankMatch` of each row they kept, which folded the row three times and the\n * query once per row, per keystroke — InlineEntityCombobox asked `rankMatch` twice of\n * most rows (0.52 code review, combobox.tsx).\n *\n * `texts(row)`: where a word may stand; the FIRST is the label the rank is for.\n * `code(row)`: a key that answers only WHOLE — a word that IS the code matches the row,\n * and the code as the whole query ranks `0` (\"ch\" is Switzerland before it is Chad) —\n * never a letter of it (\"w\" is not Aruba, AW: 0.52 code review, country-select.tsx).\n * Internal: not in the barrel.\n */\nexport function searchRanked<T>(\n rows: readonly T[],\n query: string,\n texts: (row: T) => readonly (string | undefined)[],\n { code, locale }: { code?: (row: T) => string; locale?: string } = {},\n): RankedHit<T>[] {\n const q = foldForSearch(query.trim(), locale);\n const words = q.split(/\\s+/).filter((word) => word !== \"\");\n if (words.length === 0) return rows.map((row) => ({ row, rank: 0 }));\n const hits: RankedHit<T>[] = [];\n for (const row of rows) {\n const folded = texts(row)\n .filter((t): t is string => t !== undefined)\n .map((t) => foldForSearch(t, locale));\n const key = code === undefined ? undefined : foldForSearch(code(row), locale);\n // One haystack, as `matchesSearch` has it: a word holds no whitespace, so it cannot\n // match across the newline between two texts.\n const hay = folded.join(\"\\n\");\n if (!words.every((word) => word === key || hay.includes(word))) continue;\n const rank = rankFolded(folded[0] ?? \"\", q, key);\n hits.push({ row, rank: rank === -1 ? 3 : rank });\n }\n return hits;\n}\n"],"mappings":";AAqBO,SAAS,cAAc,MAAc,QAAyB;AACnE,MAAI;AACJ,MAAI;AACF,YAAQ,KAAK,kBAAkB,MAAM;AAAA,EACvC,QAAQ;AACN,YAAQ,KAAK,kBAAkB;AAAA,EACjC;AACA,SAAO,MACJ,UAAU,KAAK,EACf,QAAQ,YAAY,EAAE,EACtB,QAAQ,MAAM,IAAI;AACvB;AAUO,SAAS,SAAS,GAAW,GAAW,QAA0B;AACvE,SAAO,cAAc,EAAE,KAAK,GAAG,MAAM,MAAM,cAAc,EAAE,KAAK,GAAG,MAAM;AAC3E;AAMO,SAAS,YAAY,OAAe,QAA2B;AACpE,SAAO,cAAc,OAAO,MAAM,EAC/B,MAAM,KAAK,EACX,OAAO,CAAC,SAAS,SAAS,EAAE;AACjC;AAgBO,SAAS,cACd,OACA,OACA,QACS;AACT,MAAI,MAAM,WAAW,EAAG,QAAO;AAG/B,QAAM,MAAM,MACT,OAAO,CAAC,MAAmB,MAAM,MAAS,EAC1C,IAAI,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,EACnC,KAAK,IAAI;AACZ,SAAO,MAAM,MAAM,CAAC,SAAS,IAAI,SAAS,IAAI,CAAC;AACjD;AAmBO,SAAS,UACd,OACA,OACA,EAAE,MAAM,OAAO,IAAwC,CAAC,GAC7C;AACX,QAAM,IAAI,cAAc,MAAM,KAAK,GAAG,MAAM;AAC5C,MAAI,CAAC,EAAG,QAAO;AACf,SAAO,WAAW,cAAc,OAAO,MAAM,GAAG,GAAG,SAAS,SAAY,SAAY,cAAc,MAAM,MAAM,CAAC;AACjH;AAGA,SAAS,WAAW,MAAc,GAAW,MAAqC;AAChF,MAAI,SAAS,KAAK,SAAS,EAAG,QAAO;AACrC,MAAI,KAAK,WAAW,CAAC,EAAG,QAAO;AAC/B,SAAO,KAAK,SAAS,CAAC,IAAI,IAAI;AAChC;AA4BO,SAAS,aACd,MACA,OACA,OACA,EAAE,MAAM,OAAO,IAAoD,CAAC,GACpD;AAChB,QAAM,IAAI,cAAc,MAAM,KAAK,GAAG,MAAM;AAC5C,QAAM,QAAQ,EAAE,MAAM,KAAK,EAAE,OAAO,CAAC,SAAS,SAAS,EAAE;AACzD,MAAI,MAAM,WAAW,EAAG,QAAO,KAAK,IAAI,CAAC,SAAS,EAAE,KAAK,MAAM,EAAE,EAAE;AACnE,QAAM,OAAuB,CAAC;AAC9B,aAAW,OAAO,MAAM;AACtB,UAAM,SAAS,MAAM,GAAG,EACrB,OAAO,CAAC,MAAmB,MAAM,MAAS,EAC1C,IAAI,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC;AACtC,UAAM,MAAM,SAAS,SAAY,SAAY,cAAc,KAAK,GAAG,GAAG,MAAM;AAG5E,UAAM,MAAM,OAAO,KAAK,IAAI;AAC5B,QAAI,CAAC,MAAM,MAAM,CAAC,SAAS,SAAS,OAAO,IAAI,SAAS,IAAI,CAAC,EAAG;AAChE,UAAM,OAAO,WAAW,OAAO,CAAC,KAAK,IAAI,GAAG,GAAG;AAC/C,SAAK,KAAK,EAAE,KAAK,MAAM,SAAS,KAAK,IAAI,KAAK,CAAC;AAAA,EACjD;AACA,SAAO;AACT;","names":[]}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Whether a callback's answer is a promise to wait for — an object or a function with a
3
+ * `then` (Promises/A+ "thenable"). A callback that answers nothing is done when it
4
+ * returns.
5
+ *
6
+ * The auth forms (`auth/status-parts`), the admin confirm and the feedback mutation each
7
+ * wrote their own, three spellings of one check — and the admin's turned a thenable
8
+ * FUNCTION away (0.52 code review, feedback/feedback-mutation.ts; the refactor audit's
9
+ * DUP findings).
10
+ *
11
+ * @internal Package-private — not re-exported from the barrel.
12
+ */
13
+ export declare function isThenable(value: unknown): value is PromiseLike<unknown>;
@@ -0,0 +1,8 @@
1
+ "use client";
2
+ function isThenable(value) {
3
+ return (typeof value === "object" || typeof value === "function") && value !== null && typeof value.then === "function";
4
+ }
5
+ export {
6
+ isThenable
7
+ };
8
+ //# sourceMappingURL=thenable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/thenable.ts"],"sourcesContent":["/**\n * Whether a callback's answer is a promise to wait for — an object or a function with a\n * `then` (Promises/A+ \"thenable\"). A callback that answers nothing is done when it\n * returns.\n *\n * The auth forms (`auth/status-parts`), the admin confirm and the feedback mutation each\n * wrote their own, three spellings of one check — and the admin's turned a thenable\n * FUNCTION away (0.52 code review, feedback/feedback-mutation.ts; the refactor audit's\n * DUP findings).\n *\n * @internal Package-private — not re-exported from the barrel.\n */\nexport function isThenable(value: unknown): value is PromiseLike<unknown> {\n return (\n (typeof value === \"object\" || typeof value === \"function\") &&\n value !== null &&\n typeof (value as { then?: unknown }).then === \"function\"\n );\n}\n"],"mappings":";AAYO,SAAS,WAAW,OAA+C;AACxE,UACG,OAAO,UAAU,YAAY,OAAO,UAAU,eAC/C,UAAU,QACV,OAAQ,MAA6B,SAAS;AAElD;","names":[]}
@@ -228,12 +228,17 @@ export interface TranslationReviewFilter {
228
228
  source: string;
229
229
  namespace: string;
230
230
  /** Searched in the key, the text and the reference, folded as every kit list's search
231
- * is (`foldForSearch`): no case, no accents — "uberweisung" finds "Überweisung". */
231
+ * is (`foldForSearch`): no case, no accents — "uberweisung" finds "Überweisung". Every
232
+ * word on its own (0.52): "budget assign" finds `budget.rta`, "Ready to Assign". */
232
233
  query: string;
233
234
  placeholdersOnly: boolean;
234
235
  }
235
236
  export declare const DEFAULT_TRANSLATION_REVIEW_FILTER: TranslationReviewFilter;
236
- export declare function filterTranslationRows(rows: readonly TranslationRow[], filter: Partial<TranslationReviewFilter>): TranslationRow[];
237
+ /**
238
+ * The rows the filters leave. `locale` (0.52) folds the query and the texts as the kit's
239
+ * other lists do — the panel passes the provider's; left out, the runtime's default.
240
+ */
241
+ export declare function filterTranslationRows(rows: readonly TranslationRow[], filter: Partial<TranslationReviewFilter>, locale?: string): TranslationRow[];
237
242
  /** One group of rows — an area, a source — and how they stand. */
238
243
  export interface TranslationRowGroup {
239
244
  key: string;
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { foldForSearch } from "./text-match.js";
2
+ import { matchesSearch, searchWords } from "./text-match.js";
3
3
  const REVIEW_STATUSES = [
4
4
  "missing",
5
5
  "unreviewed",
@@ -139,11 +139,11 @@ const DEFAULT_TRANSLATION_REVIEW_FILTER = {
139
139
  query: "",
140
140
  placeholdersOnly: false
141
141
  };
142
- function filterTranslationRows(rows, filter) {
142
+ function filterTranslationRows(rows, filter, locale) {
143
143
  const f = { ...DEFAULT_TRANSLATION_REVIEW_FILTER, ...filter };
144
- const q = foldForSearch(f.query.trim());
144
+ const words = searchWords(f.query, locale);
145
145
  return rows.filter(
146
- (row) => (f.status === "all" || row.status === f.status) && (f.source === "all" || row.source === f.source) && (f.namespace === "all" || row.namespace === f.namespace) && (!f.placeholdersOnly || row.placeholderMismatch) && (!q || [row.key, row.text, row.reference].some((s) => foldForSearch(s).includes(q)))
146
+ (row) => (f.status === "all" || row.status === f.status) && (f.source === "all" || row.source === f.source) && (f.namespace === "all" || row.namespace === f.namespace) && (!f.placeholdersOnly || row.placeholderMismatch) && matchesSearch(words, [row.key, row.text, row.reference], locale)
147
147
  );
148
148
  }
149
149
  function groupTranslationRows(rows, groupOf) {