@eifi1/ui-kit 0.51.7 → 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 (746) hide show
  1. package/README.md +141 -23
  2. package/dist/account/account-parts.d.ts +10 -8
  3. package/dist/account/account-parts.js +1 -18
  4. package/dist/account/account-parts.js.map +1 -1
  5. package/dist/account/data-export-setting-labels.js +1 -1
  6. package/dist/account/data-export-setting-labels.js.map +1 -1
  7. package/dist/account/delete-account-setting-labels.js +1 -1
  8. package/dist/account/delete-account-setting-labels.js.map +1 -1
  9. package/dist/account/email-change-setting-labels.js +1 -1
  10. package/dist/account/email-change-setting-labels.js.map +1 -1
  11. package/dist/account/passkeys-setting.d.ts +9 -2
  12. package/dist/account/passkeys-setting.js +41 -29
  13. package/dist/account/passkeys-setting.js.map +1 -1
  14. package/dist/account/sessions-setting.d.ts +3 -2
  15. package/dist/account/sessions-setting.js +10 -8
  16. package/dist/account/sessions-setting.js.map +1 -1
  17. package/dist/admin/admin-action-confirm.js +1 -3
  18. package/dist/admin/admin-action-confirm.js.map +1 -1
  19. package/dist/admin/admin-parts.d.ts +6 -3
  20. package/dist/admin/admin-parts.js +6 -21
  21. package/dist/admin/admin-parts.js.map +1 -1
  22. package/dist/admin/mail-test-card-labels.js +1 -1
  23. package/dist/admin/mail-test-card-labels.js.map +1 -1
  24. package/dist/admin/plan-change-labels.d.ts +2 -2
  25. package/dist/admin/plan-change-labels.js +1 -17
  26. package/dist/admin/plan-change-labels.js.map +1 -1
  27. package/dist/admin/plan-change.d.ts +3 -42
  28. package/dist/admin/plan-change.js +1 -1
  29. package/dist/admin/plan-change.js.map +1 -1
  30. package/dist/admin/reviewer-scope-editor.js +3 -3
  31. package/dist/admin/reviewer-scope-editor.js.map +1 -1
  32. package/dist/auth/auth-errors-labels.d.ts +11 -0
  33. package/dist/auth/auth-errors-labels.js +13 -0
  34. package/dist/auth/auth-errors-labels.js.map +1 -0
  35. package/dist/auth/auth-errors.d.ts +7 -23
  36. package/dist/auth/auth-errors.js +2 -15
  37. package/dist/auth/auth-errors.js.map +1 -1
  38. package/dist/auth/complete-name-dialog.js +2 -1
  39. package/dist/auth/complete-name-dialog.js.map +1 -1
  40. package/dist/auth/forgot-password-form-labels.js +1 -1
  41. package/dist/auth/forgot-password-form-labels.js.map +1 -1
  42. package/dist/auth/forgot-password-form.js +7 -2
  43. package/dist/auth/forgot-password-form.js.map +1 -1
  44. package/dist/auth/register-form-labels.js +1 -1
  45. package/dist/auth/register-form-labels.js.map +1 -1
  46. package/dist/auth/reset-password-form-labels.js +1 -1
  47. package/dist/auth/reset-password-form-labels.js.map +1 -1
  48. package/dist/auth/reset-password-form.js +5 -1
  49. package/dist/auth/reset-password-form.js.map +1 -1
  50. package/dist/auth/sign-in-form-labels.js +1 -1
  51. package/dist/auth/sign-in-form-labels.js.map +1 -1
  52. package/dist/auth/status-parts.d.ts +0 -3
  53. package/dist/auth/status-parts.js +0 -4
  54. package/dist/auth/status-parts.js.map +1 -1
  55. package/dist/auth/verify-email-labels.js +1 -1
  56. package/dist/auth/verify-email-labels.js.map +1 -1
  57. package/dist/auth/verify-email.js +7 -2
  58. package/dist/auth/verify-email.js.map +1 -1
  59. package/dist/billing/cancel-subscription.d.ts +20 -8
  60. package/dist/billing/cancel-subscription.js +7 -5
  61. package/dist/billing/cancel-subscription.js.map +1 -1
  62. package/dist/billing/dialog-close.d.ts +29 -0
  63. package/dist/billing/dialog-close.js +8 -0
  64. package/dist/billing/dialog-close.js.map +1 -0
  65. package/dist/billing/plan-card.d.ts +14 -4
  66. package/dist/billing/plan-card.js +15 -8
  67. package/dist/billing/plan-card.js.map +1 -1
  68. package/dist/billing/plan-change-labels.d.ts +42 -0
  69. package/dist/billing/plan-change-labels.js +22 -0
  70. package/dist/billing/plan-change-labels.js.map +1 -0
  71. package/dist/billing/plan-switch.d.ts +19 -7
  72. package/dist/billing/plan-switch.js +8 -4
  73. package/dist/billing/plan-switch.js.map +1 -1
  74. package/dist/chart.d.ts +12 -0
  75. package/dist/chart.js +16 -1
  76. package/dist/chart.js.map +1 -1
  77. package/dist/components/account-chips.d.ts +5 -5
  78. package/dist/components/account-chips.js +11 -13
  79. package/dist/components/account-chips.js.map +1 -1
  80. package/dist/components/alert-banner.d.ts +16 -5
  81. package/dist/components/alert-banner.js +46 -1
  82. package/dist/components/alert-banner.js.map +1 -1
  83. package/dist/components/amount-input.d.ts +13 -0
  84. package/dist/components/amount-input.js +1 -0
  85. package/dist/components/amount-input.js.map +1 -1
  86. package/dist/components/autocomplete.js +57 -65
  87. package/dist/components/autocomplete.js.map +1 -1
  88. package/dist/components/bulk-action-bar.d.ts +12 -2
  89. package/dist/components/bulk-action-bar.js +65 -21
  90. package/dist/components/bulk-action-bar.js.map +1 -1
  91. package/dist/components/button-look.d.ts +5 -5
  92. package/dist/components/button-look.js.map +1 -1
  93. package/dist/components/button.d.ts +2 -2
  94. package/dist/components/button.js.map +1 -1
  95. package/dist/components/chart-edge.d.ts +16 -0
  96. package/dist/components/chart-edge.js +14 -1
  97. package/dist/components/chart-edge.js.map +1 -1
  98. package/dist/components/chart-figure.d.ts +13 -0
  99. package/dist/components/chart-figure.js +15 -0
  100. package/dist/components/chart-figure.js.map +1 -0
  101. package/dist/components/chart-zoom-core.js +1 -4
  102. package/dist/components/chart-zoom-core.js.map +1 -1
  103. package/dist/components/chart.d.ts +8 -1
  104. package/dist/components/chart.js +19 -10
  105. package/dist/components/chart.js.map +1 -1
  106. package/dist/components/combobox-core.d.ts +111 -2
  107. package/dist/components/combobox-core.js +148 -42
  108. package/dist/components/combobox-core.js.map +1 -1
  109. package/dist/components/combobox.d.ts +1 -1
  110. package/dist/components/combobox.js +141 -130
  111. package/dist/components/combobox.js.map +1 -1
  112. package/dist/components/copy-button.d.ts +6 -5
  113. package/dist/components/copy-button.js +5 -3
  114. package/dist/components/copy-button.js.map +1 -1
  115. package/dist/components/country-select.js +2 -2
  116. package/dist/components/country-select.js.map +1 -1
  117. package/dist/components/data-table-column-settings.d.ts +1 -1
  118. package/dist/components/data-table-column-settings.js +33 -7
  119. package/dist/components/data-table-column-settings.js.map +1 -1
  120. package/dist/components/data-table-filters.d.ts +4 -2
  121. package/dist/components/data-table-filters.js +2 -2
  122. package/dist/components/data-table-filters.js.map +1 -1
  123. package/dist/components/data-table-fit.d.ts +6 -4
  124. package/dist/components/data-table-fit.js +4 -1
  125. package/dist/components/data-table-fit.js.map +1 -1
  126. package/dist/components/data-table-groups.d.ts +1 -1
  127. package/dist/components/data-table-groups.js.map +1 -1
  128. package/dist/components/data-table-head.d.ts +1 -1
  129. package/dist/components/data-table-head.js +15 -12
  130. package/dist/components/data-table-head.js.map +1 -1
  131. package/dist/components/data-table-mobile.d.ts +12 -7
  132. package/dist/components/data-table-mobile.js +43 -30
  133. package/dist/components/data-table-mobile.js.map +1 -1
  134. package/dist/components/data-table-pagination.js +11 -6
  135. package/dist/components/data-table-pagination.js.map +1 -1
  136. package/dist/components/data-table-resize.d.ts +1 -1
  137. package/dist/components/data-table-resize.js.map +1 -1
  138. package/dist/components/data-table-row-actions.d.ts +2 -2
  139. package/dist/components/data-table-row-actions.js.map +1 -1
  140. package/dist/components/data-table-row.d.ts +6 -2
  141. package/dist/components/data-table-row.js +17 -10
  142. package/dist/components/data-table-row.js.map +1 -1
  143. package/dist/components/data-table-selection.d.ts +1 -1
  144. package/dist/components/data-table-selection.js.map +1 -1
  145. package/dist/components/data-table-shared.d.ts +10 -2
  146. package/dist/components/data-table-shared.js +18 -0
  147. package/dist/components/data-table-shared.js.map +1 -1
  148. package/dist/components/data-table-sort.d.ts +21 -2
  149. package/dist/components/data-table-sort.js +24 -0
  150. package/dist/components/data-table-sort.js.map +1 -1
  151. package/dist/components/data-table-virtual.d.ts +1 -1
  152. package/dist/components/data-table-virtual.js +18 -6
  153. package/dist/components/data-table-virtual.js.map +1 -1
  154. package/dist/components/data-table.d.ts +13 -4
  155. package/dist/components/data-table.js +139 -36
  156. package/dist/components/data-table.js.map +1 -1
  157. package/dist/components/date-field.d.ts +24 -2
  158. package/dist/components/date-field.js +33 -8
  159. package/dist/components/date-field.js.map +1 -1
  160. package/dist/components/date-picker.d.ts +15 -2
  161. package/dist/components/date-picker.js +55 -26
  162. package/dist/components/date-picker.js.map +1 -1
  163. package/dist/components/date-range-picker.d.ts +47 -20
  164. package/dist/components/date-range-picker.js +23 -7
  165. package/dist/components/date-range-picker.js.map +1 -1
  166. package/dist/components/error-boundary.js +37 -30
  167. package/dist/components/error-boundary.js.map +1 -1
  168. package/dist/components/{ui-labels.d.ts → field-base-labels.d.ts} +0 -2
  169. package/dist/components/{ui-labels.js → field-base-labels.js} +2 -9
  170. package/dist/components/field-base-labels.js.map +1 -0
  171. package/dist/components/field-base.d.ts +4 -3
  172. package/dist/components/field-base.js +3 -3
  173. package/dist/components/field-base.js.map +1 -1
  174. package/dist/components/field-sync-labels.d.ts +1 -0
  175. package/dist/components/field-sync-labels.js.map +1 -1
  176. package/dist/components/field-sync.d.ts +23 -1
  177. package/dist/components/field-sync.js.map +1 -1
  178. package/dist/components/form-actions.js +11 -5
  179. package/dist/components/form-actions.js.map +1 -1
  180. package/dist/components/hover-menu.d.ts +3 -0
  181. package/dist/components/hover-menu.js +5 -3
  182. package/dist/components/hover-menu.js.map +1 -1
  183. package/dist/components/icon-button.js +14 -9
  184. package/dist/components/icon-button.js.map +1 -1
  185. package/dist/components/inline-confirm.d.ts +43 -0
  186. package/dist/components/inline-confirm.js +67 -0
  187. package/dist/components/inline-confirm.js.map +1 -0
  188. package/dist/components/inline-entity-combobox.d.ts +4 -3
  189. package/dist/components/inline-entity-combobox.js +148 -140
  190. package/dist/components/inline-entity-combobox.js.map +1 -1
  191. package/dist/components/input.js +1 -1
  192. package/dist/components/input.js.map +1 -1
  193. package/dist/components/jump-calendar.js +28 -112
  194. package/dist/components/jump-calendar.js.map +1 -1
  195. package/dist/components/legal-page.d.ts +3 -1
  196. package/dist/components/legal-page.js +3 -2
  197. package/dist/components/legal-page.js.map +1 -1
  198. package/dist/components/legal.d.ts +4 -2
  199. package/dist/components/legal.js.map +1 -1
  200. package/dist/components/lightbox.d.ts +2 -2
  201. package/dist/components/lightbox.js +17 -6
  202. package/dist/components/lightbox.js.map +1 -1
  203. package/dist/components/list.d.ts +6 -2
  204. package/dist/components/list.js +3 -2
  205. package/dist/components/list.js.map +1 -1
  206. package/dist/components/measured-grid.d.ts +1 -1
  207. package/dist/components/measured-grid.js +2 -1
  208. package/dist/components/measured-grid.js.map +1 -1
  209. package/dist/components/mini-calendar.js +3 -5
  210. package/dist/components/mini-calendar.js.map +1 -1
  211. package/dist/components/modal.js +3 -1
  212. package/dist/components/modal.js.map +1 -1
  213. package/dist/components/month-grid.d.ts +55 -0
  214. package/dist/components/month-grid.js +148 -0
  215. package/dist/components/month-grid.js.map +1 -0
  216. package/dist/components/month-picker.d.ts +36 -4
  217. package/dist/components/month-picker.js +123 -146
  218. package/dist/components/month-picker.js.map +1 -1
  219. package/dist/components/multi-select.d.ts +2 -2
  220. package/dist/components/multi-select.js +11 -6
  221. package/dist/components/multi-select.js.map +1 -1
  222. package/dist/components/picker-sheet.js +2 -1
  223. package/dist/components/picker-sheet.js.map +1 -1
  224. package/dist/components/pie-chart-labels.d.ts +7 -0
  225. package/dist/components/pie-chart-labels.js +1 -0
  226. package/dist/components/pie-chart-labels.js.map +1 -1
  227. package/dist/components/pie-chart.d.ts +5 -3
  228. package/dist/components/pie-chart.js +7 -22
  229. package/dist/components/pie-chart.js.map +1 -1
  230. package/dist/components/popover.d.ts +3 -0
  231. package/dist/components/popover.js +7 -1
  232. package/dist/components/popover.js.map +1 -1
  233. package/dist/components/row-actions.js +2 -1
  234. package/dist/components/row-actions.js.map +1 -1
  235. package/dist/components/section-nav.d.ts +7 -0
  236. package/dist/components/section-nav.js.map +1 -1
  237. package/dist/components/series-chart-axes.d.ts +3 -3
  238. package/dist/components/series-chart-axes.js.map +1 -1
  239. package/dist/components/series-chart-labels.d.ts +7 -4
  240. package/dist/components/series-chart-labels.js.map +1 -1
  241. package/dist/components/series-chart.d.ts +31 -7
  242. package/dist/components/series-chart.js +72 -29
  243. package/dist/components/series-chart.js.map +1 -1
  244. package/dist/components/share-card.js +3 -3
  245. package/dist/components/share-card.js.map +1 -1
  246. package/dist/components/sign-chip-labels.d.ts +1 -0
  247. package/dist/components/sign-chip-labels.js.map +1 -1
  248. package/dist/components/sign-chip.d.ts +10 -1
  249. package/dist/components/sign-chip.js.map +1 -1
  250. package/dist/components/tabs-labels.d.ts +2 -0
  251. package/dist/components/tabs-labels.js +11 -0
  252. package/dist/components/tabs-labels.js.map +1 -0
  253. package/dist/components/tabs.d.ts +1 -1
  254. package/dist/components/tabs.js +1 -1
  255. package/dist/components/tabs.js.map +1 -1
  256. package/dist/components/toggle-legend.d.ts +2 -2
  257. package/dist/components/toggle-legend.js +6 -6
  258. package/dist/components/toggle-legend.js.map +1 -1
  259. package/dist/components/translation-review-editor.d.ts +4 -2
  260. package/dist/components/translation-review-editor.js +2 -3
  261. package/dist/components/translation-review-editor.js.map +1 -1
  262. package/dist/components/translation-review.d.ts +2 -1
  263. package/dist/components/translation-review.js +2 -2
  264. package/dist/components/translation-review.js.map +1 -1
  265. package/dist/components/treemap-labels.d.ts +26 -0
  266. package/dist/components/treemap-labels.js +10 -0
  267. package/dist/components/treemap-labels.js.map +1 -0
  268. package/dist/components/treemap-layout.d.ts +62 -0
  269. package/dist/components/treemap-layout.js +126 -0
  270. package/dist/components/treemap-layout.js.map +1 -0
  271. package/dist/components/treemap.d.ts +140 -17
  272. package/dist/components/treemap.js +324 -97
  273. package/dist/components/treemap.js.map +1 -1
  274. package/dist/components/use-table-state.d.ts +5 -5
  275. package/dist/components/use-table-state.js +4 -1
  276. package/dist/components/use-table-state.js.map +1 -1
  277. package/dist/data-table.d.ts +6 -2
  278. package/dist/data-table.js +7 -2
  279. package/dist/data-table.js.map +1 -1
  280. package/dist/eslint/index.js +1 -1
  281. package/dist/eslint/role-utilities.d.ts +36 -7
  282. package/dist/eslint/role-utilities.js +42 -9
  283. package/dist/eslint/role-utilities.js.map +1 -1
  284. package/dist/feedback/feedback-dialog.d.ts +25 -16
  285. package/dist/feedback/feedback-dialog.js +6 -2
  286. package/dist/feedback/feedback-dialog.js.map +1 -1
  287. package/dist/feedback/feedback-mutation.d.ts +30 -0
  288. package/dist/feedback/feedback-mutation.js +16 -0
  289. package/dist/feedback/feedback-mutation.js.map +1 -0
  290. package/dist/feedback/feedback-page-link.d.ts +53 -0
  291. package/dist/feedback/feedback-page-link.js +37 -0
  292. package/dist/feedback/feedback-page-link.js.map +1 -0
  293. package/dist/feedback/feedback-public-list.d.ts +12 -3
  294. package/dist/feedback/feedback-public-list.js +2 -1
  295. package/dist/feedback/feedback-public-list.js.map +1 -1
  296. package/dist/feedback/feedback-replies.d.ts +3 -1
  297. package/dist/feedback/feedback-replies.js +5 -4
  298. package/dist/feedback/feedback-replies.js.map +1 -1
  299. package/dist/feedback/feedback-row-detail-labels.js +5 -0
  300. package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
  301. package/dist/feedback/feedback-row-detail.d.ts +64 -28
  302. package/dist/feedback/feedback-row-detail.js +30 -28
  303. package/dist/feedback/feedback-row-detail.js.map +1 -1
  304. package/dist/feedback/feedback-status-undo.d.ts +30 -11
  305. package/dist/feedback/feedback-status-undo.js +10 -15
  306. package/dist/feedback/feedback-status-undo.js.map +1 -1
  307. package/dist/feedback/feedback-submit-dialog.d.ts +1 -1
  308. package/dist/feedback/feedback-submit-dialog.js +1 -1
  309. package/dist/feedback/feedback-submit-dialog.js.map +1 -1
  310. package/dist/feedback/feedback-submit.js +9 -3
  311. package/dist/feedback/feedback-submit.js.map +1 -1
  312. package/dist/feedback/feedback-swipe.d.ts +1 -1
  313. package/dist/feedback/feedback-swipe.js.map +1 -1
  314. package/dist/feedback/feedback-table.d.ts +21 -9
  315. package/dist/feedback/feedback-table.js +45 -25
  316. package/dist/feedback/feedback-table.js.map +1 -1
  317. package/dist/feedback/feedback-thread-labels.d.ts +2 -2
  318. package/dist/feedback/feedback-thread-labels.js.map +1 -1
  319. package/dist/feedback/feedback-thread.d.ts +24 -23
  320. package/dist/feedback/feedback-thread.js +2 -1
  321. package/dist/feedback/feedback-thread.js.map +1 -1
  322. package/dist/feedback.d.ts +34 -0
  323. package/dist/feedback.js +42 -0
  324. package/dist/feedback.js.map +1 -1
  325. package/dist/hooks/use-focus-trap.d.ts +35 -0
  326. package/dist/hooks/use-focus-trap.js +6 -4
  327. package/dist/hooks/use-focus-trap.js.map +1 -1
  328. package/dist/hooks/use-hotkey.d.ts +5 -2
  329. package/dist/hooks/use-hotkey.js +5 -4
  330. package/dist/hooks/use-hotkey.js.map +1 -1
  331. package/dist/hooks/use-long-press.d.ts +7 -4
  332. package/dist/hooks/use-long-press.js +3 -0
  333. package/dist/hooks/use-long-press.js.map +1 -1
  334. package/dist/hooks/use-media-query.js +12 -4
  335. package/dist/hooks/use-media-query.js.map +1 -1
  336. package/dist/hooks/use-mounted.d.ts +22 -0
  337. package/dist/hooks/use-mounted.js +24 -0
  338. package/dist/hooks/use-mounted.js.map +1 -0
  339. package/dist/hooks/use-overlay-history.js +19 -3
  340. package/dist/hooks/use-overlay-history.js.map +1 -1
  341. package/dist/i18n/defaults.js +4 -5
  342. package/dist/i18n/defaults.js.map +1 -1
  343. package/dist/i18n/german.js +7 -1
  344. package/dist/i18n/german.js.map +1 -1
  345. package/dist/i18n/kit-labels.d.ts +9 -2
  346. package/dist/i18n/kit-labels.js.map +1 -1
  347. package/dist/i18n/locales/en.js +20 -41
  348. package/dist/i18n/locales/en.js.map +3 -3
  349. package/dist/i18n/locales/es.js +7 -1
  350. package/dist/i18n/locales/es.js.map +1 -1
  351. package/dist/i18n/locales/fr.js +20 -10
  352. package/dist/i18n/locales/fr.js.map +1 -1
  353. package/dist/i18n/locales/hu.js +9 -1
  354. package/dist/i18n/locales/hu.js.map +1 -1
  355. package/dist/i18n/locales/it.js +7 -1
  356. package/dist/i18n/locales/it.js.map +1 -1
  357. package/dist/i18n/locales/zh.js +8 -1
  358. package/dist/i18n/locales/zh.js.map +1 -1
  359. package/dist/i18n/review.js +5 -0
  360. package/dist/i18n/review.js.map +1 -1
  361. package/dist/index.d.ts +64 -17
  362. package/dist/index.js +77 -12
  363. package/dist/index.js.map +1 -1
  364. package/dist/landing/routing.d.ts +3 -1
  365. package/dist/landing/routing.js +4 -7
  366. package/dist/landing/routing.js.map +1 -1
  367. package/dist/lib/broadcast.d.ts +17 -0
  368. package/dist/lib/broadcast.js +30 -0
  369. package/dist/lib/broadcast.js.map +1 -0
  370. package/dist/lib/date-format.d.ts +37 -2
  371. package/dist/lib/date-format.js +34 -8
  372. package/dist/lib/date-format.js.map +1 -1
  373. package/dist/lib/day-format.d.ts +46 -0
  374. package/dist/lib/day-format.js +46 -0
  375. package/dist/lib/day-format.js.map +1 -0
  376. package/dist/lib/describe-failure.d.ts +14 -0
  377. package/dist/lib/describe-failure.js +9 -0
  378. package/dist/lib/describe-failure.js.map +1 -0
  379. package/dist/lib/direction.d.ts +16 -0
  380. package/dist/lib/direction.js +9 -1
  381. package/dist/lib/direction.js.map +1 -1
  382. package/dist/lib/focus-left.d.ts +63 -0
  383. package/dist/lib/focus-left.js +67 -0
  384. package/dist/lib/focus-left.js.map +1 -0
  385. package/dist/lib/format.js +5 -14
  386. package/dist/lib/format.js.map +1 -1
  387. package/dist/lib/http-status.d.ts +24 -0
  388. package/dist/lib/http-status.js +14 -0
  389. package/dist/lib/http-status.js.map +1 -0
  390. package/dist/lib/intl-cache.d.ts +14 -0
  391. package/dist/lib/intl-cache.js +15 -0
  392. package/dist/lib/intl-cache.js.map +1 -0
  393. package/dist/lib/isin.d.ts +10 -0
  394. package/dist/lib/isin.js.map +1 -1
  395. package/dist/lib/iso-day.d.ts +11 -0
  396. package/dist/lib/iso-day.js +8 -0
  397. package/dist/lib/iso-day.js.map +1 -0
  398. package/dist/lib/lazy-with-fallback.d.ts +31 -0
  399. package/dist/lib/lazy-with-fallback.js +40 -0
  400. package/dist/lib/lazy-with-fallback.js.map +1 -0
  401. package/dist/lib/path-pattern.d.ts +34 -0
  402. package/dist/lib/path-pattern.js +24 -0
  403. package/dist/lib/path-pattern.js.map +1 -0
  404. package/dist/lib/reduced-motion.d.ts +2 -1
  405. package/dist/lib/reduced-motion.js.map +1 -1
  406. package/dist/lib/round.d.ts +2 -1
  407. package/dist/lib/round.js +2 -1
  408. package/dist/lib/round.js.map +1 -1
  409. package/dist/lib/safe-storage.d.ts +24 -5
  410. package/dist/lib/safe-storage.js +27 -2
  411. package/dist/lib/safe-storage.js.map +1 -1
  412. package/dist/lib/session-refresh.js +1 -1
  413. package/dist/lib/session-refresh.js.map +1 -1
  414. package/dist/lib/share-format.d.ts +13 -0
  415. package/dist/lib/share-format.js +10 -0
  416. package/dist/lib/share-format.js.map +1 -0
  417. package/dist/lib/text-case.d.ts +17 -0
  418. package/dist/lib/text-case.js +8 -0
  419. package/dist/lib/text-case.js.map +1 -0
  420. package/dist/lib/text-match.d.ts +29 -0
  421. package/dist/lib/text-match.js +20 -2
  422. package/dist/lib/text-match.js.map +1 -1
  423. package/dist/lib/thenable.d.ts +13 -0
  424. package/dist/lib/thenable.js +8 -0
  425. package/dist/lib/thenable.js.map +1 -0
  426. package/dist/lib/translation-review.d.ts +7 -2
  427. package/dist/lib/translation-review.js +4 -4
  428. package/dist/lib/translation-review.js.map +1 -1
  429. package/dist/notifications/device-zone-report.js +1 -1
  430. package/dist/notifications/device-zone-report.js.map +1 -1
  431. package/dist/notifications/notification-bell.d.ts +2 -1
  432. package/dist/notifications/notification-bell.js +3 -3
  433. package/dist/notifications/notification-bell.js.map +1 -1
  434. package/dist/notifications/notification-inbox.d.ts +10 -6
  435. package/dist/notifications/notification-inbox.js +26 -48
  436. package/dist/notifications/notification-inbox.js.map +1 -1
  437. package/dist/notifications/notification-parts.d.ts +3 -2
  438. package/dist/notifications/notification-parts.js +1 -3
  439. package/dist/notifications/notification-parts.js.map +1 -1
  440. package/dist/notifications/notification-types-setting.js +1 -1
  441. package/dist/notifications/notification-types-setting.js.map +1 -1
  442. package/dist/notifications/notifications-labels.d.ts +6 -0
  443. package/dist/notifications/notifications-labels.js +1 -0
  444. package/dist/notifications/notifications-labels.js.map +1 -1
  445. package/dist/notifications/push-device-setting.js +1 -1
  446. package/dist/notifications/push-device-setting.js.map +1 -1
  447. package/dist/notifications/push-devices-setting.d.ts +2 -1
  448. package/dist/notifications/push-devices-setting.js +27 -49
  449. package/dist/notifications/push-devices-setting.js.map +1 -1
  450. package/dist/notifications/quiet-hours-setting.d.ts +10 -3
  451. package/dist/notifications/quiet-hours-setting.js +65 -26
  452. package/dist/notifications/quiet-hours-setting.js.map +1 -1
  453. package/dist/notifications/unsubscribe-panel.js +4 -7
  454. package/dist/notifications/unsubscribe-panel.js.map +1 -1
  455. package/dist/notifications/use-push-device.d.ts +39 -1
  456. package/dist/notifications/use-push-device.js +28 -7
  457. package/dist/notifications/use-push-device.js.map +1 -1
  458. package/dist/offline/query-persistence.d.ts +3 -1
  459. package/dist/offline/query-persistence.js +19 -2
  460. package/dist/offline/query-persistence.js.map +1 -1
  461. package/dist/rhf/fields.d.ts +11 -4
  462. package/dist/rhf/fields.js.map +1 -1
  463. package/dist/search/command-palette.js +1 -1
  464. package/dist/search/command-palette.js.map +1 -1
  465. package/dist/search/global-search.js +19 -9
  466. package/dist/search/global-search.js.map +1 -1
  467. package/dist/search/use-command-key.d.ts +12 -1
  468. package/dist/search/use-command-key.js +3 -12
  469. package/dist/search/use-command-key.js.map +1 -1
  470. package/dist/search.d.ts +7 -0
  471. package/dist/search.js +10 -0
  472. package/dist/search.js.map +1 -1
  473. package/dist/settings/use-settings-focus.js +1 -0
  474. package/dist/settings/use-settings-focus.js.map +1 -1
  475. package/dist/shell/account-menu-language.d.ts +21 -5
  476. package/dist/shell/account-menu-language.js +3 -1
  477. package/dist/shell/account-menu-language.js.map +1 -1
  478. package/dist/shell/account-menu-text-size.d.ts +26 -9
  479. package/dist/shell/account-menu-text-size.js +3 -2
  480. package/dist/shell/account-menu-text-size.js.map +1 -1
  481. package/dist/shell/dev-menu.js +5 -18
  482. package/dist/shell/dev-menu.js.map +1 -1
  483. package/dist/shell/role-switcher.d.ts +7 -0
  484. package/dist/shell/role-switcher.js.map +1 -1
  485. package/dist/shell/top-bar-actions.d.ts +1 -1
  486. package/dist/shell/top-bar-actions.js.map +1 -1
  487. package/dist/shell/top-bar-parts-labels.d.ts +0 -2
  488. package/dist/shell/top-bar-parts-labels.js +0 -1
  489. package/dist/shell/top-bar-parts-labels.js.map +1 -1
  490. package/dist/shell.d.ts +27 -8
  491. package/dist/shell.js +28 -7
  492. package/dist/shell.js.map +1 -1
  493. package/dist/sync/engine.d.ts +30 -4
  494. package/dist/sync/engine.js +172 -52
  495. package/dist/sync/engine.js.map +1 -1
  496. package/dist/sync.d.ts +6 -2
  497. package/dist/sync.js +6 -1
  498. package/dist/sync.js.map +1 -1
  499. package/dist/testing/css-variables.js +3 -8
  500. package/dist/testing/css-variables.js.map +1 -1
  501. package/dist/theme/contrast.d.ts +2 -1
  502. package/dist/theme/contrast.js +3 -8
  503. package/dist/theme/contrast.js.map +1 -1
  504. package/dist/theme/palette-store.js +6 -6
  505. package/dist/theme/palette-store.js.map +1 -1
  506. package/dist/theme/text-size.js +3 -8
  507. package/dist/theme/text-size.js.map +1 -1
  508. package/dist/theme/theme-store.js +3 -6
  509. package/dist/theme/theme-store.js.map +1 -1
  510. package/dist/tour/tour-catalogue.d.ts +7 -4
  511. package/dist/tour/tour-catalogue.js +12 -10
  512. package/dist/tour/tour-catalogue.js.map +1 -1
  513. package/dist/tour/tour-dock.d.ts +0 -2
  514. package/dist/tour/tour-dock.js +0 -2
  515. package/dist/tour/tour-dock.js.map +1 -1
  516. package/dist/tour/tour-gates.d.ts +4 -0
  517. package/dist/tour/tour-gates.js +14 -0
  518. package/dist/tour/tour-gates.js.map +1 -1
  519. package/dist/tour/tour-overlay.d.ts +27 -0
  520. package/dist/tour/tour-overlay.js +741 -0
  521. package/dist/tour/tour-overlay.js.map +1 -0
  522. package/dist/tour/tour.d.ts +13 -3
  523. package/dist/tour/tour.js +59 -781
  524. package/dist/tour/tour.js.map +1 -1
  525. package/dist/tour/use-tour-progress.js +1 -1
  526. package/dist/tour/use-tour-progress.js.map +1 -1
  527. package/dist/wizard/stepper-nav.d.ts +5 -1
  528. package/dist/wizard/stepper-nav.js.map +1 -1
  529. package/dist/wizard/types.d.ts +6 -2
  530. package/dist/wizard/types.js.map +1 -1
  531. package/dist/wizard/use-wizard.d.ts +3 -1
  532. package/dist/wizard/use-wizard.js.map +1 -1
  533. package/dist/wizard/validation.d.ts +3 -1
  534. package/dist/wizard/validation.js.map +1 -1
  535. package/package.json +10 -6
  536. package/src/account/account-parts.ts +5 -25
  537. package/src/account/data-export-setting-labels.ts +1 -1
  538. package/src/account/delete-account-setting-labels.ts +1 -1
  539. package/src/account/email-change-setting-labels.ts +1 -1
  540. package/src/account/passkeys-setting.tsx +60 -38
  541. package/src/account/sessions-setting.tsx +18 -12
  542. package/src/admin/admin-action-confirm.tsx +1 -5
  543. package/src/admin/admin-parts.ts +11 -25
  544. package/src/admin/mail-test-card-labels.ts +1 -1
  545. package/src/admin/plan-change-labels.ts +5 -23
  546. package/src/admin/plan-change.tsx +7 -44
  547. package/src/admin/reviewer-scope-editor.tsx +3 -3
  548. package/src/auth/auth-errors-labels.ts +23 -0
  549. package/src/auth/auth-errors.ts +11 -38
  550. package/src/auth/complete-name-dialog.tsx +4 -1
  551. package/src/auth/forgot-password-form-labels.ts +1 -1
  552. package/src/auth/forgot-password-form.tsx +11 -6
  553. package/src/auth/register-form-labels.ts +1 -1
  554. package/src/auth/reset-password-form-labels.ts +1 -1
  555. package/src/auth/reset-password-form.tsx +8 -4
  556. package/src/auth/sign-in-form-labels.ts +1 -1
  557. package/src/auth/status-parts.tsx +0 -6
  558. package/src/auth/verify-email-labels.ts +1 -1
  559. package/src/auth/verify-email.tsx +11 -6
  560. package/src/billing/cancel-subscription.tsx +26 -12
  561. package/src/billing/dialog-close.ts +34 -0
  562. package/src/billing/plan-card.tsx +36 -11
  563. package/src/billing/plan-change-labels.ts +66 -0
  564. package/src/billing/plan-switch.tsx +26 -12
  565. package/src/chart.ts +21 -3
  566. package/src/components/account-chips.tsx +20 -23
  567. package/src/components/alert-banner.tsx +82 -5
  568. package/src/components/amount-input.tsx +18 -7
  569. package/src/components/autocomplete.tsx +64 -70
  570. package/src/components/bulk-action-bar.tsx +70 -2
  571. package/src/components/button-look.ts +5 -5
  572. package/src/components/button.tsx +2 -2
  573. package/src/components/chart-edge.ts +34 -1
  574. package/src/components/chart-figure.tsx +35 -0
  575. package/src/components/chart-zoom-core.tsx +6 -5
  576. package/src/components/chart.tsx +26 -5
  577. package/src/components/combobox-core.tsx +293 -62
  578. package/src/components/combobox.tsx +183 -153
  579. package/src/components/copy-button.tsx +34 -23
  580. package/src/components/country-select.tsx +16 -11
  581. package/src/components/data-table-column-settings.tsx +52 -10
  582. package/src/components/data-table-filters.ts +6 -4
  583. package/src/components/data-table-fit.ts +19 -5
  584. package/src/components/data-table-groups.tsx +1 -1
  585. package/src/components/data-table-head.tsx +32 -22
  586. package/src/components/data-table-mobile.tsx +61 -26
  587. package/src/components/data-table-pagination.tsx +21 -6
  588. package/src/components/data-table-resize.ts +1 -1
  589. package/src/components/data-table-row-actions.tsx +3 -3
  590. package/src/components/data-table-row.tsx +25 -14
  591. package/src/components/data-table-selection.ts +1 -1
  592. package/src/components/data-table-shared.tsx +33 -1
  593. package/src/components/data-table-sort.ts +57 -2
  594. package/src/components/data-table-virtual.ts +32 -8
  595. package/src/components/data-table.tsx +239 -68
  596. package/src/components/date-field.tsx +65 -8
  597. package/src/components/date-picker.tsx +125 -48
  598. package/src/components/date-range-picker.tsx +82 -31
  599. package/src/components/error-boundary.tsx +53 -39
  600. package/src/components/field-base-labels.ts +20 -0
  601. package/src/components/field-base.tsx +13 -7
  602. package/src/components/field-sync-labels.ts +1 -0
  603. package/src/components/field-sync.tsx +23 -1
  604. package/src/components/form-actions.tsx +20 -5
  605. package/src/components/hover-menu.tsx +18 -8
  606. package/src/components/icon-button.tsx +14 -9
  607. package/src/components/inline-confirm.tsx +116 -0
  608. package/src/components/inline-entity-combobox.tsx +197 -153
  609. package/src/components/input.tsx +1 -1
  610. package/src/components/jump-calendar.tsx +30 -146
  611. package/src/components/legal-page.tsx +10 -7
  612. package/src/components/legal.tsx +4 -2
  613. package/src/components/lightbox.tsx +21 -8
  614. package/src/components/list.tsx +10 -4
  615. package/src/components/measured-grid.tsx +8 -4
  616. package/src/components/mini-calendar.tsx +6 -5
  617. package/src/components/modal.tsx +6 -1
  618. package/src/components/month-grid.tsx +229 -0
  619. package/src/components/month-picker.tsx +192 -200
  620. package/src/components/multi-select.tsx +21 -9
  621. package/src/components/picker-sheet.tsx +12 -9
  622. package/src/components/pie-chart-labels.ts +8 -0
  623. package/src/components/pie-chart.tsx +22 -28
  624. package/src/components/popover.tsx +18 -1
  625. package/src/components/row-actions.tsx +2 -1
  626. package/src/components/section-nav.tsx +7 -0
  627. package/src/components/series-chart-axes.ts +3 -3
  628. package/src/components/series-chart-labels.ts +7 -4
  629. package/src/components/series-chart.tsx +178 -69
  630. package/src/components/share-card.tsx +6 -6
  631. package/src/components/sign-chip-labels.ts +1 -0
  632. package/src/components/sign-chip.tsx +10 -1
  633. package/src/components/tabs-labels.ts +13 -0
  634. package/src/components/tabs.tsx +2 -2
  635. package/src/components/toggle-legend.tsx +16 -11
  636. package/src/components/translation-review-editor.tsx +6 -5
  637. package/src/components/translation-review.tsx +4 -3
  638. package/src/components/treemap-labels.ts +39 -0
  639. package/src/components/treemap-layout.ts +210 -0
  640. package/src/components/treemap.tsx +722 -133
  641. package/src/components/use-table-state.ts +12 -9
  642. package/src/data-table.ts +9 -2
  643. package/src/eslint/role-utilities.ts +97 -19
  644. package/src/feedback/feedback-dialog.tsx +30 -19
  645. package/src/feedback/feedback-mutation.ts +41 -0
  646. package/src/feedback/feedback-page-link.ts +90 -0
  647. package/src/feedback/feedback-public-list.tsx +17 -5
  648. package/src/feedback/feedback-replies.tsx +13 -6
  649. package/src/feedback/feedback-row-detail-labels.ts +5 -0
  650. package/src/feedback/feedback-row-detail.tsx +103 -67
  651. package/src/feedback/feedback-status-undo.ts +41 -27
  652. package/src/feedback/feedback-submit-dialog.tsx +2 -2
  653. package/src/feedback/feedback-submit.tsx +21 -7
  654. package/src/feedback/feedback-swipe.tsx +1 -1
  655. package/src/feedback/feedback-table.tsx +86 -38
  656. package/src/feedback/feedback-thread-labels.ts +2 -2
  657. package/src/feedback/feedback-thread.tsx +27 -25
  658. package/src/feedback.ts +42 -0
  659. package/src/hooks/use-focus-trap.ts +8 -24
  660. package/src/hooks/use-hotkey.ts +24 -7
  661. package/src/hooks/use-long-press.ts +16 -2
  662. package/src/hooks/use-media-query.ts +20 -4
  663. package/src/hooks/use-mounted.ts +39 -0
  664. package/src/hooks/use-overlay-history.ts +32 -3
  665. package/src/i18n/defaults.ts +4 -5
  666. package/src/i18n/german.ts +8 -2
  667. package/src/i18n/kit-labels.tsx +9 -2
  668. package/src/i18n/locales/es.ts +8 -2
  669. package/src/i18n/locales/fr.ts +21 -13
  670. package/src/i18n/locales/hu.ts +10 -2
  671. package/src/i18n/locales/it.ts +8 -2
  672. package/src/i18n/locales/zh.ts +9 -2
  673. package/src/i18n/review.ts +5 -0
  674. package/src/index.ts +103 -13
  675. package/src/landing/routing.tsx +14 -10
  676. package/src/lib/broadcast.ts +52 -0
  677. package/src/lib/date-format.ts +89 -13
  678. package/src/lib/day-format.ts +96 -0
  679. package/src/lib/describe-failure.ts +18 -0
  680. package/src/lib/direction.ts +39 -0
  681. package/src/lib/focus-left.ts +156 -0
  682. package/src/lib/format.ts +5 -17
  683. package/src/lib/http-status.ts +38 -0
  684. package/src/lib/intl-cache.ts +25 -0
  685. package/src/lib/isin.ts +10 -0
  686. package/src/lib/iso-day.ts +13 -0
  687. package/src/lib/lazy-with-fallback.ts +82 -0
  688. package/src/lib/path-pattern.ts +55 -0
  689. package/src/lib/reduced-motion.ts +2 -1
  690. package/src/lib/round.ts +7 -2
  691. package/src/lib/safe-storage.ts +48 -6
  692. package/src/lib/session-refresh.ts +1 -1
  693. package/src/lib/share-format.ts +18 -0
  694. package/src/lib/text-case.ts +19 -0
  695. package/src/lib/text-match.ts +57 -2
  696. package/src/lib/thenable.ts +19 -0
  697. package/src/lib/translation-review.ts +13 -5
  698. package/src/notifications/device-zone-report.ts +1 -1
  699. package/src/notifications/notification-bell.tsx +7 -4
  700. package/src/notifications/notification-inbox.tsx +40 -58
  701. package/src/notifications/notification-parts.tsx +3 -4
  702. package/src/notifications/notification-types-setting.tsx +1 -1
  703. package/src/notifications/notifications-labels.ts +7 -0
  704. package/src/notifications/push-device-setting.tsx +1 -1
  705. package/src/notifications/push-devices-setting.tsx +30 -55
  706. package/src/notifications/quiet-hours-setting.tsx +93 -31
  707. package/src/notifications/unsubscribe-panel.tsx +4 -7
  708. package/src/notifications/use-push-device.ts +76 -10
  709. package/src/offline/query-persistence.ts +35 -3
  710. package/src/rhf/fields.tsx +11 -4
  711. package/src/search/command-palette.tsx +2 -4
  712. package/src/search/global-search.tsx +36 -15
  713. package/src/search/use-command-key.ts +13 -12
  714. package/src/search.ts +9 -0
  715. package/src/settings/use-settings-focus.ts +3 -0
  716. package/src/shell/account-menu-language.tsx +27 -5
  717. package/src/shell/account-menu-text-size.tsx +32 -10
  718. package/src/shell/dev-menu.tsx +7 -21
  719. package/src/shell/role-switcher.tsx +7 -0
  720. package/src/shell/top-bar-actions.tsx +1 -1
  721. package/src/shell/top-bar-parts-labels.ts +0 -3
  722. package/src/shell.ts +40 -9
  723. package/src/sync/engine.ts +339 -61
  724. package/src/sync.ts +8 -1
  725. package/src/testing/css-variables.ts +8 -9
  726. package/src/theme/contrast.ts +8 -9
  727. package/src/theme/palette-store.ts +9 -9
  728. package/src/theme/text-size.ts +6 -8
  729. package/src/theme/theme-store.ts +7 -9
  730. package/src/tour/tour-catalogue.tsx +26 -15
  731. package/src/tour/tour-dock.ts +0 -2
  732. package/src/tour/tour-gates.ts +23 -0
  733. package/src/tour/tour-overlay.tsx +1170 -0
  734. package/src/tour/tour.tsx +108 -1202
  735. package/src/tour/use-tour-progress.ts +1 -1
  736. package/src/wizard/stepper-nav.tsx +3 -1
  737. package/src/wizard/types.ts +11 -2
  738. package/src/wizard/use-wizard.ts +2 -1
  739. package/src/wizard/validation.ts +8 -2
  740. package/tokens.css +2 -1
  741. package/dist/components/ui-labels.js.map +0 -1
  742. package/dist/shell/dev-switchers.d.ts +0 -6
  743. package/dist/shell/dev-switchers.js +0 -12
  744. package/dist/shell/dev-switchers.js.map +0 -1
  745. package/src/components/ui-labels.ts +0 -24
  746. package/src/shell/dev-switchers.tsx +0 -18
@@ -1,4 +1,5 @@
1
- import { httpStatusOf } from "../auth/auth-errors";
1
+ import { listenOn, postOnce } from "../lib/broadcast";
2
+ import { httpStatusOf } from "../lib/http-status";
2
3
  import { isOfflineError, stampAfter } from "../offline/local-first";
3
4
  import { isSharedComputer } from "../offline/shared-computer";
4
5
  import type { OutboxChange, OutboxStatus, SyncDb, SyncedRow } from "./db";
@@ -16,6 +17,15 @@ import type { SyncRequestBody, SyncResponseBody, SyncWireChange } from "./wire";
16
17
  * the generation is unchanged (D15); follow `page_cursor` with pull-only requests, the cursor
17
18
  * written with the last page only (D12). Then the app's `steps`, then one more pass if a step
18
19
  * changed something. One round at a time; a request during a round runs once more after it.
20
+ *
21
+ * Across tabs (0.52; refactor audit 2026-10-30, FLW-1): every tab runs its own engine on the one
22
+ * IndexedDB copy, and before 0.52 nothing stood between them — a second tab took the first's
23
+ * `sending` changes as a closed tab's and sent them again, its failure laid them back so an edit
24
+ * merged into a change already out and was deleted with it, and a sign-out in one tab was pulled
25
+ * straight back from 0 by the other's poll. Now a round runs under a Web Lock named after the
26
+ * database, so the rounds of all tabs take turns, first come first served; and `forget()`
27
+ * writes a new epoch into `meta` with the clear, which every round reads, so another tab's
28
+ * round drops its answer and that tab's engine stops.
19
29
  */
20
30
 
21
31
  export interface SyncCounts {
@@ -27,7 +37,8 @@ export interface SyncCounts {
27
37
 
28
38
  export interface SyncStepContext {
29
39
  signal: AbortSignal;
30
- /** False once `forget()` bumped the generation: the step writes nothing more (D15). */
40
+ /** False once `forget()` bumped the generation — in this tab, or (0.52) in another tab on
41
+ * the same database, heard over a BroadcastChannel: the step writes nothing more (D15). */
31
42
  isCurrent(): boolean;
32
43
  }
33
44
 
@@ -60,7 +71,9 @@ export interface SyncEngineState {
60
71
  }
61
72
 
62
73
  export interface SyncRoundResult {
63
- /** `paused`: `paused()` held the round. `dropped`: `forget()` ran during it, its answer unused. */
74
+ /** `paused`: `paused()` held the round. (A round asked for while another tab's round holds
75
+ * the database waits its turn, 0.52.) `dropped`: `forget()` ran during it, here or in
76
+ * another tab, its answer unused. */
64
77
  outcome: "synced" | "offline" | "error" | "paused" | "dropped";
65
78
  /** Changes this round set `waiting`, `blocked` or `rejected`. */
66
79
  waiting: number;
@@ -97,6 +110,18 @@ export interface SyncEngineOptions<D extends SyncDb = SyncDb> {
97
110
  backoffMs?: readonly number[];
98
111
  /** Default `isSharedComputer`: on a computer marked shared the engine does not start (D15). */
99
112
  sharedComputer?: () => boolean;
113
+ /**
114
+ * 0.52: the backstop on one round, counted from the moment it holds the database's lock
115
+ * (a tab waiting behind a stuck round is not cut with it). Past it the round is cut: its
116
+ * requests and steps are aborted, anything they answer or write later is dropped, and the
117
+ * round ends `error` with a `TimeoutError` (`DOMException`), not `offline`, so the backoff
118
+ * starts and the lock goes to the next tab even when a request or a step ignores its
119
+ * signal. The app's own request timeouts come first; this only bounds what they miss.
120
+ * Default 10 minutes, long on purpose: kastlan's photo backlog can take minutes, and a cut
121
+ * round has still made progress (each photo uploaded before the cut is off the queue).
122
+ * `0` or `Infinity`: none.
123
+ */
124
+ roundTimeoutMs?: number;
100
125
  }
101
126
 
102
127
  export interface SyncEngine {
@@ -110,13 +135,20 @@ export interface SyncEngine {
110
135
  op: "upsert" | "delete",
111
136
  data?: Record<string, unknown>,
112
137
  ): Promise<void>;
138
+ /** Starts the poll and the round on `online` (nothing on a computer marked shared).
139
+ * An engine another tab's `forget()` stopped stays stopped through `start()`: only its
140
+ * own `forget()` (the app's session end) or a new engine (`releaseSyncEngine`) re-arms
141
+ * it. A start cannot tell a new sign-in from a remount in a tab whose session never
142
+ * ended, and the latter would pull the signed-out account back into the wiped copy
143
+ * (D15; the 0.52 code review asked, and the contract stands). */
113
144
  start(): void;
114
145
  /** Stops the poll and the timers, aborts the round in flight and drops its answer (the
115
146
  * queue is laid back). A new engine's round on the same database waits for it. */
116
147
  stop(): void;
117
148
  /** Sync soon (debounced): after a write. */
118
149
  requestSync(): void;
119
- /** "Sync now": resets the backoff, runs a round (after the one under way). */
150
+ /** "Sync now": resets the backoff, runs a round of its own after the one under way — another
151
+ * tab's too (0.52) — and answers its result. */
120
152
  syncNow(): Promise<SyncRoundResult>;
121
153
  /** A round that sends the `blocked` changes too. */
122
154
  retryBlocked(): Promise<void>;
@@ -125,7 +157,11 @@ export interface SyncEngine {
125
157
  /** The rejected changes with their reasons, for the app's banner. */
126
158
  rejected(): Promise<OutboxChange[]>;
127
159
  /** Sign-out: bump the generation (a round in flight writes nothing more), then clear the
128
- * rows and `meta` — and the outbox and the app's tables unless `keepQueue` (D15). */
160
+ * rows and `meta` — and the outbox and the app's tables unless `keepQueue` (D15). In every
161
+ * tab (0.52): the clear writes a new epoch into `meta`, so a round of another tab's engine on
162
+ * this database drops its answer, and that engine stops — the session it ran for has ended.
163
+ * Another tab's `forget()` heard while this clear runs is decided by the epoch the copy is
164
+ * left with: one that committed first was undone by this clear and stops nothing. */
129
165
  forget(opts: { keepQueue: boolean }): Promise<void>;
130
166
  getState(): SyncEngineState;
131
167
  subscribe(listener: () => void): () => void;
@@ -133,8 +169,14 @@ export interface SyncEngine {
133
169
 
134
170
  export const DEFAULT_SYNC_BACKOFF_MS: readonly number[] = [5_000, 15_000, 60_000, 300_000];
135
171
  export const DEFAULT_HOLD_CODES: readonly string[] = ["billing_read_only"];
172
+ /** `roundTimeoutMs`'s default (see there). */
173
+ const ROUND_TIMEOUT_MS = 10 * 60_000;
174
+ /** The longest delay `setTimeout` keeps (2^31 - 1 ms, ~24.8 days); a longer one fires at once. */
175
+ const MAX_TIMEOUT_MS = 2_147_483_647;
136
176
 
137
177
  const LAST_SYNCED_AT = "lastSyncedAt";
178
+ /** The copy's epoch in `meta` (0.52): a new one with every `forget()`, in its clear. */
179
+ const EPOCH = "epoch";
138
180
  const lastKnowledgeKey = (scope: string) => `lastKnowledge:${scope}`;
139
181
  const UNSENT: readonly OutboxStatus[] = ["pending", "sending", "waiting", "blocked"];
140
182
  const ZERO: SyncCounts = { pending: 0, waiting: 0, blocked: 0, rejected: 0 };
@@ -152,6 +194,22 @@ const kept = new Map<string, { db: SyncDb; engine: SyncEngine }>();
152
194
  /** The generation changed under a round: its answer is dropped. */
153
195
  class Dropped extends Error {}
154
196
 
197
+ /** The copy's epoch moved since this engine started: `forget()` ran in another tab. The
198
+ * round's answer is dropped as well. */
199
+ class ForgottenElsewhere extends Dropped {}
200
+
201
+ /** The browser's Web Locks: a secure context (https, localhost), Safari 15.4+. */
202
+ function webLocks(): LockManager | undefined {
203
+ return typeof navigator === "undefined" ? undefined : (navigator as { locks?: LockManager }).locks;
204
+ }
205
+
206
+ /** A `forget()`'s epoch: unique enough to tell two apart, and on plain http too, where
207
+ * `crypto.randomUUID` is missing (refactor audit 2026-10-30, DUP-7). */
208
+ function freshEpoch(): string {
209
+ const uuid = (globalThis.crypto as Partial<Crypto> | undefined)?.randomUUID;
210
+ return uuid ? uuid.call(globalThis.crypto) : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
211
+ }
212
+
155
213
  /** A row created on this device and never echoed by the server carries `local: true`. */
156
214
  type CopyRow = SyncedRow & { local?: boolean };
157
215
 
@@ -204,6 +262,9 @@ export function createSyncEngine<D extends SyncDb = SyncDb>(options: SyncEngineO
204
262
  const soonMs = options.soonMs ?? 800;
205
263
  const backoffMs = options.backoffMs?.length ? options.backoffMs : DEFAULT_SYNC_BACKOFF_MS;
206
264
  const sharedComputer = options.sharedComputer ?? isSharedComputer;
265
+ const roundTimeoutMs = options.roundTimeoutMs ?? ROUND_TIMEOUT_MS;
266
+ /** The rounds the deadline cut, by their signal: their late work lays nothing back. */
267
+ const cut = new WeakSet<AbortSignal>();
207
268
 
208
269
  let generation = 0;
209
270
  let state: SyncEngineState = {
@@ -224,6 +285,20 @@ export function createSyncEngine<D extends SyncDb = SyncDb>(options: SyncEngineO
224
285
  let controller: AbortController | null = null;
225
286
  let stepCounts: Record<string, Partial<SyncCounts>> = {};
226
287
 
288
+ // ── Across tabs (0.52, FLW-1) ───────────────────────────────────────────────
289
+ /** The name of this database's Web Lock and BroadcastChannel (one origin's tabs share both,
290
+ * as they share the IndexedDB). */
291
+ const across = `eifi1-sync:${db.name}`;
292
+ /** The copy's epochs this engine runs under: the one read when it started (or at its first
293
+ * round), and each its own `forget()` wrote. Any other one means `forget()` ran elsewhere. */
294
+ const epochs = new Set<string | null>();
295
+ let epochRead: Promise<void> | null = null;
296
+ /** `forget()` ran in another tab: this engine runs no round until its own `forget()`. */
297
+ let forgotten = false;
298
+ /** This engine's own `forget()`s whose clear is under way. */
299
+ let forgetting = 0;
300
+ let unlisten: (() => void) | null = null;
301
+
227
302
  function set(next: Partial<SyncEngineState>) {
228
303
  state = { ...state, ...next };
229
304
  listeners.forEach((listener) => listener());
@@ -253,12 +328,89 @@ export function createSyncEngine<D extends SyncDb = SyncDb>(options: SyncEngineO
253
328
  set({ counts, lastSyncedAt: typeof last === "string" ? last : null });
254
329
  }
255
330
 
331
+ function readEpoch(): Promise<string | null> {
332
+ return db.meta.get(EPOCH).then((entry) => (typeof entry?.value === "string" ? entry.value : null));
333
+ }
334
+
335
+ /** Read once per engine — at `start()`, before its first round can wait on another tab's —
336
+ * so a `forget()` that lands while it waits is seen as another tab's. A failed read is
337
+ * tried again by the next round. */
338
+ function captureEpoch(): Promise<void> {
339
+ if (epochRead) return epochRead;
340
+ const read = readEpoch().then((epoch) => void epochs.add(epoch));
341
+ epochRead = read;
342
+ read.catch(() => {
343
+ if (epochRead === read) epochRead = null;
344
+ });
345
+ return read;
346
+ }
347
+
348
+ /** While started, hear `forget()` in another tab at once: a step in flight sees
349
+ * `isCurrent()` turn false, the request in flight is aborted, and the poll stops. Without
350
+ * BroadcastChannel the next round's read of the epoch stops it instead. */
351
+ function listen() {
352
+ unlisten ??= listenOn(across, (data) => {
353
+ const epoch = (data as { epoch?: unknown } | null)?.epoch;
354
+ if (typeof epoch === "string" && !epochs.has(epoch)) void forgetHere();
355
+ });
356
+ }
357
+
358
+ /** `forget()` ran in another tab: the session this engine ran for has ended there. Its
359
+ * round in flight writes nothing more, and it runs none of its own any more.
360
+ *
361
+ * Not while its own `forget()` clears the copy: that clear decides, by the epoch it leaves
362
+ * in `meta`. 0.52 code review (src/sync/engine.ts): in kastlan's sign-out cascade tab A's
363
+ * epoch reached tab B while B's own clear waited behind A's, so B's `forget()` resolved
364
+ * with B stopped and forgotten — the forget that re-arms an engine left it dead. */
365
+ function forgetHere(): Promise<void> {
366
+ if (forgotten || forgetting > 0) return Promise.resolve();
367
+ forgotten = true;
368
+ if (started) halt();
369
+ else drop();
370
+ return endSession();
371
+ }
372
+
373
+ /** The round in flight, or waiting for the lock, writes nothing more: its wait and its
374
+ * request are aborted, its answer dropped, the queue laid back. */
375
+ function drop() {
376
+ generation += 1;
377
+ controller?.abort();
378
+ }
379
+
380
+ /** What the engine shows of a session that ended — by its own `forget()` or another tab's —
381
+ * goes with it: the steps' counts, the last sync, an error and its retry. 0.52 code review
382
+ * (src/sync/engine.ts): the two paths each reset part of it, and an engine another tab
383
+ * stopped showed "error" for a retry that never came. */
384
+ function endSession(): Promise<void> {
385
+ stepCounts = {};
386
+ failures = 0;
387
+ clearRetry();
388
+ set({ phase: "idle", lastSyncedAt: null, nextRetryAt: null });
389
+ return refreshCounts();
390
+ }
391
+
392
+ /** The round's epoch still stands in `meta`, read in the caller's transaction, so another
393
+ * tab's `forget()` commits wholly before it or after it. Else `forget()` ran: in this tab
394
+ * (the generation moved too) or in another — then the engine stops. */
395
+ async function standing(gen: number, epoch: string | null): Promise<void> {
396
+ const now = await readEpoch();
397
+ if (gen !== generation) throw new Dropped();
398
+ if (now !== epoch) throw epochs.has(now) ? new Dropped() : new ForgottenElsewhere();
399
+ }
400
+
256
401
  // ── The queue ──────────────────────────────────────────────────────────────
257
402
 
258
403
  /** Mark the scope's queue as in the request (a `sending` left by a closed tab goes back
259
- * first); answers it with the statuses it had. */
260
- function takeQueue(scope: string, withBlocked: boolean): Promise<OutboxChange[]> {
261
- return db.transaction("rw", db.outbox, async () => {
404
+ * first); answers it with the statuses it had. Under the Web Lock (0.52) a `sending` here
405
+ * is a closed tab's, as this assumes: no other tab's round is out. Without Web Locks
406
+ * (plain http) it may be another open tab's, as before 0.52.
407
+ *
408
+ * Only under the round's epoch. 0.52 code review (src/sync/engine.ts): after another tab's
409
+ * `forget()` had landed mid-round, every scope left still posted the kept queue under the
410
+ * session that had ended and pulled itself from 0, only for `apply` to drop the answer. */
411
+ function takeQueue(scope: string, withBlocked: boolean, gen: number, epoch: string | null): Promise<OutboxChange[]> {
412
+ return db.transaction("rw", db.outbox, db.meta, async () => {
413
+ await standing(gen, epoch);
262
414
  await db.outbox
263
415
  .where("scope")
264
416
  .equals(scope)
@@ -273,8 +425,14 @@ export function createSyncEngine<D extends SyncDb = SyncDb>(options: SyncEngineO
273
425
  });
274
426
  }
275
427
 
276
- /** The request failed: each change back to the status it had. */
277
- function restore(sent: OutboxChange[]) {
428
+ /** The request failed or its answer was dropped: each change back to the status it had.
429
+ * Not by a round the deadline cut: its late work ends after the next round — this tab's
430
+ * or another's — may have taken these changes `sending` again, and laid back `pending`
431
+ * under that round an edit would merge into a change already out and be deleted with it
432
+ * (FLW-1's lost edit). The next round's `takeQueue` takes them back instead, as a closed
433
+ * tab's. */
434
+ function layBack(sent: OutboxChange[], signal: AbortSignal) {
435
+ if (cut.has(signal)) return;
278
436
  return db.outbox.bulkUpdate(sent.map((c) => ({ key: c.seq!, changes: { status: c.status } })));
279
437
  }
280
438
 
@@ -282,10 +440,19 @@ export function createSyncEngine<D extends SyncDb = SyncDb>(options: SyncEngineO
282
440
  return db.outbox.bulkUpdate(changes.map((c) => ({ key: c.seq!, changes: fields })));
283
441
  }
284
442
 
285
- /** Put one page's rows in the copy, settle the sent changes, lay the unsent ones back. */
286
- async function apply(scope: string, answer: SyncResponseBody, sent: OutboxChange[], gen: number, lastPage: boolean) {
443
+ /** Put one page's rows in the copy, settle the sent changes, lay the unsent ones back. The
444
+ * epoch is read in the same transaction: a `forget()` of another tab either commits before
445
+ * it (this answer is dropped) or after it (and clears what it wrote). */
446
+ async function apply(
447
+ scope: string,
448
+ answer: SyncResponseBody,
449
+ sent: OutboxChange[],
450
+ gen: number,
451
+ epoch: string | null,
452
+ lastPage: boolean,
453
+ ) {
287
454
  return db.transaction("rw", db.rows, db.outbox, db.meta, async () => {
288
- if (gen !== generation) throw new Dropped();
455
+ await standing(gen, epoch);
289
456
  const refused = answer.refused ?? null;
290
457
  const refusedIds = new Set(refused?.change_ids ?? []);
291
458
  const rejections = new Map((answer.rejections ?? []).map((r) => [r.change_id, r]));
@@ -367,10 +534,11 @@ export function createSyncEngine<D extends SyncDb = SyncDb>(options: SyncEngineO
367
534
  async function syncScope(
368
535
  scope: string,
369
536
  gen: number,
537
+ epoch: string | null,
370
538
  signal: AbortSignal,
371
539
  withBlocked: boolean,
372
540
  ): Promise<ScopeResult> {
373
- const sent = await takeQueue(scope, withBlocked);
541
+ const sent = await takeQueue(scope, withBlocked, gen, epoch);
374
542
  const last = Number((await db.meta.get(lastKnowledgeKey(scope)))?.value ?? 0) || 0;
375
543
  const body = (changes: SyncWireChange[], page_cursor: SyncRequestBody["page_cursor"]): SyncRequestBody => ({
376
544
  scope,
@@ -384,25 +552,30 @@ export function createSyncEngine<D extends SyncDb = SyncDb>(options: SyncEngineO
384
552
  try {
385
553
  answer = await options.send(body(sent.map(toWire), null), signal);
386
554
  } catch (err) {
387
- await restore(sent);
555
+ await layBack(sent, signal);
388
556
  if (gen !== generation) throw new Dropped();
389
557
  if (isOffline(err)) return { offline: true };
390
558
  const plan = planCode(err);
391
- if (plan) {
392
- // The whole request refused for the plan: every change waits for it.
393
- await mark(sent, { status: "waiting", code: plan, reason: plan });
394
- return { offline: false, waiting: sent.length, blocked: 0, rejected: 0 };
395
- }
396
- if (httpStatusOf(err) === 403)
559
+ if (!plan && httpStatusOf(err) !== 403) throw err;
560
+ // A refusal is the answer, kept only under the round's epoch as `apply` keeps one. 0.52
561
+ // code review (src/sync/engine.ts): a 403 heard after another tab's `forget()` marked
562
+ // the kept queue `rejected`, never sent again, for a session that had ended.
563
+ return db.transaction("rw", db.outbox, db.meta, async (): Promise<ScopeResult> => {
564
+ await standing(gen, epoch);
565
+ if (plan) {
566
+ // The whole request refused for the plan: every change waits for it.
567
+ await mark(sent, { status: "waiting", code: plan, reason: plan });
568
+ return { offline: false, waiting: sent.length, blocked: 0, rejected: 0 };
569
+ }
397
570
  return { offline: false, waiting: 0, blocked: 0, rejected: await loseScope(scope, err) };
398
- throw err;
571
+ });
399
572
  }
400
573
  let cursor = options.paged ? (answer.page_cursor ?? null) : null;
401
574
  let tally: ScopeResult;
402
575
  try {
403
- tally = { offline: false, ...(await apply(scope, answer, sent, gen, cursor == null)) };
576
+ tally = { offline: false, ...(await apply(scope, answer, sent, gen, epoch, cursor == null)) };
404
577
  } catch (err) {
405
- if (err instanceof Dropped) await restore(sent);
578
+ if (err instanceof Dropped) await layBack(sent, signal);
406
579
  throw err;
407
580
  }
408
581
  // The rest of a paged pull: pull-only; the cursor is written with the last page alone,
@@ -417,7 +590,7 @@ export function createSyncEngine<D extends SyncDb = SyncDb>(options: SyncEngineO
417
590
  throw err;
418
591
  }
419
592
  cursor = page.page_cursor ?? null;
420
- await apply(scope, page, [], gen, cursor == null);
593
+ await apply(scope, page, [], gen, epoch, cursor == null);
421
594
  }
422
595
  return tally;
423
596
  }
@@ -426,17 +599,100 @@ export function createSyncEngine<D extends SyncDb = SyncDb>(options: SyncEngineO
426
599
 
427
600
  async function round(withBlocked: boolean, gen: number): Promise<SyncRoundResult> {
428
601
  const result: SyncRoundResult = { outcome: "synced", waiting: 0, blocked: 0, rejected: 0 };
429
- if (gen !== generation) return { ...result, outcome: "dropped" };
430
- if (options.paused?.()) return { ...result, outcome: "paused" };
431
- if (!browserOnline()) return { ...result, outcome: "offline" };
602
+ if (gen !== generation || forgotten) return { ...result, outcome: "dropped" };
603
+ // Aborted by stop() and forget(): the wait for the lock, then the round's requests.
432
604
  const own = new AbortController();
433
605
  controller = own;
434
- set({ phase: "syncing" });
435
606
  try {
607
+ const locks = webLocks();
608
+ if (!locks) return await begin(result, withBlocked, gen, own);
609
+ let granted = false;
610
+ try {
611
+ // Queued, first come first served: the rounds of every tab on this database take
612
+ // turns, and one asked for meanwhile waits for its turn (`current` and `again` keep
613
+ // one per engine). 0.52 code review (src/sync/engine.ts): an `ifAvailable` round
614
+ // that found the lock taken answered `paused` at once, so the sign-out dialog's
615
+ // "Sync now" closed as if it had synced; its one deferred round lost a
616
+ // `retryBlocked()`'s `blocked`; and two tabs' waits refused each other's next try —
617
+ // the spec refuses `ifAvailable` while any request is queued. A closed tab's lock
618
+ // is released with it.
619
+ return await locks.request(across, { signal: own.signal }, () => {
620
+ granted = true;
621
+ return begin(result, withBlocked, gen, own);
622
+ });
623
+ } catch (err) {
624
+ // `begin` throws only what the app's `paused()` throws. A wait aborted by stop() or
625
+ // forget() is dropped there; a lock manager that refuses (an opaque origin, a
626
+ // document no longer active) runs the round as before 0.52.
627
+ if (granted) return { ...result, outcome: "error", error: err };
628
+ return await begin(result, withBlocked, gen, own);
629
+ }
630
+ } finally {
631
+ if (controller === own) controller = null;
632
+ }
633
+ }
634
+
635
+ /** The round begins — under the lock, where there is one, after however long it waited:
636
+ * the decision point, and the start of its deadline. */
637
+ async function begin(
638
+ result: SyncRoundResult,
639
+ withBlocked: boolean,
640
+ gen: number,
641
+ own: AbortController,
642
+ ): Promise<SyncRoundResult> {
643
+ if (gen !== generation || forgotten) return { ...result, outcome: "dropped" };
644
+ if (options.paused?.()) return { ...result, outcome: "paused" };
645
+ if (!browserOnline()) return { ...result, outcome: "offline" };
646
+ return withDeadline(locked(result, withBlocked, gen, own.signal), result, gen, own);
647
+ }
648
+
649
+ /** The round, or its cut at `roundTimeoutMs`, whichever comes first. 0.52 code review,
650
+ * finding 3 (Marcel: "kit backstop + app timeouts"): a round holds the lock of every tab
651
+ * on its database, and one whose request never settles (axios has no default timeout) or
652
+ * whose step never does stopped sync in all of them until that tab was closed. The cut
653
+ * settles the round by the race, so the lock is released even when the app ignores the
654
+ * abort; the generation moves, so what the cut work answers or writes later is dropped. */
655
+ function withDeadline(
656
+ round: Promise<SyncRoundResult>,
657
+ result: SyncRoundResult,
658
+ gen: number,
659
+ own: AbortController,
660
+ ): Promise<SyncRoundResult> {
661
+ if (!(roundTimeoutMs > 0 && Number.isFinite(roundTimeoutMs))) return round;
662
+ let timer: ReturnType<typeof setTimeout> | undefined;
663
+ const deadline = new Promise<SyncRoundResult>((resolve) => {
664
+ timer = setTimeout(
665
+ () => {
666
+ const error = new DOMException(`The sync round ran past ${roundTimeoutMs} ms`, "TimeoutError");
667
+ // Settled first: the race ends on the cut, whatever the abort below sets off.
668
+ resolve({ ...result, outcome: "error", error });
669
+ cut.add(own.signal);
670
+ if (gen === generation) generation += 1;
671
+ own.abort(error);
672
+ },
673
+ Math.min(roundTimeoutMs, MAX_TIMEOUT_MS),
674
+ );
675
+ });
676
+ return Promise.race([round, deadline]).finally(() => clearTimeout(timer));
677
+ }
678
+
679
+ /** The round itself. Never throws. */
680
+ async function locked(
681
+ result: SyncRoundResult,
682
+ withBlocked: boolean,
683
+ gen: number,
684
+ signal: AbortSignal,
685
+ ): Promise<SyncRoundResult> {
686
+ try {
687
+ await captureEpoch();
688
+ const epoch = await readEpoch();
689
+ if (gen !== generation) throw new Dropped();
690
+ if (!epochs.has(epoch)) throw new ForgottenElsewhere();
691
+ set({ phase: "syncing" });
436
692
  for (let pass = 0; ; pass++) {
437
693
  for (const scope of options.scopes()) {
438
694
  if (gen !== generation) throw new Dropped();
439
- const r = await syncScope(scope, gen, own.signal, withBlocked);
695
+ const r = await syncScope(scope, gen, epoch, signal, withBlocked);
440
696
  if (r.offline) return { ...result, outcome: "offline" };
441
697
  result.waiting += r.waiting;
442
698
  result.blocked += r.blocked;
@@ -446,7 +702,7 @@ export function createSyncEngine<D extends SyncDb = SyncDb>(options: SyncEngineO
446
702
  let changed = false;
447
703
  for (const step of steps) {
448
704
  if (gen !== generation) throw new Dropped();
449
- const ran = await step.run(db, { signal: own.signal, isCurrent: () => gen === generation });
705
+ const ran = await step.run(db, { signal, isCurrent: () => gen === generation });
450
706
  if (gen !== generation) throw new Dropped();
451
707
  if (ran.counts) stepCounts = { ...stepCounts, [step.name]: ran.counts };
452
708
  if (ran.offline) return { ...result, outcome: "offline" };
@@ -456,10 +712,9 @@ export function createSyncEngine<D extends SyncDb = SyncDb>(options: SyncEngineO
456
712
  }
457
713
  return result;
458
714
  } catch (err) {
715
+ if (err instanceof ForgottenElsewhere) void forgetHere();
459
716
  if (err instanceof Dropped || gen !== generation) return { ...result, outcome: "dropped" };
460
717
  return { ...result, outcome: "error", error: err };
461
- } finally {
462
- if (controller === own) controller = null;
463
718
  }
464
719
  }
465
720
 
@@ -548,9 +803,12 @@ export function createSyncEngine<D extends SyncDb = SyncDb>(options: SyncEngineO
548
803
  if (started && !inBackoff()) void run(false);
549
804
  }
550
805
 
806
+ /** A round of its own, begun after the one under way — and after one that a request made
807
+ * meanwhile started as that one ended — so `retryBlocked()` never joins a round that
808
+ * leaves `blocked` out (0.52 code review, src/sync/engine.ts). */
551
809
  async function runFresh(withBlocked: boolean): Promise<SyncRoundResult> {
552
810
  resetBackoff();
553
- if (current) {
811
+ while (current) {
554
812
  again = false;
555
813
  await current.catch(() => undefined);
556
814
  }
@@ -564,6 +822,26 @@ export function createSyncEngine<D extends SyncDb = SyncDb>(options: SyncEngineO
564
822
  };
565
823
  const onOffline = () => set({ online: false });
566
824
 
825
+ /** `stop()`, and the end `forgetHere` puts to a started engine. */
826
+ function halt() {
827
+ if (!started) return;
828
+ started = false;
829
+ // The next engine's round waits for the dropped one to settle.
830
+ drop();
831
+ if (running.get(db.name) === engine) running.delete(db.name);
832
+ if (poll) clearInterval(poll);
833
+ poll = null;
834
+ if (soon) clearTimeout(soon);
835
+ soon = null;
836
+ clearRetry();
837
+ if (typeof window !== "undefined") {
838
+ window.removeEventListener("online", onOnline);
839
+ window.removeEventListener("offline", onOffline);
840
+ }
841
+ unlisten?.();
842
+ unlisten = null;
843
+ }
844
+
567
845
  const engine: SyncEngine = {
568
846
  async write(scope, entity, id, op, data = {}) {
569
847
  await db.transaction("rw", db.rows, db.outbox, async () => {
@@ -608,11 +886,13 @@ export function createSyncEngine<D extends SyncDb = SyncDb>(options: SyncEngineO
608
886
  },
609
887
 
610
888
  start() {
611
- if (started || sharedComputer()) return;
889
+ if (started || forgotten || sharedComputer()) return;
612
890
  const other = running.get(db.name);
613
891
  if (other && other !== engine) other.stop();
614
892
  running.set(db.name, engine);
615
893
  started = true;
894
+ captureEpoch().catch(() => undefined);
895
+ listen();
616
896
  if (typeof window !== "undefined") {
617
897
  window.addEventListener("online", onOnline);
618
898
  window.addEventListener("offline", onOffline);
@@ -622,24 +902,7 @@ export function createSyncEngine<D extends SyncDb = SyncDb>(options: SyncEngineO
622
902
  void run(false);
623
903
  },
624
904
 
625
- stop() {
626
- if (!started) return;
627
- started = false;
628
- // The round in flight writes nothing more: its request is aborted and its answer
629
- // dropped, the queue laid back; the next engine's round waits for it to settle.
630
- generation += 1;
631
- controller?.abort();
632
- if (running.get(db.name) === engine) running.delete(db.name);
633
- if (poll) clearInterval(poll);
634
- poll = null;
635
- if (soon) clearTimeout(soon);
636
- soon = null;
637
- clearRetry();
638
- if (typeof window !== "undefined") {
639
- window.removeEventListener("online", onOnline);
640
- window.removeEventListener("offline", onOffline);
641
- }
642
- },
905
+ stop: halt,
643
906
 
644
907
  requestSync() {
645
908
  void refreshCounts();
@@ -680,15 +943,30 @@ export function createSyncEngine<D extends SyncDb = SyncDb>(options: SyncEngineO
680
943
  },
681
944
 
682
945
  async forget({ keepQueue }) {
683
- generation += 1;
684
- controller?.abort();
946
+ drop();
947
+ // Its own forget() starts this engine's next session: it runs again where another
948
+ // tab's forget() had ended it.
949
+ forgotten = false;
950
+ const epoch = freshEpoch();
951
+ epochs.add(epoch);
685
952
  const tables = keepQueue ? [db.rows, db.meta] : db.tables;
686
- await db.transaction("rw", tables, async () => {
687
- for (const table of tables) await table.clear();
688
- });
689
- stepCounts = {};
690
- set({ lastSyncedAt: null });
691
- await refreshCounts();
953
+ forgetting += 1;
954
+ try {
955
+ await db.transaction("rw", tables, async () => {
956
+ for (const table of tables) await table.clear();
957
+ await db.meta.put({ key: EPOCH, value: epoch });
958
+ });
959
+ } finally {
960
+ forgetting -= 1;
961
+ }
962
+ // Best-effort: the epoch in `meta` still stops another tab's next round. This engine's
963
+ // own channel hears it too, and keeps running: the epoch is already one of its own.
964
+ postOnce(across, { epoch });
965
+ // Another tab's forget() met while this clear ran was left to the copy: one committed
966
+ // before this clear was undone by it; one committed after it ended this session too.
967
+ // (Its message is posted after its commit, so that commit is in the epoch read here.)
968
+ const now = await readEpoch().catch(() => epoch);
969
+ await (epochs.has(now) ? endSession() : forgetHere());
692
970
  },
693
971
 
694
972
  getState: () => state,
package/src/sync.ts CHANGED
@@ -4,7 +4,13 @@
4
4
  // The only entry point that imports `dexie` — an optional peer for that reason. It never loads
5
5
  // `/offline`'s TanStack and idb-keyval peers: the leaf utilities both entries export live in
6
6
  // one module of their own (src/offline/local-first.ts). NOT re-exported from the main barrel.
7
- export { createSyncDb, rowByPk, SYNC_DB_PK_INDEX, SYNC_DB_STORES } from "./sync/db";
7
+ export {
8
+ createSyncDb,
9
+ rowByPk,
10
+ SYNC_DB_PK_INDEX,
11
+ /** @deprecated 0.52 — `createSyncDb`, which declares them (`extraStores` for an app's own) (API-4); removed in 0.53 */
12
+ SYNC_DB_STORES,
13
+ } from "./sync/db";
8
14
  export type {
9
15
  CreateSyncDbOptions,
10
16
  OutboxChange,
@@ -17,6 +23,7 @@ export type {
17
23
  export {
18
24
  createSyncEngine,
19
25
  DEFAULT_HOLD_CODES,
26
+ /** @deprecated 0.52 — `createSyncEngine`, whose `backoffMs` defaults to it (API-4); removed in 0.53 */
20
27
  DEFAULT_SYNC_BACKOFF_MS,
21
28
  releaseSyncEngine,
22
29
  syncEngineFor,
@@ -1,3 +1,4 @@
1
+ import { themeBlocks } from "../eslint/role-utilities";
1
2
  import { KIT_NAMES, TAILWIND_THEME_NAMES } from "./css-variables.generated";
2
3
 
3
4
  /**
@@ -68,17 +69,15 @@ function stripComments(text: string, isCss: boolean): string {
68
69
  return isCss ? noBlocks : noBlocks.replace(/(^|[^:])(\/\/[^\n]*)/g, (_, pre: string, c: string) => pre + blank(c));
69
70
  }
70
71
 
71
- /** The text with every `@theme … inline … { … }` block blanked out, braces and all. */
72
+ /** The text with every `@theme … inline … { … }` block blanked out, braces and all. The
73
+ * blocks are the kit's one block finder's, `themeBlocks` (src/eslint/role-utilities.ts,
74
+ * which imports nothing): this was a second brace-counting copy of it, kept equal to the
75
+ * generator's (scripts/gen-kit-css-variables.mjs, on `themeBlocks` since DUP-13) only by
76
+ * token-vars-declared.test.ts (0.52 code review). */
72
77
  function withoutInlineTheme(css: string): string {
73
78
  let out = css;
74
- for (const m of css.matchAll(/@theme\b[^{;]*\binline\b[^{;]*\{/g)) {
75
- let depth = 0;
76
- let end = m.index + m[0].length - 1;
77
- for (; end < css.length; end++) {
78
- if (css[end] === "{") depth++;
79
- else if (css[end] === "}" && --depth === 0) break;
80
- }
81
- out = out.slice(0, m.index) + css.slice(m.index, end + 1).replace(/[^\n]/g, " ") + out.slice(end + 1);
79
+ for (const { start, end } of themeBlocks(css, { inline: true })) {
80
+ out = out.slice(0, start) + out.slice(start, end).replace(/[^\n]/g, " ") + out.slice(end);
82
81
  }
83
82
  return out;
84
83
  }