@eifi1/ui-kit 0.53.0 → 0.55.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 (902) hide show
  1. package/README.md +96 -82
  2. package/dist/account/delete-account-setting.d.ts +3 -2
  3. package/dist/account/delete-account-setting.js.map +1 -1
  4. package/dist/admin/plan-change.js +2 -2
  5. package/dist/admin/plan-change.js.map +1 -1
  6. package/dist/admin/user-roster.d.ts +6 -0
  7. package/dist/admin/user-roster.js +4 -0
  8. package/dist/admin/user-roster.js.map +1 -1
  9. package/dist/auth/auth-errors.d.ts +36 -0
  10. package/dist/auth/auth-errors.js +20 -0
  11. package/dist/auth/auth-errors.js.map +1 -1
  12. package/dist/billing/billing-labels.d.ts +26 -0
  13. package/dist/billing/billing-labels.js +9 -0
  14. package/dist/billing/billing-labels.js.map +1 -1
  15. package/dist/billing/subscription-standing.d.ts +61 -0
  16. package/dist/billing/subscription-standing.js +51 -0
  17. package/dist/billing/subscription-standing.js.map +1 -0
  18. package/dist/billing/usage-meter.d.ts +52 -0
  19. package/dist/billing/usage-meter.js +52 -0
  20. package/dist/billing/usage-meter.js.map +1 -0
  21. package/dist/chart.d.ts +1 -0
  22. package/dist/chart.js +4 -0
  23. package/dist/chart.js.map +1 -1
  24. package/dist/components/alert-banner.js +2 -3
  25. package/dist/components/alert-banner.js.map +1 -1
  26. package/dist/components/amount-input.d.ts +2 -1
  27. package/dist/components/amount-input.js +3 -15
  28. package/dist/components/amount-input.js.map +1 -1
  29. package/dist/components/autocomplete.js +2 -2
  30. package/dist/components/autocomplete.js.map +1 -1
  31. package/dist/components/bulk-action-bar.js +2 -1
  32. package/dist/components/bulk-action-bar.js.map +1 -1
  33. package/dist/components/button.d.ts +35 -1
  34. package/dist/components/button.js +5 -9
  35. package/dist/components/button.js.map +1 -1
  36. package/dist/components/calendar-heatmap.js +6 -4
  37. package/dist/components/calendar-heatmap.js.map +1 -1
  38. package/dist/components/camera-button.d.ts +89 -0
  39. package/dist/components/camera-button.js +122 -0
  40. package/dist/components/camera-button.js.map +1 -0
  41. package/dist/components/camera-capture.d.ts +66 -0
  42. package/dist/components/camera-capture.js +190 -0
  43. package/dist/components/camera-capture.js.map +1 -0
  44. package/dist/components/camera-labels.d.ts +20 -0
  45. package/dist/components/camera-labels.js +15 -0
  46. package/dist/components/camera-labels.js.map +1 -0
  47. package/dist/components/chart-edge.d.ts +9 -0
  48. package/dist/components/chart-edge.js +14 -3
  49. package/dist/components/chart-edge.js.map +1 -1
  50. package/dist/components/chart-zoom-core.js +34 -29
  51. package/dist/components/chart-zoom-core.js.map +1 -1
  52. package/dist/components/checkbox-group.js +1 -1
  53. package/dist/components/checkbox-group.js.map +1 -1
  54. package/dist/components/combobox-core.js +3 -2
  55. package/dist/components/combobox-core.js.map +1 -1
  56. package/dist/components/combobox.js +2 -2
  57. package/dist/components/combobox.js.map +1 -1
  58. package/dist/components/consent-again-notice-labels.d.ts +25 -0
  59. package/dist/components/consent-again-notice-labels.js +15 -0
  60. package/dist/components/consent-again-notice-labels.js.map +1 -0
  61. package/dist/components/consent-again-notice.d.ts +85 -0
  62. package/dist/components/consent-again-notice.js +94 -0
  63. package/dist/components/consent-again-notice.js.map +1 -0
  64. package/dist/components/country-select.js +2 -7
  65. package/dist/components/country-select.js.map +1 -1
  66. package/dist/components/currency-option-list.d.ts +2 -1
  67. package/dist/components/currency-option-list.js +10 -3
  68. package/dist/components/currency-option-list.js.map +1 -1
  69. package/dist/components/currency-select.d.ts +3 -1
  70. package/dist/components/currency-select.js +2 -2
  71. package/dist/components/currency-select.js.map +1 -1
  72. package/dist/components/danger-confirm.d.ts +22 -1
  73. package/dist/components/danger-confirm.js +52 -30
  74. package/dist/components/danger-confirm.js.map +1 -1
  75. package/dist/components/data-table-column-settings.js +2 -2
  76. package/dist/components/data-table-column-settings.js.map +1 -1
  77. package/dist/components/data-table-filter-popover.d.ts +2 -1
  78. package/dist/components/data-table-filter-popover.js +3 -2
  79. package/dist/components/data-table-filter-popover.js.map +1 -1
  80. package/dist/components/data-table-filters.d.ts +33 -3
  81. package/dist/components/data-table-filters.js +6 -4
  82. package/dist/components/data-table-filters.js.map +1 -1
  83. package/dist/components/data-table-fit.js.map +1 -1
  84. package/dist/components/data-table-virtual.js +13 -3
  85. package/dist/components/data-table-virtual.js.map +1 -1
  86. package/dist/components/date-field.js +2 -2
  87. package/dist/components/date-field.js.map +1 -1
  88. package/dist/components/date-picker.js +2 -2
  89. package/dist/components/date-picker.js.map +1 -1
  90. package/dist/components/date-range-picker.js +2 -6
  91. package/dist/components/date-range-picker.js.map +1 -1
  92. package/dist/components/dialog-frame.d.ts +5 -1
  93. package/dist/components/dialog-frame.js +6 -2
  94. package/dist/components/dialog-frame.js.map +1 -1
  95. package/dist/components/dropdown.js +7 -3
  96. package/dist/components/dropdown.js.map +1 -1
  97. package/dist/components/entity-combobox.js +2 -2
  98. package/dist/components/entity-combobox.js.map +1 -1
  99. package/dist/components/field-parts.d.ts +108 -6
  100. package/dist/components/field-parts.js +59 -6
  101. package/dist/components/field-parts.js.map +1 -1
  102. package/dist/components/field-suggestion-labels.d.ts +2 -0
  103. package/dist/components/field-suggestion-labels.js +10 -0
  104. package/dist/components/field-suggestion-labels.js.map +1 -0
  105. package/dist/components/field.js +1 -1
  106. package/dist/components/field.js.map +1 -1
  107. package/dist/components/file-button.d.ts +1 -1
  108. package/dist/components/file-button.js.map +1 -1
  109. package/dist/components/floating-panel.d.ts +7 -6
  110. package/dist/components/floating-panel.js +3 -17
  111. package/dist/components/floating-panel.js.map +1 -1
  112. package/dist/components/form-actions.d.ts +3 -2
  113. package/dist/components/form-actions.js.map +1 -1
  114. package/dist/components/hover-menu.js +3 -2
  115. package/dist/components/hover-menu.js.map +1 -1
  116. package/dist/components/icon-button.d.ts +23 -5
  117. package/dist/components/icon-button.js +6 -8
  118. package/dist/components/icon-button.js.map +1 -1
  119. package/dist/components/inline-confirm.js +2 -1
  120. package/dist/components/inline-confirm.js.map +1 -1
  121. package/dist/components/inline-entity-combobox.js +2 -2
  122. package/dist/components/inline-entity-combobox.js.map +1 -1
  123. package/dist/components/input.js +2 -2
  124. package/dist/components/input.js.map +1 -1
  125. package/dist/components/jump-calendar.js +3 -3
  126. package/dist/components/jump-calendar.js.map +1 -1
  127. package/dist/components/legal.d.ts +13 -1
  128. package/dist/components/legal.js +12 -0
  129. package/dist/components/legal.js.map +1 -1
  130. package/dist/components/legended-series-chart.d.ts +80 -0
  131. package/dist/components/legended-series-chart.js +70 -0
  132. package/dist/components/legended-series-chart.js.map +1 -0
  133. package/dist/components/lightbox.js +2 -1
  134. package/dist/components/lightbox.js.map +1 -1
  135. package/dist/components/mini-calendar.js +16 -7
  136. package/dist/components/mini-calendar.js.map +1 -1
  137. package/dist/components/modal.d.ts +48 -0
  138. package/dist/components/modal.js +26 -6
  139. package/dist/components/modal.js.map +1 -1
  140. package/dist/components/month-picker.d.ts +1 -1
  141. package/dist/components/month-picker.js +6 -6
  142. package/dist/components/month-picker.js.map +1 -1
  143. package/dist/components/multi-entity-combobox.js +2 -2
  144. package/dist/components/multi-entity-combobox.js.map +1 -1
  145. package/dist/components/multi-select.js +2 -6
  146. package/dist/components/multi-select.js.map +1 -1
  147. package/dist/components/number-input.js +2 -2
  148. package/dist/components/number-input.js.map +1 -1
  149. package/dist/components/page-header-layout.d.ts +18 -0
  150. package/dist/components/page-header-layout.js +23 -0
  151. package/dist/components/page-header-layout.js.map +1 -0
  152. package/dist/components/page-header-skeleton.d.ts +41 -0
  153. package/dist/components/page-header-skeleton.js +52 -0
  154. package/dist/components/page-header-skeleton.js.map +1 -0
  155. package/dist/components/page-header.js +4 -17
  156. package/dist/components/page-header.js.map +1 -1
  157. package/dist/components/popover.js +7 -2
  158. package/dist/components/popover.js.map +1 -1
  159. package/dist/components/release-notes-labels.d.ts +16 -0
  160. package/dist/components/release-notes-labels.js +18 -0
  161. package/dist/components/release-notes-labels.js.map +1 -0
  162. package/dist/components/release-notes.d.ts +65 -0
  163. package/dist/components/release-notes.js +65 -0
  164. package/dist/components/release-notes.js.map +1 -0
  165. package/dist/components/select.js +2 -2
  166. package/dist/components/select.js.map +1 -1
  167. package/dist/components/series-chart-labels.d.ts +9 -0
  168. package/dist/components/series-chart-labels.js +3 -1
  169. package/dist/components/series-chart-labels.js.map +1 -1
  170. package/dist/components/series-chart.d.ts +5 -0
  171. package/dist/components/series-chart.js +89 -36
  172. package/dist/components/series-chart.js.map +1 -1
  173. package/dist/components/stat-tile.d.ts +21 -2
  174. package/dist/components/stat-tile.js +4 -2
  175. package/dist/components/stat-tile.js.map +1 -1
  176. package/dist/components/toggle-group.js +3 -2
  177. package/dist/components/toggle-group.js.map +1 -1
  178. package/dist/components/treemap-layout.js.map +1 -1
  179. package/dist/components/treemap.d.ts +17 -15
  180. package/dist/components/treemap.js +167 -85
  181. package/dist/components/treemap.js.map +1 -1
  182. package/dist/currencies.d.ts +16 -2
  183. package/dist/currencies.js +2 -1
  184. package/dist/currencies.js.map +1 -1
  185. package/dist/eslint/index.js +1 -1
  186. package/dist/eslint/role-utilities.d.ts +2 -2
  187. package/dist/eslint/role-utilities.js.map +1 -1
  188. package/dist/feedback/feedback-context-labels.js +2 -1
  189. package/dist/feedback/feedback-context-labels.js.map +1 -1
  190. package/dist/feedback/feedback-context.d.ts +19 -3
  191. package/dist/feedback/feedback-context.js +12 -1
  192. package/dist/feedback/feedback-context.js.map +1 -1
  193. package/dist/feedback/feedback-diagnostics.d.ts +17 -0
  194. package/dist/feedback/feedback-diagnostics.js +27 -0
  195. package/dist/feedback/feedback-diagnostics.js.map +1 -0
  196. package/dist/feedback/feedback-dialog.d.ts +10 -0
  197. package/dist/feedback/feedback-dialog.js +6 -4
  198. package/dist/feedback/feedback-dialog.js.map +1 -1
  199. package/dist/feedback/feedback-inbox-page.d.ts +260 -0
  200. package/dist/feedback/feedback-inbox-page.js +207 -0
  201. package/dist/feedback/feedback-inbox-page.js.map +1 -0
  202. package/dist/feedback/feedback-record.d.ts +5 -0
  203. package/dist/feedback/feedback-record.js.map +1 -1
  204. package/dist/feedback/feedback-row-detail-labels.js +1 -0
  205. package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
  206. package/dist/feedback/feedback-row-detail.d.ts +3 -0
  207. package/dist/feedback/feedback-row-detail.js +3 -0
  208. package/dist/feedback/feedback-row-detail.js.map +1 -1
  209. package/dist/feedback/feedback-submit-dialog.d.ts +5 -1
  210. package/dist/feedback/feedback-submit-dialog.js +15 -3
  211. package/dist/feedback/feedback-submit-dialog.js.map +1 -1
  212. package/dist/feedback/feedback-submit.d.ts +30 -2
  213. package/dist/feedback/feedback-submit.js +5 -2
  214. package/dist/feedback/feedback-submit.js.map +1 -1
  215. package/dist/feedback/feedback-vocabulary.js +2 -2
  216. package/dist/feedback/feedback-vocabulary.js.map +1 -1
  217. package/dist/feedback.d.ts +1 -0
  218. package/dist/feedback.js +6 -0
  219. package/dist/feedback.js.map +1 -1
  220. package/dist/hooks/use-debounce.d.ts +32 -5
  221. package/dist/hooks/use-debounce.js +6 -5
  222. package/dist/hooks/use-debounce.js.map +1 -1
  223. package/dist/hooks/use-file-picker.d.ts +3 -1
  224. package/dist/hooks/use-file-picker.js.map +1 -1
  225. package/dist/hooks/use-focus-trap.d.ts +19 -3
  226. package/dist/hooks/use-focus-trap.js +15 -5
  227. package/dist/hooks/use-focus-trap.js.map +1 -1
  228. package/dist/hooks/use-has-camera.d.ts +36 -0
  229. package/dist/hooks/use-has-camera.js +53 -0
  230. package/dist/hooks/use-has-camera.js.map +1 -0
  231. package/dist/hooks/use-hotkey.js +2 -1
  232. package/dist/hooks/use-hotkey.js.map +1 -1
  233. package/dist/hooks/use-overlay-history.d.ts +6 -1
  234. package/dist/hooks/use-overlay-history.js +10 -2
  235. package/dist/hooks/use-overlay-history.js.map +1 -1
  236. package/dist/i18n/defaults.js +11 -1
  237. package/dist/i18n/defaults.js.map +1 -1
  238. package/dist/i18n/german.js +78 -4
  239. package/dist/i18n/german.js.map +1 -1
  240. package/dist/i18n/kit-labels.d.ts +20 -0
  241. package/dist/i18n/kit-labels.js.map +1 -1
  242. package/dist/i18n/locales/en.js +95 -5
  243. package/dist/i18n/locales/en.js.map +3 -3
  244. package/dist/i18n/locales/es.js +74 -4
  245. package/dist/i18n/locales/es.js.map +1 -1
  246. package/dist/i18n/locales/fr.js +75 -3
  247. package/dist/i18n/locales/fr.js.map +1 -1
  248. package/dist/i18n/locales/hu.js +77 -4
  249. package/dist/i18n/locales/hu.js.map +1 -1
  250. package/dist/i18n/locales/it.js +77 -4
  251. package/dist/i18n/locales/it.js.map +1 -1
  252. package/dist/i18n/locales/zh.js +74 -4
  253. package/dist/i18n/locales/zh.js.map +1 -1
  254. package/dist/i18n/review.js +22 -1
  255. package/dist/i18n/review.js.map +1 -1
  256. package/dist/index.d.ts +27 -8
  257. package/dist/index.js +41 -2
  258. package/dist/index.js.map +1 -1
  259. package/dist/landing/landing-labels.d.ts +8 -0
  260. package/dist/landing/landing-labels.js +2 -1
  261. package/dist/landing/landing-labels.js.map +1 -1
  262. package/dist/lib/changelog.d.ts +130 -0
  263. package/dist/lib/changelog.js +137 -0
  264. package/dist/lib/changelog.js.map +1 -0
  265. package/dist/lib/clipboard-minus.js +2 -2
  266. package/dist/lib/clipboard-minus.js.map +1 -1
  267. package/dist/lib/dates.d.ts +36 -4
  268. package/dist/lib/dates.js +49 -13
  269. package/dist/lib/dates.js.map +1 -1
  270. package/dist/lib/editable-content.d.ts +33 -0
  271. package/dist/lib/editable-content.js +15 -0
  272. package/dist/lib/editable-content.js.map +1 -0
  273. package/dist/lib/focus-left.d.ts +47 -1
  274. package/dist/lib/focus-left.js +11 -2
  275. package/dist/lib/focus-left.js.map +1 -1
  276. package/dist/lib/format.d.ts +37 -2
  277. package/dist/lib/format.js +25 -1
  278. package/dist/lib/format.js.map +1 -1
  279. package/dist/lib/intl-cache.d.ts +4 -2
  280. package/dist/lib/intl-cache.js.map +1 -1
  281. package/dist/lib/server-wake.d.ts +49 -9
  282. package/dist/lib/server-wake.js +25 -14
  283. package/dist/lib/server-wake.js.map +1 -1
  284. package/dist/lib/session-refresh.d.ts +53 -0
  285. package/dist/lib/session-refresh.js +27 -1
  286. package/dist/lib/session-refresh.js.map +1 -1
  287. package/dist/lib/translation-review.d.ts +26 -0
  288. package/dist/lib/translation-review.js +14 -0
  289. package/dist/lib/translation-review.js.map +1 -1
  290. package/dist/rhf/fields.js +3 -3
  291. package/dist/rhf/fields.js.map +1 -1
  292. package/dist/shell/app-shell.d.ts +25 -1
  293. package/dist/shell/app-shell.js +3 -1
  294. package/dist/shell/app-shell.js.map +1 -1
  295. package/dist/shell/dev-menu.d.ts +3 -0
  296. package/dist/shell/dev-menu.js +4 -2
  297. package/dist/shell/dev-menu.js.map +1 -1
  298. package/dist/shell/held-changes-banner-labels.d.ts +23 -0
  299. package/dist/shell/held-changes-banner-labels.js +14 -0
  300. package/dist/shell/held-changes-banner-labels.js.map +1 -0
  301. package/dist/shell/held-changes-banner.d.ts +75 -0
  302. package/dist/shell/held-changes-banner.js +106 -0
  303. package/dist/shell/held-changes-banner.js.map +1 -0
  304. package/dist/shell/sync-status-indicator.d.ts +27 -12
  305. package/dist/shell/sync-status-indicator.js +13 -4
  306. package/dist/shell/sync-status-indicator.js.map +1 -1
  307. package/dist/shell/top-bar-parts-labels.d.ts +5 -0
  308. package/dist/shell/top-bar-parts-labels.js +3 -1
  309. package/dist/shell/top-bar-parts-labels.js.map +1 -1
  310. package/dist/shell.d.ts +2 -0
  311. package/dist/shell.js +3 -0
  312. package/dist/shell.js.map +1 -1
  313. package/dist/sync/engine.d.ts +11 -2
  314. package/dist/sync/engine.js +13 -5
  315. package/dist/sync/engine.js.map +1 -1
  316. package/dist/sync/hooks.d.ts +3 -2
  317. package/dist/sync/hooks.js +4 -2
  318. package/dist/sync/hooks.js.map +1 -1
  319. package/dist/testing/css-variables.generated.d.ts +2 -2
  320. package/dist/testing/css-variables.generated.js +1 -1
  321. package/dist/testing/css-variables.generated.js.map +1 -1
  322. package/dist/theme/chart-palette.d.ts +18 -0
  323. package/dist/theme/chart-palette.js +17 -1
  324. package/dist/theme/chart-palette.js.map +1 -1
  325. package/dist/theme/contrast-tokens.d.ts +3 -3
  326. package/dist/theme/contrast-tokens.js.map +1 -1
  327. package/dist/theme/contrast.d.ts +4 -4
  328. package/dist/theme/contrast.js.map +1 -1
  329. package/dist/tour/tour-overlay.js +2 -1
  330. package/dist/tour/tour-overlay.js.map +1 -1
  331. package/package.json +5 -9
  332. package/tokens.css +19 -54
  333. package/src/account/account-parts.ts +0 -47
  334. package/src/account/account-settings-labels.ts +0 -240
  335. package/src/account/account-settings.tsx +0 -698
  336. package/src/account/data-export-setting-labels.ts +0 -20
  337. package/src/account/data-export-setting.tsx +0 -200
  338. package/src/account/delete-account-setting-labels.ts +0 -33
  339. package/src/account/delete-account-setting.tsx +0 -341
  340. package/src/account/email-change-setting-labels.ts +0 -31
  341. package/src/account/email-change-setting.tsx +0 -443
  342. package/src/account/passkeys-setting.tsx +0 -445
  343. package/src/account/sessions-setting-labels.ts +0 -24
  344. package/src/account/sessions-setting.tsx +0 -283
  345. package/src/admin/admin-action-confirm-labels.ts +0 -19
  346. package/src/admin/admin-action-confirm.tsx +0 -509
  347. package/src/admin/admin-action-log-labels.ts +0 -83
  348. package/src/admin/admin-action-log.tsx +0 -464
  349. package/src/admin/admin-area-labels.ts +0 -114
  350. package/src/admin/admin-forbidden.tsx +0 -75
  351. package/src/admin/admin-overview.tsx +0 -258
  352. package/src/admin/admin-parts.ts +0 -68
  353. package/src/admin/admin-sections.tsx +0 -208
  354. package/src/admin/admin-tool-links.tsx +0 -90
  355. package/src/admin/invitations-panel-labels.ts +0 -31
  356. package/src/admin/invitations-panel.tsx +0 -516
  357. package/src/admin/mail-test-card-labels.ts +0 -19
  358. package/src/admin/mail-test-card.tsx +0 -189
  359. package/src/admin/plan-change-labels.ts +0 -5
  360. package/src/admin/plan-change.tsx +0 -474
  361. package/src/admin/reviewer-scope-editor-labels.ts +0 -16
  362. package/src/admin/reviewer-scope-editor.tsx +0 -224
  363. package/src/admin/role-select-labels.ts +0 -16
  364. package/src/admin/role-select.tsx +0 -282
  365. package/src/admin/transfer-ownership-dialog-labels.ts +0 -20
  366. package/src/admin/transfer-ownership-dialog.tsx +0 -162
  367. package/src/admin/user-roster-labels.ts +0 -17
  368. package/src/admin/user-roster.tsx +0 -559
  369. package/src/auth/accept-invitation-labels.ts +0 -21
  370. package/src/auth/accept-invitation.tsx +0 -210
  371. package/src/auth/auth-errors-labels.ts +0 -23
  372. package/src/auth/auth-errors.ts +0 -370
  373. package/src/auth/complete-name-dialog-labels.ts +0 -16
  374. package/src/auth/complete-name-dialog.tsx +0 -253
  375. package/src/auth/email-tag.ts +0 -75
  376. package/src/auth/forgot-password-form-labels.ts +0 -20
  377. package/src/auth/forgot-password-form.tsx +0 -221
  378. package/src/auth/form-rules.ts +0 -40
  379. package/src/auth/new-password-fields.tsx +0 -85
  380. package/src/auth/not-found-page-labels.ts +0 -12
  381. package/src/auth/not-found-page.tsx +0 -95
  382. package/src/auth/register-form-labels.ts +0 -31
  383. package/src/auth/register-form.tsx +0 -456
  384. package/src/auth/reset-password-form-labels.ts +0 -27
  385. package/src/auth/reset-password-form.tsx +0 -341
  386. package/src/auth/sign-in-form-labels.ts +0 -44
  387. package/src/auth/sign-in-form.tsx +0 -858
  388. package/src/auth/status-parts.tsx +0 -132
  389. package/src/auth/verify-email-labels.ts +0 -26
  390. package/src/auth/verify-email.tsx +0 -445
  391. package/src/billing/billing-banner.tsx +0 -258
  392. package/src/billing/billing-labels.ts +0 -381
  393. package/src/billing/billing-lock.ts +0 -108
  394. package/src/billing/billing-overview.ts +0 -157
  395. package/src/billing/billing-standing.ts +0 -80
  396. package/src/billing/cancel-request.ts +0 -97
  397. package/src/billing/cancel-subscription.tsx +0 -98
  398. package/src/billing/checkout-processing.ts +0 -394
  399. package/src/billing/checkout-return.ts +0 -90
  400. package/src/billing/pay-page.ts +0 -330
  401. package/src/billing/plan-card.tsx +0 -562
  402. package/src/billing/plan-change-labels.ts +0 -66
  403. package/src/billing/plan-limit-notice.tsx +0 -148
  404. package/src/billing/plan-limit-toast.tsx +0 -90
  405. package/src/billing/plan-limit.ts +0 -52
  406. package/src/billing/plan-price.ts +0 -98
  407. package/src/billing/plan-switch.tsx +0 -216
  408. package/src/billing/provider-page.ts +0 -84
  409. package/src/billing/subscription-actions.tsx +0 -247
  410. package/src/billing/subscription-status-chip.tsx +0 -95
  411. package/src/chart.ts +0 -103
  412. package/src/components/account-chips-labels.ts +0 -16
  413. package/src/components/account-chips.tsx +0 -474
  414. package/src/components/alert-banner.tsx +0 -670
  415. package/src/components/amount-input.tsx +0 -815
  416. package/src/components/appearance-labels.ts +0 -63
  417. package/src/components/authed-image-labels.ts +0 -11
  418. package/src/components/authed-image.tsx +0 -209
  419. package/src/components/autocomplete.tsx +0 -645
  420. package/src/components/breadcrumbs-labels.ts +0 -10
  421. package/src/components/breadcrumbs.tsx +0 -205
  422. package/src/components/bulk-action-bar-labels.ts +0 -11
  423. package/src/components/bulk-action-bar.tsx +0 -443
  424. package/src/components/button-group.tsx +0 -223
  425. package/src/components/button-look.ts +0 -256
  426. package/src/components/button.tsx +0 -339
  427. package/src/components/calculator.tsx +0 -341
  428. package/src/components/calendar-heatmap-labels.ts +0 -15
  429. package/src/components/calendar-heatmap.tsx +0 -594
  430. package/src/components/card.tsx +0 -377
  431. package/src/components/chart-edge.ts +0 -106
  432. package/src/components/chart-figure.tsx +0 -35
  433. package/src/components/chart-height.ts +0 -30
  434. package/src/components/chart-zoom-core.tsx +0 -627
  435. package/src/components/chart-zoom.tsx +0 -22
  436. package/src/components/chart.tsx +0 -554
  437. package/src/components/checkbox-group.tsx +0 -247
  438. package/src/components/checkbox.tsx +0 -346
  439. package/src/components/chip-input.tsx +0 -370
  440. package/src/components/chip-labels.ts +0 -13
  441. package/src/components/chip.tsx +0 -843
  442. package/src/components/choice-card.tsx +0 -544
  443. package/src/components/column-mapper-labels.ts +0 -38
  444. package/src/components/column-mapper.tsx +0 -655
  445. package/src/components/combobox-core.tsx +0 -1321
  446. package/src/components/combobox-suggestions.tsx +0 -131
  447. package/src/components/combobox.tsx +0 -716
  448. package/src/components/confirm-context.ts +0 -7
  449. package/src/components/confirm-dialog-labels.ts +0 -13
  450. package/src/components/confirm-dialog.tsx +0 -341
  451. package/src/components/copy-button-labels.ts +0 -13
  452. package/src/components/copy-button.tsx +0 -192
  453. package/src/components/country-select-labels.ts +0 -11
  454. package/src/components/country-select.tsx +0 -539
  455. package/src/components/currency-option-list.tsx +0 -180
  456. package/src/components/currency-select.tsx +0 -341
  457. package/src/components/danger-confirm-labels.ts +0 -20
  458. package/src/components/danger-confirm.tsx +0 -934
  459. package/src/components/data-table-cells.tsx +0 -118
  460. package/src/components/data-table-column-settings.tsx +0 -198
  461. package/src/components/data-table-filter-popover.tsx +0 -281
  462. package/src/components/data-table-filters.ts +0 -287
  463. package/src/components/data-table-fit.ts +0 -301
  464. package/src/components/data-table-groups.tsx +0 -288
  465. package/src/components/data-table-head.tsx +0 -280
  466. package/src/components/data-table-labels.ts +0 -198
  467. package/src/components/data-table-leading-row.tsx +0 -58
  468. package/src/components/data-table-mobile.tsx +0 -1117
  469. package/src/components/data-table-pagination.tsx +0 -202
  470. package/src/components/data-table-resize.ts +0 -188
  471. package/src/components/data-table-row-actions.tsx +0 -302
  472. package/src/components/data-table-row.tsx +0 -496
  473. package/src/components/data-table-selection.ts +0 -71
  474. package/src/components/data-table-shared.tsx +0 -287
  475. package/src/components/data-table-sort.ts +0 -163
  476. package/src/components/data-table-virtual.ts +0 -576
  477. package/src/components/data-table.tsx +0 -2598
  478. package/src/components/date-field.tsx +0 -684
  479. package/src/components/date-format-setting.tsx +0 -64
  480. package/src/components/date-picker.tsx +0 -404
  481. package/src/components/date-range-picker.tsx +0 -518
  482. package/src/components/description-list-labels.ts +0 -9
  483. package/src/components/description-list.tsx +0 -386
  484. package/src/components/dialog-frame-labels.ts +0 -7
  485. package/src/components/dialog-frame.tsx +0 -276
  486. package/src/components/disclosure.tsx +0 -476
  487. package/src/components/dropdown.tsx +0 -461
  488. package/src/components/empty-state.tsx +0 -136
  489. package/src/components/entity-combobox.tsx +0 -422
  490. package/src/components/error-boundary-labels.ts +0 -20
  491. package/src/components/error-boundary.tsx +0 -675
  492. package/src/components/facing-pair.ts +0 -93
  493. package/src/components/field-base-labels.ts +0 -20
  494. package/src/components/field-base.tsx +0 -656
  495. package/src/components/field-parts.tsx +0 -825
  496. package/src/components/field-strip.tsx +0 -129
  497. package/src/components/field.tsx +0 -206
  498. package/src/components/file-button.tsx +0 -295
  499. package/src/components/file-dropzone.tsx +0 -559
  500. package/src/components/floating-panel-labels.ts +0 -10
  501. package/src/components/floating-panel.tsx +0 -828
  502. package/src/components/focus-ring.ts +0 -16
  503. package/src/components/form-actions-labels.ts +0 -11
  504. package/src/components/form-actions.tsx +0 -578
  505. package/src/components/full-bleed-dialog.tsx +0 -294
  506. package/src/components/grouped-picker.tsx +0 -184
  507. package/src/components/help-popover.tsx +0 -117
  508. package/src/components/hover-menu.tsx +0 -343
  509. package/src/components/iban-input-labels.ts +0 -14
  510. package/src/components/iban-input.tsx +0 -236
  511. package/src/components/icon-button.tsx +0 -959
  512. package/src/components/icon-picker-labels.ts +0 -13
  513. package/src/components/icon-picker.tsx +0 -232
  514. package/src/components/image-grid-labels.ts +0 -11
  515. package/src/components/image-grid.tsx +0 -202
  516. package/src/components/inline-confirm.tsx +0 -116
  517. package/src/components/inline-edit-field-labels.ts +0 -11
  518. package/src/components/inline-edit-field.tsx +0 -552
  519. package/src/components/inline-entity-combobox.tsx +0 -849
  520. package/src/components/input.tsx +0 -394
  521. package/src/components/jump-calendar.tsx +0 -265
  522. package/src/components/language-select.tsx +0 -93
  523. package/src/components/legal-labels.ts +0 -105
  524. package/src/components/legal-page.tsx +0 -711
  525. package/src/components/legal.tsx +0 -668
  526. package/src/components/lightbox-labels.ts +0 -18
  527. package/src/components/lightbox.tsx +0 -399
  528. package/src/components/line-items-labels.ts +0 -14
  529. package/src/components/line-items.tsx +0 -602
  530. package/src/components/list-labels.ts +0 -12
  531. package/src/components/list.tsx +0 -870
  532. package/src/components/loading-state.tsx +0 -78
  533. package/src/components/measured-grid-labels.ts +0 -25
  534. package/src/components/measured-grid.tsx +0 -837
  535. package/src/components/menu-item.tsx +0 -241
  536. package/src/components/mini-calendar-labels.ts +0 -17
  537. package/src/components/mini-calendar.tsx +0 -789
  538. package/src/components/modal.tsx +0 -505
  539. package/src/components/money-draft.ts +0 -83
  540. package/src/components/money-field.tsx +0 -76
  541. package/src/components/month-grid.tsx +0 -229
  542. package/src/components/month-picker-labels.ts +0 -22
  543. package/src/components/month-picker.tsx +0 -1087
  544. package/src/components/multi-entity-combobox.tsx +0 -369
  545. package/src/components/multi-select.tsx +0 -376
  546. package/src/components/nav-pills.tsx +0 -198
  547. package/src/components/number-field.tsx +0 -327
  548. package/src/components/number-input.tsx +0 -558
  549. package/src/components/numeric-field.ts +0 -169
  550. package/src/components/numpad-sheet.tsx +0 -285
  551. package/src/components/one-time-code-input.tsx +0 -270
  552. package/src/components/page-contents-labels.ts +0 -9
  553. package/src/components/page-contents.tsx +0 -282
  554. package/src/components/page-header-context.ts +0 -8
  555. package/src/components/page-header.tsx +0 -298
  556. package/src/components/password-strength-labels.ts +0 -22
  557. package/src/components/password-strength.tsx +0 -241
  558. package/src/components/phone-input-labels.ts +0 -10
  559. package/src/components/phone-input.tsx +0 -282
  560. package/src/components/picker-sheet.tsx +0 -330
  561. package/src/components/pie-chart-labels.ts +0 -40
  562. package/src/components/pie-chart.tsx +0 -480
  563. package/src/components/popover-labels.ts +0 -14
  564. package/src/components/popover.tsx +0 -268
  565. package/src/components/progress-bar-labels.ts +0 -10
  566. package/src/components/progress-bar.tsx +0 -479
  567. package/src/components/qr-code.tsx +0 -76
  568. package/src/components/reauth-dialog-labels.ts +0 -13
  569. package/src/components/reauth-dialog.tsx +0 -265
  570. package/src/components/row-actions-labels.ts +0 -12
  571. package/src/components/row-actions.tsx +0 -795
  572. package/src/components/scroll-area.tsx +0 -123
  573. package/src/components/search-field.tsx +0 -192
  574. package/src/components/select.tsx +0 -294
  575. package/src/components/separator.tsx +0 -33
  576. package/src/components/series-chart-axes.ts +0 -342
  577. package/src/components/series-chart-budget.ts +0 -150
  578. package/src/components/series-chart-labels.ts +0 -33
  579. package/src/components/series-chart-thin.ts +0 -180
  580. package/src/components/series-chart-ticks.ts +0 -194
  581. package/src/components/series-chart.tsx +0 -1906
  582. package/src/components/server-wake-labels.ts +0 -11
  583. package/src/components/server-wake.tsx +0 -129
  584. package/src/components/settings-fields.tsx +0 -237
  585. package/src/components/share-card-labels.ts +0 -30
  586. package/src/components/share-card.tsx +0 -675
  587. package/src/components/signature-pad-labels.ts +0 -24
  588. package/src/components/signature-pad.tsx +0 -747
  589. package/src/components/signed-amount-labels.ts +0 -10
  590. package/src/components/signed-amount.tsx +0 -336
  591. package/src/components/skeleton.tsx +0 -80
  592. package/src/components/slider.tsx +0 -338
  593. package/src/components/sparkline-labels.ts +0 -14
  594. package/src/components/sparkline.tsx +0 -322
  595. package/src/components/spinner.tsx +0 -81
  596. package/src/components/stat-tile-labels.ts +0 -15
  597. package/src/components/stat-tile.tsx +0 -725
  598. package/src/components/status-dot.tsx +0 -170
  599. package/src/components/swatch-picker-labels.ts +0 -10
  600. package/src/components/swatch-picker.tsx +0 -283
  601. package/src/components/swipeable-row.tsx +0 -386
  602. package/src/components/switch.tsx +0 -273
  603. package/src/components/table-skeleton.tsx +0 -136
  604. package/src/components/table.tsx +0 -647
  605. package/src/components/tabs-labels.ts +0 -13
  606. package/src/components/tabs.tsx +0 -693
  607. package/src/components/text-link.tsx +0 -359
  608. package/src/components/text.tsx +0 -150
  609. package/src/components/textarea.tsx +0 -330
  610. package/src/components/tile-radio.tsx +0 -332
  611. package/src/components/time-input.tsx +0 -121
  612. package/src/components/toast-labels.ts +0 -12
  613. package/src/components/toast.tsx +0 -519
  614. package/src/components/toggle-group.tsx +0 -752
  615. package/src/components/toggle-legend.tsx +0 -394
  616. package/src/components/tooltip.tsx +0 -1149
  617. package/src/components/translation-review-editor.tsx +0 -290
  618. package/src/components/translation-review-labels.ts +0 -181
  619. package/src/components/translation-review-parts.tsx +0 -272
  620. package/src/components/translation-review.tsx +0 -1240
  621. package/src/components/tree-view.tsx +0 -589
  622. package/src/components/treemap-labels.ts +0 -47
  623. package/src/components/treemap-layout.ts +0 -210
  624. package/src/components/treemap.tsx +0 -1184
  625. package/src/components/trigger-aria.ts +0 -44
  626. package/src/components/use-anchor-dir.ts +0 -20
  627. package/src/components/use-mobile-reveal.ts +0 -62
  628. package/src/components/use-table-state.ts +0 -753
  629. package/src/components/user-avatar.tsx +0 -116
  630. package/src/components/wizard-stepper.tsx +0 -42
  631. package/src/components/write-lock-labels.ts +0 -10
  632. package/src/components/write-lock.tsx +0 -253
  633. package/src/currencies.ts +0 -70
  634. package/src/data-table.ts +0 -42
  635. package/src/demo/demo-banner.tsx +0 -153
  636. package/src/demo/demo-countdown.ts +0 -136
  637. package/src/demo/demo-ended.tsx +0 -90
  638. package/src/demo/demo-labels.ts +0 -78
  639. package/src/demo/demo-session.ts +0 -32
  640. package/src/demo/demo-start.tsx +0 -156
  641. package/src/eslint/index.ts +0 -161
  642. package/src/eslint/role-utilities.generated.ts +0 -146
  643. package/src/eslint/role-utilities.ts +0 -233
  644. package/src/feedback/feedback-attachment-download.tsx +0 -77
  645. package/src/feedback/feedback-attachment-labels.ts +0 -37
  646. package/src/feedback/feedback-attachment.tsx +0 -1239
  647. package/src/feedback/feedback-capture.ts +0 -287
  648. package/src/feedback/feedback-comment-thread.tsx +0 -353
  649. package/src/feedback/feedback-context-labels.ts +0 -10
  650. package/src/feedback/feedback-context.tsx +0 -213
  651. package/src/feedback/feedback-crash.ts +0 -547
  652. package/src/feedback/feedback-dialog-labels.ts +0 -29
  653. package/src/feedback/feedback-dialog.tsx +0 -440
  654. package/src/feedback/feedback-entry-file.tsx +0 -150
  655. package/src/feedback/feedback-inbox.tsx +0 -552
  656. package/src/feedback/feedback-labels.ts +0 -144
  657. package/src/feedback/feedback-menu-labels.ts +0 -12
  658. package/src/feedback/feedback-menu.tsx +0 -177
  659. package/src/feedback/feedback-mutation.ts +0 -20
  660. package/src/feedback/feedback-page-link.ts +0 -90
  661. package/src/feedback/feedback-public-labels.ts +0 -81
  662. package/src/feedback/feedback-public-list.tsx +0 -524
  663. package/src/feedback/feedback-public-switch.tsx +0 -102
  664. package/src/feedback/feedback-public-types.ts +0 -101
  665. package/src/feedback/feedback-record.ts +0 -423
  666. package/src/feedback/feedback-replies-context.ts +0 -9
  667. package/src/feedback/feedback-replies.tsx +0 -343
  668. package/src/feedback/feedback-rework-entry-chip.tsx +0 -18
  669. package/src/feedback/feedback-row-detail-labels.ts +0 -30
  670. package/src/feedback/feedback-row-detail.tsx +0 -858
  671. package/src/feedback/feedback-status-undo.ts +0 -109
  672. package/src/feedback/feedback-submit-dialog.tsx +0 -118
  673. package/src/feedback/feedback-submit.tsx +0 -237
  674. package/src/feedback/feedback-swipe.tsx +0 -192
  675. package/src/feedback/feedback-table-labels.ts +0 -27
  676. package/src/feedback/feedback-table.tsx +0 -912
  677. package/src/feedback/feedback-thread-labels.ts +0 -22
  678. package/src/feedback/feedback-thread-parts-labels.ts +0 -41
  679. package/src/feedback/feedback-thread.tsx +0 -905
  680. package/src/feedback/feedback-vocabulary.ts +0 -263
  681. package/src/feedback/feedback-vote-button.tsx +0 -124
  682. package/src/feedback.ts +0 -116
  683. package/src/hooks/use-anchored-panel.ts +0 -396
  684. package/src/hooks/use-anchored-rect.ts +0 -77
  685. package/src/hooks/use-announce.ts +0 -85
  686. package/src/hooks/use-authed-src.ts +0 -167
  687. package/src/hooks/use-body-scroll-lock.ts +0 -119
  688. package/src/hooks/use-breakpoint.ts +0 -133
  689. package/src/hooks/use-close-transition.ts +0 -107
  690. package/src/hooks/use-copy-to-clipboard.ts +0 -122
  691. package/src/hooks/use-debounce.ts +0 -114
  692. package/src/hooks/use-dismiss.ts +0 -371
  693. package/src/hooks/use-document-title.ts +0 -33
  694. package/src/hooks/use-file-drop.ts +0 -121
  695. package/src/hooks/use-file-picker-labels.ts +0 -28
  696. package/src/hooks/use-file-picker.tsx +0 -359
  697. package/src/hooks/use-focus-trap.ts +0 -199
  698. package/src/hooks/use-hotkey.ts +0 -189
  699. package/src/hooks/use-large-text.ts +0 -41
  700. package/src/hooks/use-long-press.ts +0 -140
  701. package/src/hooks/use-media-query.ts +0 -39
  702. package/src/hooks/use-mounted.ts +0 -39
  703. package/src/hooks/use-noindex.ts +0 -37
  704. package/src/hooks/use-overlay-history.ts +0 -714
  705. package/src/hooks/use-row-swipe.ts +0 -207
  706. package/src/hooks/use-search-param-state.ts +0 -434
  707. package/src/hooks/use-windowed-rows.ts +0 -225
  708. package/src/i18n/defaults.ts +0 -267
  709. package/src/i18n/german.ts +0 -2057
  710. package/src/i18n/kit-labels.tsx +0 -1003
  711. package/src/i18n/languages.ts +0 -349
  712. package/src/i18n/locales/de-CH.ts +0 -24
  713. package/src/i18n/locales/en.ts +0 -267
  714. package/src/i18n/locales/es.ts +0 -1997
  715. package/src/i18n/locales/fr.ts +0 -2035
  716. package/src/i18n/locales/hu.ts +0 -2002
  717. package/src/i18n/locales/it.ts +0 -2012
  718. package/src/i18n/locales/zh.ts +0 -1910
  719. package/src/i18n/plurals.ts +0 -86
  720. package/src/i18n/review.ts +0 -475
  721. package/src/i18n/shared-labels.ts +0 -138
  722. package/src/i18n/swiss.ts +0 -29
  723. package/src/index.ts +0 -1179
  724. package/src/landing/access.ts +0 -93
  725. package/src/landing/landing-actions.tsx +0 -97
  726. package/src/landing/landing-labels.ts +0 -59
  727. package/src/landing/landing-sections.tsx +0 -341
  728. package/src/landing/page-seo.ts +0 -261
  729. package/src/landing/public-header.tsx +0 -121
  730. package/src/landing/public-page.tsx +0 -106
  731. package/src/landing/routing.tsx +0 -225
  732. package/src/lib/broadcast.ts +0 -52
  733. package/src/lib/calc.ts +0 -323
  734. package/src/lib/chip-edge-snap.ts +0 -212
  735. package/src/lib/clipboard-minus.ts +0 -107
  736. package/src/lib/clipping.ts +0 -34
  737. package/src/lib/cn.ts +0 -28
  738. package/src/lib/column-mapping.ts +0 -234
  739. package/src/lib/countries.ts +0 -91
  740. package/src/lib/data-attributes.ts +0 -14
  741. package/src/lib/date-format.ts +0 -227
  742. package/src/lib/dates.ts +0 -266
  743. package/src/lib/day-format.ts +0 -96
  744. package/src/lib/decimal-marks.ts +0 -108
  745. package/src/lib/describe-failure.ts +0 -18
  746. package/src/lib/device-settings.ts +0 -147
  747. package/src/lib/direction.ts +0 -74
  748. package/src/lib/document-navigation.ts +0 -17
  749. package/src/lib/document-title.ts +0 -41
  750. package/src/lib/download.ts +0 -172
  751. package/src/lib/float-clearance.ts +0 -70
  752. package/src/lib/floating-stack.ts +0 -70
  753. package/src/lib/focus-left.ts +0 -156
  754. package/src/lib/format.ts +0 -522
  755. package/src/lib/http-status.ts +0 -38
  756. package/src/lib/iban.ts +0 -251
  757. package/src/lib/ime.ts +0 -24
  758. package/src/lib/intl-cache.ts +0 -25
  759. package/src/lib/iso-day.ts +0 -13
  760. package/src/lib/lazy-with-fallback.ts +0 -82
  761. package/src/lib/logger.ts +0 -69
  762. package/src/lib/minus.ts +0 -50
  763. package/src/lib/path-pattern.ts +0 -55
  764. package/src/lib/person-name.ts +0 -155
  765. package/src/lib/phone.ts +0 -208
  766. package/src/lib/plain-text.ts +0 -22
  767. package/src/lib/presence.ts +0 -18
  768. package/src/lib/qr-encode.ts +0 -444
  769. package/src/lib/quiet-focus.ts +0 -32
  770. package/src/lib/reduced-motion.ts +0 -22
  771. package/src/lib/refs.ts +0 -22
  772. package/src/lib/round.ts +0 -43
  773. package/src/lib/safe-storage.ts +0 -119
  774. package/src/lib/server-wake.ts +0 -315
  775. package/src/lib/session-refresh.ts +0 -472
  776. package/src/lib/share-format.ts +0 -18
  777. package/src/lib/status-tone.ts +0 -43
  778. package/src/lib/strip-fade.ts +0 -287
  779. package/src/lib/table-text.ts +0 -554
  780. package/src/lib/text-case.ts +0 -19
  781. package/src/lib/text-match.ts +0 -169
  782. package/src/lib/thenable.ts +0 -19
  783. package/src/lib/translation-review.ts +0 -717
  784. package/src/notifications/device-zone-report.ts +0 -67
  785. package/src/notifications/feedback-notification-types-labels.ts +0 -33
  786. package/src/notifications/feedback-notification-types.ts +0 -71
  787. package/src/notifications/notification-bell.tsx +0 -119
  788. package/src/notifications/notification-inbox.tsx +0 -309
  789. package/src/notifications/notification-parts.tsx +0 -81
  790. package/src/notifications/notification-types-setting.tsx +0 -406
  791. package/src/notifications/notification-types.ts +0 -89
  792. package/src/notifications/notifications-labels.ts +0 -287
  793. package/src/notifications/push-device-setting.tsx +0 -230
  794. package/src/notifications/push-devices-setting.tsx +0 -374
  795. package/src/notifications/push-worker.ts +0 -168
  796. package/src/notifications/quiet-hours-setting.tsx +0 -238
  797. package/src/notifications/unsubscribe-panel.tsx +0 -200
  798. package/src/notifications/use-push-device.ts +0 -448
  799. package/src/offline/local-first.ts +0 -100
  800. package/src/offline/pwa-update-prompt-labels.ts +0 -9
  801. package/src/offline/pwa-update-prompt.tsx +0 -78
  802. package/src/offline/query-persistence.ts +0 -391
  803. package/src/offline/service-worker-updates.ts +0 -81
  804. package/src/offline/shared-computer-labels.ts +0 -8
  805. package/src/offline/shared-computer.tsx +0 -87
  806. package/src/offline/sign-out-pending-dialog-labels.ts +0 -21
  807. package/src/offline/sign-out-pending-dialog.tsx +0 -135
  808. package/src/offline.ts +0 -37
  809. package/src/push-worker.ts +0 -25
  810. package/src/rhf/fields.tsx +0 -2187
  811. package/src/rhf/form-core.tsx +0 -324
  812. package/src/rhf/form.tsx +0 -12
  813. package/src/rhf/line-items.tsx +0 -135
  814. package/src/rhf/use-rhf-wizard-step.ts +0 -104
  815. package/src/rhf.ts +0 -21
  816. package/src/search/command-palette-labels.ts +0 -16
  817. package/src/search/command-palette.tsx +0 -624
  818. package/src/search/global-search-labels.ts +0 -13
  819. package/src/search/global-search.tsx +0 -660
  820. package/src/search/search-index.ts +0 -283
  821. package/src/search/use-command-key.ts +0 -23
  822. package/src/search.ts +0 -14
  823. package/src/settings/device-reset-setting-labels.ts +0 -20
  824. package/src/settings/device-reset-setting.tsx +0 -116
  825. package/src/settings/settings-card-classes.ts +0 -11
  826. package/src/settings/settings-catalogue.ts +0 -150
  827. package/src/settings/settings-context.ts +0 -33
  828. package/src/settings/settings-heading.tsx +0 -67
  829. package/src/settings/settings-labels.ts +0 -87
  830. package/src/settings/settings-layout.tsx +0 -471
  831. package/src/settings/settings-section.tsx +0 -108
  832. package/src/settings/use-account-appearance.ts +0 -148
  833. package/src/settings/use-account-language.ts +0 -146
  834. package/src/settings/use-settings-focus.ts +0 -128
  835. package/src/settings/use-settings-route.ts +0 -258
  836. package/src/shell/account-menu-language.tsx +0 -130
  837. package/src/shell/account-menu-text-size.tsx +0 -80
  838. package/src/shell/admin-menu.tsx +0 -183
  839. package/src/shell/app-shell-labels.ts +0 -11
  840. package/src/shell/app-shell.tsx +0 -1560
  841. package/src/shell/auth-layout.tsx +0 -186
  842. package/src/shell/company-switcher-labels.ts +0 -13
  843. package/src/shell/company-switcher.tsx +0 -136
  844. package/src/shell/dev-menu.tsx +0 -257
  845. package/src/shell/fill-height.tsx +0 -45
  846. package/src/shell/language-code.ts +0 -5
  847. package/src/shell/option-switcher-menu.tsx +0 -84
  848. package/src/shell/sync-status-indicator.tsx +0 -303
  849. package/src/shell/top-bar-actions.tsx +0 -87
  850. package/src/shell/top-bar-brand.tsx +0 -78
  851. package/src/shell/top-bar-parts-labels.ts +0 -148
  852. package/src/shell/top-bar.tsx +0 -40
  853. package/src/shell/topbar-action-menu.tsx +0 -567
  854. package/src/shell/topbar-controls.tsx +0 -269
  855. package/src/shell/tour-menu.tsx +0 -187
  856. package/src/shell.ts +0 -108
  857. package/src/sync/db.ts +0 -129
  858. package/src/sync/engine.ts +0 -1011
  859. package/src/sync/hooks.ts +0 -98
  860. package/src/sync/testing.ts +0 -73
  861. package/src/sync/wire.ts +0 -61
  862. package/src/sync.ts +0 -37
  863. package/src/table-text.ts +0 -8
  864. package/src/testing/css-variables.generated.ts +0 -112
  865. package/src/testing/css-variables.ts +0 -189
  866. package/src/testing/index.ts +0 -29
  867. package/src/testing/react.ts +0 -58
  868. package/src/testing/tour.ts +0 -41
  869. package/src/testing/viewport.ts +0 -357
  870. package/src/theme/chart-palette.ts +0 -232
  871. package/src/theme/color.ts +0 -272
  872. package/src/theme/contrast-tokens.ts +0 -268
  873. package/src/theme/contrast.ts +0 -250
  874. package/src/theme/palette-derive.ts +0 -825
  875. package/src/theme/palette-presets.ts +0 -544
  876. package/src/theme/palette-store.ts +0 -108
  877. package/src/theme/text-size.ts +0 -289
  878. package/src/theme/theme-store.ts +0 -157
  879. package/src/tour/tour-catalogue-labels.ts +0 -75
  880. package/src/tour/tour-catalogue.tsx +0 -335
  881. package/src/tour/tour-dock.ts +0 -134
  882. package/src/tour/tour-engine.ts +0 -37
  883. package/src/tour/tour-gates.ts +0 -218
  884. package/src/tour/tour-guide-button.tsx +0 -64
  885. package/src/tour/tour-guide-labels.ts +0 -15
  886. package/src/tour/tour-info.ts +0 -144
  887. package/src/tour/tour-labels.ts +0 -23
  888. package/src/tour/tour-overlay.tsx +0 -1170
  889. package/src/tour/tour.tsx +0 -750
  890. package/src/tour/use-tour-progress.ts +0 -671
  891. package/src/tour/use-tour-url-sync.ts +0 -266
  892. package/src/tour.ts +0 -21
  893. package/src/vite-env.d.ts +0 -23
  894. package/src/wizard/stepper-nav.tsx +0 -360
  895. package/src/wizard/types.ts +0 -275
  896. package/src/wizard/use-wizard.ts +0 -560
  897. package/src/wizard/validation.ts +0 -39
  898. package/src/wizard/wizard-context.tsx +0 -74
  899. package/src/wizard/wizard-labels.ts +0 -29
  900. package/src/wizard/wizard-step.tsx +0 -76
  901. package/src/wizard/wizard-summary.tsx +0 -84
  902. package/src/wizard.ts +0 -15
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/top-bar-parts-labels.ts"],"sourcesContent":["import { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\n\n/**\n * The `topBarParts` namespace (0.42, docs/top-bar-harmonization.md §3.1): the words of the\n * top bar's shared parts — `TourMenu`, `SyncStatusIndicator` and `DevMenu` (0.42's\n * `DevSwitchers`, whose words these still are, §2.7.1). A module\n * of words with no component code, like every `*-labels.ts` (0.34, KS U8). The language\n * sub-list (`AccountMenuLanguage`) names itself with `topBar.language`, and the role in the\n * dev flask's name is `topBar.role`.\n */\n\n/** `TourMenu`'s words (§2.4). */\nexport interface TourMenuLabels {\n /** The compass's name, and the menu's. */\n menu: string;\n /** The NEW dot on the compass, for a reader: it joins the trigger's name. */\n newDot: string;\n /** The heading over the tours that start on the open page. */\n thisPage: string;\n /** The heading over the untaken new tours. */\n newTours: string;\n /** The last row, the way to the app's tours page (only with `href`). */\n allTours: string;\n /** The chip on a new tour's row. */\n badgeNew: string;\n /** What the ✓ on a taken tour's row says to a reader. */\n completed: string;\n /** The chip on a tour reset by an update (0.43, docs/tours-harmonization.md §8). */\n badgeUpdated: string;\n /** The dot on the compass while only updated tours light it, for a reader. */\n updatedDot: string;\n /** The heading over the untaken new tours when updated ones are among them. */\n newAndUpdated: string;\n}\n\n/** `SyncStatusIndicator`'s words (§2.6). The \"waiting for a plan\" state is\n * `billing.waitingChanges`, the kit's words for it since billing §12.5. */\nexport interface SyncStatusLabels {\n /** Writes this device cannot send (keksdose: no key for the budget). Ranked first. */\n blocked: (count: number) => string;\n offline: string;\n /** The server refused the last round. */\n refused: string;\n syncing: string;\n pending: (count: number) => string;\n /** Settled, with the time of the last good round (already formatted). */\n syncedAt: (time: string) => string;\n /** Settled, before the first round. */\n idle: string;\n /** Joined to the state when a tap syncs: what the tap does. */\n syncHint: string;\n /** Joined to the state while offline, when a tap can do nothing. */\n offlineHint: string;\n /** The state and what a tap does, as one tooltip: \"Offline — nothing to sync…\". */\n withHint: (state: string, hint: string) => string;\n /** The state with the app's `scope` words before it: \"Handovers on this device: …\". */\n scoped: (scope: string, state: string) => string;\n /** The short words beside the cloud (`showLabel`): offline. */\n offlineShort: string;\n /** The short words while syncing. */\n syncingShort: string;\n /** The short words while changes wait (pending, held for a plan, or blocked). */\n waitingShort: (count: number) => string;\n /** The short words after a refused round. */\n refusedShort: string;\n /** Offline in an online-only app (`storage=\"none\"`): nothing is kept on the device. */\n offlineUnsaved: string;\n /** A round under way in an online-only app — it saves, it doesn't sync. Also its short\n * words beside the cloud. */\n saving: string;\n}\n\n/**\n * `DevMenu`'s words (§2.7, §2.7.1, §6.2, §6.3). Still named for 0.42's `DevSwitchers`: the\n * namespace is the same `topBarParts.dev`, and an app's label tree keeps compiling.\n */\nexport interface DevSwitchersLabels {\n /** 0.51: the flask's name, and the menu's — the role and the plan follow it. */\n menu: string;\n /** 0.51: what the dot on the flask means while `active`, joined to its name — an\n * override the developer could forget is on (a \"today\", a slow server). */\n overridesOn: string;\n /** The heading of the role group. */\n roleHeading: string;\n /** The plan in the flask's name, given the active plan. */\n plan: (value: string) => string;\n /** The heading of the plan group. */\n planHeading: string;\n /** The row back to the role the session started with. */\n backTo: (role: string) => string;\n /** The plan group's note while billing is off locally. */\n billingOff: string;\n}\n\nexport interface TopBarPartsLabels {\n tours: TourMenuLabels;\n sync: SyncStatusLabels;\n dev: DevSwitchersLabels;\n}\n\nexport const DEFAULT_TOP_BAR_PARTS_LABELS: TopBarPartsLabels = {\n tours: {\n menu: \"Guided tours\",\n newDot: \"New tours\",\n thisPage: \"On this page\",\n newTours: \"New tours\",\n allTours: \"All tours…\",\n badgeNew: \"New\",\n completed: \"Completed\",\n badgeUpdated: \"Updated\",\n updatedDot: \"Updated tours\",\n newAndUpdated: \"New and updated\",\n },\n sync: {\n blocked: (count) =>\n count === 1 ? \"1 change cannot be sent from this device\" : `${count} changes cannot be sent from this device`,\n offline: \"Offline — changes are saved on this device and will sync when you reconnect\",\n refused: \"The server refused the last sync\",\n syncing: \"Syncing…\",\n pending: (count) => (count === 1 ? \"1 change waiting to sync\" : `${count} changes waiting to sync`),\n syncedAt: (time) => `Synced at ${time}`,\n idle: \"Up to date\",\n syncHint: \"Tap to sync now\",\n offlineHint: \"nothing to sync while offline\",\n withHint: (state, hint) => `${state} — ${hint}`,\n scoped: (scope, state) => `${scope}: ${state}`,\n offlineShort: \"Offline\",\n syncingShort: \"Syncing…\",\n waitingShort: (count) => `${count} waiting`,\n refusedShort: \"Not synced\",\n offlineUnsaved: \"Offline — changes can't be saved until you reconnect\",\n saving: \"Saving…\",\n },\n dev: {\n menu: \"Dev tools\",\n overridesOn: \"Overrides on\",\n roleHeading: \"Dev · role\",\n plan: (value) => `Plan: ${value}`,\n planHeading: \"Dev · plan\",\n backTo: (role) => `Back to ${role}`,\n billingOff: \"Billing is off locally: plans change nothing\",\n },\n};\n\n/** The `topBarParts` namespace, resolved — English, then the provider, then `labels`. */\nexport function useTopBarPartsLabels(labels?: LabelOverride<TopBarPartsLabels>): TopBarPartsLabels {\n return useKitLabels(\"topBarParts\", DEFAULT_TOP_BAR_PARTS_LABELS, labels);\n}\n"],"mappings":";AAAA,SAAS,oBAAwC;AAoG1C,MAAM,+BAAkD;AAAA,EAC7D,OAAO;AAAA,IACL,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV,WAAW;AAAA,IACX,cAAc;AAAA,IACd,YAAY;AAAA,IACZ,eAAe;AAAA,EACjB;AAAA,EACA,MAAM;AAAA,IACJ,SAAS,CAAC,UACR,UAAU,IAAI,6CAA6C,GAAG,KAAK;AAAA,IACrE,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS,CAAC,UAAW,UAAU,IAAI,6BAA6B,GAAG,KAAK;AAAA,IACxE,UAAU,CAAC,SAAS,aAAa,IAAI;AAAA,IACrC,MAAM;AAAA,IACN,UAAU;AAAA,IACV,aAAa;AAAA,IACb,UAAU,CAAC,OAAO,SAAS,GAAG,KAAK,WAAM,IAAI;AAAA,IAC7C,QAAQ,CAAC,OAAO,UAAU,GAAG,KAAK,KAAK,KAAK;AAAA,IAC5C,cAAc;AAAA,IACd,cAAc;AAAA,IACd,cAAc,CAAC,UAAU,GAAG,KAAK;AAAA,IACjC,cAAc;AAAA,IACd,gBAAgB;AAAA,IAChB,QAAQ;AAAA,EACV;AAAA,EACA,KAAK;AAAA,IACH,MAAM;AAAA,IACN,aAAa;AAAA,IACb,aAAa;AAAA,IACb,MAAM,CAAC,UAAU,SAAS,KAAK;AAAA,IAC/B,aAAa;AAAA,IACb,QAAQ,CAAC,SAAS,WAAW,IAAI;AAAA,IACjC,YAAY;AAAA,EACd;AACF;AAGO,SAAS,qBAAqB,QAA8D;AACjG,SAAO,aAAa,eAAe,8BAA8B,MAAM;AACzE;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/top-bar-parts-labels.ts"],"sourcesContent":["import { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\n\n/**\n * The `topBarParts` namespace (0.42, docs/top-bar-harmonization.md §3.1): the words of the\n * top bar's shared parts — `TourMenu`, `SyncStatusIndicator` and `DevMenu` (0.42's\n * `DevSwitchers`, whose words these still are, §2.7.1). A module\n * of words with no component code, like every `*-labels.ts` (0.34, KS U8). The language\n * sub-list (`AccountMenuLanguage`) names itself with `topBar.language`, and the role in the\n * dev flask's name is `topBar.role`.\n */\n\n/** `TourMenu`'s words (§2.4). */\nexport interface TourMenuLabels {\n /** The compass's name, and the menu's. */\n menu: string;\n /** The NEW dot on the compass, for a reader: it joins the trigger's name. */\n newDot: string;\n /** The heading over the tours that start on the open page. */\n thisPage: string;\n /** The heading over the untaken new tours. */\n newTours: string;\n /** The last row, the way to the app's tours page (only with `href`). */\n allTours: string;\n /** The chip on a new tour's row. */\n badgeNew: string;\n /** What the ✓ on a taken tour's row says to a reader. */\n completed: string;\n /** The chip on a tour reset by an update (0.43, docs/tours-harmonization.md §8). */\n badgeUpdated: string;\n /** The dot on the compass while only updated tours light it, for a reader. */\n updatedDot: string;\n /** The heading over the untaken new tours when updated ones are among them. */\n newAndUpdated: string;\n}\n\n/** `SyncStatusIndicator`'s words (§2.6). The \"waiting for a plan\" state is\n * `billing.waitingChanges`, the kit's words for it since billing §12.5. */\nexport interface SyncStatusLabels {\n /** Writes this device cannot send (keksdose: no key for the budget). Ranked first. */\n blocked: (count: number) => string;\n offline: string;\n /** The server refused the last round. */\n refused: string;\n syncing: string;\n pending: (count: number) => string;\n /** Settled, with the time of the last good round (already formatted). */\n syncedAt: (time: string) => string;\n /** Settled, before the first round. */\n idle: string;\n /** Joined to the state when a tap syncs: what the tap does. */\n syncHint: string;\n /** Joined to the state while offline, when a tap can do nothing. */\n offlineHint: string;\n /** The state and what a tap does, as one tooltip: \"Offline — nothing to sync…\". */\n withHint: (state: string, hint: string) => string;\n /** The state with the app's `scope` words before it: \"Handovers on this device: …\". */\n scoped: (scope: string, state: string) => string;\n /** The short words beside the cloud (`showLabel`): offline. */\n offlineShort: string;\n /** The short words while syncing. */\n syncingShort: string;\n /** The short words while changes wait (pending, held for a plan, or blocked). */\n waitingShort: (count: number) => string;\n /** The short words after a refused round. */\n refusedShort: string;\n /** Offline in an online-only app (`storage=\"none\"`): nothing is kept on the device. */\n offlineUnsaved: string;\n /** A round under way in an online-only app — it saves, it doesn't sync. Also its short\n * words beside the cloud. */\n saving: string;\n /** 0.55 (held back from 0.53): a round waits for another tab's on the same device copy\n * (the sync engine's Web Lock lets one tab sync at a time). */\n waitingForTab: string;\n /** Its short words beside the cloud. */\n waitingForTabShort: string;\n}\n\n/**\n * `DevMenu`'s words (§2.7, §2.7.1, §6.2, §6.3). Still named for 0.42's `DevSwitchers`: the\n * namespace is the same `topBarParts.dev`, and an app's label tree keeps compiling.\n */\nexport interface DevSwitchersLabels {\n /** 0.51: the flask's name, and the menu's — the role and the plan follow it. */\n menu: string;\n /** 0.51: what the dot on the flask means while `active`, joined to its name — an\n * override the developer could forget is on (a \"today\", a slow server). */\n overridesOn: string;\n /** The heading of the role group. */\n roleHeading: string;\n /** The plan in the flask's name, given the active plan. */\n plan: (value: string) => string;\n /** The heading of the plan group. */\n planHeading: string;\n /** The row back to the role the session started with. */\n backTo: (role: string) => string;\n /** The plan group's note while billing is off locally. */\n billingOff: string;\n}\n\nexport interface TopBarPartsLabels {\n tours: TourMenuLabels;\n sync: SyncStatusLabels;\n dev: DevSwitchersLabels;\n}\n\nexport const DEFAULT_TOP_BAR_PARTS_LABELS: TopBarPartsLabels = {\n tours: {\n menu: \"Guided tours\",\n newDot: \"New tours\",\n thisPage: \"On this page\",\n newTours: \"New tours\",\n allTours: \"All tours…\",\n badgeNew: \"New\",\n completed: \"Completed\",\n badgeUpdated: \"Updated\",\n updatedDot: \"Updated tours\",\n newAndUpdated: \"New and updated\",\n },\n sync: {\n blocked: (count) =>\n count === 1 ? \"1 change cannot be sent from this device\" : `${count} changes cannot be sent from this device`,\n offline: \"Offline — changes are saved on this device and will sync when you reconnect\",\n refused: \"The server refused the last sync\",\n syncing: \"Syncing…\",\n pending: (count) => (count === 1 ? \"1 change waiting to sync\" : `${count} changes waiting to sync`),\n syncedAt: (time) => `Synced at ${time}`,\n idle: \"Up to date\",\n syncHint: \"Tap to sync now\",\n offlineHint: \"nothing to sync while offline\",\n withHint: (state, hint) => `${state} — ${hint}`,\n scoped: (scope, state) => `${scope}: ${state}`,\n offlineShort: \"Offline\",\n syncingShort: \"Syncing…\",\n waitingShort: (count) => `${count} waiting`,\n refusedShort: \"Not synced\",\n offlineUnsaved: \"Offline — changes can't be saved until you reconnect\",\n saving: \"Saving…\",\n waitingForTab: \"Waiting for another tab to finish syncing\",\n waitingForTabShort: \"Waiting for tab\",\n },\n dev: {\n menu: \"Dev tools\",\n overridesOn: \"Overrides on\",\n roleHeading: \"Dev · role\",\n plan: (value) => `Plan: ${value}`,\n planHeading: \"Dev · plan\",\n backTo: (role) => `Back to ${role}`,\n billingOff: \"Billing is off locally: plans change nothing\",\n },\n};\n\n/** The `topBarParts` namespace, resolved — English, then the provider, then `labels`. */\nexport function useTopBarPartsLabels(labels?: LabelOverride<TopBarPartsLabels>): TopBarPartsLabels {\n return useKitLabels(\"topBarParts\", DEFAULT_TOP_BAR_PARTS_LABELS, labels);\n}\n"],"mappings":";AAAA,SAAS,oBAAwC;AAyG1C,MAAM,+BAAkD;AAAA,EAC7D,OAAO;AAAA,IACL,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV,WAAW;AAAA,IACX,cAAc;AAAA,IACd,YAAY;AAAA,IACZ,eAAe;AAAA,EACjB;AAAA,EACA,MAAM;AAAA,IACJ,SAAS,CAAC,UACR,UAAU,IAAI,6CAA6C,GAAG,KAAK;AAAA,IACrE,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS,CAAC,UAAW,UAAU,IAAI,6BAA6B,GAAG,KAAK;AAAA,IACxE,UAAU,CAAC,SAAS,aAAa,IAAI;AAAA,IACrC,MAAM;AAAA,IACN,UAAU;AAAA,IACV,aAAa;AAAA,IACb,UAAU,CAAC,OAAO,SAAS,GAAG,KAAK,WAAM,IAAI;AAAA,IAC7C,QAAQ,CAAC,OAAO,UAAU,GAAG,KAAK,KAAK,KAAK;AAAA,IAC5C,cAAc;AAAA,IACd,cAAc;AAAA,IACd,cAAc,CAAC,UAAU,GAAG,KAAK;AAAA,IACjC,cAAc;AAAA,IACd,gBAAgB;AAAA,IAChB,QAAQ;AAAA,IACR,eAAe;AAAA,IACf,oBAAoB;AAAA,EACtB;AAAA,EACA,KAAK;AAAA,IACH,MAAM;AAAA,IACN,aAAa;AAAA,IACb,aAAa;AAAA,IACb,MAAM,CAAC,UAAU,SAAS,KAAK;AAAA,IAC/B,aAAa;AAAA,IACb,QAAQ,CAAC,SAAS,WAAW,IAAI;AAAA,IACjC,YAAY;AAAA,EACd;AACF;AAGO,SAAS,qBAAqB,QAA8D;AACjG,SAAO,aAAa,eAAe,8BAA8B,MAAM;AACzE;","names":[]}
package/dist/shell.d.ts CHANGED
@@ -40,6 +40,8 @@ export { TourMenu } from "./shell/tour-menu.js";
40
40
  export type { TourMenuProps, TourMenuTour } from "./shell/tour-menu.js";
41
41
  export { SyncStatusIndicator } from "./shell/sync-status-indicator.js";
42
42
  export type { SyncStatus, SyncState, SyncStatusScope, SyncStatusIndicatorProps } from "./shell/sync-status-indicator.js";
43
+ export { HeldChangesBanner, DEFAULT_HELD_CHANGES_BANNER_LABELS } from "./shell/held-changes-banner.js";
44
+ export type { HeldChangesBannerProps, HeldChangesBannerLabels } from "./shell/held-changes-banner.js";
43
45
  export { DevMenu } from "./shell/dev-menu.js";
44
46
  export type { DevMenuProps, DevMenuSection } from "./shell/dev-menu.js";
45
47
  export { DEFAULT_TOP_BAR_PARTS_LABELS, useTopBarPartsLabels } from "./shell/top-bar-parts-labels.js";
package/dist/shell.js CHANGED
@@ -42,6 +42,7 @@ import { AccountMenuLanguage } from "./shell/account-menu-language.js";
42
42
  import { AccountMenuTextSize } from "./shell/account-menu-text-size.js";
43
43
  import { TourMenu } from "./shell/tour-menu.js";
44
44
  import { SyncStatusIndicator } from "./shell/sync-status-indicator.js";
45
+ import { HeldChangesBanner, DEFAULT_HELD_CHANGES_BANNER_LABELS } from "./shell/held-changes-banner.js";
45
46
  import { DevMenu } from "./shell/dev-menu.js";
46
47
  import { DEFAULT_TOP_BAR_PARTS_LABELS, useTopBarPartsLabels } from "./shell/top-bar-parts-labels.js";
47
48
  export {
@@ -51,12 +52,14 @@ export {
51
52
  AdminWaitingBadge,
52
53
  AppShell,
53
54
  DEFAULT_APP_SHELL_MORE_LABELS,
55
+ DEFAULT_HELD_CHANGES_BANNER_LABELS,
54
56
  DEFAULT_LAST_VISITED_EXCLUDES,
55
57
  DEFAULT_MOBILE_MAX_ITEMS,
56
58
  DEFAULT_TOP_BAR_PARTS_LABELS,
57
59
  DevMenu,
58
60
  FILL_HEIGHT_CLASS,
59
61
  FillHeight,
62
+ HeldChangesBanner,
60
63
  NEXT_PARAM,
61
64
  PublicHeader,
62
65
  RedirectIfAuthed,
package/dist/shell.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/shell.ts"],"sourcesContent":["// `@eifi1/ui-kit/shell` — the composable app chrome.\n//\n// A re-slicing of the main barrel, not a new API. This entry point requires\n// `react-router`: AppShell renders NavLink and TopBarActionMenu renders Link.\nexport * from \"./shell/topbar-controls\";\nexport * from \"./shell/top-bar\";\n// Named, not `*`: 0.53 took `pickMobileBar` off the surface (deprecated in 0.52, refactor\n// audit 2026-10-30, API-4) — AppShell's own — and removed `splitMobileBar`, which nothing\n// called any more.\nexport {\n DEFAULT_APP_SHELL_MORE_LABELS,\n DEFAULT_MOBILE_MAX_ITEMS,\n AppShell,\n type AppShellSubItem,\n type AppShellNavItem,\n type AppShellSidebarFooterItem,\n type AppShellMoreLabels,\n type AppShellProps,\n} from \"./shell/app-shell\";\nexport * from \"./shell/option-switcher-menu\";\nexport * from \"./shell/topbar-action-menu\";\nexport * from \"./shell/top-bar-brand\";\nexport * from \"./shell/auth-layout\";\n// 0.40.0: the list page's fill (docs/page-layout-harmonization.md §1.3).\nexport { FillHeight, FILL_HEIGHT_CLASS } from \"./shell/fill-height\";\nexport type { FillHeightProps } from \"./shell/fill-height\";\n// 0.42.0: the account menu's \"Administration\" row, as an entry and as a HoverMenu row\n// (docs/admin-harmonization.md §2.1, §6.6).\nexport { adminMenuEntry, AdminMenuRow } from \"./shell/admin-menu\";\nexport type { AdminMenuEntryOptions, AdminMenuRowProps } from \"./shell/admin-menu\";\n// 0.43.0 (kastlan): the entry with the provider's words.\nexport { useAdminMenuEntry } from \"./shell/admin-menu\";\nexport type { UseAdminMenuEntryOptions } from \"./shell/admin-menu\";\n// 0.45.0 (docs/admin-harmonization.md §9.3.4): the row's waiting pill on its own, for\n// IconButton's `badge` (kastlan's platform button); its name is `adminArea.waitingLabel`.\nexport { AdminWaitingBadge } from \"./shell/admin-menu\";\nexport type { AdminWaitingBadgeProps } from \"./shell/admin-menu\";\n// 0.31.0: the settings and admin pages' shell (docs/settings-harmonization.md §3, §5).\nexport { SettingsLayout } from \"./settings/settings-layout\";\nexport type { SettingsLayoutProps, SettingsLayoutWidth, SettingsDocumentTitleParts } from \"./settings/settings-layout\";\nexport { useSettingsRoute, settingsFromListState } from \"./settings/use-settings-route\";\nexport type {\n UseSettingsRouteOptions,\n SettingsRoute,\n SettingsLayoutMode,\n SettingsLocation,\n SettingsLocationRules,\n SettingsLocationResult,\n} from \"./settings/use-settings-route\";\nexport { useSettingsFocus, useFocusParam } from \"./settings/use-settings-focus\";\nexport { SettingsSection } from \"./settings/settings-section\";\nexport type { SettingsSectionProps } from \"./settings/settings-section\";\nexport { useSettingsLayout } from \"./settings/settings-context\";\nexport type { SettingsLayoutContextValue } from \"./settings/settings-context\";\nexport { SettingsHeadingLevel, useSettingsHeadingLevel } from \"./settings/settings-heading\";\nexport type { SettingsHeadingLevelProps, SettingsHeadingTag } from \"./settings/settings-heading\";\nexport {\n settingsSearchEntries,\n visibleSettingsGroups,\n visibleSettingsEntries,\n settingsHref,\n} from \"./settings/settings-catalogue\";\nexport type { SettingsGroup, SettingsEntry, SettingsSearchEntriesOptions } from \"./settings/settings-catalogue\";\n// 0.31.0: the public pages' header and the resume (docs/landing-demo-harmonization.md §3, §4.1).\nexport {\n RootEntry,\n RedirectIfAuthed,\n useLastVisitedPage,\n readLastVisitedPage,\n clearLastVisitedPage,\n safeNextPath,\n NEXT_PARAM,\n DEFAULT_LAST_VISITED_EXCLUDES,\n} from \"./landing/routing\";\nexport type {\n RootEntryProps,\n RedirectIfAuthedProps,\n RoutingSession,\n LastVisitedPageOptions,\n PathPattern,\n} from \"./landing/routing\";\nexport { PublicHeader } from \"./landing/public-header\";\nexport type { PublicHeaderProps, PublicHeaderBrand } from \"./landing/public-header\";\n// 0.42.0: the top bar's shared parts (docs/top-bar-harmonization.md §3.1) — the slots in\n// §2.1's order, the account menu's language sub-list, the tours menu, the sync status, the\n// local-dev menu, and their words.\nexport { TopBarActions } from \"./shell/top-bar-actions\";\nexport type { TopBarActionsProps } from \"./shell/top-bar-actions\";\nexport { AccountMenuLanguage } from \"./shell/account-menu-language\";\nexport type { AccountMenuLanguageProps, AccountMenuLanguageOption } from \"./shell/account-menu-language\";\n// 0.46.0 (keksdose live #426): A− / A+ for the text size, beside the language.\nexport { AccountMenuTextSize } from \"./shell/account-menu-text-size\";\nexport type { AccountMenuTextSizeProps } from \"./shell/account-menu-text-size\";\nexport { TourMenu } from \"./shell/tour-menu\";\nexport type { TourMenuProps, TourMenuTour } from \"./shell/tour-menu\";\nexport { SyncStatusIndicator } from \"./shell/sync-status-indicator\";\nexport type { SyncStatus, SyncState, SyncStatusScope, SyncStatusIndicatorProps } from \"./shell/sync-status-indicator\";\n// 0.51 (§2.7.1, keksdose): one flask, the kit's Account section and the app's own dev\n// sections. 0.42's `DevSwitchers`, a DevMenu with no sections since 0.51, went in 0.52.\nexport { DevMenu } from \"./shell/dev-menu\";\nexport type { DevMenuProps, DevMenuSection } from \"./shell/dev-menu\";\nexport { DEFAULT_TOP_BAR_PARTS_LABELS, useTopBarPartsLabels } from \"./shell/top-bar-parts-labels\";\nexport type {\n TopBarPartsLabels,\n TourMenuLabels,\n SyncStatusLabels,\n DevSwitchersLabels,\n} from \"./shell/top-bar-parts-labels\";\n"],"mappings":";AAIA,cAAc;AACd,cAAc;AAId;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAMK;AACP,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAEd,SAAS,YAAY,yBAAyB;AAI9C,SAAS,gBAAgB,oBAAoB;AAG7C,SAAS,yBAAyB;AAIlC,SAAS,yBAAyB;AAGlC,SAAS,sBAAsB;AAE/B,SAAS,kBAAkB,6BAA6B;AASxD,SAAS,kBAAkB,qBAAqB;AAChD,SAAS,uBAAuB;AAEhC,SAAS,yBAAyB;AAElC,SAAS,sBAAsB,+BAA+B;AAE9D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAQP,SAAS,oBAAoB;AAK7B,SAAS,qBAAqB;AAE9B,SAAS,2BAA2B;AAGpC,SAAS,2BAA2B;AAEpC,SAAS,gBAAgB;AAEzB,SAAS,2BAA2B;AAIpC,SAAS,eAAe;AAExB,SAAS,8BAA8B,4BAA4B;","names":[]}
1
+ {"version":3,"sources":["../src/shell.ts"],"sourcesContent":["// `@eifi1/ui-kit/shell` — the composable app chrome.\n//\n// A re-slicing of the main barrel, not a new API. This entry point requires\n// `react-router`: AppShell renders NavLink and TopBarActionMenu renders Link.\nexport * from \"./shell/topbar-controls\";\nexport * from \"./shell/top-bar\";\n// Named, not `*`: 0.53 took `pickMobileBar` off the surface (deprecated in 0.52, refactor\n// audit 2026-10-30, API-4) — AppShell's own — and removed `splitMobileBar`, which nothing\n// called any more.\nexport {\n DEFAULT_APP_SHELL_MORE_LABELS,\n DEFAULT_MOBILE_MAX_ITEMS,\n AppShell,\n type AppShellSubItem,\n type AppShellNavItem,\n type AppShellSidebarFooterItem,\n type AppShellMoreLabels,\n type AppShellProps,\n} from \"./shell/app-shell\";\nexport * from \"./shell/option-switcher-menu\";\nexport * from \"./shell/topbar-action-menu\";\nexport * from \"./shell/top-bar-brand\";\nexport * from \"./shell/auth-layout\";\n// 0.40.0: the list page's fill (docs/page-layout-harmonization.md §1.3).\nexport { FillHeight, FILL_HEIGHT_CLASS } from \"./shell/fill-height\";\nexport type { FillHeightProps } from \"./shell/fill-height\";\n// 0.42.0: the account menu's \"Administration\" row, as an entry and as a HoverMenu row\n// (docs/admin-harmonization.md §2.1, §6.6).\nexport { adminMenuEntry, AdminMenuRow } from \"./shell/admin-menu\";\nexport type { AdminMenuEntryOptions, AdminMenuRowProps } from \"./shell/admin-menu\";\n// 0.43.0 (kastlan): the entry with the provider's words.\nexport { useAdminMenuEntry } from \"./shell/admin-menu\";\nexport type { UseAdminMenuEntryOptions } from \"./shell/admin-menu\";\n// 0.45.0 (docs/admin-harmonization.md §9.3.4): the row's waiting pill on its own, for\n// IconButton's `badge` (kastlan's platform button); its name is `adminArea.waitingLabel`.\nexport { AdminWaitingBadge } from \"./shell/admin-menu\";\nexport type { AdminWaitingBadgeProps } from \"./shell/admin-menu\";\n// 0.31.0: the settings and admin pages' shell (docs/settings-harmonization.md §3, §5).\nexport { SettingsLayout } from \"./settings/settings-layout\";\nexport type { SettingsLayoutProps, SettingsLayoutWidth, SettingsDocumentTitleParts } from \"./settings/settings-layout\";\nexport { useSettingsRoute, settingsFromListState } from \"./settings/use-settings-route\";\nexport type {\n UseSettingsRouteOptions,\n SettingsRoute,\n SettingsLayoutMode,\n SettingsLocation,\n SettingsLocationRules,\n SettingsLocationResult,\n} from \"./settings/use-settings-route\";\nexport { useSettingsFocus, useFocusParam } from \"./settings/use-settings-focus\";\nexport { SettingsSection } from \"./settings/settings-section\";\nexport type { SettingsSectionProps } from \"./settings/settings-section\";\nexport { useSettingsLayout } from \"./settings/settings-context\";\nexport type { SettingsLayoutContextValue } from \"./settings/settings-context\";\nexport { SettingsHeadingLevel, useSettingsHeadingLevel } from \"./settings/settings-heading\";\nexport type { SettingsHeadingLevelProps, SettingsHeadingTag } from \"./settings/settings-heading\";\nexport {\n settingsSearchEntries,\n visibleSettingsGroups,\n visibleSettingsEntries,\n settingsHref,\n} from \"./settings/settings-catalogue\";\nexport type { SettingsGroup, SettingsEntry, SettingsSearchEntriesOptions } from \"./settings/settings-catalogue\";\n// 0.31.0: the public pages' header and the resume (docs/landing-demo-harmonization.md §3, §4.1).\nexport {\n RootEntry,\n RedirectIfAuthed,\n useLastVisitedPage,\n readLastVisitedPage,\n clearLastVisitedPage,\n safeNextPath,\n NEXT_PARAM,\n DEFAULT_LAST_VISITED_EXCLUDES,\n} from \"./landing/routing\";\nexport type {\n RootEntryProps,\n RedirectIfAuthedProps,\n RoutingSession,\n LastVisitedPageOptions,\n PathPattern,\n} from \"./landing/routing\";\nexport { PublicHeader } from \"./landing/public-header\";\nexport type { PublicHeaderProps, PublicHeaderBrand } from \"./landing/public-header\";\n// 0.42.0: the top bar's shared parts (docs/top-bar-harmonization.md §3.1) — the slots in\n// §2.1's order, the account menu's language sub-list, the tours menu, the sync status, the\n// local-dev menu, and their words.\nexport { TopBarActions } from \"./shell/top-bar-actions\";\nexport type { TopBarActionsProps } from \"./shell/top-bar-actions\";\nexport { AccountMenuLanguage } from \"./shell/account-menu-language\";\nexport type { AccountMenuLanguageProps, AccountMenuLanguageOption } from \"./shell/account-menu-language\";\n// 0.46.0 (keksdose live #426): A− / A+ for the text size, beside the language.\nexport { AccountMenuTextSize } from \"./shell/account-menu-text-size\";\nexport type { AccountMenuTextSizeProps } from \"./shell/account-menu-text-size\";\nexport { TourMenu } from \"./shell/tour-menu\";\nexport type { TourMenuProps, TourMenuTour } from \"./shell/tour-menu\";\nexport { SyncStatusIndicator } from \"./shell/sync-status-indicator\";\nexport type { SyncStatus, SyncState, SyncStatusScope, SyncStatusIndicatorProps } from \"./shell/sync-status-indicator\";\n// 0.55 (§2.6.1): the indicator's `blocked` with room for the action — \"Switch back\" and a\n// discard that asks first (keksdose's budget switch, kastlan's handover sync banner).\nexport { HeldChangesBanner, DEFAULT_HELD_CHANGES_BANNER_LABELS } from \"./shell/held-changes-banner\";\nexport type { HeldChangesBannerProps, HeldChangesBannerLabels } from \"./shell/held-changes-banner\";\n// 0.51 (§2.7.1, keksdose): one flask, the kit's Account section and the app's own dev\n// sections. 0.42's `DevSwitchers`, a DevMenu with no sections since 0.51, went in 0.52.\nexport { DevMenu } from \"./shell/dev-menu\";\nexport type { DevMenuProps, DevMenuSection } from \"./shell/dev-menu\";\nexport { DEFAULT_TOP_BAR_PARTS_LABELS, useTopBarPartsLabels } from \"./shell/top-bar-parts-labels\";\nexport type {\n TopBarPartsLabels,\n TourMenuLabels,\n SyncStatusLabels,\n DevSwitchersLabels,\n} from \"./shell/top-bar-parts-labels\";\n"],"mappings":";AAIA,cAAc;AACd,cAAc;AAId;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAMK;AACP,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AAEd,SAAS,YAAY,yBAAyB;AAI9C,SAAS,gBAAgB,oBAAoB;AAG7C,SAAS,yBAAyB;AAIlC,SAAS,yBAAyB;AAGlC,SAAS,sBAAsB;AAE/B,SAAS,kBAAkB,6BAA6B;AASxD,SAAS,kBAAkB,qBAAqB;AAChD,SAAS,uBAAuB;AAEhC,SAAS,yBAAyB;AAElC,SAAS,sBAAsB,+BAA+B;AAE9D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAQP,SAAS,oBAAoB;AAK7B,SAAS,qBAAqB;AAE9B,SAAS,2BAA2B;AAGpC,SAAS,2BAA2B;AAEpC,SAAS,gBAAgB;AAEzB,SAAS,2BAA2B;AAIpC,SAAS,mBAAmB,0CAA0C;AAItE,SAAS,eAAe;AAExB,SAAS,8BAA8B,4BAA4B;","names":[]}
@@ -36,11 +36,20 @@ export interface SyncEngineState {
36
36
  lastSyncedAt: string | null;
37
37
  /** When the next attempt after an error runs (epoch ms), or `null`. */
38
38
  nextRetryAt: number | null;
39
+ /**
40
+ * 0.55 (held back from 0.53): a round of this engine waits for the database's Web Lock,
41
+ * which another tab's round holds (or other tabs' rounds wait for ahead of it). True from
42
+ * the moment the round finds the lock taken until its turn comes, or until `stop()` or
43
+ * `forget()` takes it out of the queue; `phase` is left as it was meanwhile (`syncing`
44
+ * once the round runs). The kit's engine always sets it; optional, so a state built by
45
+ * hand (a test's fake engine) still compiles.
46
+ */
47
+ waitingForTab?: boolean;
39
48
  }
40
49
  export interface SyncRoundResult {
41
50
  /** `paused`: `paused()` held the round. (A round asked for while another tab's round holds
42
- * the database waits its turn, 0.52.) `dropped`: `forget()` ran during it, here or in
43
- * another tab, its answer unused. */
51
+ * the database waits its turn, 0.52, with `waitingForTab` in the state meanwhile, 0.55.)
52
+ * `dropped`: `forget()` ran during it, here or in another tab, its answer unused. */
44
53
  outcome: "synced" | "offline" | "error" | "paused" | "dropped";
45
54
  /** Changes this round set `waiting`, `blocked` or `rejected`. */
46
55
  waiting: number;
@@ -19,6 +19,7 @@ class Dropped extends Error {
19
19
  }
20
20
  class ForgottenElsewhere extends Dropped {
21
21
  }
22
+ const TAKEN = /* @__PURE__ */ Symbol("taken");
22
23
  function webLocks() {
23
24
  return typeof navigator === "undefined" ? void 0 : navigator.locks;
24
25
  }
@@ -68,7 +69,8 @@ function createSyncEngine(options) {
68
69
  online: browserOnline(),
69
70
  counts: ZERO,
70
71
  lastSyncedAt: null,
71
- nextRetryAt: null
72
+ nextRetryAt: null,
73
+ waitingForTab: false
72
74
  };
73
75
  const listeners = /* @__PURE__ */ new Set();
74
76
  let started = false;
@@ -299,16 +301,22 @@ function createSyncEngine(options) {
299
301
  const locks = webLocks();
300
302
  if (!locks) return await begin(result, withBlocked, gen, own);
301
303
  let granted = false;
304
+ const turn = () => {
305
+ granted = true;
306
+ if (state.waitingForTab) set({ waitingForTab: false });
307
+ return begin(result, withBlocked, gen, own);
308
+ };
302
309
  try {
303
- return await locks.request(across, { signal: own.signal }, () => {
304
- granted = true;
305
- return begin(result, withBlocked, gen, own);
306
- });
310
+ const now = await locks.request(across, { ifAvailable: true }, (lock) => lock ? turn() : TAKEN);
311
+ if (now !== TAKEN) return now;
312
+ if (gen === generation) set({ waitingForTab: true });
313
+ return await locks.request(across, { signal: own.signal }, turn);
307
314
  } catch (err) {
308
315
  if (granted) return { ...result, outcome: "error", error: err };
309
316
  return await begin(result, withBlocked, gen, own);
310
317
  }
311
318
  } finally {
319
+ if (state.waitingForTab) set({ waitingForTab: false });
312
320
  if (controller === own) controller = null;
313
321
  }
314
322
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/sync/engine.ts"],"sourcesContent":["import { listenOn, postOnce } from \"../lib/broadcast\";\nimport { httpStatusOf } from \"../lib/http-status\";\nimport { isOfflineError, stampAfter } from \"../offline/local-first\";\nimport { isSharedComputer } from \"../offline/shared-computer\";\nimport type { OutboxChange, OutboxStatus, SyncDb, SyncedRow } from \"./db\";\nimport type { SyncRequestBody, SyncResponseBody, SyncWireChange } from \"./wire\";\n\n/*\n * The sync engine (docs/local-first-harmonization.md §2.3): kastlan's handover client,\n * generalised (D10). Keksdose's SERVER rules and wire shapes; the client policies are ka's —\n * a new edit merges into the row's last `pending` upsert, a refused change is held per change\n * (`waiting` / `blocked` / `rejected`), a capped backoff on errors, the queue kept at sign-out.\n *\n * A round, per scope in `scopes()` order (read every round, D11): take the scope's queue and\n * mark it `sending`; post `{scope, last_knowledge, changes, refusals, client_now, paged?}`\n * with `client_now` read fresh (D2 amendment); apply the answer in ONE transaction, only if\n * the generation is unchanged (D15); follow `page_cursor` with pull-only requests, the cursor\n * written with the last page only (D12). Then the app's `steps`, then one more pass if a step\n * changed something. One round at a time; a request during a round runs once more after it.\n *\n * Across tabs (0.52; refactor audit 2026-10-30, FLW-1): every tab runs its own engine on the one\n * IndexedDB copy, and before 0.52 nothing stood between them — a second tab took the first's\n * `sending` changes as a closed tab's and sent them again, its failure laid them back so an edit\n * merged into a change already out and was deleted with it, and a sign-out in one tab was pulled\n * straight back from 0 by the other's poll. Now a round runs under a Web Lock named after the\n * database, so the rounds of all tabs take turns, first come first served; and `forget()`\n * writes a new epoch into `meta` with the clear, which every round reads, so another tab's\n * round drops its answer and that tab's engine stops.\n */\n\nexport interface SyncCounts {\n pending: number;\n waiting: number;\n blocked: number;\n rejected: number;\n}\n\nexport interface SyncStepContext {\n signal: AbortSignal;\n /** False once `forget()` bumped the generation — in this tab, or (0.52) in another tab on\n * the same database, heard over a BroadcastChannel: the step writes nothing more (D15). */\n isCurrent(): boolean;\n}\n\nexport interface SyncStepResult {\n /** The step met no connection: the round ends offline. */\n offline?: boolean;\n /** One more push/pull in the same round (kastlan's second pass after its photos). */\n changed?: boolean;\n /** What the step keeps on the device, added to the engine's counts. */\n counts?: Partial<SyncCounts>;\n}\n\n/** Work after the push/pull, such as kastlan's photo upload. `D` is the app's database type\n * (`createSyncDb<HandoverDb>`), so a step reaches the app's own tables without a cast. */\nexport interface SyncStep<D extends SyncDb = SyncDb> {\n name: string;\n run(db: D, ctx: SyncStepContext): Promise<SyncStepResult>;\n /** Its counts read from the device between rounds (a photo taken offline shows at once). */\n counts?(db: D): Promise<Partial<SyncCounts>>;\n}\n\nexport interface SyncEngineState {\n phase: \"idle\" | \"syncing\" | \"offline\" | \"error\";\n online: boolean;\n counts: SyncCounts;\n /** When a scope's walk last completed (ISO), kept in `meta` (D9). */\n lastSyncedAt: string | null;\n /** When the next attempt after an error runs (epoch ms), or `null`. */\n nextRetryAt: number | null;\n}\n\nexport interface SyncRoundResult {\n /** `paused`: `paused()` held the round. (A round asked for while another tab's round holds\n * the database waits its turn, 0.52.) `dropped`: `forget()` ran during it, here or in\n * another tab, its answer unused. */\n outcome: \"synced\" | \"offline\" | \"error\" | \"paused\" | \"dropped\";\n /** Changes this round set `waiting`, `blocked` or `rejected`. */\n waiting: number;\n blocked: number;\n rejected: number;\n error?: unknown;\n}\n\nexport interface SyncEngineOptions<D extends SyncDb = SyncDb> {\n /** The database; its type `D` is the one the `steps` get. */\n db: D;\n /** The scopes this session's role holds, in round order (kastlan: handover, meetings, meters,\n * maintenance, contacts). Read at the start of every round: roles are per company. */\n scopes: () => readonly string[];\n /** Posts one request (`POST /sync`) and answers its body; throws as the app's client does. */\n send: (body: SyncRequestBody, signal: AbortSignal) => Promise<SyncResponseBody>;\n /** Default {@link isOfflineError}: no answer, or the session refresh's passing 401. */\n isOffline?: (err: unknown) => boolean;\n /** A whole request refused for the plan → every sent change `waiting` with this code.\n * Default: a 402 whose body names a `code` (or `detail.code`). */\n planCode?: (err: unknown) => string | null;\n /** `refused.code`s kept as `waiting` and sent again every round; any other code is\n * `blocked`, sent again only on `retryBlocked()`. Default `[\"billing_read_only\"]`. */\n holdCodes?: readonly string[];\n /** Ask for a paged pull and follow its cursor (D12). */\n paged?: boolean;\n steps?: readonly SyncStep<D>[];\n /** No round while this holds (kastlan: a practice tour fakes the server's answers). */\n paused?: () => boolean;\n /** The poll (30 s) and the debounce after a write (800 ms). */\n pollMs?: number;\n soonMs?: number;\n /** The waits after consecutive errors; the last repeats (D13). */\n backoffMs?: readonly number[];\n /** Default `isSharedComputer`: on a computer marked shared the engine does not start (D15). */\n sharedComputer?: () => boolean;\n /**\n * 0.52: the backstop on one round, counted from the moment it holds the database's lock\n * (a tab waiting behind a stuck round is not cut with it). Past it the round is cut: its\n * requests and steps are aborted, anything they answer or write later is dropped, and the\n * round ends `error` with a `TimeoutError` (`DOMException`), not `offline`, so the backoff\n * starts and the lock goes to the next tab even when a request or a step ignores its\n * signal. The app's own request timeouts come first; this only bounds what they miss.\n * Default 10 minutes, long on purpose: kastlan's photo backlog can take minutes, and a cut\n * round has still made progress (each photo uploaded before the cut is off the queue).\n * `0` or `Infinity`: none.\n */\n roundTimeoutMs?: number;\n}\n\nexport interface SyncEngine {\n /** Into the copy at once and into the queue, in the scope the caller names (D11). An edit\n * merges into the row's last `pending` upsert; deleting a row that never reached the\n * server forgets its queue; stamped with `stampAfter`. */\n write(\n scope: string,\n entity: string,\n id: string,\n op: \"upsert\" | \"delete\",\n data?: Record<string, unknown>,\n ): Promise<void>;\n /** Starts the poll and the round on `online` (nothing on a computer marked shared).\n * An engine another tab's `forget()` stopped stays stopped through `start()`: only its\n * own `forget()` (the app's session end) or a new engine (`releaseSyncEngine`) re-arms\n * it. A start cannot tell a new sign-in from a remount in a tab whose session never\n * ended, and the latter would pull the signed-out account back into the wiped copy\n * (D15; the 0.52 code review asked, and the contract stands). */\n start(): void;\n /** Stops the poll and the timers, aborts the round in flight and drops its answer (the\n * queue is laid back). A new engine's round on the same database waits for it. */\n stop(): void;\n /** Sync soon (debounced): after a write. */\n requestSync(): void;\n /** \"Sync now\": resets the backoff, runs a round of its own after the one under way — another\n * tab's too (0.52) — and answers its result. */\n syncNow(): Promise<SyncRoundResult>;\n /** A round that sends the `blocked` changes too. */\n retryBlocked(): Promise<void>;\n /** Drop the rejected changes (of one scope) and pull that scope afresh. */\n discardRejected(scope?: string): Promise<void>;\n /** The rejected changes with their reasons, for the app's banner. */\n rejected(): Promise<OutboxChange[]>;\n /** Sign-out: bump the generation (a round in flight writes nothing more), then clear the\n * rows and `meta` — and the outbox and the app's tables unless `keepQueue` (D15). In every\n * tab (0.52): the clear writes a new epoch into `meta`, so a round of another tab's engine on\n * this database drops its answer, and that engine stops — the session it ran for has ended.\n * Another tab's `forget()` heard while this clear runs is decided by the epoch the copy is\n * left with: one that committed first was undone by this clear and stops nothing. */\n forget(opts: { keepQueue: boolean }): Promise<void>;\n getState(): SyncEngineState;\n subscribe(listener: () => void): () => void;\n}\n\nexport const DEFAULT_SYNC_BACKOFF_MS: readonly number[] = [5_000, 15_000, 60_000, 300_000];\nexport const DEFAULT_HOLD_CODES: readonly string[] = [\"billing_read_only\"];\n/** `roundTimeoutMs`'s default (see there). */\nconst ROUND_TIMEOUT_MS = 10 * 60_000;\n/** The longest delay `setTimeout` keeps (2^31 - 1 ms, ~24.8 days); a longer one fires at once. */\nconst MAX_TIMEOUT_MS = 2_147_483_647;\n\nconst LAST_SYNCED_AT = \"lastSyncedAt\";\n/** The copy's epoch in `meta` (0.52): a new one with every `forget()`, in its clear. */\nconst EPOCH = \"epoch\";\nconst lastKnowledgeKey = (scope: string) => `lastKnowledge:${scope}`;\nconst UNSENT: readonly OutboxStatus[] = [\"pending\", \"sending\", \"waiting\", \"blocked\"];\nconst ZERO: SyncCounts = { pending: 0, waiting: 0, blocked: 0, rejected: 0 };\n\n/** The started engine of each database: one engine per database (D15). */\nconst running = new Map<string, SyncEngine>();\n\n/** The round in flight on each database, of whichever engine: a new engine's round waits\n * for it, so two never take the same queue (a company switch A→B→A within one round). */\nconst flights = new Map<string, Promise<unknown>>();\n\n/** The engines {@link syncEngineFor} keeps, by database name. */\nconst kept = new Map<string, { db: SyncDb; engine: SyncEngine }>();\n\n/** The generation changed under a round: its answer is dropped. */\nclass Dropped extends Error {}\n\n/** The copy's epoch moved since this engine started: `forget()` ran in another tab. The\n * round's answer is dropped as well. */\nclass ForgottenElsewhere extends Dropped {}\n\n/** The browser's Web Locks: a secure context (https, localhost), Safari 15.4+. */\nfunction webLocks(): LockManager | undefined {\n return typeof navigator === \"undefined\" ? undefined : (navigator as { locks?: LockManager }).locks;\n}\n\n/** A `forget()`'s epoch: unique enough to tell two apart, and on plain http too, where\n * `crypto.randomUUID` is missing (refactor audit 2026-10-30, DUP-7). */\nfunction freshEpoch(): string {\n const uuid = (globalThis.crypto as Partial<Crypto> | undefined)?.randomUUID;\n return uuid ? uuid.call(globalThis.crypto) : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;\n}\n\n/** A row created on this device and never echoed by the server carries `local: true`. */\ntype CopyRow = SyncedRow & { local?: boolean };\n\nfunction browserOnline(): boolean {\n return typeof navigator === \"undefined\" || navigator.onLine !== false;\n}\n\nfunction defaultPlanCode(err: unknown): string | null {\n if (httpStatusOf(err) !== 402) return null;\n const body = (err as { response?: { data?: { code?: unknown; detail?: { code?: unknown } } } }).response?.data;\n const code = typeof body?.code === \"string\" ? body.code : body?.detail?.code;\n return typeof code === \"string\" ? code : null;\n}\n\n/** A refusal's `code` and `detail` from an error's body: `{code, detail}` or FastAPI's\n * `{detail: {code, detail}}`. */\nfunction refusalOf(err: unknown): { code?: string; detail?: string } {\n const body = (err as { response?: { data?: unknown } }).response?.data as\n { code?: unknown; detail?: unknown } | undefined;\n const inner =\n body?.detail && typeof body.detail === \"object\" ? (body.detail as { code?: unknown; detail?: unknown }) : body;\n const text = (v: unknown) => (typeof v === \"string\" && v ? v : undefined);\n return { code: text(inner?.code), detail: text(inner?.detail) };\n}\n\nfunction latest(stamps: unknown[]): string | undefined {\n let best: string | undefined;\n for (const s of stamps) {\n if (typeof s !== \"string\" || !Number.isFinite(Date.parse(s))) continue;\n if (best === undefined || Date.parse(s) > Date.parse(best)) best = s;\n }\n return best;\n}\n\nconst toWire = ({ entity, id, op, data, updated_at }: OutboxChange): SyncWireChange =>\n op === \"delete\" ? { entity, id, op, updated_at } : { entity, id, op, data, updated_at };\n\n/**\n * The engine on one database (D15: one engine per database; a company switch builds a new\n * engine on the new database and stops the old one). Nothing runs until `start()`\n * (`useSyncWorker`), and `start()` does nothing on a computer marked shared.\n */\nexport function createSyncEngine<D extends SyncDb = SyncDb>(options: SyncEngineOptions<D>): SyncEngine {\n const { db } = options;\n const isOffline = options.isOffline ?? isOfflineError;\n const planCode = options.planCode ?? defaultPlanCode;\n const holdCodes = new Set(options.holdCodes ?? DEFAULT_HOLD_CODES);\n const steps = options.steps ?? [];\n const pollMs = options.pollMs ?? 30_000;\n const soonMs = options.soonMs ?? 800;\n const backoffMs = options.backoffMs?.length ? options.backoffMs : DEFAULT_SYNC_BACKOFF_MS;\n const sharedComputer = options.sharedComputer ?? isSharedComputer;\n const roundTimeoutMs = options.roundTimeoutMs ?? ROUND_TIMEOUT_MS;\n /** The rounds the deadline cut, by their signal: their late work lays nothing back. */\n const cut = new WeakSet<AbortSignal>();\n\n let generation = 0;\n let state: SyncEngineState = {\n phase: \"idle\",\n online: browserOnline(),\n counts: ZERO,\n lastSyncedAt: null,\n nextRetryAt: null,\n };\n const listeners = new Set<() => void>();\n let started = false;\n let poll: ReturnType<typeof setInterval> | null = null;\n let soon: ReturnType<typeof setTimeout> | null = null;\n let retry: ReturnType<typeof setTimeout> | null = null;\n let failures = 0;\n let current: Promise<SyncRoundResult> | null = null;\n let again = false;\n let controller: AbortController | null = null;\n let stepCounts: Record<string, Partial<SyncCounts>> = {};\n\n // ── Across tabs (0.52, FLW-1) ───────────────────────────────────────────────\n /** The name of this database's Web Lock and BroadcastChannel (one origin's tabs share both,\n * as they share the IndexedDB). */\n const across = `eifi1-sync:${db.name}`;\n /** The copy's epochs this engine runs under: the one read when it started (or at its first\n * round), and each its own `forget()` wrote. Any other one means `forget()` ran elsewhere. */\n const epochs = new Set<string | null>();\n let epochRead: Promise<void> | null = null;\n /** `forget()` ran in another tab: this engine runs no round until its own `forget()`. */\n let forgotten = false;\n /** This engine's own `forget()`s whose clear is under way. */\n let forgetting = 0;\n let unlisten: (() => void) | null = null;\n\n function set(next: Partial<SyncEngineState>) {\n state = { ...state, ...next };\n listeners.forEach((listener) => listener());\n }\n\n /** The counts and `lastSyncedAt` read again from the device. Never throws: a closed or\n * deleted database leaves the last counts standing. */\n function refreshCounts(): Promise<void> {\n return readCounts().catch(() => undefined);\n }\n\n async function readCounts(): Promise<void> {\n const gen = generation;\n const count = (status: OutboxStatus) => db.outbox.where(\"status\").equals(status).count();\n const [pending, sending, waiting, blocked, rejected] = await Promise.all(\n ([\"pending\", \"sending\", \"waiting\", \"blocked\", \"rejected\"] as const).map(count),\n );\n const fromSteps = { ...stepCounts };\n for (const step of steps) if (step.counts) fromSteps[step.name] = await step.counts(db);\n const last = (await db.meta.get(LAST_SYNCED_AT))?.value;\n if (gen !== generation) return;\n if (Object.keys(fromSteps).length) stepCounts = fromSteps;\n const counts = { pending: pending + sending, waiting, blocked, rejected };\n for (const extra of Object.values(stepCounts)) {\n for (const key of Object.keys(counts) as (keyof SyncCounts)[]) counts[key] += extra[key] ?? 0;\n }\n set({ counts, lastSyncedAt: typeof last === \"string\" ? last : null });\n }\n\n function readEpoch(): Promise<string | null> {\n return db.meta.get(EPOCH).then((entry) => (typeof entry?.value === \"string\" ? entry.value : null));\n }\n\n /** Read once per engine — at `start()`, before its first round can wait on another tab's —\n * so a `forget()` that lands while it waits is seen as another tab's. A failed read is\n * tried again by the next round. */\n function captureEpoch(): Promise<void> {\n if (epochRead) return epochRead;\n const read = readEpoch().then((epoch) => void epochs.add(epoch));\n epochRead = read;\n read.catch(() => {\n if (epochRead === read) epochRead = null;\n });\n return read;\n }\n\n /** While started, hear `forget()` in another tab at once: a step in flight sees\n * `isCurrent()` turn false, the request in flight is aborted, and the poll stops. Without\n * BroadcastChannel the next round's read of the epoch stops it instead. */\n function listen() {\n unlisten ??= listenOn(across, (data) => {\n const epoch = (data as { epoch?: unknown } | null)?.epoch;\n if (typeof epoch === \"string\" && !epochs.has(epoch)) void forgetHere();\n });\n }\n\n /** `forget()` ran in another tab: the session this engine ran for has ended there. Its\n * round in flight writes nothing more, and it runs none of its own any more.\n *\n * Not while its own `forget()` clears the copy: that clear decides, by the epoch it leaves\n * in `meta`. 0.52 code review (src/sync/engine.ts): in kastlan's sign-out cascade tab A's\n * epoch reached tab B while B's own clear waited behind A's, so B's `forget()` resolved\n * with B stopped and forgotten — the forget that re-arms an engine left it dead. */\n function forgetHere(): Promise<void> {\n if (forgotten || forgetting > 0) return Promise.resolve();\n forgotten = true;\n if (started) halt();\n else drop();\n return endSession();\n }\n\n /** The round in flight, or waiting for the lock, writes nothing more: its wait and its\n * request are aborted, its answer dropped, the queue laid back. */\n function drop() {\n generation += 1;\n controller?.abort();\n }\n\n /** What the engine shows of a session that ended — by its own `forget()` or another tab's —\n * goes with it: the steps' counts, the last sync, an error and its retry. 0.52 code review\n * (src/sync/engine.ts): the two paths each reset part of it, and an engine another tab\n * stopped showed \"error\" for a retry that never came. */\n function endSession(): Promise<void> {\n stepCounts = {};\n failures = 0;\n clearRetry();\n set({ phase: \"idle\", lastSyncedAt: null, nextRetryAt: null });\n return refreshCounts();\n }\n\n /** The round's epoch still stands in `meta`, read in the caller's transaction, so another\n * tab's `forget()` commits wholly before it or after it. Else `forget()` ran: in this tab\n * (the generation moved too) or in another — then the engine stops. */\n async function standing(gen: number, epoch: string | null): Promise<void> {\n const now = await readEpoch();\n if (gen !== generation) throw new Dropped();\n if (now !== epoch) throw epochs.has(now) ? new Dropped() : new ForgottenElsewhere();\n }\n\n // ── The queue ──────────────────────────────────────────────────────────────\n\n /** Mark the scope's queue as in the request (a `sending` left by a closed tab goes back\n * first); answers it with the statuses it had. Under the Web Lock (0.52) a `sending` here\n * is a closed tab's, as this assumes: no other tab's round is out. Without Web Locks\n * (plain http) it may be another open tab's, as before 0.52.\n *\n * Only under the round's epoch. 0.52 code review (src/sync/engine.ts): after another tab's\n * `forget()` had landed mid-round, every scope left still posted the kept queue under the\n * session that had ended and pulled itself from 0, only for `apply` to drop the answer. */\n function takeQueue(scope: string, withBlocked: boolean, gen: number, epoch: string | null): Promise<OutboxChange[]> {\n return db.transaction(\"rw\", db.outbox, db.meta, async () => {\n await standing(gen, epoch);\n await db.outbox\n .where(\"scope\")\n .equals(scope)\n .and((c) => c.status === \"sending\")\n .modify({ status: \"pending\" });\n const take: OutboxStatus[] = withBlocked ? [\"pending\", \"waiting\", \"blocked\"] : [\"pending\", \"waiting\"];\n const queue = (await db.outbox.where(\"scope\").equals(scope).toArray())\n .filter((c) => take.includes(c.status))\n .sort((a, b) => a.seq! - b.seq!);\n await db.outbox.bulkUpdate(queue.map((c) => ({ key: c.seq!, changes: { status: \"sending\" as const } })));\n return queue;\n });\n }\n\n /** The request failed or its answer was dropped: each change back to the status it had.\n * Not by a round the deadline cut: its late work ends after the next round — this tab's\n * or another's — may have taken these changes `sending` again, and laid back `pending`\n * under that round an edit would merge into a change already out and be deleted with it\n * (FLW-1's lost edit). The next round's `takeQueue` takes them back instead, as a closed\n * tab's. */\n function layBack(sent: OutboxChange[], signal: AbortSignal) {\n if (cut.has(signal)) return;\n return db.outbox.bulkUpdate(sent.map((c) => ({ key: c.seq!, changes: { status: c.status } })));\n }\n\n function mark(changes: OutboxChange[], fields: Partial<OutboxChange>) {\n return db.outbox.bulkUpdate(changes.map((c) => ({ key: c.seq!, changes: fields })));\n }\n\n /** Put one page's rows in the copy, settle the sent changes, lay the unsent ones back. The\n * epoch is read in the same transaction: a `forget()` of another tab either commits before\n * it (this answer is dropped) or after it (and clears what it wrote). */\n async function apply(\n scope: string,\n answer: SyncResponseBody,\n sent: OutboxChange[],\n gen: number,\n epoch: string | null,\n lastPage: boolean,\n ) {\n return db.transaction(\"rw\", db.rows, db.outbox, db.meta, async () => {\n await standing(gen, epoch);\n const refused = answer.refused ?? null;\n const refusedIds = new Set(refused?.change_ids ?? []);\n const rejections = new Map((answer.rejections ?? []).map((r) => [r.change_id, r]));\n const kept = sent.filter((c) => refusedIds.has(c.id));\n const rejected = sent.filter((c) => !refusedIds.has(c.id) && rejections.has(c.id));\n await db.outbox.bulkDelete(sent.filter((c) => !refusedIds.has(c.id) && !rejections.has(c.id)).map((c) => c.seq!));\n const hold = refused != null && holdCodes.has(refused.code);\n if (refused)\n await mark(kept, { status: hold ? \"waiting\" : \"blocked\", code: refused.code, reason: refused.detail });\n for (const change of rejected) {\n const why = rejections.get(change.id)!;\n await db.outbox.update(change.seq!, { status: \"rejected\", code: why.code, reason: why.detail });\n }\n const pulled = new Set<string>();\n for (const change of answer.changes) {\n pulled.add(`${change.entity}\\u0000${change.id}`);\n if (change.op === \"delete\") await db.rows.delete([change.entity, change.id]);\n else {\n const row: CopyRow = {\n entity: change.entity,\n id: change.id,\n pk: change.pk ?? null,\n local: false,\n data: change.data ?? {},\n // The server's stamp rides beside `data`: kept, so the next edit is stamped after it.\n updated_at: change.updated_at ?? null,\n };\n await db.rows.put(row);\n }\n }\n // A pull never undoes what was typed while the request was out.\n const unsent = (await db.outbox.where(\"status\").anyOf(UNSENT).toArray())\n .filter((c) => pulled.has(`${c.entity}\\u0000${c.id}`))\n .sort((a, b) => a.seq! - b.seq!);\n for (const change of unsent) {\n if (change.op === \"delete\") {\n await db.rows.delete([change.entity, change.id]);\n continue;\n }\n const row = (await db.rows.get([change.entity, change.id])) as CopyRow | undefined;\n await db.rows.put({\n ...row,\n entity: change.entity,\n id: change.id,\n pk: row?.pk ?? null,\n data: { ...row?.data, ...change.data },\n });\n }\n if (lastPage) {\n await db.meta.put({ key: lastKnowledgeKey(scope), value: answer.server_knowledge });\n await db.meta.put({ key: LAST_SYNCED_AT, value: new Date().toISOString() });\n }\n return {\n waiting: hold ? kept.length : 0,\n blocked: refused && !hold ? kept.length : 0,\n rejected: rejected.length,\n };\n });\n }\n\n /** A scope the role no longer holds (403): its queue is rejected and it leaves the round.\n * The server's `code` and `detail` are kept (a demo's `demo_read_only`); without them,\n * `forbidden` and \"Not allowed to sync <scope>\". */\n async function loseScope(scope: string, err: unknown): Promise<number> {\n const why = refusalOf(err);\n return db.transaction(\"rw\", db.outbox, async () => {\n const queue = (await db.outbox.where(\"scope\").equals(scope).toArray()).filter((c) => UNSENT.includes(c.status));\n await mark(queue, {\n status: \"rejected\",\n code: why.code ?? \"forbidden\",\n reason: why.code && why.detail ? why.detail : `Not allowed to sync ${scope}`,\n });\n return queue.length;\n });\n }\n\n type ScopeResult = { offline: true } | { offline: false; waiting: number; blocked: number; rejected: number };\n\n async function syncScope(\n scope: string,\n gen: number,\n epoch: string | null,\n signal: AbortSignal,\n withBlocked: boolean,\n ): Promise<ScopeResult> {\n const sent = await takeQueue(scope, withBlocked, gen, epoch);\n const last = Number((await db.meta.get(lastKnowledgeKey(scope)))?.value ?? 0) || 0;\n const body = (changes: SyncWireChange[], page_cursor: SyncRequestBody[\"page_cursor\"]): SyncRequestBody => ({\n scope,\n last_knowledge: last,\n changes,\n refusals: true,\n client_now: new Date().toISOString(),\n ...(options.paged ? { paged: true, page_cursor } : {}),\n });\n let answer: SyncResponseBody;\n try {\n answer = await options.send(body(sent.map(toWire), null), signal);\n } catch (err) {\n await layBack(sent, signal);\n if (gen !== generation) throw new Dropped();\n if (isOffline(err)) return { offline: true };\n const plan = planCode(err);\n if (!plan && httpStatusOf(err) !== 403) throw err;\n // A refusal is the answer, kept only under the round's epoch as `apply` keeps one. 0.52\n // code review (src/sync/engine.ts): a 403 heard after another tab's `forget()` marked\n // the kept queue `rejected`, never sent again, for a session that had ended.\n return db.transaction(\"rw\", db.outbox, db.meta, async (): Promise<ScopeResult> => {\n await standing(gen, epoch);\n if (plan) {\n // The whole request refused for the plan: every change waits for it.\n await mark(sent, { status: \"waiting\", code: plan, reason: plan });\n return { offline: false, waiting: sent.length, blocked: 0, rejected: 0 };\n }\n return { offline: false, waiting: 0, blocked: 0, rejected: await loseScope(scope, err) };\n });\n }\n let cursor = options.paged ? (answer.page_cursor ?? null) : null;\n let tally: ScopeResult;\n try {\n tally = { offline: false, ...(await apply(scope, answer, sent, gen, epoch, cursor == null)) };\n } catch (err) {\n if (err instanceof Dropped) await layBack(sent, signal);\n throw err;\n }\n // The rest of a paged pull: pull-only; the cursor is written with the last page alone,\n // so an interrupted walk starts again from the old one (D12).\n while (cursor) {\n let page: SyncResponseBody;\n try {\n page = await options.send(body([], cursor), signal);\n } catch (err) {\n if (gen !== generation) throw new Dropped();\n if (isOffline(err)) return { offline: true };\n throw err;\n }\n cursor = page.page_cursor ?? null;\n await apply(scope, page, [], gen, epoch, cursor == null);\n }\n return tally;\n }\n\n // ── The round ──────────────────────────────────────────────────────────────\n\n async function round(withBlocked: boolean, gen: number): Promise<SyncRoundResult> {\n const result: SyncRoundResult = { outcome: \"synced\", waiting: 0, blocked: 0, rejected: 0 };\n if (gen !== generation || forgotten) return { ...result, outcome: \"dropped\" };\n // Aborted by stop() and forget(): the wait for the lock, then the round's requests.\n const own = new AbortController();\n controller = own;\n try {\n const locks = webLocks();\n if (!locks) return await begin(result, withBlocked, gen, own);\n let granted = false;\n try {\n // Queued, first come first served: the rounds of every tab on this database take\n // turns, and one asked for meanwhile waits for its turn (`current` and `again` keep\n // one per engine). 0.52 code review (src/sync/engine.ts): an `ifAvailable` round\n // that found the lock taken answered `paused` at once, so the sign-out dialog's\n // \"Sync now\" closed as if it had synced; its one deferred round lost a\n // `retryBlocked()`'s `blocked`; and two tabs' waits refused each other's next try —\n // the spec refuses `ifAvailable` while any request is queued. A closed tab's lock\n // is released with it.\n return await locks.request(across, { signal: own.signal }, () => {\n granted = true;\n return begin(result, withBlocked, gen, own);\n });\n } catch (err) {\n // `begin` throws only what the app's `paused()` throws. A wait aborted by stop() or\n // forget() is dropped there; a lock manager that refuses (an opaque origin, a\n // document no longer active) runs the round as before 0.52.\n if (granted) return { ...result, outcome: \"error\", error: err };\n return await begin(result, withBlocked, gen, own);\n }\n } finally {\n if (controller === own) controller = null;\n }\n }\n\n /** The round begins — under the lock, where there is one, after however long it waited:\n * the decision point, and the start of its deadline. */\n async function begin(\n result: SyncRoundResult,\n withBlocked: boolean,\n gen: number,\n own: AbortController,\n ): Promise<SyncRoundResult> {\n if (gen !== generation || forgotten) return { ...result, outcome: \"dropped\" };\n if (options.paused?.()) return { ...result, outcome: \"paused\" };\n if (!browserOnline()) return { ...result, outcome: \"offline\" };\n return withDeadline(locked(result, withBlocked, gen, own.signal), result, gen, own);\n }\n\n /** The round, or its cut at `roundTimeoutMs`, whichever comes first. 0.52 code review,\n * finding 3 (Marcel: \"kit backstop + app timeouts\"): a round holds the lock of every tab\n * on its database, and one whose request never settles (axios has no default timeout) or\n * whose step never does stopped sync in all of them until that tab was closed. The cut\n * settles the round by the race, so the lock is released even when the app ignores the\n * abort; the generation moves, so what the cut work answers or writes later is dropped. */\n function withDeadline(\n round: Promise<SyncRoundResult>,\n result: SyncRoundResult,\n gen: number,\n own: AbortController,\n ): Promise<SyncRoundResult> {\n if (!(roundTimeoutMs > 0 && Number.isFinite(roundTimeoutMs))) return round;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const deadline = new Promise<SyncRoundResult>((resolve) => {\n timer = setTimeout(\n () => {\n const error = new DOMException(`The sync round ran past ${roundTimeoutMs} ms`, \"TimeoutError\");\n // Settled first: the race ends on the cut, whatever the abort below sets off.\n resolve({ ...result, outcome: \"error\", error });\n cut.add(own.signal);\n if (gen === generation) generation += 1;\n own.abort(error);\n },\n Math.min(roundTimeoutMs, MAX_TIMEOUT_MS),\n );\n });\n return Promise.race([round, deadline]).finally(() => clearTimeout(timer));\n }\n\n /** The round itself. Never throws. */\n async function locked(\n result: SyncRoundResult,\n withBlocked: boolean,\n gen: number,\n signal: AbortSignal,\n ): Promise<SyncRoundResult> {\n try {\n await captureEpoch();\n const epoch = await readEpoch();\n if (gen !== generation) throw new Dropped();\n if (!epochs.has(epoch)) throw new ForgottenElsewhere();\n set({ phase: \"syncing\" });\n for (let pass = 0; ; pass++) {\n for (const scope of options.scopes()) {\n if (gen !== generation) throw new Dropped();\n const r = await syncScope(scope, gen, epoch, signal, withBlocked);\n if (r.offline) return { ...result, outcome: \"offline\" };\n result.waiting += r.waiting;\n result.blocked += r.blocked;\n result.rejected += r.rejected;\n }\n if (pass > 0) break;\n let changed = false;\n for (const step of steps) {\n if (gen !== generation) throw new Dropped();\n const ran = await step.run(db, { signal, isCurrent: () => gen === generation });\n if (gen !== generation) throw new Dropped();\n if (ran.counts) stepCounts = { ...stepCounts, [step.name]: ran.counts };\n if (ran.offline) return { ...result, outcome: \"offline\" };\n if (ran.changed) changed = true;\n }\n if (!changed) break;\n }\n return result;\n } catch (err) {\n if (err instanceof ForgottenElsewhere) void forgetHere();\n if (err instanceof Dropped || gen !== generation) return { ...result, outcome: \"dropped\" };\n return { ...result, outcome: \"error\", error: err };\n }\n }\n\n function clearRetry() {\n if (retry) clearTimeout(retry);\n retry = null;\n }\n\n function resetBackoff() {\n failures = 0;\n clearRetry();\n if (state.nextRetryAt !== null) set({ nextRetryAt: null });\n }\n\n const inBackoff = () => state.nextRetryAt !== null && Date.now() < state.nextRetryAt;\n\n function settle(result: SyncRoundResult) {\n switch (result.outcome) {\n case \"synced\":\n failures = 0;\n clearRetry();\n set({ phase: \"idle\", online: true, nextRetryAt: null });\n break;\n case \"offline\":\n // No retry timer offline: the `online` event or the poll brings the next round (D13).\n clearRetry();\n set({ phase: \"offline\", online: false, nextRetryAt: null });\n break;\n case \"error\": {\n const wait = backoffMs[Math.min(failures, backoffMs.length - 1)];\n failures += 1;\n clearRetry();\n if (started) {\n retry = setTimeout(() => {\n retry = null;\n void run(false);\n }, wait);\n }\n set({ phase: \"error\", nextRetryAt: started ? Date.now() + wait : null });\n break;\n }\n case \"dropped\":\n set({ phase: \"idle\" });\n break;\n case \"paused\":\n break;\n }\n }\n\n function run(withBlocked: boolean): Promise<SyncRoundResult> {\n if (current) {\n again = true;\n return current;\n }\n const before = flights.get(db.name);\n // The generation is read before any wait: a stop() while this waits drops the round, so\n // it never sends this copy's queue with another account's client nor pulls into it.\n const gen = generation;\n const flight = (async () => {\n // Another engine's round on this database (stopped, still settling) ends first.\n if (before) await before.catch(() => undefined);\n try {\n const result = await round(withBlocked, gen);\n settle(result);\n await refreshCounts();\n return result;\n } finally {\n current = null;\n if (again) {\n again = false;\n tick();\n }\n }\n })();\n current = flight;\n flights.set(db.name, flight);\n const done = () => {\n if (flights.get(db.name) === flight) flights.delete(db.name);\n };\n void flight.then(done, done);\n return flight;\n }\n\n /** A round from the poll, a write or a request during a round: not while backing off. */\n function tick() {\n if (started && !inBackoff()) void run(false);\n }\n\n /** A round of its own, begun after the one under way — and after one that a request made\n * meanwhile started as that one ended — so `retryBlocked()` never joins a round that\n * leaves `blocked` out (0.52 code review, src/sync/engine.ts). */\n async function runFresh(withBlocked: boolean): Promise<SyncRoundResult> {\n resetBackoff();\n while (current) {\n again = false;\n await current.catch(() => undefined);\n }\n return run(withBlocked);\n }\n\n const onOnline = () => {\n set({ online: true });\n resetBackoff();\n void run(false);\n };\n const onOffline = () => set({ online: false });\n\n /** `stop()`, and the end `forgetHere` puts to a started engine. */\n function halt() {\n if (!started) return;\n started = false;\n // The next engine's round waits for the dropped one to settle.\n drop();\n if (running.get(db.name) === engine) running.delete(db.name);\n if (poll) clearInterval(poll);\n poll = null;\n if (soon) clearTimeout(soon);\n soon = null;\n clearRetry();\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"online\", onOnline);\n window.removeEventListener(\"offline\", onOffline);\n }\n unlisten?.();\n unlisten = null;\n }\n\n const engine: SyncEngine = {\n async write(scope, entity, id, op, data = {}) {\n await db.transaction(\"rw\", db.rows, db.outbox, async () => {\n const row = (await db.rows.get([entity, id])) as CopyRow | undefined;\n const ofRow = await db.outbox.where(\"[entity+id]\").equals([entity, id]).toArray();\n ofRow.sort((a, b) => a.seq! - b.seq!);\n const updated_at = stampAfter(\n latest([row?.updated_at, row?.data.updated_at, ...ofRow.map((c) => c.updated_at)]),\n );\n // A new edit to the row moves a rejected change out of the way (D13).\n const rejected = ofRow.filter((c) => c.scope === scope && c.status === \"rejected\");\n if (op === \"delete\") {\n await db.rows.delete([entity, id]);\n const neverSynced = row != null && (row.local ?? row.pk == null);\n if (neverSynced && !ofRow.some((c) => c.status === \"sending\")) {\n // Never reached the server: forgetting its queue is enough — a waiting change\n // included, or it would create the row once the plan is back.\n await db.outbox.bulkDelete(ofRow.map((c) => c.seq!));\n return;\n }\n await db.outbox.bulkDelete(rejected.map((c) => c.seq!));\n await db.outbox.add({ scope, entity, id, op, data: {}, updated_at, status: \"pending\" });\n return;\n }\n const next: CopyRow = { ...row, entity, id, pk: row?.pk ?? null, data: { ...row?.data, ...data } };\n if (!row) next.local = true;\n await db.rows.put(next);\n const carried = Object.assign({}, ...rejected.filter((c) => c.op === \"upsert\").map((c) => c.data)) as Record<\n string,\n unknown\n >;\n await db.outbox.bulkDelete(rejected.map((c) => c.seq!));\n const last = ofRow.filter((c) => c.scope === scope && c.status === \"pending\").at(-1);\n if (last && last.op === \"upsert\") {\n // Several edits before a round go up as one change, with the last one's time.\n await db.outbox.update(last.seq!, { data: { ...carried, ...last.data, ...data }, updated_at });\n } else {\n await db.outbox.add({ scope, entity, id, op, data: { ...carried, ...data }, updated_at, status: \"pending\" });\n }\n });\n engine.requestSync();\n },\n\n start() {\n if (started || forgotten || sharedComputer()) return;\n const other = running.get(db.name);\n if (other && other !== engine) other.stop();\n running.set(db.name, engine);\n started = true;\n captureEpoch().catch(() => undefined);\n listen();\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"online\", onOnline);\n window.addEventListener(\"offline\", onOffline);\n }\n poll = setInterval(tick, pollMs);\n void refreshCounts();\n void run(false);\n },\n\n stop: halt,\n\n requestSync() {\n void refreshCounts();\n if (!started) return;\n if (soon) clearTimeout(soon);\n soon = setTimeout(() => {\n soon = null;\n tick();\n }, soonMs);\n },\n\n syncNow: () => runFresh(false),\n\n async retryBlocked() {\n await runFresh(true);\n },\n\n async discardRejected(scope) {\n const gone = (await db.outbox.where(\"status\").equals(\"rejected\").toArray()).filter(\n (c) => !scope || c.scope === scope,\n );\n if (gone.length === 0) return;\n await db.transaction(\"rw\", db.rows, db.outbox, db.meta, async () => {\n await db.outbox.bulkDelete(gone.map((c) => c.seq!));\n for (const change of gone) {\n const others = await db.outbox.where(\"[entity+id]\").equals([change.entity, change.id]).count();\n if (others === 0) await db.rows.delete([change.entity, change.id]);\n }\n // The scope pulls afresh, so the copy shows what the server holds.\n for (const s of new Set(gone.map((c) => c.scope))) await db.meta.put({ key: lastKnowledgeKey(s), value: 0 });\n });\n await refreshCounts();\n engine.requestSync();\n },\n\n async rejected() {\n return db.outbox.where(\"status\").equals(\"rejected\").sortBy(\"seq\");\n },\n\n async forget({ keepQueue }) {\n drop();\n // Its own forget() starts this engine's next session: it runs again where another\n // tab's forget() had ended it.\n forgotten = false;\n const epoch = freshEpoch();\n epochs.add(epoch);\n const tables = keepQueue ? [db.rows, db.meta] : db.tables;\n forgetting += 1;\n try {\n await db.transaction(\"rw\", tables, async () => {\n for (const table of tables) await table.clear();\n await db.meta.put({ key: EPOCH, value: epoch });\n });\n } finally {\n forgetting -= 1;\n }\n // Best-effort: the epoch in `meta` still stops another tab's next round. This engine's\n // own channel hears it too, and keeps running: the epoch is already one of its own.\n postOnce(across, { epoch });\n // Another tab's forget() met while this clear ran was left to the copy: one committed\n // before this clear was undone by it; one committed after it ended this session too.\n // (Its message is posted after its commit, so that commit is in the epoch read here.)\n const now = await readEpoch().catch(() => epoch);\n await (epochs.has(now) ? endSession() : forgetHere());\n },\n\n getState: () => state,\n\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n };\n return engine;\n}\n\n/**\n * The engine kept for `options.db` (by its name), built on first use: one engine per\n * database for the session (D15), so a company switch A→B→A finds A's engine again instead\n * of building a second one beside a round in flight. The first call's options stand: read\n * changing values (scopes, the client) through functions. A database object opened afresh\n * under the same name replaces the kept engine (the old one is stopped).\n */\nexport function syncEngineFor<D extends SyncDb = SyncDb>(options: SyncEngineOptions<D>): SyncEngine {\n const name = options.db.name;\n const found = kept.get(name);\n if (found && found.db === options.db) return found.engine;\n found?.engine.stop();\n const engine = createSyncEngine(options);\n kept.set(name, { db: options.db, engine });\n return engine;\n}\n\n/**\n * Let go of the engine {@link syncEngineFor} keeps for the database `name` (0.46): it is\n * stopped, its database closed and forgotten, so a later `syncEngineFor` builds afresh. Call it\n * on sign-out (and for a company left for good): every database visited in a tab otherwise\n * keeps its engine and an open IndexedDB connection until a reload. Nothing kept: a no-op.\n */\nexport function releaseSyncEngine(name: string): void {\n const found = kept.get(name);\n if (!found) return;\n kept.delete(name);\n found.engine.stop();\n found.db.close();\n}\n"],"mappings":"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","names":["kept","round"]}
1
+ {"version":3,"sources":["../../src/sync/engine.ts"],"sourcesContent":["import { listenOn, postOnce } from \"../lib/broadcast\";\nimport { httpStatusOf } from \"../lib/http-status\";\nimport { isOfflineError, stampAfter } from \"../offline/local-first\";\nimport { isSharedComputer } from \"../offline/shared-computer\";\nimport type { OutboxChange, OutboxStatus, SyncDb, SyncedRow } from \"./db\";\nimport type { SyncRequestBody, SyncResponseBody, SyncWireChange } from \"./wire\";\n\n/*\n * The sync engine (docs/local-first-harmonization.md §2.3): kastlan's handover client,\n * generalised (D10). Keksdose's SERVER rules and wire shapes; the client policies are ka's —\n * a new edit merges into the row's last `pending` upsert, a refused change is held per change\n * (`waiting` / `blocked` / `rejected`), a capped backoff on errors, the queue kept at sign-out.\n *\n * A round, per scope in `scopes()` order (read every round, D11): take the scope's queue and\n * mark it `sending`; post `{scope, last_knowledge, changes, refusals, client_now, paged?}`\n * with `client_now` read fresh (D2 amendment); apply the answer in ONE transaction, only if\n * the generation is unchanged (D15); follow `page_cursor` with pull-only requests, the cursor\n * written with the last page only (D12). Then the app's `steps`, then one more pass if a step\n * changed something. One round at a time; a request during a round runs once more after it.\n *\n * Across tabs (0.52; refactor audit 2026-10-30, FLW-1): every tab runs its own engine on the one\n * IndexedDB copy, and before 0.52 nothing stood between them — a second tab took the first's\n * `sending` changes as a closed tab's and sent them again, its failure laid them back so an edit\n * merged into a change already out and was deleted with it, and a sign-out in one tab was pulled\n * straight back from 0 by the other's poll. Now a round runs under a Web Lock named after the\n * database, so the rounds of all tabs take turns, first come first served; and `forget()`\n * writes a new epoch into `meta` with the clear, which every round reads, so another tab's\n * round drops its answer and that tab's engine stops.\n *\n * 0.55 (held back from 0.53): the wait shows. A round that finds the lock taken says so in\n * the state (`waitingForTab`) until its turn comes, and the indicator reads it as \"Waiting\n * for another tab\" — before, a tap on \"Sync now\" spun for as long as the other tab's round\n * took, with nothing to say why.\n */\n\nexport interface SyncCounts {\n pending: number;\n waiting: number;\n blocked: number;\n rejected: number;\n}\n\nexport interface SyncStepContext {\n signal: AbortSignal;\n /** False once `forget()` bumped the generation — in this tab, or (0.52) in another tab on\n * the same database, heard over a BroadcastChannel: the step writes nothing more (D15). */\n isCurrent(): boolean;\n}\n\nexport interface SyncStepResult {\n /** The step met no connection: the round ends offline. */\n offline?: boolean;\n /** One more push/pull in the same round (kastlan's second pass after its photos). */\n changed?: boolean;\n /** What the step keeps on the device, added to the engine's counts. */\n counts?: Partial<SyncCounts>;\n}\n\n/** Work after the push/pull, such as kastlan's photo upload. `D` is the app's database type\n * (`createSyncDb<HandoverDb>`), so a step reaches the app's own tables without a cast. */\nexport interface SyncStep<D extends SyncDb = SyncDb> {\n name: string;\n run(db: D, ctx: SyncStepContext): Promise<SyncStepResult>;\n /** Its counts read from the device between rounds (a photo taken offline shows at once). */\n counts?(db: D): Promise<Partial<SyncCounts>>;\n}\n\nexport interface SyncEngineState {\n phase: \"idle\" | \"syncing\" | \"offline\" | \"error\";\n online: boolean;\n counts: SyncCounts;\n /** When a scope's walk last completed (ISO), kept in `meta` (D9). */\n lastSyncedAt: string | null;\n /** When the next attempt after an error runs (epoch ms), or `null`. */\n nextRetryAt: number | null;\n /**\n * 0.55 (held back from 0.53): a round of this engine waits for the database's Web Lock,\n * which another tab's round holds (or other tabs' rounds wait for ahead of it). True from\n * the moment the round finds the lock taken until its turn comes, or until `stop()` or\n * `forget()` takes it out of the queue; `phase` is left as it was meanwhile (`syncing`\n * once the round runs). The kit's engine always sets it; optional, so a state built by\n * hand (a test's fake engine) still compiles.\n */\n waitingForTab?: boolean;\n}\n\nexport interface SyncRoundResult {\n /** `paused`: `paused()` held the round. (A round asked for while another tab's round holds\n * the database waits its turn, 0.52, with `waitingForTab` in the state meanwhile, 0.55.)\n * `dropped`: `forget()` ran during it, here or in another tab, its answer unused. */\n outcome: \"synced\" | \"offline\" | \"error\" | \"paused\" | \"dropped\";\n /** Changes this round set `waiting`, `blocked` or `rejected`. */\n waiting: number;\n blocked: number;\n rejected: number;\n error?: unknown;\n}\n\nexport interface SyncEngineOptions<D extends SyncDb = SyncDb> {\n /** The database; its type `D` is the one the `steps` get. */\n db: D;\n /** The scopes this session's role holds, in round order (kastlan: handover, meetings, meters,\n * maintenance, contacts). Read at the start of every round: roles are per company. */\n scopes: () => readonly string[];\n /** Posts one request (`POST /sync`) and answers its body; throws as the app's client does. */\n send: (body: SyncRequestBody, signal: AbortSignal) => Promise<SyncResponseBody>;\n /** Default {@link isOfflineError}: no answer, or the session refresh's passing 401. */\n isOffline?: (err: unknown) => boolean;\n /** A whole request refused for the plan → every sent change `waiting` with this code.\n * Default: a 402 whose body names a `code` (or `detail.code`). */\n planCode?: (err: unknown) => string | null;\n /** `refused.code`s kept as `waiting` and sent again every round; any other code is\n * `blocked`, sent again only on `retryBlocked()`. Default `[\"billing_read_only\"]`. */\n holdCodes?: readonly string[];\n /** Ask for a paged pull and follow its cursor (D12). */\n paged?: boolean;\n steps?: readonly SyncStep<D>[];\n /** No round while this holds (kastlan: a practice tour fakes the server's answers). */\n paused?: () => boolean;\n /** The poll (30 s) and the debounce after a write (800 ms). */\n pollMs?: number;\n soonMs?: number;\n /** The waits after consecutive errors; the last repeats (D13). */\n backoffMs?: readonly number[];\n /** Default `isSharedComputer`: on a computer marked shared the engine does not start (D15). */\n sharedComputer?: () => boolean;\n /**\n * 0.52: the backstop on one round, counted from the moment it holds the database's lock\n * (a tab waiting behind a stuck round is not cut with it). Past it the round is cut: its\n * requests and steps are aborted, anything they answer or write later is dropped, and the\n * round ends `error` with a `TimeoutError` (`DOMException`), not `offline`, so the backoff\n * starts and the lock goes to the next tab even when a request or a step ignores its\n * signal. The app's own request timeouts come first; this only bounds what they miss.\n * Default 10 minutes, long on purpose: kastlan's photo backlog can take minutes, and a cut\n * round has still made progress (each photo uploaded before the cut is off the queue).\n * `0` or `Infinity`: none.\n */\n roundTimeoutMs?: number;\n}\n\nexport interface SyncEngine {\n /** Into the copy at once and into the queue, in the scope the caller names (D11). An edit\n * merges into the row's last `pending` upsert; deleting a row that never reached the\n * server forgets its queue; stamped with `stampAfter`. */\n write(\n scope: string,\n entity: string,\n id: string,\n op: \"upsert\" | \"delete\",\n data?: Record<string, unknown>,\n ): Promise<void>;\n /** Starts the poll and the round on `online` (nothing on a computer marked shared).\n * An engine another tab's `forget()` stopped stays stopped through `start()`: only its\n * own `forget()` (the app's session end) or a new engine (`releaseSyncEngine`) re-arms\n * it. A start cannot tell a new sign-in from a remount in a tab whose session never\n * ended, and the latter would pull the signed-out account back into the wiped copy\n * (D15; the 0.52 code review asked, and the contract stands). */\n start(): void;\n /** Stops the poll and the timers, aborts the round in flight and drops its answer (the\n * queue is laid back). A new engine's round on the same database waits for it. */\n stop(): void;\n /** Sync soon (debounced): after a write. */\n requestSync(): void;\n /** \"Sync now\": resets the backoff, runs a round of its own after the one under way — another\n * tab's too (0.52) — and answers its result. */\n syncNow(): Promise<SyncRoundResult>;\n /** A round that sends the `blocked` changes too. */\n retryBlocked(): Promise<void>;\n /** Drop the rejected changes (of one scope) and pull that scope afresh. */\n discardRejected(scope?: string): Promise<void>;\n /** The rejected changes with their reasons, for the app's banner. */\n rejected(): Promise<OutboxChange[]>;\n /** Sign-out: bump the generation (a round in flight writes nothing more), then clear the\n * rows and `meta` — and the outbox and the app's tables unless `keepQueue` (D15). In every\n * tab (0.52): the clear writes a new epoch into `meta`, so a round of another tab's engine on\n * this database drops its answer, and that engine stops — the session it ran for has ended.\n * Another tab's `forget()` heard while this clear runs is decided by the epoch the copy is\n * left with: one that committed first was undone by this clear and stops nothing. */\n forget(opts: { keepQueue: boolean }): Promise<void>;\n getState(): SyncEngineState;\n subscribe(listener: () => void): () => void;\n}\n\nexport const DEFAULT_SYNC_BACKOFF_MS: readonly number[] = [5_000, 15_000, 60_000, 300_000];\nexport const DEFAULT_HOLD_CODES: readonly string[] = [\"billing_read_only\"];\n/** `roundTimeoutMs`'s default (see there). */\nconst ROUND_TIMEOUT_MS = 10 * 60_000;\n/** The longest delay `setTimeout` keeps (2^31 - 1 ms, ~24.8 days); a longer one fires at once. */\nconst MAX_TIMEOUT_MS = 2_147_483_647;\n\nconst LAST_SYNCED_AT = \"lastSyncedAt\";\n/** The copy's epoch in `meta` (0.52): a new one with every `forget()`, in its clear. */\nconst EPOCH = \"epoch\";\nconst lastKnowledgeKey = (scope: string) => `lastKnowledge:${scope}`;\nconst UNSENT: readonly OutboxStatus[] = [\"pending\", \"sending\", \"waiting\", \"blocked\"];\nconst ZERO: SyncCounts = { pending: 0, waiting: 0, blocked: 0, rejected: 0 };\n\n/** The started engine of each database: one engine per database (D15). */\nconst running = new Map<string, SyncEngine>();\n\n/** The round in flight on each database, of whichever engine: a new engine's round waits\n * for it, so two never take the same queue (a company switch A→B→A within one round). */\nconst flights = new Map<string, Promise<unknown>>();\n\n/** The engines {@link syncEngineFor} keeps, by database name. */\nconst kept = new Map<string, { db: SyncDb; engine: SyncEngine }>();\n\n/** The generation changed under a round: its answer is dropped. */\nclass Dropped extends Error {}\n\n/** The copy's epoch moved since this engine started: `forget()` ran in another tab. The\n * round's answer is dropped as well. */\nclass ForgottenElsewhere extends Dropped {}\n\n/** What a round's first ask for the lock answers when another tab's round holds it (0.55). */\nconst TAKEN = Symbol(\"taken\");\n\n/** The browser's Web Locks: a secure context (https, localhost), Safari 15.4+. */\nfunction webLocks(): LockManager | undefined {\n return typeof navigator === \"undefined\" ? undefined : (navigator as { locks?: LockManager }).locks;\n}\n\n/** A `forget()`'s epoch: unique enough to tell two apart, and on plain http too, where\n * `crypto.randomUUID` is missing (refactor audit 2026-10-30, DUP-7). */\nfunction freshEpoch(): string {\n const uuid = (globalThis.crypto as Partial<Crypto> | undefined)?.randomUUID;\n return uuid ? uuid.call(globalThis.crypto) : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;\n}\n\n/** A row created on this device and never echoed by the server carries `local: true`. */\ntype CopyRow = SyncedRow & { local?: boolean };\n\nfunction browserOnline(): boolean {\n return typeof navigator === \"undefined\" || navigator.onLine !== false;\n}\n\nfunction defaultPlanCode(err: unknown): string | null {\n if (httpStatusOf(err) !== 402) return null;\n const body = (err as { response?: { data?: { code?: unknown; detail?: { code?: unknown } } } }).response?.data;\n const code = typeof body?.code === \"string\" ? body.code : body?.detail?.code;\n return typeof code === \"string\" ? code : null;\n}\n\n/** A refusal's `code` and `detail` from an error's body: `{code, detail}` or FastAPI's\n * `{detail: {code, detail}}`. */\nfunction refusalOf(err: unknown): { code?: string; detail?: string } {\n const body = (err as { response?: { data?: unknown } }).response?.data as\n { code?: unknown; detail?: unknown } | undefined;\n const inner =\n body?.detail && typeof body.detail === \"object\" ? (body.detail as { code?: unknown; detail?: unknown }) : body;\n const text = (v: unknown) => (typeof v === \"string\" && v ? v : undefined);\n return { code: text(inner?.code), detail: text(inner?.detail) };\n}\n\nfunction latest(stamps: unknown[]): string | undefined {\n let best: string | undefined;\n for (const s of stamps) {\n if (typeof s !== \"string\" || !Number.isFinite(Date.parse(s))) continue;\n if (best === undefined || Date.parse(s) > Date.parse(best)) best = s;\n }\n return best;\n}\n\nconst toWire = ({ entity, id, op, data, updated_at }: OutboxChange): SyncWireChange =>\n op === \"delete\" ? { entity, id, op, updated_at } : { entity, id, op, data, updated_at };\n\n/**\n * The engine on one database (D15: one engine per database; a company switch builds a new\n * engine on the new database and stops the old one). Nothing runs until `start()`\n * (`useSyncWorker`), and `start()` does nothing on a computer marked shared.\n */\nexport function createSyncEngine<D extends SyncDb = SyncDb>(options: SyncEngineOptions<D>): SyncEngine {\n const { db } = options;\n const isOffline = options.isOffline ?? isOfflineError;\n const planCode = options.planCode ?? defaultPlanCode;\n const holdCodes = new Set(options.holdCodes ?? DEFAULT_HOLD_CODES);\n const steps = options.steps ?? [];\n const pollMs = options.pollMs ?? 30_000;\n const soonMs = options.soonMs ?? 800;\n const backoffMs = options.backoffMs?.length ? options.backoffMs : DEFAULT_SYNC_BACKOFF_MS;\n const sharedComputer = options.sharedComputer ?? isSharedComputer;\n const roundTimeoutMs = options.roundTimeoutMs ?? ROUND_TIMEOUT_MS;\n /** The rounds the deadline cut, by their signal: their late work lays nothing back. */\n const cut = new WeakSet<AbortSignal>();\n\n let generation = 0;\n let state: SyncEngineState = {\n phase: \"idle\",\n online: browserOnline(),\n counts: ZERO,\n lastSyncedAt: null,\n nextRetryAt: null,\n waitingForTab: false,\n };\n const listeners = new Set<() => void>();\n let started = false;\n let poll: ReturnType<typeof setInterval> | null = null;\n let soon: ReturnType<typeof setTimeout> | null = null;\n let retry: ReturnType<typeof setTimeout> | null = null;\n let failures = 0;\n let current: Promise<SyncRoundResult> | null = null;\n let again = false;\n let controller: AbortController | null = null;\n let stepCounts: Record<string, Partial<SyncCounts>> = {};\n\n // ── Across tabs (0.52, FLW-1) ───────────────────────────────────────────────\n /** The name of this database's Web Lock and BroadcastChannel (one origin's tabs share both,\n * as they share the IndexedDB). */\n const across = `eifi1-sync:${db.name}`;\n /** The copy's epochs this engine runs under: the one read when it started (or at its first\n * round), and each its own `forget()` wrote. Any other one means `forget()` ran elsewhere. */\n const epochs = new Set<string | null>();\n let epochRead: Promise<void> | null = null;\n /** `forget()` ran in another tab: this engine runs no round until its own `forget()`. */\n let forgotten = false;\n /** This engine's own `forget()`s whose clear is under way. */\n let forgetting = 0;\n let unlisten: (() => void) | null = null;\n\n function set(next: Partial<SyncEngineState>) {\n state = { ...state, ...next };\n listeners.forEach((listener) => listener());\n }\n\n /** The counts and `lastSyncedAt` read again from the device. Never throws: a closed or\n * deleted database leaves the last counts standing. */\n function refreshCounts(): Promise<void> {\n return readCounts().catch(() => undefined);\n }\n\n async function readCounts(): Promise<void> {\n const gen = generation;\n const count = (status: OutboxStatus) => db.outbox.where(\"status\").equals(status).count();\n const [pending, sending, waiting, blocked, rejected] = await Promise.all(\n ([\"pending\", \"sending\", \"waiting\", \"blocked\", \"rejected\"] as const).map(count),\n );\n const fromSteps = { ...stepCounts };\n for (const step of steps) if (step.counts) fromSteps[step.name] = await step.counts(db);\n const last = (await db.meta.get(LAST_SYNCED_AT))?.value;\n if (gen !== generation) return;\n if (Object.keys(fromSteps).length) stepCounts = fromSteps;\n const counts = { pending: pending + sending, waiting, blocked, rejected };\n for (const extra of Object.values(stepCounts)) {\n for (const key of Object.keys(counts) as (keyof SyncCounts)[]) counts[key] += extra[key] ?? 0;\n }\n set({ counts, lastSyncedAt: typeof last === \"string\" ? last : null });\n }\n\n function readEpoch(): Promise<string | null> {\n return db.meta.get(EPOCH).then((entry) => (typeof entry?.value === \"string\" ? entry.value : null));\n }\n\n /** Read once per engine — at `start()`, before its first round can wait on another tab's —\n * so a `forget()` that lands while it waits is seen as another tab's. A failed read is\n * tried again by the next round. */\n function captureEpoch(): Promise<void> {\n if (epochRead) return epochRead;\n const read = readEpoch().then((epoch) => void epochs.add(epoch));\n epochRead = read;\n read.catch(() => {\n if (epochRead === read) epochRead = null;\n });\n return read;\n }\n\n /** While started, hear `forget()` in another tab at once: a step in flight sees\n * `isCurrent()` turn false, the request in flight is aborted, and the poll stops. Without\n * BroadcastChannel the next round's read of the epoch stops it instead. */\n function listen() {\n unlisten ??= listenOn(across, (data) => {\n const epoch = (data as { epoch?: unknown } | null)?.epoch;\n if (typeof epoch === \"string\" && !epochs.has(epoch)) void forgetHere();\n });\n }\n\n /** `forget()` ran in another tab: the session this engine ran for has ended there. Its\n * round in flight writes nothing more, and it runs none of its own any more.\n *\n * Not while its own `forget()` clears the copy: that clear decides, by the epoch it leaves\n * in `meta`. 0.52 code review (src/sync/engine.ts): in kastlan's sign-out cascade tab A's\n * epoch reached tab B while B's own clear waited behind A's, so B's `forget()` resolved\n * with B stopped and forgotten — the forget that re-arms an engine left it dead. */\n function forgetHere(): Promise<void> {\n if (forgotten || forgetting > 0) return Promise.resolve();\n forgotten = true;\n if (started) halt();\n else drop();\n return endSession();\n }\n\n /** The round in flight, or waiting for the lock, writes nothing more: its wait and its\n * request are aborted, its answer dropped, the queue laid back. */\n function drop() {\n generation += 1;\n controller?.abort();\n }\n\n /** What the engine shows of a session that ended — by its own `forget()` or another tab's —\n * goes with it: the steps' counts, the last sync, an error and its retry. 0.52 code review\n * (src/sync/engine.ts): the two paths each reset part of it, and an engine another tab\n * stopped showed \"error\" for a retry that never came. */\n function endSession(): Promise<void> {\n stepCounts = {};\n failures = 0;\n clearRetry();\n set({ phase: \"idle\", lastSyncedAt: null, nextRetryAt: null });\n return refreshCounts();\n }\n\n /** The round's epoch still stands in `meta`, read in the caller's transaction, so another\n * tab's `forget()` commits wholly before it or after it. Else `forget()` ran: in this tab\n * (the generation moved too) or in another — then the engine stops. */\n async function standing(gen: number, epoch: string | null): Promise<void> {\n const now = await readEpoch();\n if (gen !== generation) throw new Dropped();\n if (now !== epoch) throw epochs.has(now) ? new Dropped() : new ForgottenElsewhere();\n }\n\n // ── The queue ──────────────────────────────────────────────────────────────\n\n /** Mark the scope's queue as in the request (a `sending` left by a closed tab goes back\n * first); answers it with the statuses it had. Under the Web Lock (0.52) a `sending` here\n * is a closed tab's, as this assumes: no other tab's round is out. Without Web Locks\n * (plain http) it may be another open tab's, as before 0.52.\n *\n * Only under the round's epoch. 0.52 code review (src/sync/engine.ts): after another tab's\n * `forget()` had landed mid-round, every scope left still posted the kept queue under the\n * session that had ended and pulled itself from 0, only for `apply` to drop the answer. */\n function takeQueue(scope: string, withBlocked: boolean, gen: number, epoch: string | null): Promise<OutboxChange[]> {\n return db.transaction(\"rw\", db.outbox, db.meta, async () => {\n await standing(gen, epoch);\n await db.outbox\n .where(\"scope\")\n .equals(scope)\n .and((c) => c.status === \"sending\")\n .modify({ status: \"pending\" });\n const take: OutboxStatus[] = withBlocked ? [\"pending\", \"waiting\", \"blocked\"] : [\"pending\", \"waiting\"];\n const queue = (await db.outbox.where(\"scope\").equals(scope).toArray())\n .filter((c) => take.includes(c.status))\n .sort((a, b) => a.seq! - b.seq!);\n await db.outbox.bulkUpdate(queue.map((c) => ({ key: c.seq!, changes: { status: \"sending\" as const } })));\n return queue;\n });\n }\n\n /** The request failed or its answer was dropped: each change back to the status it had.\n * Not by a round the deadline cut: its late work ends after the next round — this tab's\n * or another's — may have taken these changes `sending` again, and laid back `pending`\n * under that round an edit would merge into a change already out and be deleted with it\n * (FLW-1's lost edit). The next round's `takeQueue` takes them back instead, as a closed\n * tab's. */\n function layBack(sent: OutboxChange[], signal: AbortSignal) {\n if (cut.has(signal)) return;\n return db.outbox.bulkUpdate(sent.map((c) => ({ key: c.seq!, changes: { status: c.status } })));\n }\n\n function mark(changes: OutboxChange[], fields: Partial<OutboxChange>) {\n return db.outbox.bulkUpdate(changes.map((c) => ({ key: c.seq!, changes: fields })));\n }\n\n /** Put one page's rows in the copy, settle the sent changes, lay the unsent ones back. The\n * epoch is read in the same transaction: a `forget()` of another tab either commits before\n * it (this answer is dropped) or after it (and clears what it wrote). */\n async function apply(\n scope: string,\n answer: SyncResponseBody,\n sent: OutboxChange[],\n gen: number,\n epoch: string | null,\n lastPage: boolean,\n ) {\n return db.transaction(\"rw\", db.rows, db.outbox, db.meta, async () => {\n await standing(gen, epoch);\n const refused = answer.refused ?? null;\n const refusedIds = new Set(refused?.change_ids ?? []);\n const rejections = new Map((answer.rejections ?? []).map((r) => [r.change_id, r]));\n const kept = sent.filter((c) => refusedIds.has(c.id));\n const rejected = sent.filter((c) => !refusedIds.has(c.id) && rejections.has(c.id));\n await db.outbox.bulkDelete(sent.filter((c) => !refusedIds.has(c.id) && !rejections.has(c.id)).map((c) => c.seq!));\n const hold = refused != null && holdCodes.has(refused.code);\n if (refused)\n await mark(kept, { status: hold ? \"waiting\" : \"blocked\", code: refused.code, reason: refused.detail });\n for (const change of rejected) {\n const why = rejections.get(change.id)!;\n await db.outbox.update(change.seq!, { status: \"rejected\", code: why.code, reason: why.detail });\n }\n const pulled = new Set<string>();\n for (const change of answer.changes) {\n pulled.add(`${change.entity}\\u0000${change.id}`);\n if (change.op === \"delete\") await db.rows.delete([change.entity, change.id]);\n else {\n const row: CopyRow = {\n entity: change.entity,\n id: change.id,\n pk: change.pk ?? null,\n local: false,\n data: change.data ?? {},\n // The server's stamp rides beside `data`: kept, so the next edit is stamped after it.\n updated_at: change.updated_at ?? null,\n };\n await db.rows.put(row);\n }\n }\n // A pull never undoes what was typed while the request was out.\n const unsent = (await db.outbox.where(\"status\").anyOf(UNSENT).toArray())\n .filter((c) => pulled.has(`${c.entity}\\u0000${c.id}`))\n .sort((a, b) => a.seq! - b.seq!);\n for (const change of unsent) {\n if (change.op === \"delete\") {\n await db.rows.delete([change.entity, change.id]);\n continue;\n }\n const row = (await db.rows.get([change.entity, change.id])) as CopyRow | undefined;\n await db.rows.put({\n ...row,\n entity: change.entity,\n id: change.id,\n pk: row?.pk ?? null,\n data: { ...row?.data, ...change.data },\n });\n }\n if (lastPage) {\n await db.meta.put({ key: lastKnowledgeKey(scope), value: answer.server_knowledge });\n await db.meta.put({ key: LAST_SYNCED_AT, value: new Date().toISOString() });\n }\n return {\n waiting: hold ? kept.length : 0,\n blocked: refused && !hold ? kept.length : 0,\n rejected: rejected.length,\n };\n });\n }\n\n /** A scope the role no longer holds (403): its queue is rejected and it leaves the round.\n * The server's `code` and `detail` are kept (a demo's `demo_read_only`); without them,\n * `forbidden` and \"Not allowed to sync <scope>\". */\n async function loseScope(scope: string, err: unknown): Promise<number> {\n const why = refusalOf(err);\n return db.transaction(\"rw\", db.outbox, async () => {\n const queue = (await db.outbox.where(\"scope\").equals(scope).toArray()).filter((c) => UNSENT.includes(c.status));\n await mark(queue, {\n status: \"rejected\",\n code: why.code ?? \"forbidden\",\n reason: why.code && why.detail ? why.detail : `Not allowed to sync ${scope}`,\n });\n return queue.length;\n });\n }\n\n type ScopeResult = { offline: true } | { offline: false; waiting: number; blocked: number; rejected: number };\n\n async function syncScope(\n scope: string,\n gen: number,\n epoch: string | null,\n signal: AbortSignal,\n withBlocked: boolean,\n ): Promise<ScopeResult> {\n const sent = await takeQueue(scope, withBlocked, gen, epoch);\n const last = Number((await db.meta.get(lastKnowledgeKey(scope)))?.value ?? 0) || 0;\n const body = (changes: SyncWireChange[], page_cursor: SyncRequestBody[\"page_cursor\"]): SyncRequestBody => ({\n scope,\n last_knowledge: last,\n changes,\n refusals: true,\n client_now: new Date().toISOString(),\n ...(options.paged ? { paged: true, page_cursor } : {}),\n });\n let answer: SyncResponseBody;\n try {\n answer = await options.send(body(sent.map(toWire), null), signal);\n } catch (err) {\n await layBack(sent, signal);\n if (gen !== generation) throw new Dropped();\n if (isOffline(err)) return { offline: true };\n const plan = planCode(err);\n if (!plan && httpStatusOf(err) !== 403) throw err;\n // A refusal is the answer, kept only under the round's epoch as `apply` keeps one. 0.52\n // code review (src/sync/engine.ts): a 403 heard after another tab's `forget()` marked\n // the kept queue `rejected`, never sent again, for a session that had ended.\n return db.transaction(\"rw\", db.outbox, db.meta, async (): Promise<ScopeResult> => {\n await standing(gen, epoch);\n if (plan) {\n // The whole request refused for the plan: every change waits for it.\n await mark(sent, { status: \"waiting\", code: plan, reason: plan });\n return { offline: false, waiting: sent.length, blocked: 0, rejected: 0 };\n }\n return { offline: false, waiting: 0, blocked: 0, rejected: await loseScope(scope, err) };\n });\n }\n let cursor = options.paged ? (answer.page_cursor ?? null) : null;\n let tally: ScopeResult;\n try {\n tally = { offline: false, ...(await apply(scope, answer, sent, gen, epoch, cursor == null)) };\n } catch (err) {\n if (err instanceof Dropped) await layBack(sent, signal);\n throw err;\n }\n // The rest of a paged pull: pull-only; the cursor is written with the last page alone,\n // so an interrupted walk starts again from the old one (D12).\n while (cursor) {\n let page: SyncResponseBody;\n try {\n page = await options.send(body([], cursor), signal);\n } catch (err) {\n if (gen !== generation) throw new Dropped();\n if (isOffline(err)) return { offline: true };\n throw err;\n }\n cursor = page.page_cursor ?? null;\n await apply(scope, page, [], gen, epoch, cursor == null);\n }\n return tally;\n }\n\n // ── The round ──────────────────────────────────────────────────────────────\n\n async function round(withBlocked: boolean, gen: number): Promise<SyncRoundResult> {\n const result: SyncRoundResult = { outcome: \"synced\", waiting: 0, blocked: 0, rejected: 0 };\n if (gen !== generation || forgotten) return { ...result, outcome: \"dropped\" };\n // Aborted by stop() and forget(): the wait for the lock, then the round's requests.\n const own = new AbortController();\n controller = own;\n try {\n const locks = webLocks();\n if (!locks) return await begin(result, withBlocked, gen, own);\n let granted = false;\n const turn = () => {\n granted = true;\n if (state.waitingForTab) set({ waitingForTab: false });\n return begin(result, withBlocked, gen, own);\n };\n try {\n // 0.55 (held back from 0.53): asked first without waiting, only to learn whether\n // another tab's round holds the lock (or waits for it ahead of this one), so the\n // state can say so while this round waits. Free, the round runs under this ask.\n // No `signal`: the spec refuses one beside `ifAvailable`, and this ask never waits.\n const now = await locks.request(across, { ifAvailable: true }, (lock) => (lock ? turn() : TAKEN));\n if (now !== TAKEN) return now;\n if (gen === generation) set({ waitingForTab: true });\n // Queued, first come first served: the rounds of every tab on this database take\n // turns, and one asked for meanwhile waits for its turn (`current` and `again` keep\n // one per engine). 0.52 code review (src/sync/engine.ts): an `ifAvailable` round\n // that found the lock taken answered `paused` at once, so the sign-out dialog's\n // \"Sync now\" closed as if it had synced; its one deferred round lost a\n // `retryBlocked()`'s `blocked`; and two tabs' waits refused each other's next try —\n // the spec refuses `ifAvailable` while any request is queued. (The ask above only\n // tells; a refused one queues here.) A closed tab's lock is released with it.\n return await locks.request(across, { signal: own.signal }, turn);\n } catch (err) {\n // `begin` throws only what the app's `paused()` throws. A wait aborted by stop() or\n // forget() is dropped there; a lock manager that refuses (an opaque origin, a\n // document no longer active) runs the round as before 0.52.\n if (granted) return { ...result, outcome: \"error\", error: err };\n return await begin(result, withBlocked, gen, own);\n }\n } finally {\n // A wait taken out of the queue (stop(), forget()) or refused waits no more.\n if (state.waitingForTab) set({ waitingForTab: false });\n if (controller === own) controller = null;\n }\n }\n\n /** The round begins — under the lock, where there is one, after however long it waited:\n * the decision point, and the start of its deadline. */\n async function begin(\n result: SyncRoundResult,\n withBlocked: boolean,\n gen: number,\n own: AbortController,\n ): Promise<SyncRoundResult> {\n if (gen !== generation || forgotten) return { ...result, outcome: \"dropped\" };\n if (options.paused?.()) return { ...result, outcome: \"paused\" };\n if (!browserOnline()) return { ...result, outcome: \"offline\" };\n return withDeadline(locked(result, withBlocked, gen, own.signal), result, gen, own);\n }\n\n /** The round, or its cut at `roundTimeoutMs`, whichever comes first. 0.52 code review,\n * finding 3 (Marcel: \"kit backstop + app timeouts\"): a round holds the lock of every tab\n * on its database, and one whose request never settles (axios has no default timeout) or\n * whose step never does stopped sync in all of them until that tab was closed. The cut\n * settles the round by the race, so the lock is released even when the app ignores the\n * abort; the generation moves, so what the cut work answers or writes later is dropped. */\n function withDeadline(\n round: Promise<SyncRoundResult>,\n result: SyncRoundResult,\n gen: number,\n own: AbortController,\n ): Promise<SyncRoundResult> {\n if (!(roundTimeoutMs > 0 && Number.isFinite(roundTimeoutMs))) return round;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const deadline = new Promise<SyncRoundResult>((resolve) => {\n timer = setTimeout(\n () => {\n const error = new DOMException(`The sync round ran past ${roundTimeoutMs} ms`, \"TimeoutError\");\n // Settled first: the race ends on the cut, whatever the abort below sets off.\n resolve({ ...result, outcome: \"error\", error });\n cut.add(own.signal);\n if (gen === generation) generation += 1;\n own.abort(error);\n },\n Math.min(roundTimeoutMs, MAX_TIMEOUT_MS),\n );\n });\n return Promise.race([round, deadline]).finally(() => clearTimeout(timer));\n }\n\n /** The round itself. Never throws. */\n async function locked(\n result: SyncRoundResult,\n withBlocked: boolean,\n gen: number,\n signal: AbortSignal,\n ): Promise<SyncRoundResult> {\n try {\n await captureEpoch();\n const epoch = await readEpoch();\n if (gen !== generation) throw new Dropped();\n if (!epochs.has(epoch)) throw new ForgottenElsewhere();\n set({ phase: \"syncing\" });\n for (let pass = 0; ; pass++) {\n for (const scope of options.scopes()) {\n if (gen !== generation) throw new Dropped();\n const r = await syncScope(scope, gen, epoch, signal, withBlocked);\n if (r.offline) return { ...result, outcome: \"offline\" };\n result.waiting += r.waiting;\n result.blocked += r.blocked;\n result.rejected += r.rejected;\n }\n if (pass > 0) break;\n let changed = false;\n for (const step of steps) {\n if (gen !== generation) throw new Dropped();\n const ran = await step.run(db, { signal, isCurrent: () => gen === generation });\n if (gen !== generation) throw new Dropped();\n if (ran.counts) stepCounts = { ...stepCounts, [step.name]: ran.counts };\n if (ran.offline) return { ...result, outcome: \"offline\" };\n if (ran.changed) changed = true;\n }\n if (!changed) break;\n }\n return result;\n } catch (err) {\n if (err instanceof ForgottenElsewhere) void forgetHere();\n if (err instanceof Dropped || gen !== generation) return { ...result, outcome: \"dropped\" };\n return { ...result, outcome: \"error\", error: err };\n }\n }\n\n function clearRetry() {\n if (retry) clearTimeout(retry);\n retry = null;\n }\n\n function resetBackoff() {\n failures = 0;\n clearRetry();\n if (state.nextRetryAt !== null) set({ nextRetryAt: null });\n }\n\n const inBackoff = () => state.nextRetryAt !== null && Date.now() < state.nextRetryAt;\n\n function settle(result: SyncRoundResult) {\n switch (result.outcome) {\n case \"synced\":\n failures = 0;\n clearRetry();\n set({ phase: \"idle\", online: true, nextRetryAt: null });\n break;\n case \"offline\":\n // No retry timer offline: the `online` event or the poll brings the next round (D13).\n clearRetry();\n set({ phase: \"offline\", online: false, nextRetryAt: null });\n break;\n case \"error\": {\n const wait = backoffMs[Math.min(failures, backoffMs.length - 1)];\n failures += 1;\n clearRetry();\n if (started) {\n retry = setTimeout(() => {\n retry = null;\n void run(false);\n }, wait);\n }\n set({ phase: \"error\", nextRetryAt: started ? Date.now() + wait : null });\n break;\n }\n case \"dropped\":\n set({ phase: \"idle\" });\n break;\n case \"paused\":\n break;\n }\n }\n\n function run(withBlocked: boolean): Promise<SyncRoundResult> {\n if (current) {\n again = true;\n return current;\n }\n const before = flights.get(db.name);\n // The generation is read before any wait: a stop() while this waits drops the round, so\n // it never sends this copy's queue with another account's client nor pulls into it.\n const gen = generation;\n const flight = (async () => {\n // Another engine's round on this database (stopped, still settling) ends first.\n if (before) await before.catch(() => undefined);\n try {\n const result = await round(withBlocked, gen);\n settle(result);\n await refreshCounts();\n return result;\n } finally {\n current = null;\n if (again) {\n again = false;\n tick();\n }\n }\n })();\n current = flight;\n flights.set(db.name, flight);\n const done = () => {\n if (flights.get(db.name) === flight) flights.delete(db.name);\n };\n void flight.then(done, done);\n return flight;\n }\n\n /** A round from the poll, a write or a request during a round: not while backing off. */\n function tick() {\n if (started && !inBackoff()) void run(false);\n }\n\n /** A round of its own, begun after the one under way — and after one that a request made\n * meanwhile started as that one ended — so `retryBlocked()` never joins a round that\n * leaves `blocked` out (0.52 code review, src/sync/engine.ts). */\n async function runFresh(withBlocked: boolean): Promise<SyncRoundResult> {\n resetBackoff();\n while (current) {\n again = false;\n await current.catch(() => undefined);\n }\n return run(withBlocked);\n }\n\n const onOnline = () => {\n set({ online: true });\n resetBackoff();\n void run(false);\n };\n const onOffline = () => set({ online: false });\n\n /** `stop()`, and the end `forgetHere` puts to a started engine. */\n function halt() {\n if (!started) return;\n started = false;\n // The next engine's round waits for the dropped one to settle.\n drop();\n if (running.get(db.name) === engine) running.delete(db.name);\n if (poll) clearInterval(poll);\n poll = null;\n if (soon) clearTimeout(soon);\n soon = null;\n clearRetry();\n if (typeof window !== \"undefined\") {\n window.removeEventListener(\"online\", onOnline);\n window.removeEventListener(\"offline\", onOffline);\n }\n unlisten?.();\n unlisten = null;\n }\n\n const engine: SyncEngine = {\n async write(scope, entity, id, op, data = {}) {\n await db.transaction(\"rw\", db.rows, db.outbox, async () => {\n const row = (await db.rows.get([entity, id])) as CopyRow | undefined;\n const ofRow = await db.outbox.where(\"[entity+id]\").equals([entity, id]).toArray();\n ofRow.sort((a, b) => a.seq! - b.seq!);\n const updated_at = stampAfter(\n latest([row?.updated_at, row?.data.updated_at, ...ofRow.map((c) => c.updated_at)]),\n );\n // A new edit to the row moves a rejected change out of the way (D13).\n const rejected = ofRow.filter((c) => c.scope === scope && c.status === \"rejected\");\n if (op === \"delete\") {\n await db.rows.delete([entity, id]);\n const neverSynced = row != null && (row.local ?? row.pk == null);\n if (neverSynced && !ofRow.some((c) => c.status === \"sending\")) {\n // Never reached the server: forgetting its queue is enough — a waiting change\n // included, or it would create the row once the plan is back.\n await db.outbox.bulkDelete(ofRow.map((c) => c.seq!));\n return;\n }\n await db.outbox.bulkDelete(rejected.map((c) => c.seq!));\n await db.outbox.add({ scope, entity, id, op, data: {}, updated_at, status: \"pending\" });\n return;\n }\n const next: CopyRow = { ...row, entity, id, pk: row?.pk ?? null, data: { ...row?.data, ...data } };\n if (!row) next.local = true;\n await db.rows.put(next);\n const carried = Object.assign({}, ...rejected.filter((c) => c.op === \"upsert\").map((c) => c.data)) as Record<\n string,\n unknown\n >;\n await db.outbox.bulkDelete(rejected.map((c) => c.seq!));\n const last = ofRow.filter((c) => c.scope === scope && c.status === \"pending\").at(-1);\n if (last && last.op === \"upsert\") {\n // Several edits before a round go up as one change, with the last one's time.\n await db.outbox.update(last.seq!, { data: { ...carried, ...last.data, ...data }, updated_at });\n } else {\n await db.outbox.add({ scope, entity, id, op, data: { ...carried, ...data }, updated_at, status: \"pending\" });\n }\n });\n engine.requestSync();\n },\n\n start() {\n if (started || forgotten || sharedComputer()) return;\n const other = running.get(db.name);\n if (other && other !== engine) other.stop();\n running.set(db.name, engine);\n started = true;\n captureEpoch().catch(() => undefined);\n listen();\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"online\", onOnline);\n window.addEventListener(\"offline\", onOffline);\n }\n poll = setInterval(tick, pollMs);\n void refreshCounts();\n void run(false);\n },\n\n stop: halt,\n\n requestSync() {\n void refreshCounts();\n if (!started) return;\n if (soon) clearTimeout(soon);\n soon = setTimeout(() => {\n soon = null;\n tick();\n }, soonMs);\n },\n\n syncNow: () => runFresh(false),\n\n async retryBlocked() {\n await runFresh(true);\n },\n\n async discardRejected(scope) {\n const gone = (await db.outbox.where(\"status\").equals(\"rejected\").toArray()).filter(\n (c) => !scope || c.scope === scope,\n );\n if (gone.length === 0) return;\n await db.transaction(\"rw\", db.rows, db.outbox, db.meta, async () => {\n await db.outbox.bulkDelete(gone.map((c) => c.seq!));\n for (const change of gone) {\n const others = await db.outbox.where(\"[entity+id]\").equals([change.entity, change.id]).count();\n if (others === 0) await db.rows.delete([change.entity, change.id]);\n }\n // The scope pulls afresh, so the copy shows what the server holds.\n for (const s of new Set(gone.map((c) => c.scope))) await db.meta.put({ key: lastKnowledgeKey(s), value: 0 });\n });\n await refreshCounts();\n engine.requestSync();\n },\n\n async rejected() {\n return db.outbox.where(\"status\").equals(\"rejected\").sortBy(\"seq\");\n },\n\n async forget({ keepQueue }) {\n drop();\n // Its own forget() starts this engine's next session: it runs again where another\n // tab's forget() had ended it.\n forgotten = false;\n const epoch = freshEpoch();\n epochs.add(epoch);\n const tables = keepQueue ? [db.rows, db.meta] : db.tables;\n forgetting += 1;\n try {\n await db.transaction(\"rw\", tables, async () => {\n for (const table of tables) await table.clear();\n await db.meta.put({ key: EPOCH, value: epoch });\n });\n } finally {\n forgetting -= 1;\n }\n // Best-effort: the epoch in `meta` still stops another tab's next round. This engine's\n // own channel hears it too, and keeps running: the epoch is already one of its own.\n postOnce(across, { epoch });\n // Another tab's forget() met while this clear ran was left to the copy: one committed\n // before this clear was undone by it; one committed after it ended this session too.\n // (Its message is posted after its commit, so that commit is in the epoch read here.)\n const now = await readEpoch().catch(() => epoch);\n await (epochs.has(now) ? endSession() : forgetHere());\n },\n\n getState: () => state,\n\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n };\n return engine;\n}\n\n/**\n * The engine kept for `options.db` (by its name), built on first use: one engine per\n * database for the session (D15), so a company switch A→B→A finds A's engine again instead\n * of building a second one beside a round in flight. The first call's options stand: read\n * changing values (scopes, the client) through functions. A database object opened afresh\n * under the same name replaces the kept engine (the old one is stopped).\n */\nexport function syncEngineFor<D extends SyncDb = SyncDb>(options: SyncEngineOptions<D>): SyncEngine {\n const name = options.db.name;\n const found = kept.get(name);\n if (found && found.db === options.db) return found.engine;\n found?.engine.stop();\n const engine = createSyncEngine(options);\n kept.set(name, { db: options.db, engine });\n return engine;\n}\n\n/**\n * Let go of the engine {@link syncEngineFor} keeps for the database `name` (0.46): it is\n * stopped, its database closed and forgotten, so a later `syncEngineFor` builds afresh. Call it\n * on sign-out (and for a company left for good): every database visited in a tab otherwise\n * keeps its engine and an open IndexedDB connection until a reload. Nothing kept: a no-op.\n */\nexport function releaseSyncEngine(name: string): void {\n const found = kept.get(name);\n if (!found) return;\n kept.delete(name);\n found.engine.stop();\n found.db.close();\n}\n"],"mappings":"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","names":["kept","round"]}
@@ -11,8 +11,9 @@ export declare function useSyncWorker(engine: SyncEngine | null): void;
11
11
  /**
12
12
  * The engine's state as the kit's `SyncStatusIndicator` reads it (§2.3): `pending` = queued
13
13
  * and in flight (and the steps' pending), `waitingForPlan` = `waiting`, `blocked` = `blocked`
14
- * + `rejected`, `lastSyncedAt` from `meta`. `onSync` is "Sync now". Without an engine, an
15
- * idle status that follows the browser's connection.
14
+ * + `rejected`, `lastSyncedAt` from `meta`, and (0.55) `waitingForTab` as the engine has it —
15
+ * a round waiting for another tab's. `onSync` is "Sync now". Without an engine, an idle
16
+ * status that follows the browser's connection.
16
17
  *
17
18
  * With a `null` engine (signed out, or a computer marked shared) `status.online` follows
18
19
  * `navigator.onLine` and its `online`/`offline` events, so an app may lean on
@@ -13,7 +13,8 @@ const IDLE = {
13
13
  online: true,
14
14
  counts: { pending: 0, waiting: 0, blocked: 0, rejected: 0 },
15
15
  lastSyncedAt: null,
16
- nextRetryAt: null
16
+ nextRetryAt: null,
17
+ waitingForTab: false
17
18
  };
18
19
  const noEngine = () => () => void 0;
19
20
  const idle = () => IDLE;
@@ -33,7 +34,8 @@ function useSyncStatus(engine) {
33
34
  pending: state.counts.pending,
34
35
  waitingForPlan: state.counts.waiting,
35
36
  blocked: state.counts.blocked + state.counts.rejected,
36
- lastSyncedAt: state.lastSyncedAt
37
+ lastSyncedAt: state.lastSyncedAt,
38
+ waitingForTab: state.waitingForTab
37
39
  };
38
40
  return { status, onSync };
39
41
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/sync/hooks.ts"],"sourcesContent":["import { useCallback, useEffect, useState, useSyncExternalStore } from \"react\";\nimport { liveQuery } from \"dexie\";\nimport type { SyncStatus } from \"../shell/sync-status-indicator\";\nimport type { SyncDb } from \"./db\";\nimport type { SyncEngine, SyncEngineState } from \"./engine\";\n\n/**\n * Keep `engine` running while it is mounted: started on mount, stopped on unmount or when\n * the engine changes (a company switch builds a new engine on its own database, D15).\n * `null` runs nothing — signed out, or a computer marked shared. Mount once, in the\n * signed-in layout (kastlan's `useHandoverSyncLoop`).\n */\nexport function useSyncWorker(engine: SyncEngine | null): void {\n useEffect(() => {\n if (!engine) return;\n engine.start();\n return () => engine.stop();\n }, [engine]);\n}\n\nconst IDLE: SyncEngineState = {\n phase: \"idle\",\n online: true,\n counts: { pending: 0, waiting: 0, blocked: 0, rejected: 0 },\n lastSyncedAt: null,\n nextRetryAt: null,\n};\nconst noEngine = () => () => undefined;\nconst idle = () => IDLE;\n\n/**\n * The engine's state as the kit's `SyncStatusIndicator` reads it (§2.3): `pending` = queued\n * and in flight (and the steps' pending), `waitingForPlan` = `waiting`, `blocked` = `blocked`\n * + `rejected`, `lastSyncedAt` from `meta`. `onSync` is \"Sync now\". Without an engine, an\n * idle status that follows the browser's connection.\n *\n * With a `null` engine (signed out, or a computer marked shared) `status.online` follows\n * `navigator.onLine` and its `online`/`offline` events, so an app may lean on\n * `status.online` for its own offline lock (kastlan's offline write lock) whether or not an\n * engine runs.\n */\nexport function useSyncStatus(engine: SyncEngine | null): { status: SyncStatus; onSync: () => Promise<void> } {\n const state = useSyncExternalStore(\n engine ? engine.subscribe : noEngine,\n engine ? engine.getState : idle,\n engine ? engine.getState : idle,\n );\n const browserOnline = useSyncExternalStore(subscribeOnline, readOnline, () => true);\n const onSync = useCallback(async () => {\n await engine?.syncNow();\n }, [engine]);\n const status: SyncStatus = {\n phase: state.phase,\n online: engine ? state.online : browserOnline,\n pending: state.counts.pending,\n waitingForPlan: state.counts.waiting,\n blocked: state.counts.blocked + state.counts.rejected,\n lastSyncedAt: state.lastSyncedAt,\n };\n return { status, onSync };\n}\n\nfunction subscribeOnline(listener: () => void) {\n window.addEventListener(\"online\", listener);\n window.addEventListener(\"offline\", listener);\n return () => {\n window.removeEventListener(\"online\", listener);\n window.removeEventListener(\"offline\", listener);\n };\n}\nconst readOnline = () => navigator.onLine !== false;\n\n/**\n * A read of the copy that follows every change to it (Dexie's `liveQuery`; kastlan's\n * `useLive`). `deps` names what `query` reads; `query` itself is rebuilt every render.\n * `query` gets the app's own database type (`HandoverDb`). Without a database (not open\n * yet) it answers `isLoading: true`, so a page shows no \"empty\" before the copy opens.\n */\nexport function useSyncQuery<T, D extends SyncDb = SyncDb>(\n db: D | null,\n query: (db: D) => Promise<T>,\n deps: readonly unknown[],\n): { data: T | undefined; isLoading: boolean } {\n const [state, setState] = useState<{ data: T | undefined; isLoading: boolean }>({ data: undefined, isLoading: true });\n useEffect(() => {\n if (!db) return;\n const subscription = liveQuery(() => query(db)).subscribe({\n next: (data) => setState({ data, isLoading: false }),\n error: () => setState({ data: undefined, isLoading: false }),\n });\n return () => subscription.unsubscribe();\n // `query` is rebuilt every render; `deps` names what it reads.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [db, ...deps]);\n return db ? state : NO_COPY;\n}\n\nconst NO_COPY = { data: undefined, isLoading: true } as const;\n"],"mappings":";AAAA,SAAS,aAAa,WAAW,UAAU,4BAA4B;AACvE,SAAS,iBAAiB;AAWnB,SAAS,cAAc,QAAiC;AAC7D,YAAU,MAAM;AACd,QAAI,CAAC,OAAQ;AACb,WAAO,MAAM;AACb,WAAO,MAAM,OAAO,KAAK;AAAA,EAC3B,GAAG,CAAC,MAAM,CAAC;AACb;AAEA,MAAM,OAAwB;AAAA,EAC5B,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,QAAQ,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,UAAU,EAAE;AAAA,EAC1D,cAAc;AAAA,EACd,aAAa;AACf;AACA,MAAM,WAAW,MAAM,MAAM;AAC7B,MAAM,OAAO,MAAM;AAaZ,SAAS,cAAc,QAAgF;AAC5G,QAAM,QAAQ;AAAA,IACZ,SAAS,OAAO,YAAY;AAAA,IAC5B,SAAS,OAAO,WAAW;AAAA,IAC3B,SAAS,OAAO,WAAW;AAAA,EAC7B;AACA,QAAM,gBAAgB,qBAAqB,iBAAiB,YAAY,MAAM,IAAI;AAClF,QAAM,SAAS,YAAY,YAAY;AACrC,UAAM,QAAQ,QAAQ;AAAA,EACxB,GAAG,CAAC,MAAM,CAAC;AACX,QAAM,SAAqB;AAAA,IACzB,OAAO,MAAM;AAAA,IACb,QAAQ,SAAS,MAAM,SAAS;AAAA,IAChC,SAAS,MAAM,OAAO;AAAA,IACtB,gBAAgB,MAAM,OAAO;AAAA,IAC7B,SAAS,MAAM,OAAO,UAAU,MAAM,OAAO;AAAA,IAC7C,cAAc,MAAM;AAAA,EACtB;AACA,SAAO,EAAE,QAAQ,OAAO;AAC1B;AAEA,SAAS,gBAAgB,UAAsB;AAC7C,SAAO,iBAAiB,UAAU,QAAQ;AAC1C,SAAO,iBAAiB,WAAW,QAAQ;AAC3C,SAAO,MAAM;AACX,WAAO,oBAAoB,UAAU,QAAQ;AAC7C,WAAO,oBAAoB,WAAW,QAAQ;AAAA,EAChD;AACF;AACA,MAAM,aAAa,MAAM,UAAU,WAAW;AAQvC,SAAS,aACd,IACA,OACA,MAC6C;AAC7C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAsD,EAAE,MAAM,QAAW,WAAW,KAAK,CAAC;AACpH,YAAU,MAAM;AACd,QAAI,CAAC,GAAI;AACT,UAAM,eAAe,UAAU,MAAM,MAAM,EAAE,CAAC,EAAE,UAAU;AAAA,MACxD,MAAM,CAAC,SAAS,SAAS,EAAE,MAAM,WAAW,MAAM,CAAC;AAAA,MACnD,OAAO,MAAM,SAAS,EAAE,MAAM,QAAW,WAAW,MAAM,CAAC;AAAA,IAC7D,CAAC;AACD,WAAO,MAAM,aAAa,YAAY;AAAA,EAGxC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;AAChB,SAAO,KAAK,QAAQ;AACtB;AAEA,MAAM,UAAU,EAAE,MAAM,QAAW,WAAW,KAAK;","names":[]}
1
+ {"version":3,"sources":["../../src/sync/hooks.ts"],"sourcesContent":["import { useCallback, useEffect, useState, useSyncExternalStore } from \"react\";\nimport { liveQuery } from \"dexie\";\nimport type { SyncStatus } from \"../shell/sync-status-indicator\";\nimport type { SyncDb } from \"./db\";\nimport type { SyncEngine, SyncEngineState } from \"./engine\";\n\n/**\n * Keep `engine` running while it is mounted: started on mount, stopped on unmount or when\n * the engine changes (a company switch builds a new engine on its own database, D15).\n * `null` runs nothing — signed out, or a computer marked shared. Mount once, in the\n * signed-in layout (kastlan's `useHandoverSyncLoop`).\n */\nexport function useSyncWorker(engine: SyncEngine | null): void {\n useEffect(() => {\n if (!engine) return;\n engine.start();\n return () => engine.stop();\n }, [engine]);\n}\n\nconst IDLE: SyncEngineState = {\n phase: \"idle\",\n online: true,\n counts: { pending: 0, waiting: 0, blocked: 0, rejected: 0 },\n lastSyncedAt: null,\n nextRetryAt: null,\n waitingForTab: false,\n};\nconst noEngine = () => () => undefined;\nconst idle = () => IDLE;\n\n/**\n * The engine's state as the kit's `SyncStatusIndicator` reads it (§2.3): `pending` = queued\n * and in flight (and the steps' pending), `waitingForPlan` = `waiting`, `blocked` = `blocked`\n * + `rejected`, `lastSyncedAt` from `meta`, and (0.55) `waitingForTab` as the engine has it —\n * a round waiting for another tab's. `onSync` is \"Sync now\". Without an engine, an idle\n * status that follows the browser's connection.\n *\n * With a `null` engine (signed out, or a computer marked shared) `status.online` follows\n * `navigator.onLine` and its `online`/`offline` events, so an app may lean on\n * `status.online` for its own offline lock (kastlan's offline write lock) whether or not an\n * engine runs.\n */\nexport function useSyncStatus(engine: SyncEngine | null): { status: SyncStatus; onSync: () => Promise<void> } {\n const state = useSyncExternalStore(\n engine ? engine.subscribe : noEngine,\n engine ? engine.getState : idle,\n engine ? engine.getState : idle,\n );\n const browserOnline = useSyncExternalStore(subscribeOnline, readOnline, () => true);\n const onSync = useCallback(async () => {\n await engine?.syncNow();\n }, [engine]);\n const status: SyncStatus = {\n phase: state.phase,\n online: engine ? state.online : browserOnline,\n pending: state.counts.pending,\n waitingForPlan: state.counts.waiting,\n blocked: state.counts.blocked + state.counts.rejected,\n lastSyncedAt: state.lastSyncedAt,\n waitingForTab: state.waitingForTab,\n };\n return { status, onSync };\n}\n\nfunction subscribeOnline(listener: () => void) {\n window.addEventListener(\"online\", listener);\n window.addEventListener(\"offline\", listener);\n return () => {\n window.removeEventListener(\"online\", listener);\n window.removeEventListener(\"offline\", listener);\n };\n}\nconst readOnline = () => navigator.onLine !== false;\n\n/**\n * A read of the copy that follows every change to it (Dexie's `liveQuery`; kastlan's\n * `useLive`). `deps` names what `query` reads; `query` itself is rebuilt every render.\n * `query` gets the app's own database type (`HandoverDb`). Without a database (not open\n * yet) it answers `isLoading: true`, so a page shows no \"empty\" before the copy opens.\n */\nexport function useSyncQuery<T, D extends SyncDb = SyncDb>(\n db: D | null,\n query: (db: D) => Promise<T>,\n deps: readonly unknown[],\n): { data: T | undefined; isLoading: boolean } {\n const [state, setState] = useState<{ data: T | undefined; isLoading: boolean }>({ data: undefined, isLoading: true });\n useEffect(() => {\n if (!db) return;\n const subscription = liveQuery(() => query(db)).subscribe({\n next: (data) => setState({ data, isLoading: false }),\n error: () => setState({ data: undefined, isLoading: false }),\n });\n return () => subscription.unsubscribe();\n // `query` is rebuilt every render; `deps` names what it reads.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [db, ...deps]);\n return db ? state : NO_COPY;\n}\n\nconst NO_COPY = { data: undefined, isLoading: true } as const;\n"],"mappings":";AAAA,SAAS,aAAa,WAAW,UAAU,4BAA4B;AACvE,SAAS,iBAAiB;AAWnB,SAAS,cAAc,QAAiC;AAC7D,YAAU,MAAM;AACd,QAAI,CAAC,OAAQ;AACb,WAAO,MAAM;AACb,WAAO,MAAM,OAAO,KAAK;AAAA,EAC3B,GAAG,CAAC,MAAM,CAAC;AACb;AAEA,MAAM,OAAwB;AAAA,EAC5B,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,QAAQ,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,UAAU,EAAE;AAAA,EAC1D,cAAc;AAAA,EACd,aAAa;AAAA,EACb,eAAe;AACjB;AACA,MAAM,WAAW,MAAM,MAAM;AAC7B,MAAM,OAAO,MAAM;AAcZ,SAAS,cAAc,QAAgF;AAC5G,QAAM,QAAQ;AAAA,IACZ,SAAS,OAAO,YAAY;AAAA,IAC5B,SAAS,OAAO,WAAW;AAAA,IAC3B,SAAS,OAAO,WAAW;AAAA,EAC7B;AACA,QAAM,gBAAgB,qBAAqB,iBAAiB,YAAY,MAAM,IAAI;AAClF,QAAM,SAAS,YAAY,YAAY;AACrC,UAAM,QAAQ,QAAQ;AAAA,EACxB,GAAG,CAAC,MAAM,CAAC;AACX,QAAM,SAAqB;AAAA,IACzB,OAAO,MAAM;AAAA,IACb,QAAQ,SAAS,MAAM,SAAS;AAAA,IAChC,SAAS,MAAM,OAAO;AAAA,IACtB,gBAAgB,MAAM,OAAO;AAAA,IAC7B,SAAS,MAAM,OAAO,UAAU,MAAM,OAAO;AAAA,IAC7C,cAAc,MAAM;AAAA,IACpB,eAAe,MAAM;AAAA,EACvB;AACA,SAAO,EAAE,QAAQ,OAAO;AAC1B;AAEA,SAAS,gBAAgB,UAAsB;AAC7C,SAAO,iBAAiB,UAAU,QAAQ;AAC1C,SAAO,iBAAiB,WAAW,QAAQ;AAC3C,SAAO,MAAM;AACX,WAAO,oBAAoB,UAAU,QAAQ;AAC7C,WAAO,oBAAoB,WAAW,QAAQ;AAAA,EAChD;AACF;AACA,MAAM,aAAa,MAAM,UAAU,WAAW;AAQvC,SAAS,aACd,IACA,OACA,MAC6C;AAC7C,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAsD,EAAE,MAAM,QAAW,WAAW,KAAK,CAAC;AACpH,YAAU,MAAM;AACd,QAAI,CAAC,GAAI;AACT,UAAM,eAAe,UAAU,MAAM,MAAM,EAAE,CAAC,EAAE,UAAU;AAAA,MACxD,MAAM,CAAC,SAAS,SAAS,EAAE,MAAM,WAAW,MAAM,CAAC;AAAA,MACnD,OAAO,MAAM,SAAS,EAAE,MAAM,QAAW,WAAW,MAAM,CAAC;AAAA,IAC7D,CAAC;AACD,WAAO,MAAM,aAAa,YAAY;AAAA,EAGxC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;AAChB,SAAO,KAAK,QAAQ;AACtB;AAEA,MAAM,UAAU,EAAE,MAAM,QAAW,WAAW,KAAK;","names":[]}
@@ -1,5 +1,5 @@
1
1
  /** Every custom property tokens.css declares, `@theme inline` names and `--app-nav-h`
2
- * left out (102 names). */
2
+ * left out (98 names). */
3
3
  export declare const KIT_NAMES: string;
4
- /** The theme variables of Tailwind 4.3.3's `theme.css` (413 names). */
4
+ /** The theme variables of Tailwind 4.3.4's `theme.css` (413 names). */
5
5
  export declare const TAILWIND_THEME_NAMES: string;