@eifi1/ui-kit 0.51.6 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (805) hide show
  1. package/README.md +146 -26
  2. package/dist/account/account-parts.d.ts +10 -8
  3. package/dist/account/account-parts.js +1 -18
  4. package/dist/account/account-parts.js.map +1 -1
  5. package/dist/account/data-export-setting-labels.js +1 -1
  6. package/dist/account/data-export-setting-labels.js.map +1 -1
  7. package/dist/account/delete-account-setting-labels.js +1 -1
  8. package/dist/account/delete-account-setting-labels.js.map +1 -1
  9. package/dist/account/email-change-setting-labels.js +1 -1
  10. package/dist/account/email-change-setting-labels.js.map +1 -1
  11. package/dist/account/passkeys-setting.d.ts +9 -2
  12. package/dist/account/passkeys-setting.js +41 -29
  13. package/dist/account/passkeys-setting.js.map +1 -1
  14. package/dist/account/sessions-setting.d.ts +3 -2
  15. package/dist/account/sessions-setting.js +10 -8
  16. package/dist/account/sessions-setting.js.map +1 -1
  17. package/dist/admin/admin-action-confirm.js +3 -7
  18. package/dist/admin/admin-action-confirm.js.map +1 -1
  19. package/dist/admin/admin-parts.d.ts +6 -3
  20. package/dist/admin/admin-parts.js +6 -21
  21. package/dist/admin/admin-parts.js.map +1 -1
  22. package/dist/admin/mail-test-card-labels.js +1 -1
  23. package/dist/admin/mail-test-card-labels.js.map +1 -1
  24. package/dist/admin/plan-change-labels.d.ts +2 -2
  25. package/dist/admin/plan-change-labels.js +1 -17
  26. package/dist/admin/plan-change-labels.js.map +1 -1
  27. package/dist/admin/plan-change.d.ts +3 -42
  28. package/dist/admin/plan-change.js +1 -1
  29. package/dist/admin/plan-change.js.map +1 -1
  30. package/dist/admin/reviewer-scope-editor.js +3 -3
  31. package/dist/admin/reviewer-scope-editor.js.map +1 -1
  32. package/dist/auth/auth-errors-labels.d.ts +11 -0
  33. package/dist/auth/auth-errors-labels.js +13 -0
  34. package/dist/auth/auth-errors-labels.js.map +1 -0
  35. package/dist/auth/auth-errors.d.ts +7 -23
  36. package/dist/auth/auth-errors.js +2 -15
  37. package/dist/auth/auth-errors.js.map +1 -1
  38. package/dist/auth/complete-name-dialog.d.ts +3 -2
  39. package/dist/auth/complete-name-dialog.js +3 -5
  40. package/dist/auth/complete-name-dialog.js.map +1 -1
  41. package/dist/auth/forgot-password-form-labels.js +1 -1
  42. package/dist/auth/forgot-password-form-labels.js.map +1 -1
  43. package/dist/auth/forgot-password-form.js +7 -2
  44. package/dist/auth/forgot-password-form.js.map +1 -1
  45. package/dist/auth/register-form-labels.js +1 -1
  46. package/dist/auth/register-form-labels.js.map +1 -1
  47. package/dist/auth/reset-password-form-labels.js +1 -1
  48. package/dist/auth/reset-password-form-labels.js.map +1 -1
  49. package/dist/auth/reset-password-form.js +5 -1
  50. package/dist/auth/reset-password-form.js.map +1 -1
  51. package/dist/auth/sign-in-form-labels.js +1 -1
  52. package/dist/auth/sign-in-form-labels.js.map +1 -1
  53. package/dist/auth/status-parts.d.ts +0 -3
  54. package/dist/auth/status-parts.js +0 -4
  55. package/dist/auth/status-parts.js.map +1 -1
  56. package/dist/auth/verify-email-labels.js +1 -1
  57. package/dist/auth/verify-email-labels.js.map +1 -1
  58. package/dist/auth/verify-email.js +7 -2
  59. package/dist/auth/verify-email.js.map +1 -1
  60. package/dist/billing/cancel-request.d.ts +11 -2
  61. package/dist/billing/cancel-request.js +15 -3
  62. package/dist/billing/cancel-request.js.map +1 -1
  63. package/dist/billing/cancel-subscription.d.ts +22 -10
  64. package/dist/billing/cancel-subscription.js +8 -11
  65. package/dist/billing/cancel-subscription.js.map +1 -1
  66. package/dist/billing/dialog-close.d.ts +29 -0
  67. package/dist/billing/dialog-close.js +8 -0
  68. package/dist/billing/dialog-close.js.map +1 -0
  69. package/dist/billing/plan-card.d.ts +14 -4
  70. package/dist/billing/plan-card.js +15 -8
  71. package/dist/billing/plan-card.js.map +1 -1
  72. package/dist/billing/plan-change-labels.d.ts +42 -0
  73. package/dist/billing/plan-change-labels.js +22 -0
  74. package/dist/billing/plan-change-labels.js.map +1 -0
  75. package/dist/billing/plan-switch.d.ts +21 -9
  76. package/dist/billing/plan-switch.js +10 -8
  77. package/dist/billing/plan-switch.js.map +1 -1
  78. package/dist/chart.d.ts +12 -0
  79. package/dist/chart.js +16 -1
  80. package/dist/chart.js.map +1 -1
  81. package/dist/components/account-chips.d.ts +5 -5
  82. package/dist/components/account-chips.js +11 -13
  83. package/dist/components/account-chips.js.map +1 -1
  84. package/dist/components/alert-banner.d.ts +16 -5
  85. package/dist/components/alert-banner.js +46 -1
  86. package/dist/components/alert-banner.js.map +1 -1
  87. package/dist/components/amount-input.d.ts +22 -4
  88. package/dist/components/amount-input.js +3 -3
  89. package/dist/components/amount-input.js.map +1 -1
  90. package/dist/components/autocomplete.js +57 -65
  91. package/dist/components/autocomplete.js.map +1 -1
  92. package/dist/components/bulk-action-bar.d.ts +12 -2
  93. package/dist/components/bulk-action-bar.js +65 -21
  94. package/dist/components/bulk-action-bar.js.map +1 -1
  95. package/dist/components/button-look.d.ts +5 -5
  96. package/dist/components/button-look.js.map +1 -1
  97. package/dist/components/button.d.ts +2 -2
  98. package/dist/components/button.js.map +1 -1
  99. package/dist/components/calculator.d.ts +21 -2
  100. package/dist/components/calculator.js +53 -14
  101. package/dist/components/calculator.js.map +1 -1
  102. package/dist/components/chart-edge.d.ts +16 -0
  103. package/dist/components/chart-edge.js +14 -1
  104. package/dist/components/chart-edge.js.map +1 -1
  105. package/dist/components/chart-figure.d.ts +13 -0
  106. package/dist/components/chart-figure.js +15 -0
  107. package/dist/components/chart-figure.js.map +1 -0
  108. package/dist/components/chart-zoom-core.js +1 -4
  109. package/dist/components/chart-zoom-core.js.map +1 -1
  110. package/dist/components/chart.d.ts +8 -1
  111. package/dist/components/chart.js +19 -10
  112. package/dist/components/chart.js.map +1 -1
  113. package/dist/components/combobox-core.d.ts +119 -7
  114. package/dist/components/combobox-core.js +151 -46
  115. package/dist/components/combobox-core.js.map +1 -1
  116. package/dist/components/combobox.d.ts +1 -1
  117. package/dist/components/combobox.js +142 -131
  118. package/dist/components/combobox.js.map +1 -1
  119. package/dist/components/copy-button.d.ts +6 -5
  120. package/dist/components/copy-button.js +5 -3
  121. package/dist/components/copy-button.js.map +1 -1
  122. package/dist/components/country-select.js +2 -2
  123. package/dist/components/country-select.js.map +1 -1
  124. package/dist/components/data-table-column-settings.d.ts +1 -1
  125. package/dist/components/data-table-column-settings.js +33 -7
  126. package/dist/components/data-table-column-settings.js.map +1 -1
  127. package/dist/components/data-table-filters.d.ts +4 -2
  128. package/dist/components/data-table-filters.js +2 -2
  129. package/dist/components/data-table-filters.js.map +1 -1
  130. package/dist/components/data-table-fit.d.ts +6 -4
  131. package/dist/components/data-table-fit.js +4 -1
  132. package/dist/components/data-table-fit.js.map +1 -1
  133. package/dist/components/data-table-groups.d.ts +1 -1
  134. package/dist/components/data-table-groups.js.map +1 -1
  135. package/dist/components/data-table-head.d.ts +1 -1
  136. package/dist/components/data-table-head.js +15 -12
  137. package/dist/components/data-table-head.js.map +1 -1
  138. package/dist/components/data-table-mobile.d.ts +36 -7
  139. package/dist/components/data-table-mobile.js +63 -31
  140. package/dist/components/data-table-mobile.js.map +1 -1
  141. package/dist/components/data-table-pagination.js +11 -6
  142. package/dist/components/data-table-pagination.js.map +1 -1
  143. package/dist/components/data-table-resize.d.ts +1 -1
  144. package/dist/components/data-table-resize.js.map +1 -1
  145. package/dist/components/data-table-row-actions.d.ts +2 -2
  146. package/dist/components/data-table-row-actions.js.map +1 -1
  147. package/dist/components/data-table-row.d.ts +6 -2
  148. package/dist/components/data-table-row.js +17 -10
  149. package/dist/components/data-table-row.js.map +1 -1
  150. package/dist/components/data-table-selection.d.ts +1 -1
  151. package/dist/components/data-table-selection.js.map +1 -1
  152. package/dist/components/data-table-shared.d.ts +10 -2
  153. package/dist/components/data-table-shared.js +18 -0
  154. package/dist/components/data-table-shared.js.map +1 -1
  155. package/dist/components/data-table-sort.d.ts +21 -2
  156. package/dist/components/data-table-sort.js +24 -0
  157. package/dist/components/data-table-sort.js.map +1 -1
  158. package/dist/components/data-table-virtual.d.ts +1 -1
  159. package/dist/components/data-table-virtual.js +18 -6
  160. package/dist/components/data-table-virtual.js.map +1 -1
  161. package/dist/components/data-table.d.ts +25 -8
  162. package/dist/components/data-table.js +154 -25
  163. package/dist/components/data-table.js.map +1 -1
  164. package/dist/components/date-field.d.ts +24 -2
  165. package/dist/components/date-field.js +33 -8
  166. package/dist/components/date-field.js.map +1 -1
  167. package/dist/components/date-picker.d.ts +15 -2
  168. package/dist/components/date-picker.js +55 -26
  169. package/dist/components/date-picker.js.map +1 -1
  170. package/dist/components/date-range-picker.d.ts +47 -20
  171. package/dist/components/date-range-picker.js +23 -7
  172. package/dist/components/date-range-picker.js.map +1 -1
  173. package/dist/components/dialog-frame.d.ts +2 -1
  174. package/dist/components/dialog-frame.js.map +1 -1
  175. package/dist/components/entity-combobox.js +4 -2
  176. package/dist/components/entity-combobox.js.map +1 -1
  177. package/dist/components/error-boundary.js +37 -30
  178. package/dist/components/error-boundary.js.map +1 -1
  179. package/dist/components/{ui-labels.d.ts → field-base-labels.d.ts} +0 -2
  180. package/dist/components/{ui-labels.js → field-base-labels.js} +2 -9
  181. package/dist/components/field-base-labels.js.map +1 -0
  182. package/dist/components/field-base.d.ts +4 -3
  183. package/dist/components/field-base.js +3 -3
  184. package/dist/components/field-base.js.map +1 -1
  185. package/dist/components/field-sync-labels.d.ts +1 -0
  186. package/dist/components/field-sync-labels.js.map +1 -1
  187. package/dist/components/field-sync.d.ts +23 -1
  188. package/dist/components/field-sync.js.map +1 -1
  189. package/dist/components/form-actions.js +11 -5
  190. package/dist/components/form-actions.js.map +1 -1
  191. package/dist/components/hover-menu.d.ts +3 -0
  192. package/dist/components/hover-menu.js +5 -3
  193. package/dist/components/hover-menu.js.map +1 -1
  194. package/dist/components/icon-button.js +14 -9
  195. package/dist/components/icon-button.js.map +1 -1
  196. package/dist/components/inline-confirm.d.ts +43 -0
  197. package/dist/components/inline-confirm.js +67 -0
  198. package/dist/components/inline-confirm.js.map +1 -0
  199. package/dist/components/inline-entity-combobox.d.ts +4 -3
  200. package/dist/components/inline-entity-combobox.js +150 -141
  201. package/dist/components/inline-entity-combobox.js.map +1 -1
  202. package/dist/components/input.js +1 -1
  203. package/dist/components/input.js.map +1 -1
  204. package/dist/components/jump-calendar.js +28 -112
  205. package/dist/components/jump-calendar.js.map +1 -1
  206. package/dist/components/legal-page.d.ts +3 -1
  207. package/dist/components/legal-page.js +3 -2
  208. package/dist/components/legal-page.js.map +1 -1
  209. package/dist/components/legal.d.ts +4 -2
  210. package/dist/components/legal.js.map +1 -1
  211. package/dist/components/lightbox.d.ts +2 -2
  212. package/dist/components/lightbox.js +17 -6
  213. package/dist/components/lightbox.js.map +1 -1
  214. package/dist/components/list.d.ts +6 -2
  215. package/dist/components/list.js +4 -3
  216. package/dist/components/list.js.map +1 -1
  217. package/dist/components/measured-grid.d.ts +1 -1
  218. package/dist/components/measured-grid.js +2 -1
  219. package/dist/components/measured-grid.js.map +1 -1
  220. package/dist/components/mini-calendar.js +3 -5
  221. package/dist/components/mini-calendar.js.map +1 -1
  222. package/dist/components/modal.d.ts +24 -0
  223. package/dist/components/modal.js +12 -4
  224. package/dist/components/modal.js.map +1 -1
  225. package/dist/components/money-draft.js +7 -5
  226. package/dist/components/money-draft.js.map +1 -1
  227. package/dist/components/money-field.d.ts +2 -2
  228. package/dist/components/money-field.js.map +1 -1
  229. package/dist/components/month-grid.d.ts +55 -0
  230. package/dist/components/month-grid.js +148 -0
  231. package/dist/components/month-grid.js.map +1 -0
  232. package/dist/components/month-picker.d.ts +36 -4
  233. package/dist/components/month-picker.js +123 -146
  234. package/dist/components/month-picker.js.map +1 -1
  235. package/dist/components/multi-entity-combobox.js +2 -1
  236. package/dist/components/multi-entity-combobox.js.map +1 -1
  237. package/dist/components/multi-select.d.ts +2 -2
  238. package/dist/components/multi-select.js +11 -6
  239. package/dist/components/multi-select.js.map +1 -1
  240. package/dist/components/number-field.d.ts +6 -4
  241. package/dist/components/number-field.js +3 -2
  242. package/dist/components/number-field.js.map +1 -1
  243. package/dist/components/number-input.d.ts +4 -1
  244. package/dist/components/number-input.js +3 -2
  245. package/dist/components/number-input.js.map +1 -1
  246. package/dist/components/numpad-sheet.js.map +1 -1
  247. package/dist/components/picker-sheet.js +2 -1
  248. package/dist/components/picker-sheet.js.map +1 -1
  249. package/dist/components/pie-chart-labels.d.ts +7 -0
  250. package/dist/components/pie-chart-labels.js +1 -0
  251. package/dist/components/pie-chart-labels.js.map +1 -1
  252. package/dist/components/pie-chart.d.ts +5 -3
  253. package/dist/components/pie-chart.js +7 -22
  254. package/dist/components/pie-chart.js.map +1 -1
  255. package/dist/components/popover.d.ts +3 -0
  256. package/dist/components/popover.js +7 -1
  257. package/dist/components/popover.js.map +1 -1
  258. package/dist/components/reauth-dialog.d.ts +4 -5
  259. package/dist/components/reauth-dialog.js +1 -4
  260. package/dist/components/reauth-dialog.js.map +1 -1
  261. package/dist/components/row-actions.js +2 -1
  262. package/dist/components/row-actions.js.map +1 -1
  263. package/dist/components/section-nav.d.ts +7 -0
  264. package/dist/components/section-nav.js.map +1 -1
  265. package/dist/components/series-chart-axes.d.ts +10 -3
  266. package/dist/components/series-chart-axes.js +1 -0
  267. package/dist/components/series-chart-axes.js.map +1 -1
  268. package/dist/components/series-chart-labels.d.ts +7 -4
  269. package/dist/components/series-chart-labels.js.map +1 -1
  270. package/dist/components/series-chart.d.ts +31 -7
  271. package/dist/components/series-chart.js +72 -29
  272. package/dist/components/series-chart.js.map +1 -1
  273. package/dist/components/share-card.js +3 -3
  274. package/dist/components/share-card.js.map +1 -1
  275. package/dist/components/sign-chip-labels.d.ts +1 -0
  276. package/dist/components/sign-chip-labels.js.map +1 -1
  277. package/dist/components/sign-chip.d.ts +10 -1
  278. package/dist/components/sign-chip.js.map +1 -1
  279. package/dist/components/tabs-labels.d.ts +2 -0
  280. package/dist/components/tabs-labels.js +11 -0
  281. package/dist/components/tabs-labels.js.map +1 -0
  282. package/dist/components/tabs.d.ts +1 -1
  283. package/dist/components/tabs.js +13 -11
  284. package/dist/components/tabs.js.map +1 -1
  285. package/dist/components/toggle-legend.d.ts +2 -2
  286. package/dist/components/toggle-legend.js +6 -6
  287. package/dist/components/toggle-legend.js.map +1 -1
  288. package/dist/components/translation-review-editor.d.ts +4 -2
  289. package/dist/components/translation-review-editor.js +2 -3
  290. package/dist/components/translation-review-editor.js.map +1 -1
  291. package/dist/components/translation-review.d.ts +2 -1
  292. package/dist/components/translation-review.js +2 -2
  293. package/dist/components/translation-review.js.map +1 -1
  294. package/dist/components/treemap-labels.d.ts +26 -0
  295. package/dist/components/treemap-labels.js +10 -0
  296. package/dist/components/treemap-labels.js.map +1 -0
  297. package/dist/components/treemap-layout.d.ts +62 -0
  298. package/dist/components/treemap-layout.js +126 -0
  299. package/dist/components/treemap-layout.js.map +1 -0
  300. package/dist/components/treemap.d.ts +140 -17
  301. package/dist/components/treemap.js +324 -97
  302. package/dist/components/treemap.js.map +1 -1
  303. package/dist/components/use-table-state.d.ts +5 -5
  304. package/dist/components/use-table-state.js +4 -1
  305. package/dist/components/use-table-state.js.map +1 -1
  306. package/dist/data-table.d.ts +6 -2
  307. package/dist/data-table.js +7 -2
  308. package/dist/data-table.js.map +1 -1
  309. package/dist/eslint/index.js +1 -1
  310. package/dist/eslint/role-utilities.d.ts +36 -7
  311. package/dist/eslint/role-utilities.js +42 -9
  312. package/dist/eslint/role-utilities.js.map +1 -1
  313. package/dist/feedback/feedback-dialog.d.ts +25 -16
  314. package/dist/feedback/feedback-dialog.js +6 -2
  315. package/dist/feedback/feedback-dialog.js.map +1 -1
  316. package/dist/feedback/feedback-mutation.d.ts +30 -0
  317. package/dist/feedback/feedback-mutation.js +16 -0
  318. package/dist/feedback/feedback-mutation.js.map +1 -0
  319. package/dist/feedback/feedback-page-link.d.ts +53 -0
  320. package/dist/feedback/feedback-page-link.js +37 -0
  321. package/dist/feedback/feedback-page-link.js.map +1 -0
  322. package/dist/feedback/feedback-public-list.d.ts +12 -3
  323. package/dist/feedback/feedback-public-list.js +2 -1
  324. package/dist/feedback/feedback-public-list.js.map +1 -1
  325. package/dist/feedback/feedback-replies.d.ts +3 -1
  326. package/dist/feedback/feedback-replies.js +5 -4
  327. package/dist/feedback/feedback-replies.js.map +1 -1
  328. package/dist/feedback/feedback-row-detail-labels.js +5 -0
  329. package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
  330. package/dist/feedback/feedback-row-detail.d.ts +64 -28
  331. package/dist/feedback/feedback-row-detail.js +30 -28
  332. package/dist/feedback/feedback-row-detail.js.map +1 -1
  333. package/dist/feedback/feedback-status-undo.d.ts +30 -11
  334. package/dist/feedback/feedback-status-undo.js +10 -15
  335. package/dist/feedback/feedback-status-undo.js.map +1 -1
  336. package/dist/feedback/feedback-submit-dialog.d.ts +1 -1
  337. package/dist/feedback/feedback-submit-dialog.js +1 -1
  338. package/dist/feedback/feedback-submit-dialog.js.map +1 -1
  339. package/dist/feedback/feedback-submit.js +9 -3
  340. package/dist/feedback/feedback-submit.js.map +1 -1
  341. package/dist/feedback/feedback-swipe.d.ts +1 -1
  342. package/dist/feedback/feedback-swipe.js.map +1 -1
  343. package/dist/feedback/feedback-table.d.ts +21 -9
  344. package/dist/feedback/feedback-table.js +45 -25
  345. package/dist/feedback/feedback-table.js.map +1 -1
  346. package/dist/feedback/feedback-thread-labels.d.ts +2 -2
  347. package/dist/feedback/feedback-thread-labels.js.map +1 -1
  348. package/dist/feedback/feedback-thread.d.ts +24 -23
  349. package/dist/feedback/feedback-thread.js +2 -1
  350. package/dist/feedback/feedback-thread.js.map +1 -1
  351. package/dist/feedback.d.ts +34 -0
  352. package/dist/feedback.js +42 -0
  353. package/dist/feedback.js.map +1 -1
  354. package/dist/hooks/use-dismiss.js.map +1 -1
  355. package/dist/hooks/use-focus-trap.d.ts +35 -0
  356. package/dist/hooks/use-focus-trap.js +6 -4
  357. package/dist/hooks/use-focus-trap.js.map +1 -1
  358. package/dist/hooks/use-hotkey.d.ts +5 -2
  359. package/dist/hooks/use-hotkey.js +5 -4
  360. package/dist/hooks/use-hotkey.js.map +1 -1
  361. package/dist/hooks/use-long-press.d.ts +7 -4
  362. package/dist/hooks/use-long-press.js +3 -0
  363. package/dist/hooks/use-long-press.js.map +1 -1
  364. package/dist/hooks/use-media-query.js +12 -4
  365. package/dist/hooks/use-media-query.js.map +1 -1
  366. package/dist/hooks/use-mounted.d.ts +22 -0
  367. package/dist/hooks/use-mounted.js +24 -0
  368. package/dist/hooks/use-mounted.js.map +1 -0
  369. package/dist/hooks/use-overlay-history.d.ts +7 -1
  370. package/dist/hooks/use-overlay-history.js +35 -5
  371. package/dist/hooks/use-overlay-history.js.map +1 -1
  372. package/dist/i18n/defaults.js +4 -5
  373. package/dist/i18n/defaults.js.map +1 -1
  374. package/dist/i18n/german.js +7 -1
  375. package/dist/i18n/german.js.map +1 -1
  376. package/dist/i18n/kit-labels.d.ts +9 -2
  377. package/dist/i18n/kit-labels.js.map +1 -1
  378. package/dist/i18n/locales/en.js +20 -41
  379. package/dist/i18n/locales/en.js.map +3 -3
  380. package/dist/i18n/locales/es.js +7 -1
  381. package/dist/i18n/locales/es.js.map +1 -1
  382. package/dist/i18n/locales/fr.js +20 -10
  383. package/dist/i18n/locales/fr.js.map +1 -1
  384. package/dist/i18n/locales/hu.js +9 -1
  385. package/dist/i18n/locales/hu.js.map +1 -1
  386. package/dist/i18n/locales/it.js +7 -1
  387. package/dist/i18n/locales/it.js.map +1 -1
  388. package/dist/i18n/locales/zh.js +8 -1
  389. package/dist/i18n/locales/zh.js.map +1 -1
  390. package/dist/i18n/review.js +5 -0
  391. package/dist/i18n/review.js.map +1 -1
  392. package/dist/index.d.ts +64 -17
  393. package/dist/index.js +77 -12
  394. package/dist/index.js.map +1 -1
  395. package/dist/landing/routing.d.ts +3 -1
  396. package/dist/landing/routing.js +4 -7
  397. package/dist/landing/routing.js.map +1 -1
  398. package/dist/lib/broadcast.d.ts +17 -0
  399. package/dist/lib/broadcast.js +30 -0
  400. package/dist/lib/broadcast.js.map +1 -0
  401. package/dist/lib/calc.d.ts +7 -1
  402. package/dist/lib/calc.js +3 -1
  403. package/dist/lib/calc.js.map +1 -1
  404. package/dist/lib/clipboard-minus.d.ts +2 -2
  405. package/dist/lib/clipboard-minus.js +1 -1
  406. package/dist/lib/clipboard-minus.js.map +1 -1
  407. package/dist/lib/date-format.d.ts +37 -2
  408. package/dist/lib/date-format.js +34 -8
  409. package/dist/lib/date-format.js.map +1 -1
  410. package/dist/lib/day-format.d.ts +46 -0
  411. package/dist/lib/day-format.js +46 -0
  412. package/dist/lib/day-format.js.map +1 -0
  413. package/dist/lib/decimal-marks.d.ts +7 -1
  414. package/dist/lib/decimal-marks.js +5 -2
  415. package/dist/lib/decimal-marks.js.map +1 -1
  416. package/dist/lib/describe-failure.d.ts +14 -0
  417. package/dist/lib/describe-failure.js +9 -0
  418. package/dist/lib/describe-failure.js.map +1 -0
  419. package/dist/lib/direction.d.ts +16 -0
  420. package/dist/lib/direction.js +9 -1
  421. package/dist/lib/direction.js.map +1 -1
  422. package/dist/lib/focus-left.d.ts +63 -0
  423. package/dist/lib/focus-left.js +67 -0
  424. package/dist/lib/focus-left.js.map +1 -0
  425. package/dist/lib/format.d.ts +5 -3
  426. package/dist/lib/format.js +16 -13
  427. package/dist/lib/format.js.map +1 -1
  428. package/dist/lib/http-status.d.ts +24 -0
  429. package/dist/lib/http-status.js +14 -0
  430. package/dist/lib/http-status.js.map +1 -0
  431. package/dist/lib/intl-cache.d.ts +14 -0
  432. package/dist/lib/intl-cache.js +15 -0
  433. package/dist/lib/intl-cache.js.map +1 -0
  434. package/dist/lib/isin.d.ts +10 -0
  435. package/dist/lib/isin.js.map +1 -1
  436. package/dist/lib/iso-day.d.ts +11 -0
  437. package/dist/lib/iso-day.js +8 -0
  438. package/dist/lib/iso-day.js.map +1 -0
  439. package/dist/lib/lazy-with-fallback.d.ts +31 -0
  440. package/dist/lib/lazy-with-fallback.js +40 -0
  441. package/dist/lib/lazy-with-fallback.js.map +1 -0
  442. package/dist/lib/path-pattern.d.ts +34 -0
  443. package/dist/lib/path-pattern.js +24 -0
  444. package/dist/lib/path-pattern.js.map +1 -0
  445. package/dist/lib/reduced-motion.d.ts +2 -1
  446. package/dist/lib/reduced-motion.js.map +1 -1
  447. package/dist/lib/round.d.ts +2 -1
  448. package/dist/lib/round.js +2 -1
  449. package/dist/lib/round.js.map +1 -1
  450. package/dist/lib/safe-storage.d.ts +24 -5
  451. package/dist/lib/safe-storage.js +27 -2
  452. package/dist/lib/safe-storage.js.map +1 -1
  453. package/dist/lib/session-refresh.js +1 -1
  454. package/dist/lib/session-refresh.js.map +1 -1
  455. package/dist/lib/share-format.d.ts +13 -0
  456. package/dist/lib/share-format.js +10 -0
  457. package/dist/lib/share-format.js.map +1 -0
  458. package/dist/lib/text-case.d.ts +17 -0
  459. package/dist/lib/text-case.js +8 -0
  460. package/dist/lib/text-case.js.map +1 -0
  461. package/dist/lib/text-match.d.ts +38 -0
  462. package/dist/lib/text-match.js +24 -2
  463. package/dist/lib/text-match.js.map +1 -1
  464. package/dist/lib/thenable.d.ts +13 -0
  465. package/dist/lib/thenable.js +8 -0
  466. package/dist/lib/thenable.js.map +1 -0
  467. package/dist/lib/translation-review.d.ts +7 -2
  468. package/dist/lib/translation-review.js +4 -4
  469. package/dist/lib/translation-review.js.map +1 -1
  470. package/dist/notifications/device-zone-report.js +1 -1
  471. package/dist/notifications/device-zone-report.js.map +1 -1
  472. package/dist/notifications/notification-bell.d.ts +2 -1
  473. package/dist/notifications/notification-bell.js +3 -3
  474. package/dist/notifications/notification-bell.js.map +1 -1
  475. package/dist/notifications/notification-inbox.d.ts +10 -6
  476. package/dist/notifications/notification-inbox.js +26 -48
  477. package/dist/notifications/notification-inbox.js.map +1 -1
  478. package/dist/notifications/notification-parts.d.ts +3 -2
  479. package/dist/notifications/notification-parts.js +1 -3
  480. package/dist/notifications/notification-parts.js.map +1 -1
  481. package/dist/notifications/notification-types-setting.js +1 -1
  482. package/dist/notifications/notification-types-setting.js.map +1 -1
  483. package/dist/notifications/notifications-labels.d.ts +6 -0
  484. package/dist/notifications/notifications-labels.js +1 -0
  485. package/dist/notifications/notifications-labels.js.map +1 -1
  486. package/dist/notifications/push-device-setting.js +1 -1
  487. package/dist/notifications/push-device-setting.js.map +1 -1
  488. package/dist/notifications/push-devices-setting.d.ts +2 -1
  489. package/dist/notifications/push-devices-setting.js +27 -49
  490. package/dist/notifications/push-devices-setting.js.map +1 -1
  491. package/dist/notifications/quiet-hours-setting.d.ts +10 -3
  492. package/dist/notifications/quiet-hours-setting.js +65 -26
  493. package/dist/notifications/quiet-hours-setting.js.map +1 -1
  494. package/dist/notifications/unsubscribe-panel.js +4 -7
  495. package/dist/notifications/unsubscribe-panel.js.map +1 -1
  496. package/dist/notifications/use-push-device.d.ts +39 -1
  497. package/dist/notifications/use-push-device.js +28 -7
  498. package/dist/notifications/use-push-device.js.map +1 -1
  499. package/dist/offline/query-persistence.d.ts +3 -1
  500. package/dist/offline/query-persistence.js +19 -2
  501. package/dist/offline/query-persistence.js.map +1 -1
  502. package/dist/rhf/fields.d.ts +11 -4
  503. package/dist/rhf/fields.js.map +1 -1
  504. package/dist/search/command-palette.js +1 -1
  505. package/dist/search/command-palette.js.map +1 -1
  506. package/dist/search/global-search.js +19 -9
  507. package/dist/search/global-search.js.map +1 -1
  508. package/dist/search/use-command-key.d.ts +12 -1
  509. package/dist/search/use-command-key.js +3 -12
  510. package/dist/search/use-command-key.js.map +1 -1
  511. package/dist/search.d.ts +7 -0
  512. package/dist/search.js +10 -0
  513. package/dist/search.js.map +1 -1
  514. package/dist/settings/use-settings-focus.js +1 -0
  515. package/dist/settings/use-settings-focus.js.map +1 -1
  516. package/dist/shell/account-menu-language.d.ts +21 -5
  517. package/dist/shell/account-menu-language.js +3 -1
  518. package/dist/shell/account-menu-language.js.map +1 -1
  519. package/dist/shell/account-menu-text-size.d.ts +26 -9
  520. package/dist/shell/account-menu-text-size.js +3 -2
  521. package/dist/shell/account-menu-text-size.js.map +1 -1
  522. package/dist/shell/dev-menu.js +5 -18
  523. package/dist/shell/dev-menu.js.map +1 -1
  524. package/dist/shell/role-switcher.d.ts +7 -0
  525. package/dist/shell/role-switcher.js.map +1 -1
  526. package/dist/shell/top-bar-actions.d.ts +1 -1
  527. package/dist/shell/top-bar-actions.js.map +1 -1
  528. package/dist/shell/top-bar-parts-labels.d.ts +0 -2
  529. package/dist/shell/top-bar-parts-labels.js +0 -1
  530. package/dist/shell/top-bar-parts-labels.js.map +1 -1
  531. package/dist/shell.d.ts +27 -8
  532. package/dist/shell.js +28 -7
  533. package/dist/shell.js.map +1 -1
  534. package/dist/sync/engine.d.ts +30 -4
  535. package/dist/sync/engine.js +172 -52
  536. package/dist/sync/engine.js.map +1 -1
  537. package/dist/sync.d.ts +6 -2
  538. package/dist/sync.js +6 -1
  539. package/dist/sync.js.map +1 -1
  540. package/dist/testing/css-variables.js +3 -8
  541. package/dist/testing/css-variables.js.map +1 -1
  542. package/dist/theme/contrast.d.ts +2 -1
  543. package/dist/theme/contrast.js +3 -8
  544. package/dist/theme/contrast.js.map +1 -1
  545. package/dist/theme/palette-store.js +6 -6
  546. package/dist/theme/palette-store.js.map +1 -1
  547. package/dist/theme/text-size.js +3 -8
  548. package/dist/theme/text-size.js.map +1 -1
  549. package/dist/theme/theme-store.js +3 -6
  550. package/dist/theme/theme-store.js.map +1 -1
  551. package/dist/tour/tour-catalogue.d.ts +7 -4
  552. package/dist/tour/tour-catalogue.js +12 -10
  553. package/dist/tour/tour-catalogue.js.map +1 -1
  554. package/dist/tour/tour-dock.d.ts +0 -2
  555. package/dist/tour/tour-dock.js +0 -2
  556. package/dist/tour/tour-dock.js.map +1 -1
  557. package/dist/tour/tour-gates.d.ts +4 -0
  558. package/dist/tour/tour-gates.js +14 -0
  559. package/dist/tour/tour-gates.js.map +1 -1
  560. package/dist/tour/tour-overlay.d.ts +27 -0
  561. package/dist/tour/tour-overlay.js +741 -0
  562. package/dist/tour/tour-overlay.js.map +1 -0
  563. package/dist/tour/tour.d.ts +16 -4
  564. package/dist/tour/tour.js +62 -778
  565. package/dist/tour/tour.js.map +1 -1
  566. package/dist/tour/use-tour-progress.js +1 -1
  567. package/dist/tour/use-tour-progress.js.map +1 -1
  568. package/dist/tour/use-tour-url-sync.js +2 -2
  569. package/dist/tour/use-tour-url-sync.js.map +1 -1
  570. package/dist/wizard/stepper-nav.d.ts +5 -1
  571. package/dist/wizard/stepper-nav.js.map +1 -1
  572. package/dist/wizard/types.d.ts +6 -2
  573. package/dist/wizard/types.js.map +1 -1
  574. package/dist/wizard/use-wizard.d.ts +3 -1
  575. package/dist/wizard/use-wizard.js.map +1 -1
  576. package/dist/wizard/validation.d.ts +3 -1
  577. package/dist/wizard/validation.js.map +1 -1
  578. package/package.json +12 -8
  579. package/src/account/account-parts.ts +5 -25
  580. package/src/account/data-export-setting-labels.ts +1 -1
  581. package/src/account/delete-account-setting-labels.ts +1 -1
  582. package/src/account/email-change-setting-labels.ts +1 -1
  583. package/src/account/passkeys-setting.tsx +60 -38
  584. package/src/account/sessions-setting.tsx +18 -12
  585. package/src/admin/admin-action-confirm.tsx +5 -10
  586. package/src/admin/admin-parts.ts +11 -25
  587. package/src/admin/mail-test-card-labels.ts +1 -1
  588. package/src/admin/plan-change-labels.ts +5 -23
  589. package/src/admin/plan-change.tsx +7 -44
  590. package/src/admin/reviewer-scope-editor.tsx +3 -3
  591. package/src/auth/auth-errors-labels.ts +23 -0
  592. package/src/auth/auth-errors.ts +11 -38
  593. package/src/auth/complete-name-dialog.tsx +9 -8
  594. package/src/auth/forgot-password-form-labels.ts +1 -1
  595. package/src/auth/forgot-password-form.tsx +11 -6
  596. package/src/auth/register-form-labels.ts +1 -1
  597. package/src/auth/reset-password-form-labels.ts +1 -1
  598. package/src/auth/reset-password-form.tsx +8 -4
  599. package/src/auth/sign-in-form-labels.ts +1 -1
  600. package/src/auth/status-parts.tsx +0 -6
  601. package/src/auth/verify-email-labels.ts +1 -1
  602. package/src/auth/verify-email.tsx +11 -6
  603. package/src/billing/cancel-request.ts +28 -6
  604. package/src/billing/cancel-subscription.tsx +29 -22
  605. package/src/billing/dialog-close.ts +34 -0
  606. package/src/billing/plan-card.tsx +36 -11
  607. package/src/billing/plan-change-labels.ts +66 -0
  608. package/src/billing/plan-switch.tsx +31 -20
  609. package/src/chart.ts +21 -3
  610. package/src/components/account-chips.tsx +20 -23
  611. package/src/components/alert-banner.tsx +82 -5
  612. package/src/components/amount-input.tsx +32 -14
  613. package/src/components/autocomplete.tsx +64 -70
  614. package/src/components/bulk-action-bar.tsx +70 -2
  615. package/src/components/button-look.ts +5 -5
  616. package/src/components/button.tsx +2 -2
  617. package/src/components/calculator.tsx +98 -17
  618. package/src/components/chart-edge.ts +34 -1
  619. package/src/components/chart-figure.tsx +35 -0
  620. package/src/components/chart-zoom-core.tsx +6 -5
  621. package/src/components/chart.tsx +26 -5
  622. package/src/components/combobox-core.tsx +303 -69
  623. package/src/components/combobox.tsx +184 -155
  624. package/src/components/copy-button.tsx +34 -23
  625. package/src/components/country-select.tsx +16 -11
  626. package/src/components/data-table-column-settings.tsx +52 -10
  627. package/src/components/data-table-filters.ts +6 -4
  628. package/src/components/data-table-fit.ts +19 -5
  629. package/src/components/data-table-groups.tsx +1 -1
  630. package/src/components/data-table-head.tsx +32 -22
  631. package/src/components/data-table-mobile.tsx +110 -27
  632. package/src/components/data-table-pagination.tsx +21 -6
  633. package/src/components/data-table-resize.ts +1 -1
  634. package/src/components/data-table-row-actions.tsx +3 -3
  635. package/src/components/data-table-row.tsx +25 -14
  636. package/src/components/data-table-selection.ts +1 -1
  637. package/src/components/data-table-shared.tsx +33 -1
  638. package/src/components/data-table-sort.ts +57 -2
  639. package/src/components/data-table-virtual.ts +32 -8
  640. package/src/components/data-table.tsx +267 -41
  641. package/src/components/date-field.tsx +65 -8
  642. package/src/components/date-picker.tsx +125 -48
  643. package/src/components/date-range-picker.tsx +82 -31
  644. package/src/components/dialog-frame.tsx +2 -1
  645. package/src/components/entity-combobox.tsx +4 -1
  646. package/src/components/error-boundary.tsx +53 -39
  647. package/src/components/field-base-labels.ts +20 -0
  648. package/src/components/field-base.tsx +13 -7
  649. package/src/components/field-sync-labels.ts +1 -0
  650. package/src/components/field-sync.tsx +23 -1
  651. package/src/components/form-actions.tsx +20 -5
  652. package/src/components/hover-menu.tsx +18 -8
  653. package/src/components/icon-button.tsx +14 -9
  654. package/src/components/inline-confirm.tsx +116 -0
  655. package/src/components/inline-entity-combobox.tsx +198 -152
  656. package/src/components/input.tsx +1 -1
  657. package/src/components/jump-calendar.tsx +30 -146
  658. package/src/components/legal-page.tsx +10 -7
  659. package/src/components/legal.tsx +4 -2
  660. package/src/components/lightbox.tsx +21 -8
  661. package/src/components/list.tsx +14 -5
  662. package/src/components/measured-grid.tsx +8 -4
  663. package/src/components/mini-calendar.tsx +6 -5
  664. package/src/components/modal.tsx +45 -4
  665. package/src/components/money-draft.ts +16 -5
  666. package/src/components/money-field.tsx +2 -2
  667. package/src/components/month-grid.tsx +229 -0
  668. package/src/components/month-picker.tsx +192 -200
  669. package/src/components/multi-entity-combobox.tsx +1 -0
  670. package/src/components/multi-select.tsx +21 -9
  671. package/src/components/number-field.tsx +14 -7
  672. package/src/components/number-input.tsx +9 -3
  673. package/src/components/numpad-sheet.tsx +4 -3
  674. package/src/components/picker-sheet.tsx +12 -9
  675. package/src/components/pie-chart-labels.ts +8 -0
  676. package/src/components/pie-chart.tsx +22 -28
  677. package/src/components/popover.tsx +18 -1
  678. package/src/components/reauth-dialog.tsx +6 -10
  679. package/src/components/row-actions.tsx +2 -1
  680. package/src/components/section-nav.tsx +7 -0
  681. package/src/components/series-chart-axes.ts +11 -3
  682. package/src/components/series-chart-labels.ts +7 -4
  683. package/src/components/series-chart.tsx +178 -69
  684. package/src/components/share-card.tsx +6 -6
  685. package/src/components/sign-chip-labels.ts +1 -0
  686. package/src/components/sign-chip.tsx +10 -1
  687. package/src/components/tabs-labels.ts +13 -0
  688. package/src/components/tabs.tsx +35 -19
  689. package/src/components/toggle-legend.tsx +16 -11
  690. package/src/components/translation-review-editor.tsx +6 -5
  691. package/src/components/translation-review.tsx +4 -3
  692. package/src/components/treemap-labels.ts +39 -0
  693. package/src/components/treemap-layout.ts +210 -0
  694. package/src/components/treemap.tsx +722 -133
  695. package/src/components/use-table-state.ts +12 -9
  696. package/src/data-table.ts +9 -2
  697. package/src/eslint/role-utilities.ts +97 -19
  698. package/src/feedback/feedback-dialog.tsx +30 -19
  699. package/src/feedback/feedback-mutation.ts +41 -0
  700. package/src/feedback/feedback-page-link.ts +90 -0
  701. package/src/feedback/feedback-public-list.tsx +17 -5
  702. package/src/feedback/feedback-replies.tsx +13 -6
  703. package/src/feedback/feedback-row-detail-labels.ts +5 -0
  704. package/src/feedback/feedback-row-detail.tsx +103 -67
  705. package/src/feedback/feedback-status-undo.ts +41 -27
  706. package/src/feedback/feedback-submit-dialog.tsx +2 -2
  707. package/src/feedback/feedback-submit.tsx +21 -7
  708. package/src/feedback/feedback-swipe.tsx +1 -1
  709. package/src/feedback/feedback-table.tsx +86 -38
  710. package/src/feedback/feedback-thread-labels.ts +2 -2
  711. package/src/feedback/feedback-thread.tsx +27 -25
  712. package/src/feedback.ts +42 -0
  713. package/src/hooks/use-dismiss.ts +3 -2
  714. package/src/hooks/use-focus-trap.ts +8 -24
  715. package/src/hooks/use-hotkey.ts +24 -7
  716. package/src/hooks/use-long-press.ts +16 -2
  717. package/src/hooks/use-media-query.ts +20 -4
  718. package/src/hooks/use-mounted.ts +39 -0
  719. package/src/hooks/use-overlay-history.ts +70 -5
  720. package/src/i18n/defaults.ts +4 -5
  721. package/src/i18n/german.ts +8 -2
  722. package/src/i18n/kit-labels.tsx +9 -2
  723. package/src/i18n/locales/es.ts +8 -2
  724. package/src/i18n/locales/fr.ts +21 -13
  725. package/src/i18n/locales/hu.ts +10 -2
  726. package/src/i18n/locales/it.ts +8 -2
  727. package/src/i18n/locales/zh.ts +9 -2
  728. package/src/i18n/review.ts +5 -0
  729. package/src/index.ts +103 -13
  730. package/src/landing/routing.tsx +14 -10
  731. package/src/lib/broadcast.ts +52 -0
  732. package/src/lib/calc.ts +13 -2
  733. package/src/lib/clipboard-minus.ts +14 -4
  734. package/src/lib/date-format.ts +89 -13
  735. package/src/lib/day-format.ts +96 -0
  736. package/src/lib/decimal-marks.ts +12 -3
  737. package/src/lib/describe-failure.ts +18 -0
  738. package/src/lib/direction.ts +39 -0
  739. package/src/lib/focus-left.ts +156 -0
  740. package/src/lib/format.ts +34 -19
  741. package/src/lib/http-status.ts +38 -0
  742. package/src/lib/intl-cache.ts +25 -0
  743. package/src/lib/isin.ts +10 -0
  744. package/src/lib/iso-day.ts +13 -0
  745. package/src/lib/lazy-with-fallback.ts +82 -0
  746. package/src/lib/path-pattern.ts +55 -0
  747. package/src/lib/reduced-motion.ts +2 -1
  748. package/src/lib/round.ts +7 -2
  749. package/src/lib/safe-storage.ts +48 -6
  750. package/src/lib/session-refresh.ts +1 -1
  751. package/src/lib/share-format.ts +18 -0
  752. package/src/lib/text-case.ts +19 -0
  753. package/src/lib/text-match.ts +69 -2
  754. package/src/lib/thenable.ts +19 -0
  755. package/src/lib/translation-review.ts +13 -5
  756. package/src/notifications/device-zone-report.ts +1 -1
  757. package/src/notifications/notification-bell.tsx +7 -4
  758. package/src/notifications/notification-inbox.tsx +40 -58
  759. package/src/notifications/notification-parts.tsx +3 -4
  760. package/src/notifications/notification-types-setting.tsx +1 -1
  761. package/src/notifications/notifications-labels.ts +7 -0
  762. package/src/notifications/push-device-setting.tsx +1 -1
  763. package/src/notifications/push-devices-setting.tsx +30 -55
  764. package/src/notifications/quiet-hours-setting.tsx +93 -31
  765. package/src/notifications/unsubscribe-panel.tsx +4 -7
  766. package/src/notifications/use-push-device.ts +76 -10
  767. package/src/offline/query-persistence.ts +35 -3
  768. package/src/rhf/fields.tsx +11 -4
  769. package/src/search/command-palette.tsx +2 -4
  770. package/src/search/global-search.tsx +36 -15
  771. package/src/search/use-command-key.ts +13 -12
  772. package/src/search.ts +9 -0
  773. package/src/settings/use-settings-focus.ts +3 -0
  774. package/src/shell/account-menu-language.tsx +27 -5
  775. package/src/shell/account-menu-text-size.tsx +32 -10
  776. package/src/shell/dev-menu.tsx +7 -21
  777. package/src/shell/role-switcher.tsx +7 -0
  778. package/src/shell/top-bar-actions.tsx +1 -1
  779. package/src/shell/top-bar-parts-labels.ts +0 -3
  780. package/src/shell.ts +40 -9
  781. package/src/sync/engine.ts +339 -61
  782. package/src/sync.ts +8 -1
  783. package/src/testing/css-variables.ts +8 -9
  784. package/src/theme/contrast.ts +8 -9
  785. package/src/theme/palette-store.ts +9 -9
  786. package/src/theme/text-size.ts +6 -8
  787. package/src/theme/theme-store.ts +7 -9
  788. package/src/tour/tour-catalogue.tsx +26 -15
  789. package/src/tour/tour-dock.ts +0 -2
  790. package/src/tour/tour-gates.ts +23 -0
  791. package/src/tour/tour-overlay.tsx +1170 -0
  792. package/src/tour/tour.tsx +116 -1193
  793. package/src/tour/use-tour-progress.ts +1 -1
  794. package/src/tour/use-tour-url-sync.ts +7 -3
  795. package/src/wizard/stepper-nav.tsx +3 -1
  796. package/src/wizard/types.ts +11 -2
  797. package/src/wizard/use-wizard.ts +2 -1
  798. package/src/wizard/validation.ts +8 -2
  799. package/tokens.css +2 -1
  800. package/dist/components/ui-labels.js.map +0 -1
  801. package/dist/shell/dev-switchers.d.ts +0 -6
  802. package/dist/shell/dev-switchers.js +0 -12
  803. package/dist/shell/dev-switchers.js.map +0 -1
  804. package/src/components/ui-labels.ts +0 -24
  805. package/src/shell/dev-switchers.tsx +0 -18
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { useCallback, useEffect, useRef, useState } from "react";
3
3
  import { useMediaQuery } from "../hooks/use-media-query.js";
4
- import { httpStatusOf } from "../auth/auth-errors.js";
4
+ import { httpStatusOf } from "../lib/http-status.js";
5
5
  import { PUSH_SUBSCRIPTION_CHANGED, keyBytes, pushSubscriptionInput as inputOf } from "./push-worker.js";
6
6
  class PushDeviceError extends Error {
7
7
  code;
@@ -39,6 +39,31 @@ async function activeRegistration() {
39
39
  return within(navigator.serviceWorker.ready, READY_TIMEOUT_MS, "no_service_worker");
40
40
  }
41
41
  const defaultEndpointTaken = (error) => httpStatusOf(error) === 409;
42
+ async function releaseSubscription(unregister) {
43
+ const registration = await activeRegistration();
44
+ const subscription = await registration.pushManager.getSubscription();
45
+ if (!subscription) return false;
46
+ await unregister(subscription.endpoint);
47
+ await within(subscription.unsubscribe(), PUSH_TIMEOUT_MS, "push_timeout");
48
+ return true;
49
+ }
50
+ async function releaseThisDevicePush(unregister) {
51
+ if (!pushSupported()) return false;
52
+ try {
53
+ const registration = await navigator.serviceWorker.getRegistration();
54
+ if (!registration?.active) return false;
55
+ const subscription = await registration.pushManager.getSubscription();
56
+ if (!subscription) return false;
57
+ try {
58
+ await unregister(subscription.endpoint);
59
+ } finally {
60
+ await within(subscription.unsubscribe(), PUSH_TIMEOUT_MS, "push_timeout");
61
+ }
62
+ return true;
63
+ } finally {
64
+ notifyPushSubscriptionChanged();
65
+ }
66
+ }
42
67
  function usePushDevice(options) {
43
68
  const latest = useRef(options);
44
69
  useEffect(() => {
@@ -183,12 +208,7 @@ function usePushDevice(options) {
183
208
  if (!supported) return;
184
209
  setBusy(true);
185
210
  try {
186
- const registration = await activeRegistration();
187
- const subscription = await registration.pushManager.getSubscription();
188
- if (subscription) {
189
- await latest.current.unregister(subscription.endpoint);
190
- await within(subscription.unsubscribe(), PUSH_TIMEOUT_MS, "push_timeout");
191
- }
211
+ await releaseSubscription((endpoint) => latest.current.unregister(endpoint));
192
212
  if (mounted.current) {
193
213
  setSubscribed(false);
194
214
  setForeignSubscription(false);
@@ -220,6 +240,7 @@ export {
220
240
  PushDeviceError,
221
241
  notifyPushSubscriptionChanged,
222
242
  pushSupported,
243
+ releaseThisDevicePush,
223
244
  usePushDevice
224
245
  };
225
246
  //# sourceMappingURL=use-push-device.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/notifications/use-push-device.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { httpStatusOf } from \"../auth/auth-errors\";\nimport { PUSH_SUBSCRIPTION_CHANGED, keyBytes, pushSubscriptionInput as inputOf } from \"./push-worker\";\nimport type { PushSubscriptionInput } from \"./push-worker\";\n\nexport { PUSH_SUBSCRIPTION_CHANGED };\nexport type { PushSubscriptionInput };\n\n/**\n * This browser's Web Push subscription for the signed-in account\n * (docs/notifications-harmonization.md §2.7, §3.5, §4.2) — keksdose's\n * `shared/push/use-push.ts`, lifted. The service worker with its `push` and\n * `notificationclick` handlers stays the app's; this hook owns the browser's side:\n * permission, subscribe with the server's VAPID key, register the subscription, and\n * unsubscribe. The kit makes no request of its own: the app hands it one function per\n * endpoint.\n */\n\n/** The browser's notification permission, or `\"unsupported\"` where it has no push. */\nexport type PushPermission = NotificationPermission | \"unsupported\";\n\n/** Why turning push on or off could not finish — a {@link PushDeviceError}'s `code`. */\nexport type PushDeviceErrorCode = \"no_service_worker\" | \"push_timeout\";\n\n/**\n * The two failures the card has words of its own for: no service worker registered (or\n * one not ready in 10 s) — \"reload and try again\" — and a browser push service that did\n * not answer in 15 s — \"check your connection\". Anything else is the app's request\n * failing, rejected as it came.\n */\nexport class PushDeviceError extends Error {\n readonly code: PushDeviceErrorCode;\n constructor(code: PushDeviceErrorCode) {\n super(code);\n this.name = \"PushDeviceError\";\n this.code = code;\n }\n}\n\nexport interface UsePushDeviceOptions {\n /** `GET /notifications/push-key` → its `key`; `\"\"` when push is off on the server. */\n pushKey: () => Promise<string>;\n /** `POST /notifications/devices`. A 409 (another account's endpoint) is recovered\n * once: the hook unsubscribes and registers a fresh endpoint. */\n register: (subscription: PushSubscriptionInput) => Promise<unknown>;\n /** `POST /notifications/devices/unregister {endpoint}` — the endpoint never goes in a\n * URL. */\n unregister: (endpoint: string) => Promise<unknown>;\n /** `GET /notifications/devices`: the account's own list, which says whether THIS\n * account holds this browser's subscription. */\n devices: () => Promise<readonly { endpoint: string }[]>;\n /** `POST /notifications/test` → how many devices the push service accepted it for. */\n test: () => Promise<{ sent: number }>;\n /** Whether `register` refused another account's endpoint. Default: HTTP 409. */\n isEndpointTaken?: (error: unknown) => boolean;\n /** After an enable or a disable: the app refreshes its device queries. */\n onChange?: () => void;\n}\n\n/** What {@link usePushDevice} answers. */\nexport interface PushDevice {\n /** The browser exposes service workers, `PushManager` and `Notification`. */\n supported: boolean;\n /** The server has VAPID keys; `undefined` until `pushKey` answered (or when it failed). */\n configured: boolean | undefined;\n /** Running as an installed app: `display-mode: standalone`, or iOS's\n * `navigator.standalone`. */\n installed: boolean;\n /** iPhone or iPad — where push reaches only the installed app. */\n ios: boolean;\n permission: PushPermission;\n /** THIS account holds this browser's subscription. An unreachable device list falls\n * back to the browser's own answer. */\n subscribed: boolean;\n /** This browser holds a subscription that belongs to another account (two accounts in\n * one browser profile): `subscribed` is false, and turning on mints a fresh one. */\n foreignSubscription: boolean;\n /** This browser's subscription endpoint, or `null` — marks \"This device\" in a list. */\n currentEndpoint: string | null;\n /** An enable or a disable is running. */\n busy: boolean;\n /** Ask for permission (the browser's one prompt), subscribe and register. `false` when\n * the permission was not granted or push is off on the server; rejects with a\n * {@link PushDeviceError} or the app's request's error. */\n enable: () => Promise<boolean>;\n /** Tell the server first, then the browser, so a dropped device stops at once. */\n disable: () => Promise<void>;\n sendTest: () => Promise<{ sent: number }>;\n}\n\n/*\n * PUSH_SUBSCRIPTION_CHANGED (push-worker.ts): the window event every mounted\n * {@link usePushDevice} re-reads its state on. Fired by every enable and disable, and by\n * `PushDevicesSetting` when it removes this device — so an app's own card on the same\n * hook (keksdose's check-up) stays in step. The hook that fires it does not re-read: it\n * knows what it changed. Since 0.46 also the message a service worker posts after a\n * rotated subscription was re-registered (`handlePushSubscriptionChange`). Namespaced\n * for the kit; keksdose's was `\"keksdose:push-subscription-changed\"`.\n */\n\n/** Tell every {@link usePushDevice} that this browser's subscription changed. */\nexport function notifyPushSubscriptionChanged(): void {\n if (typeof window !== \"undefined\") window.dispatchEvent(new Event(PUSH_SUBSCRIPTION_CHANGED));\n}\n\n/** Whether this browser has the APIs push needs. iOS exposes them only to an INSTALLED\n * app (Add to Home Screen). */\nexport function pushSupported(): boolean {\n return (\n typeof window !== \"undefined\" &&\n typeof navigator !== \"undefined\" &&\n \"serviceWorker\" in navigator &&\n \"PushManager\" in window &&\n \"Notification\" in window\n );\n}\n\n/** iPhone or iPad. iPadOS 13+ reports itself as a Mac; the touch points tell it apart. */\nfunction isIos(): boolean {\n if (typeof navigator === \"undefined\") return false;\n const ua = navigator.userAgent;\n return /iphone|ipad|ipod/i.test(ua) || (/Macintosh/.test(ua) && navigator.maxTouchPoints > 1);\n}\n\nconst READY_TIMEOUT_MS = 10_000;\nconst PUSH_TIMEOUT_MS = 15_000;\n\n/** `promise`, or a {@link PushDeviceError} after `ms`. The timer is cleared either way. */\nfunction within<T>(promise: Promise<T>, ms: number, code: PushDeviceErrorCode): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n return Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timer = setTimeout(() => reject(new PushDeviceError(code)), ms);\n }),\n ]).finally(() => clearTimeout(timer));\n}\n\n/**\n * The active registration. `navigator.serviceWorker.ready` never settles while the origin\n * has no registration, so a missing one fails at once, and one stuck installing fails\n * after 10 s (keksdose: enable() hung with the button latched before its worker was\n * served).\n */\nasync function activeRegistration(): Promise<ServiceWorkerRegistration> {\n const existing = await navigator.serviceWorker.getRegistration();\n if (!existing) throw new PushDeviceError(\"no_service_worker\");\n return within(navigator.serviceWorker.ready, READY_TIMEOUT_MS, \"no_service_worker\");\n}\n\nconst defaultEndpointTaken = (error: unknown) => httpStatusOf(error) === 409;\n\n/**\n * This browser's push for the signed-in account. Hand it one function per endpoint\n * (§3.8); {@link PushDeviceSetting} draws it, and an app's own card may use it too.\n *\n * **\"Subscribed\" means this account**, not \"this browser holds a subscription\": a\n * browser profile's subscription has no account in it, so a second account in the same\n * profile would read the first's as its own (keksdose). The endpoint is checked against\n * the account's own `devices()`; when that list cannot be read (offline), the browser's\n * answer stands — a wrong \"on\" costs what it did before the check, a wrong \"off\" invites\n * re-subscribing a device that is fine.\n *\n * **A 409 from `register`** — the endpoint belongs to another account, and the server\n * will not re-home what it cannot prove this browser holds — unsubscribes and registers\n * a fresh endpoint, once.\n *\n * **Timeouts:** no registration fails at once and a worker not ready in 10 s fails, both\n * `PushDeviceError(\"no_service_worker\")`; the browser's subscribe and unsubscribe are cut\n * at 15 s, `PushDeviceError(\"push_timeout\")` — so `busy` can never latch.\n *\n * Every mounted hook re-reads its state on {@link PUSH_SUBSCRIPTION_CHANGED}.\n */\nexport function usePushDevice(options: UsePushDeviceOptions): PushDevice {\n // The app's functions are usually new on every render; the effects read the latest.\n const latest = useRef(options);\n useEffect(() => {\n latest.current = options;\n });\n\n const supported = pushSupported();\n const ios = isIos();\n const standalone = useMediaQuery(\"(display-mode: standalone)\", false);\n const installed =\n standalone ||\n (typeof navigator !== \"undefined\" && (navigator as Navigator & { standalone?: boolean }).standalone === true);\n\n const [permission, setPermission] = useState<PushPermission>(() =>\n supported ? Notification.permission : \"unsupported\",\n );\n const [configured, setConfigured] = useState<boolean | undefined>(undefined);\n const [subscribed, setSubscribed] = useState(false);\n const [foreignSubscription, setForeignSubscription] = useState(false);\n const [currentEndpoint, setCurrentEndpoint] = useState<string | null>(null);\n const [busy, setBusy] = useState(false);\n const mounted = useRef(true);\n // Set while this hook fires the event itself: it already knows what changed, and a\n // re-read could only be worse — an app whose `devices()` answers from a cache would\n // hand back the list from before the change.\n const announcing = useRef(false);\n const announce = useCallback(() => {\n announcing.current = true;\n try {\n notifyPushSubscriptionChanged();\n } finally {\n announcing.current = false;\n }\n latest.current.onChange?.();\n }, []);\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n // Whether the server has keys (\"\" → push is off there). Once per mount.\n useEffect(() => {\n if (!supported) return;\n let cancelled = false;\n latest.current.pushKey().then(\n (key) => {\n if (!cancelled) setConfigured(key !== \"\");\n },\n () => {\n /* unknown: the card shows the ordinary states */\n },\n );\n return () => {\n cancelled = true;\n };\n }, [supported]);\n\n // What this browser holds, and whether it is this account's — at mount, and whenever\n // a subscription changes anywhere in the app.\n useEffect(() => {\n if (!supported) return;\n let cancelled = false;\n const sync = async () => {\n try {\n const registration = await activeRegistration();\n const existing = await registration.pushManager.getSubscription();\n if (cancelled) return;\n // Read again: the person may have changed it in the browser's settings.\n setPermission(Notification.permission);\n if (!existing) {\n setSubscribed(false);\n setForeignSubscription(false);\n setCurrentEndpoint(null);\n return;\n }\n let mine: boolean;\n try {\n const devices = await latest.current.devices();\n mine = devices.some((device) => device.endpoint === existing.endpoint);\n } catch {\n mine = true;\n }\n if (cancelled) return;\n setCurrentEndpoint(existing.endpoint);\n setSubscribed(mine);\n setForeignSubscription(!mine);\n } catch {\n /* no active service worker yet: not subscribed */\n }\n };\n const heard = () => {\n if (!announcing.current) void sync();\n };\n // The worker's word after it re-registered a rotated subscription (0.46).\n const fromWorker = (event: MessageEvent) => {\n if ((event.data as { type?: unknown } | null)?.type === PUSH_SUBSCRIPTION_CHANGED) void sync();\n };\n const worker = navigator.serviceWorker as ServiceWorkerContainer | undefined;\n void sync();\n window.addEventListener(PUSH_SUBSCRIPTION_CHANGED, heard);\n worker?.addEventListener?.(\"message\", fromWorker);\n return () => {\n cancelled = true;\n window.removeEventListener(PUSH_SUBSCRIPTION_CHANGED, heard);\n worker?.removeEventListener?.(\"message\", fromWorker);\n };\n }, [supported]);\n\n const enable = useCallback(async (): Promise<boolean> => {\n if (!supported) return false;\n const { pushKey, register, isEndpointTaken = defaultEndpointTaken } = latest.current;\n setBusy(true);\n try {\n const granted = await Notification.requestPermission();\n if (mounted.current) setPermission(granted);\n if (granted !== \"granted\") return false;\n\n const key = await pushKey();\n if (!key) {\n if (mounted.current) setConfigured(false);\n return false;\n }\n if (mounted.current) setConfigured(true);\n\n const registration = await activeRegistration();\n // The one place the browser is asked for a subscription, so the timeout and the\n // `{userVisibleOnly, applicationServerKey}` literal are on both paths.\n const subscribe = (reuse: boolean) =>\n within(\n (async () =>\n (reuse ? await registration.pushManager.getSubscription() : null) ??\n (await registration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey: keyBytes(key),\n })))(),\n PUSH_TIMEOUT_MS,\n \"push_timeout\",\n );\n\n let subscription = await subscribe(true);\n let input = inputOf(subscription);\n if (!input) return false;\n try {\n await register(input);\n } catch (error) {\n if (!isEndpointTaken(error)) throw error;\n await within(subscription.unsubscribe(), PUSH_TIMEOUT_MS, \"push_timeout\");\n subscription = await subscribe(false);\n input = inputOf(subscription);\n if (!input) return false;\n await register(input);\n }\n if (mounted.current) {\n setSubscribed(true);\n setForeignSubscription(false);\n setCurrentEndpoint(input.endpoint);\n }\n announce();\n return true;\n } finally {\n if (mounted.current) setBusy(false);\n }\n }, [supported, announce]);\n\n const disable = useCallback(async (): Promise<void> => {\n if (!supported) return;\n setBusy(true);\n try {\n const registration = await activeRegistration();\n const subscription = await registration.pushManager.getSubscription();\n if (subscription) {\n // The server first: a dropped device stops receiving even if the browser's\n // unsubscribe below fails.\n await latest.current.unregister(subscription.endpoint);\n await within(subscription.unsubscribe(), PUSH_TIMEOUT_MS, \"push_timeout\");\n }\n if (mounted.current) {\n setSubscribed(false);\n setForeignSubscription(false);\n setCurrentEndpoint(null);\n }\n announce();\n } finally {\n if (mounted.current) setBusy(false);\n }\n }, [supported, announce]);\n\n const sendTest = useCallback(() => latest.current.test(), []);\n\n return {\n supported,\n configured,\n installed,\n ios,\n permission,\n subscribed,\n foreignSubscription,\n currentEndpoint,\n busy,\n enable,\n disable,\n sendTest,\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/notifications/use-push-device.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { httpStatusOf } from \"../lib/http-status\";\nimport { PUSH_SUBSCRIPTION_CHANGED, keyBytes, pushSubscriptionInput as inputOf } from \"./push-worker\";\nimport type { PushSubscriptionInput } from \"./push-worker\";\n\nexport { PUSH_SUBSCRIPTION_CHANGED };\nexport type { PushSubscriptionInput };\n\n/**\n * This browser's Web Push subscription for the signed-in account\n * (docs/notifications-harmonization.md §2.7, §3.5, §4.2) — keksdose's\n * `shared/push/use-push.ts`, lifted. The service worker with its `push` and\n * `notificationclick` handlers stays the app's; this hook owns the browser's side:\n * permission, subscribe with the server's VAPID key, register the subscription, and\n * unsubscribe. The kit makes no request of its own: the app hands it one function per\n * endpoint.\n */\n\n/** The browser's notification permission, or `\"unsupported\"` where it has no push. */\nexport type PushPermission = NotificationPermission | \"unsupported\";\n\n/** Why turning push on or off could not finish — a {@link PushDeviceError}'s `code`. */\nexport type PushDeviceErrorCode = \"no_service_worker\" | \"push_timeout\";\n\n/**\n * The two failures the card has words of its own for: no service worker registered (or\n * one not ready in 10 s) — \"reload and try again\" — and a browser push service that did\n * not answer in 15 s — \"check your connection\". Anything else is the app's request\n * failing, rejected as it came.\n */\nexport class PushDeviceError extends Error {\n readonly code: PushDeviceErrorCode;\n constructor(code: PushDeviceErrorCode) {\n super(code);\n this.name = \"PushDeviceError\";\n this.code = code;\n }\n}\n\nexport interface UsePushDeviceOptions {\n /** `GET /notifications/push-key` → its `key`; `\"\"` when push is off on the server. */\n pushKey: () => Promise<string>;\n /** `POST /notifications/devices`. A 409 (another account's endpoint) is recovered\n * once: the hook unsubscribes and registers a fresh endpoint. */\n register: (subscription: PushSubscriptionInput) => Promise<unknown>;\n /** `POST /notifications/devices/unregister {endpoint}` — the endpoint never goes in a\n * URL. */\n unregister: (endpoint: string) => Promise<unknown>;\n /** `GET /notifications/devices`: the account's own list, which says whether THIS\n * account holds this browser's subscription. */\n devices: () => Promise<readonly { endpoint: string }[]>;\n /** `POST /notifications/test` → how many devices the push service accepted it for. */\n test: () => Promise<{ sent: number }>;\n /** Whether `register` refused another account's endpoint. Default: HTTP 409. */\n isEndpointTaken?: (error: unknown) => boolean;\n /** After an enable or a disable: the app refreshes its device queries. */\n onChange?: () => void;\n}\n\n/** What {@link usePushDevice} answers. */\nexport interface PushDevice {\n /** The browser exposes service workers, `PushManager` and `Notification`. */\n supported: boolean;\n /** The server has VAPID keys; `undefined` until `pushKey` answered (or when it failed). */\n configured: boolean | undefined;\n /** Running as an installed app: `display-mode: standalone`, or iOS's\n * `navigator.standalone`. */\n installed: boolean;\n /** iPhone or iPad — where push reaches only the installed app. */\n ios: boolean;\n permission: PushPermission;\n /** THIS account holds this browser's subscription. An unreachable device list falls\n * back to the browser's own answer. */\n subscribed: boolean;\n /** This browser holds a subscription that belongs to another account (two accounts in\n * one browser profile): `subscribed` is false, and turning on mints a fresh one. */\n foreignSubscription: boolean;\n /** This browser's subscription endpoint, or `null` — marks \"This device\" in a list. */\n currentEndpoint: string | null;\n /** An enable or a disable is running. */\n busy: boolean;\n /** Ask for permission (the browser's one prompt), subscribe and register. `false` when\n * the permission was not granted or push is off on the server; rejects with a\n * {@link PushDeviceError} or the app's request's error. */\n enable: () => Promise<boolean>;\n /** Tell the server first, then the browser, so a dropped device stops at once. At a\n * sign-out, with no card mounted: {@link releaseThisDevicePush}. */\n disable: () => Promise<void>;\n sendTest: () => Promise<{ sent: number }>;\n}\n\n/*\n * PUSH_SUBSCRIPTION_CHANGED (push-worker.ts): the window event every mounted\n * {@link usePushDevice} re-reads its state on. Fired by every enable and disable, and by\n * `PushDevicesSetting` when it removes this device — so an app's own card on the same\n * hook (keksdose's check-up) stays in step. The hook that fires it does not re-read: it\n * knows what it changed. Since 0.46 also the message a service worker posts after a\n * rotated subscription was re-registered (`handlePushSubscriptionChange`). Namespaced\n * for the kit; keksdose's was `\"keksdose:push-subscription-changed\"`.\n */\n\n/** Tell every {@link usePushDevice} that this browser's subscription changed. */\nexport function notifyPushSubscriptionChanged(): void {\n if (typeof window !== \"undefined\") window.dispatchEvent(new Event(PUSH_SUBSCRIPTION_CHANGED));\n}\n\n/** Whether this browser has the APIs push needs. iOS exposes them only to an INSTALLED\n * app (Add to Home Screen). */\nexport function pushSupported(): boolean {\n return (\n typeof window !== \"undefined\" &&\n typeof navigator !== \"undefined\" &&\n \"serviceWorker\" in navigator &&\n \"PushManager\" in window &&\n \"Notification\" in window\n );\n}\n\n/** iPhone or iPad. iPadOS 13+ reports itself as a Mac; the touch points tell it apart. */\nfunction isIos(): boolean {\n if (typeof navigator === \"undefined\") return false;\n const ua = navigator.userAgent;\n return /iphone|ipad|ipod/i.test(ua) || (/Macintosh/.test(ua) && navigator.maxTouchPoints > 1);\n}\n\nconst READY_TIMEOUT_MS = 10_000;\nconst PUSH_TIMEOUT_MS = 15_000;\n\n/** `promise`, or a {@link PushDeviceError} after `ms`. The timer is cleared either way. */\nfunction within<T>(promise: Promise<T>, ms: number, code: PushDeviceErrorCode): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n return Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n timer = setTimeout(() => reject(new PushDeviceError(code)), ms);\n }),\n ]).finally(() => clearTimeout(timer));\n}\n\n/**\n * The active registration. `navigator.serviceWorker.ready` never settles while the origin\n * has no registration, so a missing one fails at once, and one stuck installing fails\n * after 10 s (keksdose: enable() hung with the button latched before its worker was\n * served).\n */\nasync function activeRegistration(): Promise<ServiceWorkerRegistration> {\n const existing = await navigator.serviceWorker.getRegistration();\n if (!existing) throw new PushDeviceError(\"no_service_worker\");\n return within(navigator.serviceWorker.ready, READY_TIMEOUT_MS, \"no_service_worker\");\n}\n\nconst defaultEndpointTaken = (error: unknown) => httpStatusOf(error) === 409;\n\n/**\n * Drop this browser's subscription when the person turns push off: the server first, so a\n * dropped device stops receiving even if the browser's unsubscribe fails, then the\n * browser, cut at 15 s. Whether there was one. A refused server keeps the browser's\n * subscription: the card says the turn-off failed, still reads on, and the person tries\n * again. {@link usePushDevice}'s `disable`; a sign-out has its own\n * ({@link releaseThisDevicePush}).\n */\nasync function releaseSubscription(unregister: (endpoint: string) => Promise<unknown>): Promise<boolean> {\n const registration = await activeRegistration();\n const subscription = await registration.pushManager.getSubscription();\n if (!subscription) return false;\n await unregister(subscription.endpoint);\n await within(subscription.unsubscribe(), PUSH_TIMEOUT_MS, \"push_timeout\");\n return true;\n}\n\n/**\n * Release this browser's push at an EXPLICIT sign-out (0.52, docs/notifications-\n * harmonization.md §10.6; refactor audit 2026-10-30, FLW-2) — without the settings card\n * mounted, which `usePushDevice().disable` needs.\n *\n * A sign-out left the subscription registered to the account, so the device went on\n * showing that account's notifications, title and body (keksdose: invoices, bookings,\n * shares), to whoever used it next — while the same sign-out wiped the mirror, the outbox\n * and the query cache \"for the next person on this device\".\n *\n * Call it with the app's `unregister` (`POST /notifications/devices/unregister\n * {endpoint}`) BEFORE the session is cleared, while the token is still valid:\n *\n * async function logOut() {\n * await releaseThisDevicePush(api.unregisterDevice).catch(() => undefined);\n * clear();\n * }\n *\n * The server first, then the browser — and the browser EVEN WHEN THE SERVER REFUSED (0.52\n * code review, this file): `disable`'s order left a sign-out on a flaky network, or one\n * that met a 5xx, with the device still subscribed. Unsubscribed, the push service answers\n * the server's next send with 410 and the server prunes the row. The browser's unsubscribe\n * is cut at 15 s (`PushDeviceError(\"push_timeout\")`). Then it tells every mounted\n * {@link usePushDevice} ({@link PUSH_SUBSCRIPTION_CHANGED}).\n *\n * Resolves whether this browser held a subscription; `false` at once where push is\n * unsupported or was never set up — no service worker registration, or none ever active\n * (0.52 code review: most devices, which used to reject with `no_service_worker`, after\n * 10 s for a worker stuck installing). It asks the registration, not `ready`: a\n * subscription belongs to the registration, and a sign-out waits for no worker. Rejects\n * as the app's request did — once the browser has let go — or with `push_timeout`. Best\n * effort at a sign-out: catch it, a sign-out must not stop on push.\n *\n * Only an explicit sign-out (Marcel's decision 2 of the 2026-10-30 audit): a session that\n * merely expired keeps its device, and whoever signs back in turns push on again.\n */\nexport async function releaseThisDevicePush(unregister: (endpoint: string) => Promise<unknown>): Promise<boolean> {\n if (!pushSupported()) return false;\n try {\n const registration = await navigator.serviceWorker.getRegistration();\n // A worker never active holds no subscription: `subscribe` needs an active one.\n if (!registration?.active) return false;\n const subscription = await registration.pushManager.getSubscription();\n if (!subscription) return false;\n try {\n await unregister(subscription.endpoint);\n } finally {\n await within(subscription.unsubscribe(), PUSH_TIMEOUT_MS, \"push_timeout\");\n }\n return true;\n } finally {\n notifyPushSubscriptionChanged();\n }\n}\n\n/**\n * This browser's push for the signed-in account. Hand it one function per endpoint\n * (§3.8); {@link PushDeviceSetting} draws it, and an app's own card may use it too.\n *\n * **\"Subscribed\" means this account**, not \"this browser holds a subscription\": a\n * browser profile's subscription has no account in it, so a second account in the same\n * profile would read the first's as its own (keksdose). The endpoint is checked against\n * the account's own `devices()`; when that list cannot be read (offline), the browser's\n * answer stands — a wrong \"on\" costs what it did before the check, a wrong \"off\" invites\n * re-subscribing a device that is fine.\n *\n * **A 409 from `register`** — the endpoint belongs to another account, and the server\n * will not re-home what it cannot prove this browser holds — unsubscribes and registers\n * a fresh endpoint, once.\n *\n * **Timeouts:** no registration fails at once and a worker not ready in 10 s fails, both\n * `PushDeviceError(\"no_service_worker\")`; the browser's subscribe and unsubscribe are cut\n * at 15 s, `PushDeviceError(\"push_timeout\")` — so `busy` can never latch.\n *\n * Every mounted hook re-reads its state on {@link PUSH_SUBSCRIPTION_CHANGED}.\n */\nexport function usePushDevice(options: UsePushDeviceOptions): PushDevice {\n // The app's functions are usually new on every render; the effects read the latest.\n const latest = useRef(options);\n useEffect(() => {\n latest.current = options;\n });\n\n const supported = pushSupported();\n const ios = isIos();\n const standalone = useMediaQuery(\"(display-mode: standalone)\", false);\n const installed =\n standalone ||\n (typeof navigator !== \"undefined\" && (navigator as Navigator & { standalone?: boolean }).standalone === true);\n\n const [permission, setPermission] = useState<PushPermission>(() =>\n supported ? Notification.permission : \"unsupported\",\n );\n const [configured, setConfigured] = useState<boolean | undefined>(undefined);\n const [subscribed, setSubscribed] = useState(false);\n const [foreignSubscription, setForeignSubscription] = useState(false);\n const [currentEndpoint, setCurrentEndpoint] = useState<string | null>(null);\n const [busy, setBusy] = useState(false);\n const mounted = useRef(true);\n // Set while this hook fires the event itself: it already knows what changed, and a\n // re-read could only be worse — an app whose `devices()` answers from a cache would\n // hand back the list from before the change.\n const announcing = useRef(false);\n const announce = useCallback(() => {\n announcing.current = true;\n try {\n notifyPushSubscriptionChanged();\n } finally {\n announcing.current = false;\n }\n latest.current.onChange?.();\n }, []);\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n // Whether the server has keys (\"\" → push is off there). Once per mount.\n useEffect(() => {\n if (!supported) return;\n let cancelled = false;\n latest.current.pushKey().then(\n (key) => {\n if (!cancelled) setConfigured(key !== \"\");\n },\n () => {\n /* unknown: the card shows the ordinary states */\n },\n );\n return () => {\n cancelled = true;\n };\n }, [supported]);\n\n // What this browser holds, and whether it is this account's — at mount, and whenever\n // a subscription changes anywhere in the app.\n useEffect(() => {\n if (!supported) return;\n let cancelled = false;\n const sync = async () => {\n try {\n const registration = await activeRegistration();\n const existing = await registration.pushManager.getSubscription();\n if (cancelled) return;\n // Read again: the person may have changed it in the browser's settings.\n setPermission(Notification.permission);\n if (!existing) {\n setSubscribed(false);\n setForeignSubscription(false);\n setCurrentEndpoint(null);\n return;\n }\n let mine: boolean;\n try {\n const devices = await latest.current.devices();\n mine = devices.some((device) => device.endpoint === existing.endpoint);\n } catch {\n mine = true;\n }\n if (cancelled) return;\n setCurrentEndpoint(existing.endpoint);\n setSubscribed(mine);\n setForeignSubscription(!mine);\n } catch {\n /* no active service worker yet: not subscribed */\n }\n };\n const heard = () => {\n if (!announcing.current) void sync();\n };\n // The worker's word after it re-registered a rotated subscription (0.46).\n const fromWorker = (event: MessageEvent) => {\n if ((event.data as { type?: unknown } | null)?.type === PUSH_SUBSCRIPTION_CHANGED) void sync();\n };\n const worker = navigator.serviceWorker as ServiceWorkerContainer | undefined;\n void sync();\n window.addEventListener(PUSH_SUBSCRIPTION_CHANGED, heard);\n worker?.addEventListener?.(\"message\", fromWorker);\n return () => {\n cancelled = true;\n window.removeEventListener(PUSH_SUBSCRIPTION_CHANGED, heard);\n worker?.removeEventListener?.(\"message\", fromWorker);\n };\n }, [supported]);\n\n const enable = useCallback(async (): Promise<boolean> => {\n if (!supported) return false;\n const { pushKey, register, isEndpointTaken = defaultEndpointTaken } = latest.current;\n setBusy(true);\n try {\n const granted = await Notification.requestPermission();\n if (mounted.current) setPermission(granted);\n if (granted !== \"granted\") return false;\n\n const key = await pushKey();\n if (!key) {\n if (mounted.current) setConfigured(false);\n return false;\n }\n if (mounted.current) setConfigured(true);\n\n const registration = await activeRegistration();\n // The one place the browser is asked for a subscription, so the timeout and the\n // `{userVisibleOnly, applicationServerKey}` literal are on both paths.\n const subscribe = (reuse: boolean) =>\n within(\n (async () =>\n (reuse ? await registration.pushManager.getSubscription() : null) ??\n (await registration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey: keyBytes(key),\n })))(),\n PUSH_TIMEOUT_MS,\n \"push_timeout\",\n );\n\n let subscription = await subscribe(true);\n let input = inputOf(subscription);\n if (!input) return false;\n try {\n await register(input);\n } catch (error) {\n if (!isEndpointTaken(error)) throw error;\n await within(subscription.unsubscribe(), PUSH_TIMEOUT_MS, \"push_timeout\");\n subscription = await subscribe(false);\n input = inputOf(subscription);\n if (!input) return false;\n await register(input);\n }\n if (mounted.current) {\n setSubscribed(true);\n setForeignSubscription(false);\n setCurrentEndpoint(input.endpoint);\n }\n announce();\n return true;\n } finally {\n if (mounted.current) setBusy(false);\n }\n }, [supported, announce]);\n\n const disable = useCallback(async (): Promise<void> => {\n if (!supported) return;\n setBusy(true);\n try {\n await releaseSubscription((endpoint) => latest.current.unregister(endpoint));\n if (mounted.current) {\n setSubscribed(false);\n setForeignSubscription(false);\n setCurrentEndpoint(null);\n }\n announce();\n } finally {\n if (mounted.current) setBusy(false);\n }\n }, [supported, announce]);\n\n const sendTest = useCallback(() => latest.current.test(), []);\n\n return {\n supported,\n configured,\n installed,\n ios,\n permission,\n subscribed,\n foreignSubscription,\n currentEndpoint,\n busy,\n enable,\n disable,\n sendTest,\n };\n}\n"],"mappings":"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","names":[]}
@@ -88,7 +88,9 @@ export interface QueryPersistence {
88
88
  /** Everything `<PersistQueryClientProvider persistOptions>` needs. */
89
89
  persistOptions: Omit<PersistQueryClientOptions, "queryClient">;
90
90
  /** Sign-out: stops writing under this key, empties the in-memory cache and deletes this
91
- * key's persisted copy. Best-effort; never throws. */
91
+ * key's persisted copy. In every tab (0.52): the persistences of this key in the other tabs
92
+ * stop writing too, and delete the key once more after any save of theirs under way.
93
+ * Best-effort; never throws. */
92
94
  clear: (queryClient: QueryClient) => Promise<void>;
93
95
  /** The company switch (or any change of key without a sign-out): stops writing under this
94
96
  * key — a save already scheduled is dropped too — and empties the in-memory cache, so
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister";
3
3
  import { del, get, keys, set } from "idb-keyval";
4
+ import { listenOn, postOnce } from "../lib/broadcast.js";
4
5
  const DEFAULT_SCRUB_FIELDS = Object.freeze([
5
6
  "iban",
6
7
  "qr_iban",
@@ -78,6 +79,20 @@ function createQueryPersistence({
78
79
  const key = `${prefix}${user}${company === void 0 || company === null || company === "" ? "" : `-${company}`}`;
79
80
  const denied = new Set(denyRoots);
80
81
  let retired = false;
82
+ const across = `eifi1-query-cache:${key}`;
83
+ let unlisten = null;
84
+ const retire = () => {
85
+ retired = true;
86
+ unlisten?.();
87
+ unlisten = null;
88
+ };
89
+ const listen = () => {
90
+ unlisten ??= listenOn(across, (data) => {
91
+ if (data !== "clear") return;
92
+ retire();
93
+ Promise.resolve().then(() => storage.removeItem(key)).catch(() => void 0);
94
+ });
95
+ };
81
96
  const keyAllowed = (queryKey) => !denied.has(String(queryKey[0])) && (shouldPersistKey ? shouldPersistKey(queryKey) : true);
82
97
  const shouldDehydrateQuery = (query) => {
83
98
  if (query.state.status !== "success" || !keyAllowed(query.queryKey)) return false;
@@ -136,6 +151,7 @@ function createQueryPersistence({
136
151
  // Only the provider's restore reads: mounting this persistence again re-arms it.
137
152
  getItem: async (k) => {
138
153
  retired = false;
154
+ listen();
139
155
  return await storage.getItem(k) ?? null;
140
156
  },
141
157
  setItem: async (k, v) => {
@@ -151,14 +167,15 @@ function createQueryPersistence({
151
167
  deserialize
152
168
  });
153
169
  const leave = async (queryClient) => {
154
- retired = true;
170
+ retire();
155
171
  try {
156
172
  queryClient.clear();
157
173
  } catch {
158
174
  }
159
175
  };
160
176
  const clear = async (queryClient) => {
161
- retired = true;
177
+ retire();
178
+ postOnce(across, "clear");
162
179
  try {
163
180
  queryClient.clear();
164
181
  await persister.removeClient();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/offline/query-persistence.ts"],"sourcesContent":["import { createAsyncStoragePersister } from \"@tanstack/query-async-storage-persister\";\nimport type { QueryClient } from \"@tanstack/react-query\";\nimport type { PersistedClient, PersistQueryClientOptions } from \"@tanstack/react-query-persist-client\";\nimport { del, get, keys, set } from \"idb-keyval\";\n\n/**\n * The persisted TanStack Query cache, keksdose's policy as a kit helper\n * (docs/local-first-harmonization.md §1.5 D4, D5, D7; §1.6). keksdose's\n * shared/state/query-client.ts is the canon: an IndexedDB persister, a deny-list, the\n * plain-JSON guard on write (`shouldDehydrateQuery`) and its twin on restore that keeps\n * paused mutations. On top: a key per account (and company), a scrub list that removes\n * secret fields from payloads that are otherwise wanted offline, and `removeStale` for\n * another account's leftovers.\n *\n * The only module of the kit that imports TanStack and idb-keyval: both are optional peers\n * of `@eifi1/ui-kit/offline` and never reached from the main barrel (packaging-contract).\n */\n\n/** A field name the scrub removes: an exact name, or a pattern (`/^signature_/`). */\nexport type ScrubField = string | RegExp;\n\n/**\n * Removed from every query's data, at any depth, before it is persisted (D4, D7): bank\n * account numbers and the handover protocol's signatures. A page restored offline shows\n * such a field as missing (\"Needs a connection\").\n */\nexport const DEFAULT_SCRUB_FIELDS: readonly ScrubField[] = Object.freeze([\n \"iban\",\n \"qr_iban\",\n \"default_qr_iban\",\n /^signature_/,\n]);\n\n/** The storage the persister writes to. The default is IndexedDB through idb-keyval. */\nexport interface QueryPersistenceStorage {\n getItem: (key: string) => Promise<string | null | undefined>;\n setItem: (key: string, value: string) => Promise<unknown>;\n removeItem: (key: string) => Promise<unknown>;\n /** Every key in the store — what `removeStale` walks. */\n keys: () => Promise<readonly unknown[]>;\n}\n\nexport interface QueryPersistenceOptions {\n /** The app's name, the key's first part: `kastlan` → `kastlan-query-cache-…`. */\n app: string;\n /** The signed-in account. One cache per account (D5). */\n user: string | number;\n /** The acting company, where the app has several (kastlan): the switch changes the key,\n * so no wipe and no cross-company leak — provided the app switches as\n * {@link QueryPersistence.leave} says (leave, then remount the provider per key). */\n company?: string | number | null;\n /** The app's own rule, beside `denyRoots` — keksdose denies one exact key and every key\n * whose second segment is `\"admin\"`. `false` keeps the query out. */\n shouldPersistKey?: (queryKey: readonly unknown[]) => boolean;\n /** Query roots (`queryKey[0]`) that never reach the disk: admin, billing, reports… */\n denyRoots?: readonly string[];\n /** Replaces {@link DEFAULT_SCRUB_FIELDS}; spread it to add to it. `[]` scrubs nothing.\n * A paused mutation whose variables carry one of these fields is never persisted (a\n * scrubbed mutation would send something else), and one an older build wrote is dropped\n * on restore. */\n scrubFields?: readonly ScrubField[];\n /** Told when a query's data is not plain JSON and is therefore not persisted. Default:\n * `console.error`, as keksdose's `cacheSafeOrWarn`. */\n onUnsafe?: (queryKey: readonly unknown[], data: unknown) => void;\n /** Changes with each deploy (the app version), so a new build discards the old cache. */\n buster: string;\n /** How long a persisted cache is good for, in ms (keksdose: 24 h). The QueryClient's\n * `gcTime` must be at least this, or restore drops every query. */\n maxAge: number;\n /** Default: IndexedDB (idb-keyval). Tests pass an in-memory one. */\n storage?: QueryPersistenceStorage;\n /** Default 1000 ms, as keksdose. */\n throttleTime?: number;\n /**\n * 0.46 (kastlan K1): whether paused mutations are written with the cache. Default `true`\n * (keksdose: an offline write waits in TanStack's queue and is sent after a reload).\n * `false` keeps every mutation off the disk, and drops any an older build wrote on\n * restore — for an app that queues offline writes in its own outbox (kastlan's Dexie\n * outbox), instead of overriding `dehydrateOptions` by hand.\n */\n persistMutations?: boolean;\n}\n\n/**\n * One persisted cache, for one key. **Mount it per key**: the provider subscribes once and\n * ignores a changed `persistOptions`, so give it the key as its React key —\n *\n * ```tsx\n * <PersistQueryClientProvider key={persistence.key} client={queryClient} persistOptions={persistence.persistOptions}>\n * ```\n *\n * — and create the persistence once the account (and company) is known (`useMemo` on\n * them); before sign-in, mount the plain `QueryClientProvider`. On a company switch, call\n * {@link QueryPersistence.leave} on the old one BEFORE the new key mounts, instead of a bare\n * `queryClient.clear()`: otherwise the old subscription saves the emptied (or the next\n * company's) cache under the old key.\n */\nexport interface QueryPersistence {\n /** `<app>-query-cache-<user>[-<company>]`. */\n key: string;\n /** Everything `<PersistQueryClientProvider persistOptions>` needs. */\n persistOptions: Omit<PersistQueryClientOptions, \"queryClient\">;\n /** Sign-out: stops writing under this key, empties the in-memory cache and deletes this\n * key's persisted copy. Best-effort; never throws. */\n clear: (queryClient: QueryClient) => Promise<void>;\n /** The company switch (or any change of key without a sign-out): stops writing under this\n * key — a save already scheduled is dropped too — and empties the in-memory cache, so\n * the next key's provider starts clean. The persisted copy stays for the way back. A\n * provider that mounts this persistence again re-arms it with its restore. Never throws. */\n leave: (queryClient: QueryClient) => Promise<void>;\n /** At sign-in: deletes this app's OTHER persisted caches older than `maxAge` (another\n * account's, another company's). Other apps' keys and fresh ones stay. Never throws. */\n removeStale: () => Promise<void>;\n}\n\nconst idbStorage: QueryPersistenceStorage = {\n getItem: (key) => get<string>(key),\n setItem: (key, value) => set(key, value),\n removeItem: (key) => del(key),\n keys: () => keys(),\n};\n\nexport interface RemoveStaleQueryCopiesOptions {\n /** Default: IndexedDB (idb-keyval), the store {@link createQueryPersistence} writes to. */\n storage?: QueryPersistenceStorage;\n /** One key to spare whatever its age — the signed-in account's own\n * (`QueryPersistence.removeStale` passes its key). */\n keep?: string;\n}\n\n/**\n * 0.46 (kastlan K2): deletes this app's persisted query caches (`<app>-query-cache-…`)\n * older than `maxAge`, with no persistence at hand — for the sign-in page, where no\n * account is known. A session that ended with no user known (a refresh token refused at\n * boot) cannot name its key for `clear`; this sweeps its copy once it is stale, without\n * waiting for someone's next sign-in. Other apps' keys, other stores' keys and fresh\n * caches stay; an unreadable one is stale. Never throws.\n * {@link QueryPersistence.removeStale} is this with `keep` = its own key.\n */\nexport async function removeStaleQueryCopies(\n app: string,\n maxAge: number,\n { storage = idbStorage, keep }: RemoveStaleQueryCopiesOptions = {},\n): Promise<void> {\n const prefix = `${app}-query-cache-`;\n try {\n const now = Date.now();\n for (const k of await storage.keys()) {\n if (typeof k !== \"string\" || !k.startsWith(prefix) || k === keep) continue;\n let timestamp = 0;\n try {\n const raw = await storage.getItem(k);\n const parsed: unknown = raw ? JSON.parse(raw) : null;\n if (parsed && typeof parsed === \"object\" && typeof (parsed as PersistedClient).timestamp === \"number\") {\n timestamp = (parsed as PersistedClient).timestamp;\n }\n } catch {\n /* unreadable: stale */\n }\n if (now - timestamp > maxAge) await storage.removeItem(k);\n }\n } catch {\n /* best-effort */\n }\n}\n\n/**\n * True when `data` still means the same thing after JSON.stringify and JSON.parse —\n * keksdose's `isCacheSafe` (query-client.ts:165). Class instances, Map/Set/Date/Blob, a\n * non-finite number, a function: all refused. An `undefined` property survives.\n */\nexport function isCacheSafe(data: unknown, depth = 0): boolean {\n if (depth > 12) return false;\n if (data === null) return true;\n const t = typeof data;\n if (t === \"string\" || t === \"boolean\") return true;\n if (t === \"number\") return Number.isFinite(data as number);\n if (t !== \"object\") return false;\n if (Array.isArray(data)) return data.every((v) => isCacheSafe(v, depth + 1));\n const proto: unknown = Object.getPrototypeOf(data);\n if (proto !== Object.prototype && proto !== null) return false;\n return Object.values(data as Record<string, unknown>).every((v) => v === undefined || isCacheSafe(v, depth + 1));\n}\n\n/** A copy of `data` without the scrubbed fields, at any depth. The input is not touched. */\nfunction scrub(data: unknown, fields: readonly ScrubField[]): unknown {\n if (fields.length === 0 || data === null || typeof data !== \"object\") return data;\n if (Array.isArray(data)) return data.map((v) => scrub(v, fields));\n const out: Record<string, unknown> = {};\n for (const [name, value] of Object.entries(data as Record<string, unknown>)) {\n if (fields.some((f) => (typeof f === \"string\" ? f === name : f.test(name)))) continue;\n out[name] = scrub(value, fields);\n }\n return out;\n}\n\nconst defaultOnUnsafe = (queryKey: readonly unknown[]) => {\n console.error(\n `[query-cache] refusing to persist ${JSON.stringify(queryKey)}: the value is not plain JSON ` +\n `(a class instance, Map, Set, Date or Blob). Return plain data from the queryFn and build ` +\n `the rich object at the call site.`,\n );\n};\n\nexport function createQueryPersistence({\n app,\n user,\n company,\n shouldPersistKey,\n denyRoots = [],\n scrubFields = DEFAULT_SCRUB_FIELDS,\n onUnsafe = defaultOnUnsafe,\n buster,\n maxAge,\n storage = idbStorage,\n throttleTime = 1000,\n persistMutations = true,\n}: QueryPersistenceOptions): QueryPersistence {\n const prefix = `${app}-query-cache-`;\n const key = `${prefix}${user}${company === undefined || company === null || company === \"\" ? \"\" : `-${company}`}`;\n const denied = new Set(denyRoots);\n // Set by `leave` and `clear`: a write under this key after the person left it (a\n // throttled save of the emptied cache) is dropped. A restore re-arms it.\n let retired = false;\n\n const keyAllowed = (queryKey: readonly unknown[]) =>\n !denied.has(String(queryKey[0])) && (shouldPersistKey ? shouldPersistKey(queryKey) : true);\n\n /** The write side: a success, an allowed key, plain JSON (or the warning). */\n const shouldDehydrateQuery = (query: {\n queryKey: readonly unknown[];\n state: { status: string; data?: unknown };\n }): boolean => {\n if (query.state.status !== \"success\" || !keyAllowed(query.queryKey)) return false;\n if (isCacheSafe(query.state.data)) return true;\n onUnsafe(query.queryKey, query.state.data);\n return false;\n };\n\n /** One persisted query entry, scrubbed; `null` when it may not be restored. */\n const restorable = (entry: unknown): unknown => {\n if (!entry || typeof entry !== \"object\") return null;\n const { queryKey, state } = entry as { queryKey?: unknown; state?: { data?: unknown } };\n if (!Array.isArray(queryKey) || !keyAllowed(queryKey)) return null;\n if (!isCacheSafe(state?.data)) return null;\n return withScrubbedData(entry);\n };\n\n /** Whether `value` has a scrubbed field's name anywhere in it. */\n const carriesScrubbed = (value: unknown, depth = 0): boolean => {\n if (scrubFields.length === 0 || depth > 12 || value === null || typeof value !== \"object\") return false;\n if (Array.isArray(value)) return value.some((v) => carriesScrubbed(v, depth + 1));\n return Object.entries(value as Record<string, unknown>).some(\n ([name, v]) =>\n scrubFields.some((f) => (typeof f === \"string\" ? f === name : f.test(name))) || carriesScrubbed(v, depth + 1),\n );\n };\n\n /** The write side for mutations: TanStack's default (paused ones), minus any whose\n * variables carry a scrubbed field (S6); none at all with `persistMutations: false`. */\n const shouldDehydrateMutation = (mutation: { state: { isPaused: boolean; variables?: unknown } }): boolean =>\n persistMutations && mutation.state.isPaused && !carriesScrubbed(mutation.state.variables);\n\n const withScrubbedData = (entry: unknown): unknown => {\n const query = entry as { state?: { data?: unknown } };\n if (!query.state || !(\"data\" in query.state)) return entry;\n return { ...query, state: { ...query.state, data: scrub(query.state.data, scrubFields) } };\n };\n\n // The scrub on the way OUT: the in-memory cache keeps the field (the page that is open\n // shows it), the disk never sees it. Mutations are a paused write queue and are kept\n // whole — scrubbing one would change what it sends — so one carrying a scrubbed field is\n // not written at all (`shouldDehydrateMutation`).\n const serialize = (client: PersistedClient): string =>\n JSON.stringify({\n ...client,\n clientState: { ...client.clientState, queries: client.clientState.queries.map(withScrubbedData) },\n });\n\n // The twin on the way IN (keksdose's `sanitizePersistedCache`, :264-298): an older build\n // wrote what this one may deny. Never throws; an unreadable blob comes back with\n // `timestamp: 0`, which the restore treats as unusable and removes.\n const deserialize = (raw: string): PersistedClient => {\n const empty: PersistedClient = { timestamp: 0, buster: \"\", clientState: { mutations: [], queries: [] } };\n try {\n const parsed: unknown = JSON.parse(raw);\n if (!parsed || typeof parsed !== \"object\") return empty;\n const { timestamp, buster: stored, clientState } = parsed as Partial<PersistedClient>;\n if (typeof timestamp !== \"number\" || typeof stored !== \"string\") return empty;\n if (!clientState || typeof clientState !== \"object\" || !Array.isArray(clientState.queries)) return empty;\n return {\n timestamp,\n buster: stored,\n clientState: {\n mutations:\n persistMutations && Array.isArray(clientState.mutations)\n ? clientState.mutations.filter(\n (m) => !carriesScrubbed((m as { state?: { variables?: unknown } } | null)?.state?.variables),\n )\n : [],\n queries: clientState.queries\n .map(restorable)\n .filter((q) => q !== null) as PersistedClient[\"clientState\"][\"queries\"],\n },\n };\n } catch {\n return empty;\n }\n };\n\n const persister = createAsyncStoragePersister({\n storage: {\n // Only the provider's restore reads: mounting this persistence again re-arms it.\n getItem: async (k) => {\n retired = false;\n return (await storage.getItem(k)) ?? null;\n },\n setItem: async (k, v) => {\n if (!retired) await storage.setItem(k, v);\n },\n removeItem: async (k) => {\n await storage.removeItem(k);\n },\n },\n key,\n throttleTime,\n serialize,\n deserialize,\n });\n\n const leave = async (queryClient: QueryClient) => {\n retired = true;\n try {\n queryClient.clear();\n } catch {\n /* best-effort */\n }\n };\n\n const clear = async (queryClient: QueryClient) => {\n retired = true;\n try {\n queryClient.clear();\n await persister.removeClient();\n } catch {\n /* best-effort: private mode / IndexedDB unavailable */\n }\n };\n\n const removeStale = () => removeStaleQueryCopies(app, maxAge, { storage, keep: key });\n\n return {\n key,\n persistOptions: { persister, maxAge, buster, dehydrateOptions: { shouldDehydrateQuery, shouldDehydrateMutation } },\n clear,\n leave,\n removeStale,\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/offline/query-persistence.ts"],"sourcesContent":["import { createAsyncStoragePersister } from \"@tanstack/query-async-storage-persister\";\nimport type { QueryClient } from \"@tanstack/react-query\";\nimport type { PersistedClient, PersistQueryClientOptions } from \"@tanstack/react-query-persist-client\";\nimport { del, get, keys, set } from \"idb-keyval\";\nimport { listenOn, postOnce } from \"../lib/broadcast\";\n\n/**\n * The persisted TanStack Query cache, keksdose's policy as a kit helper\n * (docs/local-first-harmonization.md §1.5 D4, D5, D7; §1.6). keksdose's\n * shared/state/query-client.ts is the canon: an IndexedDB persister, a deny-list, the\n * plain-JSON guard on write (`shouldDehydrateQuery`) and its twin on restore that keeps\n * paused mutations. On top: a key per account (and company), a scrub list that removes\n * secret fields from payloads that are otherwise wanted offline, and `removeStale` for\n * another account's leftovers.\n *\n * The only module of the kit that imports TanStack and idb-keyval: both are optional peers\n * of `@eifi1/ui-kit/offline` and never reached from the main barrel (packaging-contract).\n */\n\n/** A field name the scrub removes: an exact name, or a pattern (`/^signature_/`). */\nexport type ScrubField = string | RegExp;\n\n/**\n * Removed from every query's data, at any depth, before it is persisted (D4, D7): bank\n * account numbers and the handover protocol's signatures. A page restored offline shows\n * such a field as missing (\"Needs a connection\").\n */\nexport const DEFAULT_SCRUB_FIELDS: readonly ScrubField[] = Object.freeze([\n \"iban\",\n \"qr_iban\",\n \"default_qr_iban\",\n /^signature_/,\n]);\n\n/** The storage the persister writes to. The default is IndexedDB through idb-keyval. */\nexport interface QueryPersistenceStorage {\n getItem: (key: string) => Promise<string | null | undefined>;\n setItem: (key: string, value: string) => Promise<unknown>;\n removeItem: (key: string) => Promise<unknown>;\n /** Every key in the store — what `removeStale` walks. */\n keys: () => Promise<readonly unknown[]>;\n}\n\nexport interface QueryPersistenceOptions {\n /** The app's name, the key's first part: `kastlan` → `kastlan-query-cache-…`. */\n app: string;\n /** The signed-in account. One cache per account (D5). */\n user: string | number;\n /** The acting company, where the app has several (kastlan): the switch changes the key,\n * so no wipe and no cross-company leak — provided the app switches as\n * {@link QueryPersistence.leave} says (leave, then remount the provider per key). */\n company?: string | number | null;\n /** The app's own rule, beside `denyRoots` — keksdose denies one exact key and every key\n * whose second segment is `\"admin\"`. `false` keeps the query out. */\n shouldPersistKey?: (queryKey: readonly unknown[]) => boolean;\n /** Query roots (`queryKey[0]`) that never reach the disk: admin, billing, reports… */\n denyRoots?: readonly string[];\n /** Replaces {@link DEFAULT_SCRUB_FIELDS}; spread it to add to it. `[]` scrubs nothing.\n * A paused mutation whose variables carry one of these fields is never persisted (a\n * scrubbed mutation would send something else), and one an older build wrote is dropped\n * on restore. */\n scrubFields?: readonly ScrubField[];\n /** Told when a query's data is not plain JSON and is therefore not persisted. Default:\n * `console.error`, as keksdose's `cacheSafeOrWarn`. */\n onUnsafe?: (queryKey: readonly unknown[], data: unknown) => void;\n /** Changes with each deploy (the app version), so a new build discards the old cache. */\n buster: string;\n /** How long a persisted cache is good for, in ms (keksdose: 24 h). The QueryClient's\n * `gcTime` must be at least this, or restore drops every query. */\n maxAge: number;\n /** Default: IndexedDB (idb-keyval). Tests pass an in-memory one. */\n storage?: QueryPersistenceStorage;\n /** Default 1000 ms, as keksdose. */\n throttleTime?: number;\n /**\n * 0.46 (kastlan K1): whether paused mutations are written with the cache. Default `true`\n * (keksdose: an offline write waits in TanStack's queue and is sent after a reload).\n * `false` keeps every mutation off the disk, and drops any an older build wrote on\n * restore — for an app that queues offline writes in its own outbox (kastlan's Dexie\n * outbox), instead of overriding `dehydrateOptions` by hand.\n */\n persistMutations?: boolean;\n}\n\n/**\n * One persisted cache, for one key. **Mount it per key**: the provider subscribes once and\n * ignores a changed `persistOptions`, so give it the key as its React key —\n *\n * ```tsx\n * <PersistQueryClientProvider key={persistence.key} client={queryClient} persistOptions={persistence.persistOptions}>\n * ```\n *\n * — and create the persistence once the account (and company) is known (`useMemo` on\n * them); before sign-in, mount the plain `QueryClientProvider`. On a company switch, call\n * {@link QueryPersistence.leave} on the old one BEFORE the new key mounts, instead of a bare\n * `queryClient.clear()`: otherwise the old subscription saves the emptied (or the next\n * company's) cache under the old key.\n */\nexport interface QueryPersistence {\n /** `<app>-query-cache-<user>[-<company>]`. */\n key: string;\n /** Everything `<PersistQueryClientProvider persistOptions>` needs. */\n persistOptions: Omit<PersistQueryClientOptions, \"queryClient\">;\n /** Sign-out: stops writing under this key, empties the in-memory cache and deletes this\n * key's persisted copy. In every tab (0.52): the persistences of this key in the other tabs\n * stop writing too, and delete the key once more after any save of theirs under way.\n * Best-effort; never throws. */\n clear: (queryClient: QueryClient) => Promise<void>;\n /** The company switch (or any change of key without a sign-out): stops writing under this\n * key — a save already scheduled is dropped too — and empties the in-memory cache, so\n * the next key's provider starts clean. The persisted copy stays for the way back. A\n * provider that mounts this persistence again re-arms it with its restore. Never throws. */\n leave: (queryClient: QueryClient) => Promise<void>;\n /** At sign-in: deletes this app's OTHER persisted caches older than `maxAge` (another\n * account's, another company's). Other apps' keys and fresh ones stay. Never throws. */\n removeStale: () => Promise<void>;\n}\n\nconst idbStorage: QueryPersistenceStorage = {\n getItem: (key) => get<string>(key),\n setItem: (key, value) => set(key, value),\n removeItem: (key) => del(key),\n keys: () => keys(),\n};\n\nexport interface RemoveStaleQueryCopiesOptions {\n /** Default: IndexedDB (idb-keyval), the store {@link createQueryPersistence} writes to. */\n storage?: QueryPersistenceStorage;\n /** One key to spare whatever its age — the signed-in account's own\n * (`QueryPersistence.removeStale` passes its key). */\n keep?: string;\n}\n\n/**\n * 0.46 (kastlan K2): deletes this app's persisted query caches (`<app>-query-cache-…`)\n * older than `maxAge`, with no persistence at hand — for the sign-in page, where no\n * account is known. A session that ended with no user known (a refresh token refused at\n * boot) cannot name its key for `clear`; this sweeps its copy once it is stale, without\n * waiting for someone's next sign-in. Other apps' keys, other stores' keys and fresh\n * caches stay; an unreadable one is stale. Never throws.\n * {@link QueryPersistence.removeStale} is this with `keep` = its own key.\n */\nexport async function removeStaleQueryCopies(\n app: string,\n maxAge: number,\n { storage = idbStorage, keep }: RemoveStaleQueryCopiesOptions = {},\n): Promise<void> {\n const prefix = `${app}-query-cache-`;\n try {\n const now = Date.now();\n for (const k of await storage.keys()) {\n if (typeof k !== \"string\" || !k.startsWith(prefix) || k === keep) continue;\n let timestamp = 0;\n try {\n const raw = await storage.getItem(k);\n const parsed: unknown = raw ? JSON.parse(raw) : null;\n if (parsed && typeof parsed === \"object\" && typeof (parsed as PersistedClient).timestamp === \"number\") {\n timestamp = (parsed as PersistedClient).timestamp;\n }\n } catch {\n /* unreadable: stale */\n }\n if (now - timestamp > maxAge) await storage.removeItem(k);\n }\n } catch {\n /* best-effort */\n }\n}\n\n/**\n * True when `data` still means the same thing after JSON.stringify and JSON.parse —\n * keksdose's `isCacheSafe` (query-client.ts:165). Class instances, Map/Set/Date/Blob, a\n * non-finite number, a function: all refused. An `undefined` property survives.\n */\nexport function isCacheSafe(data: unknown, depth = 0): boolean {\n if (depth > 12) return false;\n if (data === null) return true;\n const t = typeof data;\n if (t === \"string\" || t === \"boolean\") return true;\n if (t === \"number\") return Number.isFinite(data as number);\n if (t !== \"object\") return false;\n if (Array.isArray(data)) return data.every((v) => isCacheSafe(v, depth + 1));\n const proto: unknown = Object.getPrototypeOf(data);\n if (proto !== Object.prototype && proto !== null) return false;\n return Object.values(data as Record<string, unknown>).every((v) => v === undefined || isCacheSafe(v, depth + 1));\n}\n\n/** A copy of `data` without the scrubbed fields, at any depth. The input is not touched. */\nfunction scrub(data: unknown, fields: readonly ScrubField[]): unknown {\n if (fields.length === 0 || data === null || typeof data !== \"object\") return data;\n if (Array.isArray(data)) return data.map((v) => scrub(v, fields));\n const out: Record<string, unknown> = {};\n for (const [name, value] of Object.entries(data as Record<string, unknown>)) {\n if (fields.some((f) => (typeof f === \"string\" ? f === name : f.test(name)))) continue;\n out[name] = scrub(value, fields);\n }\n return out;\n}\n\nconst defaultOnUnsafe = (queryKey: readonly unknown[]) => {\n console.error(\n `[query-cache] refusing to persist ${JSON.stringify(queryKey)}: the value is not plain JSON ` +\n `(a class instance, Map, Set, Date or Blob). Return plain data from the queryFn and build ` +\n `the rich object at the call site.`,\n );\n};\n\nexport function createQueryPersistence({\n app,\n user,\n company,\n shouldPersistKey,\n denyRoots = [],\n scrubFields = DEFAULT_SCRUB_FIELDS,\n onUnsafe = defaultOnUnsafe,\n buster,\n maxAge,\n storage = idbStorage,\n throttleTime = 1000,\n persistMutations = true,\n}: QueryPersistenceOptions): QueryPersistence {\n const prefix = `${app}-query-cache-`;\n const key = `${prefix}${user}${company === undefined || company === null || company === \"\" ? \"\" : `-${company}`}`;\n const denied = new Set(denyRoots);\n // Set by `leave` and `clear`: a write under this key after the person left it (a\n // throttled save of the emptied cache) is dropped. A restore re-arms it.\n let retired = false;\n\n // 0.52 (refactor audit 2026-10-30, FLW-1): `retired` lived in this tab alone, so a sign-out\n // in one tab deleted the key and another tab's persister wrote its in-memory cache straight\n // back under it. While armed, a persistence listens on a channel named after its key; a\n // `clear` there retires it, and it deletes the key once more — IndexedDB runs a save of its\n // own already under way first. A `leave` posts nothing: another tab may still be on that\n // company. Without BroadcastChannel, as before 0.52.\n const across = `eifi1-query-cache:${key}`;\n let unlisten: (() => void) | null = null;\n\n const retire = () => {\n retired = true;\n unlisten?.();\n unlisten = null;\n };\n\n const listen = () => {\n unlisten ??= listenOn(across, (data) => {\n if (data !== \"clear\") return;\n retire();\n Promise.resolve()\n .then(() => storage.removeItem(key))\n .catch(() => undefined);\n });\n };\n\n const keyAllowed = (queryKey: readonly unknown[]) =>\n !denied.has(String(queryKey[0])) && (shouldPersistKey ? shouldPersistKey(queryKey) : true);\n\n /** The write side: a success, an allowed key, plain JSON (or the warning). */\n const shouldDehydrateQuery = (query: {\n queryKey: readonly unknown[];\n state: { status: string; data?: unknown };\n }): boolean => {\n if (query.state.status !== \"success\" || !keyAllowed(query.queryKey)) return false;\n if (isCacheSafe(query.state.data)) return true;\n onUnsafe(query.queryKey, query.state.data);\n return false;\n };\n\n /** One persisted query entry, scrubbed; `null` when it may not be restored. */\n const restorable = (entry: unknown): unknown => {\n if (!entry || typeof entry !== \"object\") return null;\n const { queryKey, state } = entry as { queryKey?: unknown; state?: { data?: unknown } };\n if (!Array.isArray(queryKey) || !keyAllowed(queryKey)) return null;\n if (!isCacheSafe(state?.data)) return null;\n return withScrubbedData(entry);\n };\n\n /** Whether `value` has a scrubbed field's name anywhere in it. */\n const carriesScrubbed = (value: unknown, depth = 0): boolean => {\n if (scrubFields.length === 0 || depth > 12 || value === null || typeof value !== \"object\") return false;\n if (Array.isArray(value)) return value.some((v) => carriesScrubbed(v, depth + 1));\n return Object.entries(value as Record<string, unknown>).some(\n ([name, v]) =>\n scrubFields.some((f) => (typeof f === \"string\" ? f === name : f.test(name))) || carriesScrubbed(v, depth + 1),\n );\n };\n\n /** The write side for mutations: TanStack's default (paused ones), minus any whose\n * variables carry a scrubbed field (S6); none at all with `persistMutations: false`. */\n const shouldDehydrateMutation = (mutation: { state: { isPaused: boolean; variables?: unknown } }): boolean =>\n persistMutations && mutation.state.isPaused && !carriesScrubbed(mutation.state.variables);\n\n const withScrubbedData = (entry: unknown): unknown => {\n const query = entry as { state?: { data?: unknown } };\n if (!query.state || !(\"data\" in query.state)) return entry;\n return { ...query, state: { ...query.state, data: scrub(query.state.data, scrubFields) } };\n };\n\n // The scrub on the way OUT: the in-memory cache keeps the field (the page that is open\n // shows it), the disk never sees it. Mutations are a paused write queue and are kept\n // whole — scrubbing one would change what it sends — so one carrying a scrubbed field is\n // not written at all (`shouldDehydrateMutation`).\n const serialize = (client: PersistedClient): string =>\n JSON.stringify({\n ...client,\n clientState: { ...client.clientState, queries: client.clientState.queries.map(withScrubbedData) },\n });\n\n // The twin on the way IN (keksdose's `sanitizePersistedCache`, :264-298): an older build\n // wrote what this one may deny. Never throws; an unreadable blob comes back with\n // `timestamp: 0`, which the restore treats as unusable and removes.\n const deserialize = (raw: string): PersistedClient => {\n const empty: PersistedClient = { timestamp: 0, buster: \"\", clientState: { mutations: [], queries: [] } };\n try {\n const parsed: unknown = JSON.parse(raw);\n if (!parsed || typeof parsed !== \"object\") return empty;\n const { timestamp, buster: stored, clientState } = parsed as Partial<PersistedClient>;\n if (typeof timestamp !== \"number\" || typeof stored !== \"string\") return empty;\n if (!clientState || typeof clientState !== \"object\" || !Array.isArray(clientState.queries)) return empty;\n return {\n timestamp,\n buster: stored,\n clientState: {\n mutations:\n persistMutations && Array.isArray(clientState.mutations)\n ? clientState.mutations.filter(\n (m) => !carriesScrubbed((m as { state?: { variables?: unknown } } | null)?.state?.variables),\n )\n : [],\n queries: clientState.queries\n .map(restorable)\n .filter((q) => q !== null) as PersistedClient[\"clientState\"][\"queries\"],\n },\n };\n } catch {\n return empty;\n }\n };\n\n const persister = createAsyncStoragePersister({\n storage: {\n // Only the provider's restore reads: mounting this persistence again re-arms it.\n getItem: async (k) => {\n retired = false;\n listen();\n return (await storage.getItem(k)) ?? null;\n },\n setItem: async (k, v) => {\n if (!retired) await storage.setItem(k, v);\n },\n removeItem: async (k) => {\n await storage.removeItem(k);\n },\n },\n key,\n throttleTime,\n serialize,\n deserialize,\n });\n\n const leave = async (queryClient: QueryClient) => {\n retire();\n try {\n queryClient.clear();\n } catch {\n /* best-effort */\n }\n };\n\n const clear = async (queryClient: QueryClient) => {\n // Retired first, so this tab's own channel is closed and does not hear its `clear`.\n retire();\n // Before this tab's delete: the other tabs stop writing as early as they can.\n postOnce(across, \"clear\");\n try {\n queryClient.clear();\n await persister.removeClient();\n } catch {\n /* best-effort: private mode / IndexedDB unavailable */\n }\n };\n\n const removeStale = () => removeStaleQueryCopies(app, maxAge, { storage, keep: key });\n\n return {\n key,\n persistOptions: { persister, maxAge, buster, dehydrateOptions: { shouldDehydrateQuery, shouldDehydrateMutation } },\n clear,\n leave,\n removeStale,\n };\n}\n"],"mappings":";AAAA,SAAS,mCAAmC;AAG5C,SAAS,KAAK,KAAK,MAAM,WAAW;AACpC,SAAS,UAAU,gBAAgB;AAuB5B,MAAM,uBAA8C,OAAO,OAAO;AAAA,EACvE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAsFD,MAAM,aAAsC;AAAA,EAC1C,SAAS,CAAC,QAAQ,IAAY,GAAG;AAAA,EACjC,SAAS,CAAC,KAAK,UAAU,IAAI,KAAK,KAAK;AAAA,EACvC,YAAY,CAAC,QAAQ,IAAI,GAAG;AAAA,EAC5B,MAAM,MAAM,KAAK;AACnB;AAmBA,eAAsB,uBACpB,KACA,QACA,EAAE,UAAU,YAAY,KAAK,IAAmC,CAAC,GAClD;AACf,QAAM,SAAS,GAAG,GAAG;AACrB,MAAI;AACF,UAAM,MAAM,KAAK,IAAI;AACrB,eAAW,KAAK,MAAM,QAAQ,KAAK,GAAG;AACpC,UAAI,OAAO,MAAM,YAAY,CAAC,EAAE,WAAW,MAAM,KAAK,MAAM,KAAM;AAClE,UAAI,YAAY;AAChB,UAAI;AACF,cAAM,MAAM,MAAM,QAAQ,QAAQ,CAAC;AACnC,cAAM,SAAkB,MAAM,KAAK,MAAM,GAAG,IAAI;AAChD,YAAI,UAAU,OAAO,WAAW,YAAY,OAAQ,OAA2B,cAAc,UAAU;AACrG,sBAAa,OAA2B;AAAA,QAC1C;AAAA,MACF,QAAQ;AAAA,MAER;AACA,UAAI,MAAM,YAAY,OAAQ,OAAM,QAAQ,WAAW,CAAC;AAAA,IAC1D;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAOO,SAAS,YAAY,MAAe,QAAQ,GAAY;AAC7D,MAAI,QAAQ,GAAI,QAAO;AACvB,MAAI,SAAS,KAAM,QAAO;AAC1B,QAAM,IAAI,OAAO;AACjB,MAAI,MAAM,YAAY,MAAM,UAAW,QAAO;AAC9C,MAAI,MAAM,SAAU,QAAO,OAAO,SAAS,IAAc;AACzD,MAAI,MAAM,SAAU,QAAO;AAC3B,MAAI,MAAM,QAAQ,IAAI,EAAG,QAAO,KAAK,MAAM,CAAC,MAAM,YAAY,GAAG,QAAQ,CAAC,CAAC;AAC3E,QAAM,QAAiB,OAAO,eAAe,IAAI;AACjD,MAAI,UAAU,OAAO,aAAa,UAAU,KAAM,QAAO;AACzD,SAAO,OAAO,OAAO,IAA+B,EAAE,MAAM,CAAC,MAAM,MAAM,UAAa,YAAY,GAAG,QAAQ,CAAC,CAAC;AACjH;AAGA,SAAS,MAAM,MAAe,QAAwC;AACpE,MAAI,OAAO,WAAW,KAAK,SAAS,QAAQ,OAAO,SAAS,SAAU,QAAO;AAC7E,MAAI,MAAM,QAAQ,IAAI,EAAG,QAAO,KAAK,IAAI,CAAC,MAAM,MAAM,GAAG,MAAM,CAAC;AAChE,QAAM,MAA+B,CAAC;AACtC,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,IAA+B,GAAG;AAC3E,QAAI,OAAO,KAAK,CAAC,MAAO,OAAO,MAAM,WAAW,MAAM,OAAO,EAAE,KAAK,IAAI,CAAE,EAAG;AAC7E,QAAI,IAAI,IAAI,MAAM,OAAO,MAAM;AAAA,EACjC;AACA,SAAO;AACT;AAEA,MAAM,kBAAkB,CAAC,aAAiC;AACxD,UAAQ;AAAA,IACN,qCAAqC,KAAK,UAAU,QAAQ,CAAC;AAAA,EAG/D;AACF;AAEO,SAAS,uBAAuB;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY,CAAC;AAAA,EACb,cAAc;AAAA,EACd,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,eAAe;AAAA,EACf,mBAAmB;AACrB,GAA8C;AAC5C,QAAM,SAAS,GAAG,GAAG;AACrB,QAAM,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,YAAY,UAAa,YAAY,QAAQ,YAAY,KAAK,KAAK,IAAI,OAAO,EAAE;AAC/G,QAAM,SAAS,IAAI,IAAI,SAAS;AAGhC,MAAI,UAAU;AAQd,QAAM,SAAS,qBAAqB,GAAG;AACvC,MAAI,WAAgC;AAEpC,QAAM,SAAS,MAAM;AACnB,cAAU;AACV,eAAW;AACX,eAAW;AAAA,EACb;AAEA,QAAM,SAAS,MAAM;AACnB,iBAAa,SAAS,QAAQ,CAAC,SAAS;AACtC,UAAI,SAAS,QAAS;AACtB,aAAO;AACP,cAAQ,QAAQ,EACb,KAAK,MAAM,QAAQ,WAAW,GAAG,CAAC,EAClC,MAAM,MAAM,MAAS;AAAA,IAC1B,CAAC;AAAA,EACH;AAEA,QAAM,aAAa,CAAC,aAClB,CAAC,OAAO,IAAI,OAAO,SAAS,CAAC,CAAC,CAAC,MAAM,mBAAmB,iBAAiB,QAAQ,IAAI;AAGvF,QAAM,uBAAuB,CAAC,UAGf;AACb,QAAI,MAAM,MAAM,WAAW,aAAa,CAAC,WAAW,MAAM,QAAQ,EAAG,QAAO;AAC5E,QAAI,YAAY,MAAM,MAAM,IAAI,EAAG,QAAO;AAC1C,aAAS,MAAM,UAAU,MAAM,MAAM,IAAI;AACzC,WAAO;AAAA,EACT;AAGA,QAAM,aAAa,CAAC,UAA4B;AAC9C,QAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,UAAM,EAAE,UAAU,MAAM,IAAI;AAC5B,QAAI,CAAC,MAAM,QAAQ,QAAQ,KAAK,CAAC,WAAW,QAAQ,EAAG,QAAO;AAC9D,QAAI,CAAC,YAAY,OAAO,IAAI,EAAG,QAAO;AACtC,WAAO,iBAAiB,KAAK;AAAA,EAC/B;AAGA,QAAM,kBAAkB,CAAC,OAAgB,QAAQ,MAAe;AAC9D,QAAI,YAAY,WAAW,KAAK,QAAQ,MAAM,UAAU,QAAQ,OAAO,UAAU,SAAU,QAAO;AAClG,QAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,KAAK,CAAC,MAAM,gBAAgB,GAAG,QAAQ,CAAC,CAAC;AAChF,WAAO,OAAO,QAAQ,KAAgC,EAAE;AAAA,MACtD,CAAC,CAAC,MAAM,CAAC,MACP,YAAY,KAAK,CAAC,MAAO,OAAO,MAAM,WAAW,MAAM,OAAO,EAAE,KAAK,IAAI,CAAE,KAAK,gBAAgB,GAAG,QAAQ,CAAC;AAAA,IAChH;AAAA,EACF;AAIA,QAAM,0BAA0B,CAAC,aAC/B,oBAAoB,SAAS,MAAM,YAAY,CAAC,gBAAgB,SAAS,MAAM,SAAS;AAE1F,QAAM,mBAAmB,CAAC,UAA4B;AACpD,UAAM,QAAQ;AACd,QAAI,CAAC,MAAM,SAAS,EAAE,UAAU,MAAM,OAAQ,QAAO;AACrD,WAAO,EAAE,GAAG,OAAO,OAAO,EAAE,GAAG,MAAM,OAAO,MAAM,MAAM,MAAM,MAAM,MAAM,WAAW,EAAE,EAAE;AAAA,EAC3F;AAMA,QAAM,YAAY,CAAC,WACjB,KAAK,UAAU;AAAA,IACb,GAAG;AAAA,IACH,aAAa,EAAE,GAAG,OAAO,aAAa,SAAS,OAAO,YAAY,QAAQ,IAAI,gBAAgB,EAAE;AAAA,EAClG,CAAC;AAKH,QAAM,cAAc,CAAC,QAAiC;AACpD,UAAM,QAAyB,EAAE,WAAW,GAAG,QAAQ,IAAI,aAAa,EAAE,WAAW,CAAC,GAAG,SAAS,CAAC,EAAE,EAAE;AACvG,QAAI;AACF,YAAM,SAAkB,KAAK,MAAM,GAAG;AACtC,UAAI,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO;AAClD,YAAM,EAAE,WAAW,QAAQ,QAAQ,YAAY,IAAI;AACnD,UAAI,OAAO,cAAc,YAAY,OAAO,WAAW,SAAU,QAAO;AACxE,UAAI,CAAC,eAAe,OAAO,gBAAgB,YAAY,CAAC,MAAM,QAAQ,YAAY,OAAO,EAAG,QAAO;AACnG,aAAO;AAAA,QACL;AAAA,QACA,QAAQ;AAAA,QACR,aAAa;AAAA,UACX,WACE,oBAAoB,MAAM,QAAQ,YAAY,SAAS,IACnD,YAAY,UAAU;AAAA,YACpB,CAAC,MAAM,CAAC,gBAAiB,GAAkD,OAAO,SAAS;AAAA,UAC7F,IACA,CAAC;AAAA,UACP,SAAS,YAAY,QAClB,IAAI,UAAU,EACd,OAAO,CAAC,MAAM,MAAM,IAAI;AAAA,QAC7B;AAAA,MACF;AAAA,IACF,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,YAAY,4BAA4B;AAAA,IAC5C,SAAS;AAAA;AAAA,MAEP,SAAS,OAAO,MAAM;AACpB,kBAAU;AACV,eAAO;AACP,eAAQ,MAAM,QAAQ,QAAQ,CAAC,KAAM;AAAA,MACvC;AAAA,MACA,SAAS,OAAO,GAAG,MAAM;AACvB,YAAI,CAAC,QAAS,OAAM,QAAQ,QAAQ,GAAG,CAAC;AAAA,MAC1C;AAAA,MACA,YAAY,OAAO,MAAM;AACvB,cAAM,QAAQ,WAAW,CAAC;AAAA,MAC5B;AAAA,IACF;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,QAAM,QAAQ,OAAO,gBAA6B;AAChD,WAAO;AACP,QAAI;AACF,kBAAY,MAAM;AAAA,IACpB,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,QAAQ,OAAO,gBAA6B;AAEhD,WAAO;AAEP,aAAS,QAAQ,OAAO;AACxB,QAAI;AACF,kBAAY,MAAM;AAClB,YAAM,UAAU,aAAa;AAAA,IAC/B,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,cAAc,MAAM,uBAAuB,KAAK,QAAQ,EAAE,SAAS,MAAM,IAAI,CAAC;AAEpF,SAAO;AAAA,IACL;AAAA,IACA,gBAAgB,EAAE,WAAW,QAAQ,QAAQ,kBAAkB,EAAE,sBAAsB,wBAAwB,EAAE;AAAA,IACjH;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
@@ -226,7 +226,11 @@ export type RhfDateFieldProps<TFieldValues extends FieldValues = FieldValues, TN
226
226
  * nullable schema. */
227
227
  emptyValue?: "" | null;
228
228
  };
229
- /** The kit's {@link DatePicker}, holding an ISO `"YYYY-MM-DD"` string. */
229
+ /**
230
+ * The kit's {@link DatePicker}, holding an ISO `"YYYY-MM-DD"` string. `commit` and
231
+ * `disabledReason` are the picker's own (0.52, refactor audit 2026-10-30, API-3): a date
232
+ * that saves itself stays focusable under a lock and says why.
233
+ */
230
234
  export declare function RhfDateField<TFieldValues extends FieldValues = FieldValues, TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, TTransformed = TFieldValues>({ name, control, rules, label, hint, help, required, disabled, excludeWhenDisabled, className, inputClassName, emptyValue, ...pickerProps }: RhfDateFieldProps<TFieldValues, TName, TTransformed>): import("react").JSX.Element;
231
235
  type OwnDateRangeProps = Omit<DateRangePickerProps, "from" | "to" | "onChange" | "label" | "invalid" | "disabled" | "className" | "id" | "error">;
232
236
  export type RhfDateRangePickerProps<TFieldValues extends FieldValues = FieldValues, TFromName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, TToName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, TTransformed = TFieldValues> = Omit<RhfFieldBaseProps<TFieldValues, TFromName, TTransformed>, "name" | "rules"> & OwnDateRangeProps & {
@@ -276,9 +280,11 @@ export type RhfDateRangePickerProps<TFieldValues extends FieldValues = FieldValu
276
280
  * pick, which would mark (and, with `mode: "onTouched"`, validate) an end the user
277
281
  * is still choosing.
278
282
  *
279
- * In `commit="immediate"` mode (the picker's default) the first calendar click is
280
- * reported on its own, so the form holds `(from, emptyValue)` until the second; with
281
- * `commit="apply"` nothing reaches the form until Apply.
283
+ * Without `apply` (the picker's default) the first calendar click is reported on its
284
+ * own, so the form holds `(from, emptyValue)` until the second; with `apply` nothing
285
+ * reaches the form until Apply. `apply` was `commit="apply"` until 0.52 (deprecated, gone
286
+ * in 0.53): `commit` is now the write lock's, as on every other binding — with
287
+ * `disabledReason`, the picker's own (refactor audit 2026-10-30, API-3).
282
288
  *
283
289
  * The common props (`label`, `hint`, `required`, `disabled`, `excludeWhenDisabled`,
284
290
  * `control`, `className`) apply to the pair; `rules` are per field.
@@ -383,6 +389,7 @@ export type RhfMonthPickerProps<TFieldValues extends FieldValues = FieldValues,
383
389
  * lookup until 0.34, API-3), so focus-on-error lands there; the form's error paints the
384
390
  * trigger and is the shell's message; the hint is the shell's description; neither is
385
391
  * handed to the picker, so its box never changes. Picking marks the field touched.
392
+ * `commit` and `disabledReason` are the picker's own (0.52, API-3).
386
393
  *
387
394
  * ```tsx
388
395
  * <RhfMonthPicker name="fiscalYear" label="Fiscal year" mode="year" valueAsNumber