@eifi1/ui-kit 0.51.4 → 0.51.5

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 (757) hide show
  1. package/README.md +3 -3
  2. package/dist/account/account-settings-labels.js.map +1 -1
  3. package/dist/account/account-settings.js +11 -9
  4. package/dist/account/account-settings.js.map +1 -1
  5. package/dist/account/delete-account-setting.js +56 -65
  6. package/dist/account/delete-account-setting.js.map +1 -1
  7. package/dist/account/email-change-setting-labels.js.map +1 -1
  8. package/dist/account/email-change-setting.js.map +1 -1
  9. package/dist/account/passkeys-setting.js.map +1 -1
  10. package/dist/account/sessions-setting.js.map +1 -1
  11. package/dist/admin/admin-action-confirm.js +1 -3
  12. package/dist/admin/admin-action-confirm.js.map +1 -1
  13. package/dist/admin/admin-action-log.js.map +1 -1
  14. package/dist/admin/admin-area-labels.js +4 -1
  15. package/dist/admin/admin-area-labels.js.map +1 -1
  16. package/dist/admin/admin-overview.js +19 -11
  17. package/dist/admin/admin-overview.js.map +1 -1
  18. package/dist/admin/admin-sections.js.map +1 -1
  19. package/dist/admin/admin-tool-links.d.ts +1 -1
  20. package/dist/admin/admin-tool-links.js +7 -1
  21. package/dist/admin/admin-tool-links.js.map +1 -1
  22. package/dist/admin/invitations-panel.js +2 -13
  23. package/dist/admin/invitations-panel.js.map +1 -1
  24. package/dist/admin/mail-test-card.js.map +1 -1
  25. package/dist/admin/reviewer-scope-editor.js +4 -11
  26. package/dist/admin/reviewer-scope-editor.js.map +1 -1
  27. package/dist/admin/role-select.js.map +1 -1
  28. package/dist/admin/user-roster.js +1 -9
  29. package/dist/admin/user-roster.js.map +1 -1
  30. package/dist/auth/accept-invitation.js.map +1 -1
  31. package/dist/auth/auth-errors.js.map +1 -1
  32. package/dist/auth/forgot-password-form.js +23 -31
  33. package/dist/auth/forgot-password-form.js.map +1 -1
  34. package/dist/auth/register-form.js.map +1 -1
  35. package/dist/auth/reset-password-form.js.map +1 -1
  36. package/dist/auth/sign-in-form.js +13 -5
  37. package/dist/auth/sign-in-form.js.map +1 -1
  38. package/dist/auth/verify-email.js +1 -8
  39. package/dist/auth/verify-email.js.map +1 -1
  40. package/dist/billing/billing-banner.js.map +1 -1
  41. package/dist/billing/billing-labels.js.map +1 -1
  42. package/dist/billing/checkout-processing.js.map +1 -1
  43. package/dist/billing/plan-card.js +91 -85
  44. package/dist/billing/plan-card.js.map +1 -1
  45. package/dist/billing/plan-limit-toast.js.map +1 -1
  46. package/dist/billing/plan-limit.js.map +1 -1
  47. package/dist/billing/plan-price.js.map +1 -1
  48. package/dist/billing/plan-switch.js.map +1 -1
  49. package/dist/billing/subscription-actions.js.map +1 -1
  50. package/dist/bin/eifi1-pay-page.mjs +6 -1
  51. package/dist/components/account-chips.js.map +1 -1
  52. package/dist/components/alert-banner.js +1 -5
  53. package/dist/components/alert-banner.js.map +1 -1
  54. package/dist/components/amount-input.js +29 -12
  55. package/dist/components/amount-input.js.map +1 -1
  56. package/dist/components/authed-image.js.map +1 -1
  57. package/dist/components/autocomplete.js.map +1 -1
  58. package/dist/components/bulk-action-bar.js +1 -4
  59. package/dist/components/bulk-action-bar.js.map +1 -1
  60. package/dist/components/button-group.js.map +1 -1
  61. package/dist/components/button-look.js.map +1 -1
  62. package/dist/components/button.js +1 -7
  63. package/dist/components/button.js.map +1 -1
  64. package/dist/components/calculator.js +5 -5
  65. package/dist/components/calculator.js.map +1 -1
  66. package/dist/components/calendar-heatmap.js +4 -10
  67. package/dist/components/calendar-heatmap.js.map +1 -1
  68. package/dist/components/card.js +1 -8
  69. package/dist/components/card.js.map +1 -1
  70. package/dist/components/chart-zoom-core.js +3 -17
  71. package/dist/components/chart-zoom-core.js.map +1 -1
  72. package/dist/components/chart.d.ts +1 -1
  73. package/dist/components/chart.js +4 -22
  74. package/dist/components/chart.js.map +1 -1
  75. package/dist/components/checkbox-group.js +1 -4
  76. package/dist/components/checkbox-group.js.map +1 -1
  77. package/dist/components/chip-input.js +4 -1
  78. package/dist/components/chip-input.js.map +1 -1
  79. package/dist/components/chip.js +10 -5
  80. package/dist/components/chip.js.map +1 -1
  81. package/dist/components/choice-card.js +58 -74
  82. package/dist/components/choice-card.js.map +1 -1
  83. package/dist/components/column-mapper-labels.js.map +1 -1
  84. package/dist/components/column-mapper.js +167 -227
  85. package/dist/components/column-mapper.js.map +1 -1
  86. package/dist/components/combobox-core.js +1 -4
  87. package/dist/components/combobox-core.js.map +1 -1
  88. package/dist/components/combobox.js +1 -4
  89. package/dist/components/combobox.js.map +1 -1
  90. package/dist/components/confirm-dialog.js.map +1 -1
  91. package/dist/components/copy-button.js.map +1 -1
  92. package/dist/components/country-select.d.ts +5 -4
  93. package/dist/components/country-select.js +3 -19
  94. package/dist/components/country-select.js.map +1 -1
  95. package/dist/components/currency-option-list.js +10 -8
  96. package/dist/components/currency-option-list.js.map +1 -1
  97. package/dist/components/currency-select.js +1 -6
  98. package/dist/components/currency-select.js.map +1 -1
  99. package/dist/components/danger-confirm.js.map +1 -1
  100. package/dist/components/data-table-cells.js.map +1 -1
  101. package/dist/components/data-table-column-settings.js.map +1 -1
  102. package/dist/components/data-table-filter-popover.js +2 -18
  103. package/dist/components/data-table-filter-popover.js.map +1 -1
  104. package/dist/components/data-table-filters.js.map +1 -1
  105. package/dist/components/data-table-fit.js.map +1 -1
  106. package/dist/components/data-table-groups.js +3 -1
  107. package/dist/components/data-table-groups.js.map +1 -1
  108. package/dist/components/data-table-head.js +44 -56
  109. package/dist/components/data-table-head.js.map +1 -1
  110. package/dist/components/data-table-mobile.js +31 -44
  111. package/dist/components/data-table-mobile.js.map +1 -1
  112. package/dist/components/data-table-pagination.js.map +1 -1
  113. package/dist/components/data-table-row-actions.js +19 -16
  114. package/dist/components/data-table-row-actions.js.map +1 -1
  115. package/dist/components/data-table-row.js.map +1 -1
  116. package/dist/components/data-table-shared.js.map +1 -1
  117. package/dist/components/data-table-sort.js.map +1 -1
  118. package/dist/components/data-table-virtual.js +4 -1
  119. package/dist/components/data-table-virtual.js.map +1 -1
  120. package/dist/components/data-table.js +30 -39
  121. package/dist/components/data-table.js.map +1 -1
  122. package/dist/components/date-picker.js +1 -6
  123. package/dist/components/date-picker.js.map +1 -1
  124. package/dist/components/date-range-picker.js.map +1 -1
  125. package/dist/components/description-list.js +10 -10
  126. package/dist/components/description-list.js.map +1 -1
  127. package/dist/components/dialog-frame.js +9 -2
  128. package/dist/components/dialog-frame.js.map +1 -1
  129. package/dist/components/disclosure.js +17 -27
  130. package/dist/components/disclosure.js.map +1 -1
  131. package/dist/components/dropdown.js +25 -35
  132. package/dist/components/dropdown.js.map +1 -1
  133. package/dist/components/empty-state.js.map +1 -1
  134. package/dist/components/entity-combobox.js.map +1 -1
  135. package/dist/components/error-boundary.js.map +1 -1
  136. package/dist/components/field-base.js +5 -5
  137. package/dist/components/field-base.js.map +1 -1
  138. package/dist/components/field-parts.d.ts +1 -1
  139. package/dist/components/field-parts.js +10 -14
  140. package/dist/components/field-parts.js.map +1 -1
  141. package/dist/components/field-sync.js +8 -10
  142. package/dist/components/field-sync.js.map +1 -1
  143. package/dist/components/file-button.js.map +1 -1
  144. package/dist/components/file-dropzone.js.map +1 -1
  145. package/dist/components/floating-panel.js +4 -1
  146. package/dist/components/floating-panel.js.map +1 -1
  147. package/dist/components/focus-ring.js.map +1 -1
  148. package/dist/components/form-actions.js.map +1 -1
  149. package/dist/components/grouped-picker.js +6 -8
  150. package/dist/components/grouped-picker.js.map +1 -1
  151. package/dist/components/hover-menu.js +1 -4
  152. package/dist/components/hover-menu.js.map +1 -1
  153. package/dist/components/iban-input-labels.js.map +1 -1
  154. package/dist/components/icon-button.js +4 -1
  155. package/dist/components/icon-button.js.map +1 -1
  156. package/dist/components/icon-picker.js +2 -7
  157. package/dist/components/icon-picker.js.map +1 -1
  158. package/dist/components/image-grid.js +41 -50
  159. package/dist/components/image-grid.js.map +1 -1
  160. package/dist/components/inline-edit-field.js.map +1 -1
  161. package/dist/components/inline-entity-combobox.js +2 -8
  162. package/dist/components/inline-entity-combobox.js.map +1 -1
  163. package/dist/components/input.js +14 -2
  164. package/dist/components/input.js.map +1 -1
  165. package/dist/components/jump-calendar.js +1 -7
  166. package/dist/components/jump-calendar.js.map +1 -1
  167. package/dist/components/legal-labels.js.map +1 -1
  168. package/dist/components/legal-page.js +14 -2
  169. package/dist/components/legal-page.js.map +1 -1
  170. package/dist/components/legal.js +12 -2
  171. package/dist/components/legal.js.map +1 -1
  172. package/dist/components/lightbox.js.map +1 -1
  173. package/dist/components/line-items.js +26 -36
  174. package/dist/components/line-items.js.map +1 -1
  175. package/dist/components/list.js +22 -10
  176. package/dist/components/list.js.map +1 -1
  177. package/dist/components/measured-grid.js +29 -53
  178. package/dist/components/measured-grid.js.map +1 -1
  179. package/dist/components/menu-item.js.map +1 -1
  180. package/dist/components/mini-calendar.js +13 -31
  181. package/dist/components/mini-calendar.js.map +1 -1
  182. package/dist/components/money-field.js.map +1 -1
  183. package/dist/components/month-picker.js +10 -21
  184. package/dist/components/month-picker.js.map +1 -1
  185. package/dist/components/multi-entity-combobox.js +3 -16
  186. package/dist/components/multi-entity-combobox.js.map +1 -1
  187. package/dist/components/multi-select.js.map +1 -1
  188. package/dist/components/nav-pills.js +10 -1
  189. package/dist/components/nav-pills.js.map +1 -1
  190. package/dist/components/number-field.js.map +1 -1
  191. package/dist/components/number-input.js +1 -7
  192. package/dist/components/number-input.js.map +1 -1
  193. package/dist/components/numpad-sheet.js +1 -4
  194. package/dist/components/numpad-sheet.js.map +1 -1
  195. package/dist/components/one-time-code-input.js +1 -7
  196. package/dist/components/one-time-code-input.js.map +1 -1
  197. package/dist/components/page-contents.js +13 -22
  198. package/dist/components/page-contents.js.map +1 -1
  199. package/dist/components/page-header.js +1 -11
  200. package/dist/components/page-header.js.map +1 -1
  201. package/dist/components/password-strength-labels.js.map +1 -1
  202. package/dist/components/password-strength.js +2 -8
  203. package/dist/components/password-strength.js.map +1 -1
  204. package/dist/components/phone-input.js +42 -57
  205. package/dist/components/phone-input.js.map +1 -1
  206. package/dist/components/picker-sheet.js.map +1 -1
  207. package/dist/components/pie-chart.js +1 -8
  208. package/dist/components/pie-chart.js.map +1 -1
  209. package/dist/components/popover.js.map +1 -1
  210. package/dist/components/progress-bar.js +16 -9
  211. package/dist/components/progress-bar.js.map +1 -1
  212. package/dist/components/qr-code.js +1 -8
  213. package/dist/components/qr-code.js.map +1 -1
  214. package/dist/components/row-actions.js +5 -1
  215. package/dist/components/row-actions.js.map +1 -1
  216. package/dist/components/search-field.js.map +1 -1
  217. package/dist/components/select.js +28 -38
  218. package/dist/components/select.js.map +1 -1
  219. package/dist/components/separator.js +1 -5
  220. package/dist/components/separator.js.map +1 -1
  221. package/dist/components/series-chart-axes.js +4 -1
  222. package/dist/components/series-chart-axes.js.map +1 -1
  223. package/dist/components/series-chart-budget.js.map +1 -1
  224. package/dist/components/series-chart-ticks.js.map +1 -1
  225. package/dist/components/series-chart.js +29 -45
  226. package/dist/components/series-chart.js.map +1 -1
  227. package/dist/components/settings-fields.js +21 -17
  228. package/dist/components/settings-fields.js.map +1 -1
  229. package/dist/components/share-card.js +4 -9
  230. package/dist/components/share-card.js.map +1 -1
  231. package/dist/components/sign-chip.js.map +1 -1
  232. package/dist/components/signature-pad.js +4 -13
  233. package/dist/components/signature-pad.js.map +1 -1
  234. package/dist/components/signed-amount.js +12 -5
  235. package/dist/components/signed-amount.js.map +1 -1
  236. package/dist/components/skeleton.js +2 -17
  237. package/dist/components/skeleton.js.map +1 -1
  238. package/dist/components/slider.js +13 -36
  239. package/dist/components/slider.js.map +1 -1
  240. package/dist/components/sparkline.js.map +1 -1
  241. package/dist/components/stat-tile.js +14 -10
  242. package/dist/components/stat-tile.js.map +1 -1
  243. package/dist/components/swatch-picker.js +14 -4
  244. package/dist/components/swatch-picker.js.map +1 -1
  245. package/dist/components/swipeable-row.d.ts +1 -1
  246. package/dist/components/swipeable-row.js +1 -8
  247. package/dist/components/swipeable-row.js.map +1 -1
  248. package/dist/components/table-skeleton.js +4 -1
  249. package/dist/components/table-skeleton.js.map +1 -1
  250. package/dist/components/table.js +12 -9
  251. package/dist/components/table.js.map +1 -1
  252. package/dist/components/tabs.js +5 -24
  253. package/dist/components/tabs.js.map +1 -1
  254. package/dist/components/text-link.js.map +1 -1
  255. package/dist/components/text.js.map +1 -1
  256. package/dist/components/tile-radio.js +1 -7
  257. package/dist/components/tile-radio.js.map +1 -1
  258. package/dist/components/time-input.js.map +1 -1
  259. package/dist/components/toast.js.map +1 -1
  260. package/dist/components/toggle-group.js +6 -1
  261. package/dist/components/toggle-group.js.map +1 -1
  262. package/dist/components/toggle-legend.js +2 -11
  263. package/dist/components/toggle-legend.js.map +1 -1
  264. package/dist/components/tooltip.js +2 -22
  265. package/dist/components/tooltip.js.map +1 -1
  266. package/dist/components/translation-review-editor.js +1 -4
  267. package/dist/components/translation-review-editor.js.map +1 -1
  268. package/dist/components/translation-review-parts.d.ts +1 -1
  269. package/dist/components/translation-review-parts.js +7 -1
  270. package/dist/components/translation-review-parts.js.map +1 -1
  271. package/dist/components/translation-review.js +18 -7
  272. package/dist/components/translation-review.js.map +1 -1
  273. package/dist/components/tree-view.js +14 -10
  274. package/dist/components/tree-view.js.map +1 -1
  275. package/dist/components/treemap.js +1 -13
  276. package/dist/components/treemap.js.map +1 -1
  277. package/dist/components/trigger-aria.js.map +1 -1
  278. package/dist/components/use-table-state.js +14 -11
  279. package/dist/components/use-table-state.js.map +1 -1
  280. package/dist/components/user-avatar.js +3 -26
  281. package/dist/components/user-avatar.js.map +1 -1
  282. package/dist/demo/demo-start.js +8 -4
  283. package/dist/demo/demo-start.js.map +1 -1
  284. package/dist/eslint/index.js +1 -1
  285. package/dist/feedback/feedback-attachment-labels.js.map +1 -1
  286. package/dist/feedback/feedback-attachment.js +1 -8
  287. package/dist/feedback/feedback-attachment.js.map +1 -1
  288. package/dist/feedback/feedback-capture.d.ts +1 -1
  289. package/dist/feedback/feedback-capture.js +3 -1
  290. package/dist/feedback/feedback-capture.js.map +1 -1
  291. package/dist/feedback/feedback-comment-thread.js +27 -13
  292. package/dist/feedback/feedback-comment-thread.js.map +1 -1
  293. package/dist/feedback/feedback-context.js +1 -4
  294. package/dist/feedback/feedback-context.js.map +1 -1
  295. package/dist/feedback/feedback-dialog.js.map +1 -1
  296. package/dist/feedback/feedback-inbox.js.map +1 -1
  297. package/dist/feedback/feedback-public-list.js +7 -1
  298. package/dist/feedback/feedback-public-list.js.map +1 -1
  299. package/dist/feedback/feedback-record.js +1 -5
  300. package/dist/feedback/feedback-record.js.map +1 -1
  301. package/dist/feedback/feedback-replies.js +25 -10
  302. package/dist/feedback/feedback-replies.js.map +1 -1
  303. package/dist/feedback/feedback-row-detail.js.map +1 -1
  304. package/dist/feedback/feedback-status-undo.js.map +1 -1
  305. package/dist/feedback/feedback-submit-dialog.js.map +1 -1
  306. package/dist/feedback/feedback-table.js +4 -1
  307. package/dist/feedback/feedback-table.js.map +1 -1
  308. package/dist/feedback/feedback-thread.js +2 -13
  309. package/dist/feedback/feedback-thread.js.map +1 -1
  310. package/dist/feedback/feedback-vocabulary.js.map +1 -1
  311. package/dist/feedback/feedback-vote-button.js.map +1 -1
  312. package/dist/hooks/use-anchored-rect.js.map +1 -1
  313. package/dist/hooks/use-document-title.js.map +1 -1
  314. package/dist/hooks/use-file-drop.js.map +1 -1
  315. package/dist/hooks/use-file-picker-labels.js.map +1 -1
  316. package/dist/hooks/use-file-picker.js.map +1 -1
  317. package/dist/hooks/use-focus-trap.js.map +1 -1
  318. package/dist/hooks/use-overlay-history.js +10 -5
  319. package/dist/hooks/use-overlay-history.js.map +1 -1
  320. package/dist/hooks/use-row-swipe.d.ts +1 -1
  321. package/dist/hooks/use-row-swipe.js +1 -6
  322. package/dist/hooks/use-row-swipe.js.map +1 -1
  323. package/dist/i18n/defaults.js +1 -4
  324. package/dist/i18n/defaults.js.map +1 -1
  325. package/dist/i18n/german.js +4 -1
  326. package/dist/i18n/german.js.map +1 -1
  327. package/dist/i18n/kit-labels.js +2 -8
  328. package/dist/i18n/kit-labels.js.map +1 -1
  329. package/dist/i18n/languages.js.map +1 -1
  330. package/dist/i18n/locales/en.js +4 -1
  331. package/dist/i18n/locales/en.js.map +2 -2
  332. package/dist/i18n/locales/es.js +4 -1
  333. package/dist/i18n/locales/es.js.map +1 -1
  334. package/dist/i18n/locales/fr.js +4 -1
  335. package/dist/i18n/locales/fr.js.map +1 -1
  336. package/dist/i18n/locales/hu.js +4 -1
  337. package/dist/i18n/locales/hu.js.map +1 -1
  338. package/dist/i18n/locales/it.js +4 -1
  339. package/dist/i18n/locales/it.js.map +1 -1
  340. package/dist/i18n/locales/zh.js.map +1 -1
  341. package/dist/i18n/plurals.js.map +1 -1
  342. package/dist/i18n/review.js +13 -7
  343. package/dist/i18n/review.js.map +1 -1
  344. package/dist/i18n/shared-labels.js.map +1 -1
  345. package/dist/index.d.ts +16 -16
  346. package/dist/index.js +40 -23
  347. package/dist/index.js.map +1 -1
  348. package/dist/landing/access.js +1 -4
  349. package/dist/landing/access.js.map +1 -1
  350. package/dist/landing/landing-sections.js +35 -38
  351. package/dist/landing/landing-sections.js.map +1 -1
  352. package/dist/landing/page-seo.js.map +1 -1
  353. package/dist/lib/calc.js.map +1 -1
  354. package/dist/lib/clipboard-minus.js.map +1 -1
  355. package/dist/lib/dates.js.map +1 -1
  356. package/dist/lib/decimal-marks.js.map +1 -1
  357. package/dist/lib/device-settings.js +2 -1
  358. package/dist/lib/device-settings.js.map +1 -1
  359. package/dist/lib/direction.js.map +1 -1
  360. package/dist/lib/iban.js.map +1 -1
  361. package/dist/lib/logger.js +1 -5
  362. package/dist/lib/logger.js.map +1 -1
  363. package/dist/lib/person-name.js.map +1 -1
  364. package/dist/lib/phone.js.map +1 -1
  365. package/dist/lib/qr-encode.js +344 -8
  366. package/dist/lib/qr-encode.js.map +1 -1
  367. package/dist/lib/session-refresh.js.map +1 -1
  368. package/dist/lib/table-text.js +15 -3
  369. package/dist/lib/table-text.js.map +1 -1
  370. package/dist/lib/text-match.d.ts +4 -0
  371. package/dist/lib/text-match.js.map +1 -1
  372. package/dist/lib/translation-review.js.map +1 -1
  373. package/dist/notifications/notification-inbox.js +63 -44
  374. package/dist/notifications/notification-inbox.js.map +1 -1
  375. package/dist/notifications/notification-parts.js.map +1 -1
  376. package/dist/notifications/notification-types-setting.js.map +1 -1
  377. package/dist/notifications/notifications-labels.js.map +1 -1
  378. package/dist/notifications/push-device-setting.js +6 -1
  379. package/dist/notifications/push-device-setting.js.map +1 -1
  380. package/dist/notifications/push-devices-setting.js.map +1 -1
  381. package/dist/notifications/push-worker.js.map +1 -1
  382. package/dist/notifications/quiet-hours-setting.js.map +1 -1
  383. package/dist/notifications/use-push-device.js +4 -1
  384. package/dist/notifications/use-push-device.js.map +1 -1
  385. package/dist/offline/pwa-update-prompt.d.ts +1 -1
  386. package/dist/offline/pwa-update-prompt.js +7 -1
  387. package/dist/offline/pwa-update-prompt.js.map +1 -1
  388. package/dist/offline/query-persistence.js.map +1 -1
  389. package/dist/offline.d.ts +1 -1
  390. package/dist/offline.js +6 -1
  391. package/dist/offline.js.map +1 -1
  392. package/dist/pay/pay.css +8 -1
  393. package/dist/rhf/fields.d.ts +1 -2
  394. package/dist/rhf/fields.js +1 -10
  395. package/dist/rhf/fields.js.map +1 -1
  396. package/dist/rhf/form-core.js +1 -4
  397. package/dist/rhf/form-core.js.map +1 -1
  398. package/dist/rhf/line-items.js.map +1 -1
  399. package/dist/rhf/use-rhf-wizard-step.js.map +1 -1
  400. package/dist/search/command-palette.js +71 -62
  401. package/dist/search/command-palette.js.map +1 -1
  402. package/dist/search/global-search.js +34 -5
  403. package/dist/search/global-search.js.map +1 -1
  404. package/dist/search/search-index.js.map +1 -1
  405. package/dist/settings/device-reset-setting.js +1 -6
  406. package/dist/settings/device-reset-setting.js.map +1 -1
  407. package/dist/settings/settings-heading.js.map +1 -1
  408. package/dist/settings/settings-layout.js +9 -20
  409. package/dist/settings/settings-layout.js.map +1 -1
  410. package/dist/settings/use-settings-route.js.map +1 -1
  411. package/dist/shell/app-shell.js +62 -31
  412. package/dist/shell/app-shell.js.map +1 -1
  413. package/dist/shell/auth-layout.js +1 -10
  414. package/dist/shell/auth-layout.js.map +1 -1
  415. package/dist/shell/dev-menu.js +12 -6
  416. package/dist/shell/dev-menu.js.map +1 -1
  417. package/dist/shell/option-switcher-menu.js.map +1 -1
  418. package/dist/shell/sync-status-indicator.js +7 -6
  419. package/dist/shell/sync-status-indicator.js.map +1 -1
  420. package/dist/shell/top-bar-parts-labels.js.map +1 -1
  421. package/dist/shell/topbar-action-menu.js.map +1 -1
  422. package/dist/shell/topbar-controls.js +1 -10
  423. package/dist/shell/topbar-controls.js.map +1 -1
  424. package/dist/shell/tour-menu.js +9 -1
  425. package/dist/shell/tour-menu.js.map +1 -1
  426. package/dist/sync/db.js.map +1 -1
  427. package/dist/sync/engine.js +24 -6
  428. package/dist/sync/engine.js.map +1 -1
  429. package/dist/sync.d.ts +2 -2
  430. package/dist/sync.js +7 -1
  431. package/dist/sync.js.map +1 -1
  432. package/dist/testing/css-variables.js.map +1 -1
  433. package/dist/testing/index.d.ts +1 -1
  434. package/dist/testing/index.js +1 -3
  435. package/dist/testing/index.js.map +1 -1
  436. package/dist/testing/react.js.map +1 -1
  437. package/dist/testing/viewport.js +7 -4
  438. package/dist/testing/viewport.js.map +1 -1
  439. package/dist/theme/contrast-tokens.js +13 -2
  440. package/dist/theme/contrast-tokens.js.map +1 -1
  441. package/dist/theme/palette-derive.d.ts +1 -1
  442. package/dist/theme/palette-derive.js +18 -29
  443. package/dist/theme/palette-derive.js.map +1 -1
  444. package/dist/theme/palette-presets.js.map +1 -1
  445. package/dist/theme/palette-store.js.map +1 -1
  446. package/dist/theme/text-size.js.map +1 -1
  447. package/dist/theme/theme-store.js +1 -3
  448. package/dist/theme/theme-store.js.map +1 -1
  449. package/dist/tour/tour-catalogue.js.map +1 -1
  450. package/dist/tour/tour-dock.js +5 -1
  451. package/dist/tour/tour-dock.js.map +1 -1
  452. package/dist/tour/tour-engine.js +1 -4
  453. package/dist/tour/tour-engine.js.map +1 -1
  454. package/dist/tour/tour.js +21 -12
  455. package/dist/tour/tour.js.map +1 -1
  456. package/dist/tour/use-tour-progress.js.map +1 -1
  457. package/dist/tour/use-tour-url-sync.js.map +1 -1
  458. package/dist/wizard/stepper-nav.js +3 -24
  459. package/dist/wizard/stepper-nav.js.map +1 -1
  460. package/dist/wizard/types.js.map +1 -1
  461. package/dist/wizard/use-wizard.js +24 -28
  462. package/dist/wizard/use-wizard.js.map +1 -1
  463. package/dist/wizard/validation.js.map +1 -1
  464. package/dist/wizard/wizard-context.js.map +1 -1
  465. package/dist/wizard/wizard-step.d.ts +1 -1
  466. package/dist/wizard/wizard-step.js +1 -8
  467. package/dist/wizard/wizard-step.js.map +1 -1
  468. package/dist/wizard/wizard-summary.js.map +1 -1
  469. package/package.json +5 -2
  470. package/src/account/account-settings-labels.ts +1 -2
  471. package/src/account/account-settings.tsx +11 -17
  472. package/src/account/delete-account-setting.tsx +1 -6
  473. package/src/account/email-change-setting-labels.ts +1 -2
  474. package/src/account/email-change-setting.tsx +3 -1
  475. package/src/account/passkeys-setting.tsx +1 -3
  476. package/src/account/sessions-setting.tsx +4 -2
  477. package/src/admin/admin-action-confirm.tsx +4 -7
  478. package/src/admin/admin-action-log.tsx +29 -29
  479. package/src/admin/admin-area-labels.ts +4 -1
  480. package/src/admin/admin-overview.tsx +8 -2
  481. package/src/admin/admin-sections.tsx +3 -1
  482. package/src/admin/admin-tool-links.tsx +7 -1
  483. package/src/admin/invitations-panel.tsx +5 -11
  484. package/src/admin/mail-test-card.tsx +1 -2
  485. package/src/admin/reviewer-scope-editor.tsx +4 -9
  486. package/src/admin/role-select.tsx +4 -2
  487. package/src/admin/user-roster.tsx +2 -11
  488. package/src/auth/accept-invitation.tsx +1 -7
  489. package/src/auth/auth-errors.ts +1 -6
  490. package/src/auth/forgot-password-form.tsx +2 -7
  491. package/src/auth/register-form.tsx +7 -8
  492. package/src/auth/reset-password-form.tsx +2 -5
  493. package/src/auth/sign-in-form.tsx +15 -14
  494. package/src/auth/verify-email.tsx +2 -7
  495. package/src/billing/billing-banner.tsx +3 -7
  496. package/src/billing/billing-labels.ts +6 -4
  497. package/src/billing/checkout-processing.ts +0 -1
  498. package/src/billing/plan-card.tsx +9 -8
  499. package/src/billing/plan-limit-toast.tsx +1 -2
  500. package/src/billing/plan-limit.ts +1 -2
  501. package/src/billing/plan-price.ts +1 -4
  502. package/src/billing/plan-switch.tsx +1 -2
  503. package/src/billing/subscription-actions.tsx +5 -2
  504. package/src/components/account-chips.tsx +1 -7
  505. package/src/components/alert-banner.tsx +2 -8
  506. package/src/components/amount-input.tsx +206 -178
  507. package/src/components/authed-image.tsx +7 -4
  508. package/src/components/autocomplete.tsx +7 -9
  509. package/src/components/bulk-action-bar.tsx +2 -7
  510. package/src/components/button-group.tsx +1 -2
  511. package/src/components/button-look.ts +6 -17
  512. package/src/components/button.tsx +1 -4
  513. package/src/components/calculator.tsx +6 -9
  514. package/src/components/calendar-heatmap.tsx +5 -10
  515. package/src/components/card.tsx +9 -13
  516. package/src/components/chart-zoom-core.tsx +6 -33
  517. package/src/components/chart.tsx +8 -31
  518. package/src/components/checkbox-group.tsx +1 -4
  519. package/src/components/chip-input.tsx +6 -6
  520. package/src/components/chip.tsx +14 -26
  521. package/src/components/choice-card.tsx +4 -18
  522. package/src/components/column-mapper-labels.ts +3 -10
  523. package/src/components/column-mapper.tsx +33 -122
  524. package/src/components/combobox-core.tsx +12 -42
  525. package/src/components/combobox.tsx +2 -6
  526. package/src/components/confirm-dialog.tsx +1 -3
  527. package/src/components/copy-button.tsx +2 -4
  528. package/src/components/country-select.tsx +9 -21
  529. package/src/components/currency-option-list.tsx +13 -14
  530. package/src/components/currency-select.tsx +2 -7
  531. package/src/components/danger-confirm.tsx +8 -9
  532. package/src/components/data-table-cells.tsx +1 -2
  533. package/src/components/data-table-column-settings.tsx +1 -3
  534. package/src/components/data-table-filter-popover.tsx +5 -19
  535. package/src/components/data-table-filters.ts +1 -6
  536. package/src/components/data-table-fit.ts +1 -3
  537. package/src/components/data-table-groups.tsx +3 -1
  538. package/src/components/data-table-head.tsx +27 -45
  539. package/src/components/data-table-mobile.tsx +99 -108
  540. package/src/components/data-table-pagination.tsx +2 -6
  541. package/src/components/data-table-row-actions.tsx +25 -24
  542. package/src/components/data-table-row.tsx +16 -17
  543. package/src/components/data-table-shared.tsx +10 -16
  544. package/src/components/data-table-sort.ts +1 -2
  545. package/src/components/data-table-virtual.ts +9 -4
  546. package/src/components/data-table.tsx +489 -501
  547. package/src/components/date-picker.tsx +1 -7
  548. package/src/components/date-range-picker.tsx +3 -13
  549. package/src/components/description-list.tsx +7 -13
  550. package/src/components/dialog-frame.tsx +12 -3
  551. package/src/components/disclosure.tsx +8 -12
  552. package/src/components/dropdown.tsx +3 -11
  553. package/src/components/empty-state.tsx +1 -3
  554. package/src/components/entity-combobox.tsx +2 -6
  555. package/src/components/error-boundary.tsx +7 -7
  556. package/src/components/field-base.tsx +8 -14
  557. package/src/components/field-parts.tsx +12 -23
  558. package/src/components/field-sync.tsx +7 -11
  559. package/src/components/file-button.tsx +7 -3
  560. package/src/components/file-dropzone.tsx +7 -12
  561. package/src/components/floating-panel.tsx +44 -51
  562. package/src/components/focus-ring.ts +1 -2
  563. package/src/components/form-actions.tsx +1 -4
  564. package/src/components/grouped-picker.tsx +6 -8
  565. package/src/components/hover-menu.tsx +1 -4
  566. package/src/components/iban-input-labels.ts +1 -2
  567. package/src/components/icon-button.tsx +46 -48
  568. package/src/components/icon-picker.tsx +4 -17
  569. package/src/components/image-grid.tsx +1 -6
  570. package/src/components/inline-edit-field.tsx +1 -5
  571. package/src/components/inline-entity-combobox.tsx +6 -21
  572. package/src/components/input.tsx +14 -2
  573. package/src/components/jump-calendar.tsx +2 -6
  574. package/src/components/legal-labels.ts +1 -2
  575. package/src/components/legal-page.tsx +18 -7
  576. package/src/components/legal.tsx +14 -5
  577. package/src/components/lightbox.tsx +1 -3
  578. package/src/components/line-items.tsx +7 -15
  579. package/src/components/list.tsx +29 -25
  580. package/src/components/measured-grid.tsx +8 -30
  581. package/src/components/menu-item.tsx +1 -3
  582. package/src/components/mini-calendar.tsx +129 -159
  583. package/src/components/money-field.tsx +4 -2
  584. package/src/components/month-picker.tsx +14 -21
  585. package/src/components/multi-entity-combobox.tsx +6 -26
  586. package/src/components/multi-select.tsx +5 -9
  587. package/src/components/nav-pills.tsx +10 -2
  588. package/src/components/number-field.tsx +18 -15
  589. package/src/components/number-input.tsx +3 -19
  590. package/src/components/numpad-sheet.tsx +9 -13
  591. package/src/components/one-time-code-input.tsx +6 -11
  592. package/src/components/page-contents.tsx +2 -9
  593. package/src/components/page-header.tsx +5 -10
  594. package/src/components/password-strength-labels.ts +1 -2
  595. package/src/components/password-strength.tsx +6 -9
  596. package/src/components/phone-input.tsx +2 -13
  597. package/src/components/picker-sheet.tsx +2 -5
  598. package/src/components/pie-chart.tsx +6 -12
  599. package/src/components/popover.tsx +2 -1
  600. package/src/components/progress-bar.tsx +16 -25
  601. package/src/components/qr-code.tsx +2 -9
  602. package/src/components/row-actions.tsx +9 -1
  603. package/src/components/search-field.tsx +4 -5
  604. package/src/components/select.tsx +2 -10
  605. package/src/components/separator.tsx +1 -5
  606. package/src/components/series-chart-axes.ts +5 -5
  607. package/src/components/series-chart-budget.ts +1 -4
  608. package/src/components/series-chart-ticks.ts +1 -4
  609. package/src/components/series-chart.tsx +43 -83
  610. package/src/components/settings-fields.tsx +20 -15
  611. package/src/components/share-card.tsx +6 -14
  612. package/src/components/sign-chip.tsx +16 -16
  613. package/src/components/signature-pad.tsx +21 -13
  614. package/src/components/signed-amount.tsx +6 -14
  615. package/src/components/skeleton.tsx +2 -13
  616. package/src/components/slider.tsx +9 -20
  617. package/src/components/sparkline.tsx +2 -13
  618. package/src/components/stat-tile.tsx +13 -24
  619. package/src/components/swatch-picker.tsx +8 -3
  620. package/src/components/swipeable-row.tsx +2 -11
  621. package/src/components/table-skeleton.tsx +4 -1
  622. package/src/components/table.tsx +12 -8
  623. package/src/components/tabs.tsx +24 -39
  624. package/src/components/text-link.tsx +6 -6
  625. package/src/components/text.tsx +1 -2
  626. package/src/components/tile-radio.tsx +6 -19
  627. package/src/components/time-input.tsx +4 -5
  628. package/src/components/toast.tsx +3 -12
  629. package/src/components/toggle-group.tsx +9 -9
  630. package/src/components/toggle-legend.tsx +4 -16
  631. package/src/components/tooltip.tsx +9 -48
  632. package/src/components/translation-review-editor.tsx +4 -4
  633. package/src/components/translation-review-parts.tsx +7 -1
  634. package/src/components/translation-review.tsx +15 -16
  635. package/src/components/tree-view.tsx +17 -20
  636. package/src/components/treemap.tsx +3 -15
  637. package/src/components/trigger-aria.ts +1 -4
  638. package/src/components/use-table-state.ts +16 -17
  639. package/src/components/user-avatar.tsx +4 -21
  640. package/src/demo/demo-start.tsx +8 -4
  641. package/src/feedback/feedback-attachment-labels.ts +2 -1
  642. package/src/feedback/feedback-attachment.tsx +16 -29
  643. package/src/feedback/feedback-capture.ts +3 -1
  644. package/src/feedback/feedback-comment-thread.tsx +9 -6
  645. package/src/feedback/feedback-context.tsx +1 -4
  646. package/src/feedback/feedback-dialog.tsx +3 -4
  647. package/src/feedback/feedback-inbox.tsx +3 -9
  648. package/src/feedback/feedback-public-list.tsx +7 -1
  649. package/src/feedback/feedback-record.ts +6 -14
  650. package/src/feedback/feedback-replies.tsx +10 -5
  651. package/src/feedback/feedback-row-detail.tsx +2 -5
  652. package/src/feedback/feedback-status-undo.ts +1 -4
  653. package/src/feedback/feedback-submit-dialog.tsx +4 -4
  654. package/src/feedback/feedback-table.tsx +12 -8
  655. package/src/feedback/feedback-thread.tsx +4 -16
  656. package/src/feedback/feedback-vocabulary.ts +72 -82
  657. package/src/feedback/feedback-vote-button.tsx +9 -8
  658. package/src/hooks/use-anchored-rect.ts +1 -4
  659. package/src/hooks/use-document-title.ts +3 -1
  660. package/src/hooks/use-file-drop.ts +1 -3
  661. package/src/hooks/use-file-picker-labels.ts +1 -2
  662. package/src/hooks/use-file-picker.tsx +1 -5
  663. package/src/hooks/use-focus-trap.ts +1 -2
  664. package/src/hooks/use-overlay-history.ts +10 -5
  665. package/src/hooks/use-row-swipe.ts +1 -6
  666. package/src/i18n/defaults.ts +1 -4
  667. package/src/i18n/german.ts +33 -49
  668. package/src/i18n/kit-labels.tsx +5 -20
  669. package/src/i18n/languages.ts +2 -8
  670. package/src/i18n/locales/en.ts +11 -31
  671. package/src/i18n/locales/es.ts +35 -49
  672. package/src/i18n/locales/fr.ts +34 -51
  673. package/src/i18n/locales/hu.ts +26 -37
  674. package/src/i18n/locales/it.ts +35 -52
  675. package/src/i18n/locales/zh.ts +14 -32
  676. package/src/i18n/plurals.ts +4 -1
  677. package/src/i18n/review.ts +18 -7
  678. package/src/i18n/shared-labels.ts +1 -2
  679. package/src/index.ts +106 -32
  680. package/src/landing/access.ts +2 -7
  681. package/src/landing/landing-sections.tsx +14 -15
  682. package/src/landing/page-seo.ts +1 -3
  683. package/src/lib/calc.ts +4 -1
  684. package/src/lib/clipboard-minus.ts +4 -1
  685. package/src/lib/dates.ts +2 -10
  686. package/src/lib/decimal-marks.ts +1 -3
  687. package/src/lib/device-settings.ts +2 -1
  688. package/src/lib/direction.ts +1 -3
  689. package/src/lib/iban.ts +89 -9
  690. package/src/lib/logger.ts +1 -5
  691. package/src/lib/person-name.ts +1 -3
  692. package/src/lib/phone.ts +5 -8
  693. package/src/lib/qr-encode.ts +32 -8
  694. package/src/lib/session-refresh.ts +5 -6
  695. package/src/lib/table-text.ts +24 -6
  696. package/src/lib/text-match.ts +15 -3
  697. package/src/lib/translation-review.ts +1 -3
  698. package/src/notifications/notification-inbox.tsx +17 -4
  699. package/src/notifications/notification-parts.tsx +1 -2
  700. package/src/notifications/notification-types-setting.tsx +19 -18
  701. package/src/notifications/notifications-labels.ts +1 -2
  702. package/src/notifications/push-device-setting.tsx +7 -1
  703. package/src/notifications/push-devices-setting.tsx +7 -3
  704. package/src/notifications/push-worker.ts +10 -2
  705. package/src/notifications/quiet-hours-setting.tsx +4 -1
  706. package/src/notifications/use-push-device.ts +4 -1
  707. package/src/offline/pwa-update-prompt.tsx +7 -1
  708. package/src/offline/query-persistence.ts +9 -6
  709. package/src/offline.ts +6 -1
  710. package/src/rhf/fields.tsx +50 -19
  711. package/src/rhf/form-core.tsx +2 -6
  712. package/src/rhf/line-items.tsx +5 -7
  713. package/src/rhf/use-rhf-wizard-step.ts +3 -12
  714. package/src/search/command-palette.tsx +15 -23
  715. package/src/search/global-search.tsx +38 -8
  716. package/src/search/search-index.ts +5 -1
  717. package/src/settings/device-reset-setting.tsx +5 -8
  718. package/src/settings/settings-heading.tsx +3 -1
  719. package/src/settings/settings-layout.tsx +6 -13
  720. package/src/settings/use-settings-route.ts +4 -3
  721. package/src/shell/app-shell.tsx +48 -24
  722. package/src/shell/auth-layout.tsx +1 -6
  723. package/src/shell/dev-menu.tsx +12 -6
  724. package/src/shell/option-switcher-menu.tsx +1 -3
  725. package/src/shell/sync-status-indicator.tsx +14 -12
  726. package/src/shell/top-bar-parts-labels.ts +1 -3
  727. package/src/shell/topbar-action-menu.tsx +9 -8
  728. package/src/shell/topbar-controls.tsx +5 -19
  729. package/src/shell/tour-menu.tsx +9 -1
  730. package/src/sync/db.ts +5 -1
  731. package/src/sync/engine.ts +44 -12
  732. package/src/sync.ts +16 -2
  733. package/src/testing/css-variables.ts +1 -2
  734. package/src/testing/index.ts +1 -6
  735. package/src/testing/react.ts +1 -2
  736. package/src/testing/viewport.ts +9 -9
  737. package/src/theme/contrast-tokens.ts +13 -2
  738. package/src/theme/palette-derive.ts +24 -50
  739. package/src/theme/palette-presets.ts +0 -1
  740. package/src/theme/palette-store.ts +1 -4
  741. package/src/theme/text-size.ts +1 -1
  742. package/src/theme/theme-store.ts +2 -6
  743. package/src/tour/tour-catalogue.tsx +4 -2
  744. package/src/tour/tour-dock.ts +5 -1
  745. package/src/tour/tour-engine.ts +2 -6
  746. package/src/tour/tour.tsx +28 -12
  747. package/src/tour/use-tour-progress.ts +8 -2
  748. package/src/tour/use-tour-url-sync.ts +3 -1
  749. package/src/vite-env.d.ts +1 -4
  750. package/src/wizard/stepper-nav.tsx +10 -35
  751. package/src/wizard/types.ts +1 -4
  752. package/src/wizard/use-wizard.ts +28 -43
  753. package/src/wizard/validation.ts +1 -5
  754. package/src/wizard/wizard-context.tsx +2 -6
  755. package/src/wizard/wizard-step.tsx +1 -8
  756. package/src/wizard/wizard-summary.tsx +2 -6
  757. package/tokens.css +393 -152
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tour/use-tour-progress.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from \"react\";\nimport { readStored, removeStored, writeStored } from \"../lib/safe-storage\";\nimport { httpStatusOf } from \"../auth/auth-errors\";\nimport { isTourDone, type TourCompletions } from \"./tour-info\";\n\n/**\n * Which tours this device has completed (docs/tours-harmonization.md §2.6, §6.2) —\n * keksdose's `tour-progress-store` and kastlan's `completions.ts`, as one hook.\n *\n * - **Device storage, the app's key.** `storageKey` is always the app's: an app on shared\n * devices scopes it per account (KS, bench PCs), and an app that lists its keys\n * (KS `storage-keys.ts`) spells it there. Every access is guarded: a blocked or full\n * storage keeps the marks for this page's life instead of throwing.\n * - **One store, every reader.** A change notifies every hook on the same key in this tab\n * (an in-tab emitter) and in other tabs (the `storage` event), so the tours page and\n * `TourMenu` agree without a reload (ka F3).\n * - **`migrate(raw)`** reads an older shape, so nobody loses their done marks at adoption:\n * it gets the stored JSON (or the raw text, if it is no JSON) and returns the record for\n * a shape it recognises, `undefined` for any other — the kit's own shape included,\n * which it then reads itself. keksdose: zustand's `{state: {completed}, version: 1}`;\n * kastlan: an array of ids, completed without a date (`null`). It runs only on a stored\n * text that is not the kit's shape already, once per text and key. The FIRST `migrate`\n * any reader, `TourProvider` or {@link completeTour} passes for a key is remembered for\n * that key (later identities are ignored), so a reader without one, mounted first, no\n * longer hides the old marks from the rest (keksdose B-2). **Still, define the options\n * once, at module level, and hand the provider and every reader the same object.**\n * - **Never overwritten unread** (keksdose B-1): a write (`complete`, the provider's,\n * `completeTour`) on a stored text that is neither empty nor the kit's shape, and that no\n * known `migrate` reads, THROWS instead of replacing the old marks.\n * - **Resetting the device** (settings §11): register the key with `reset: () =>\n * resetTourProgress(storageKey)`, which clears storage and the page's memory and tells\n * every reader in this tab.\n *\n * **The stored shape** (§9.3, §9.6.6), for an app's tests and seeds: the JSON record\n * `{ [id]: ISO instant | \"\" }` under `storageKey`, where `\"\"` is a completion without a\n * date (a migrated record: kastlan's id list). That is the one written form; the hooks\n * and callbacks (`completed`, `TourCompletions`) give `null` for `\"\"`. A `null` read from\n * storage (or returned by `migrate`) is accepted as the same thing and written back as\n * `\"\"`. To write a completion from outside a page, use {@link completeTour}.\n *\n * **On the account** (0.45, §10.2–§10.3): with `account`, the device key becomes the\n * account's CACHE — see {@link TourProgressAccount}. Without it, nothing changes from 0.44.\n */\nexport interface UseTourProgressOptions {\n storageKey: string;\n migrate?: (raw: unknown) => Record<string, string | null> | undefined;\n /**\n * 0.45: the account's record behind the device key (§10.2.4). Remembered per key like\n * `migrate` (the FIRST one any reader or the provider passes wins). With it,\n * `storageKey` must be per account (a prefix plus the user id), so build the options per\n * account and keep ONE stable object per key — memoise them in a `Map` by user id — and\n * let `load`/`save` read the current token when they run, never capture one:\n *\n * const byUser = new Map<string, UseTourProgressOptions>();\n * function tourProgressFor(user: { id: string; isDemo: boolean } | null) {\n * if (!user || user.isDemo) return { storageKey: \"app.tours.completions\" }; // no account\n * let options = byUser.get(user.id);\n * if (!options) byUser.set(user.id, (options = { storageKey: `app.tours.completions:${user.id}`, account }));\n * return options;\n * }\n *\n * Signed out, and in a demo, pass none (and a fixed key): the device record works as in\n * 0.44, and a demo neither PATCHes into a 403 nor leaves an orphan key per demo id.\n */\n account?: TourProgressAccount;\n}\n\n/** The record on the wire (§10.2.2): tour id → ISO instant, or `\"\"` (done, no date). */\ntype TourProgressWire = Record<string, string>;\n\n/** The `PATCH /tours/progress` body (§10.2.2): what to mark done, and what to forget.\n * An id is never in both. Either may be left out. */\nexport interface TourProgressBody {\n completed?: TourProgressWire;\n forget?: string[];\n}\n\n/**\n * Taking over a device-wide record from before the account (§10.2.5), on the FIRST load\n * of the account key on this page:\n * - `{storageKey, discard: true}` (kastlan): removes the old key and uploads nothing — a\n * shared PC must not hand one person another's marks. Removing an absent key is a no-op.\n * - `{storageKey, migrate?}`: reads the old key once (through `migrate` for an older\n * shape), uploads it to the first account that signs in on this device, then removes it.\n */\nexport type TourProgressAdopt =\n | { storageKey: string; discard: true; migrate?: never }\n | { storageKey: string; discard?: false; migrate?: UseTourProgressOptions[\"migrate\"] };\n\n/**\n * The app's account record (§10.3): `load()` is its `GET /tours/progress`, `save(body)`\n * its `PATCH`, each resolving to the parsed answer, `{completed}` — the whole record.\n *\n * - The page paints from the device key at once (and offline); the account is loaded on\n * the first reader of the key on this page, on `online`, and after each save, and its\n * answer replaces the cache.\n * - A write (`complete`, the provider's finish, `reset`) goes to the cache at once and to\n * a PENDING record beside it, `<storageKey>:pending` (`{synced, completed, forget}`),\n * sent as one PATCH and cleared by its answer. A failure with no answer (offline, 5xx,\n * 401) keeps it for the next load or `online`; a 402 (a lapsed plan) keeps it but sends it\n * no more on this page (the next session or page load tries again); a 403 or 422 drops\n * it and logs.\n * - `awarded` stays the device's answer against the cache (§10.2.4).\n * - The pending record is NOT the app's outbox: leave it out of any \"N waiting\" count\n * and any sign-out guard (losing a tour mark is acceptable).\n * - A device record never synced (no `:pending` with `synced: true`) is uploaded whole on\n * its first load, by the server's merge rule (later completion wins).\n * - On every end of a session call {@link signOutTourProgress}.\n */\nexport interface TourProgressAccount {\n load: () => Promise<{ completed: TourProgressWire }>;\n save: (body: TourProgressBody) => Promise<{ completed: TourProgressWire }>;\n adopt?: TourProgressAdopt;\n}\n\n/** What `complete` takes: a tour id, or the tour (`{id, updated}`, a `TourInfo` fits). */\ntype CompletedTour = string | { id: string; updated?: { date: string } | null };\n\nexport interface TourProgress {\n /** Tour id → the completion's ISO timestamp, or `null` when recorded without a date. */\n completed: TourCompletions;\n /**\n * Record a completion. Keeps the FIRST completion's date, so a replay changes nothing —\n * unless the tour was reset by an update since (§8): pass the tour (`{id, updated}`) and\n * a completion from before `updated.date` is replaced by today's.\n *\n * Returns `true` when this counts as a new completion — the moment to award the tour's\n * points and show the app's toast (§6.1) — and `false` for a replay.\n *\n * A tour started with `start(steps, {tour})` under a `TourProvider` that has `progress`\n * is recorded by the provider (§9.6.2): do not call this from that start's `onFinish`\n * too, or the second write answers `false`.\n */\n complete: (tour: CompletedTour) => boolean;\n /** \"Mark as not done\": forget the completion. */\n reset: (id: string) => void;\n}\n\n/* ── The store: one per key, shared by every hook ───────────────────────────── */\n\nconst EMPTY: TourCompletions = Object.freeze({});\n/** In-tab listeners per key. */\nconst listeners = new Map<string, Set<() => void>>();\n/** What this page wrote where storage refused to keep it (blocked, full): per key. */\nconst memory = new Map<string, string>();\n/**\n * The last raw text read per key and what it parsed to, so a snapshot keeps its identity:\n * the same text gives the same object, whichever reader asks and whatever `migrate` it\n * holds. Several readers of one key with inline `migrate` arrows used to evict each other\n * here and loop (kastlan B1, 0.43).\n */\nconst cache = new Map<string, { raw: string | null; value: TourCompletions; readable: boolean }>();\ntype Migrate = NonNullable<UseTourProgressOptions[\"migrate\"]>;\n/** Each key's migrate: the first one any reader, the provider or `completeTour` passes (B-2). */\nconst migrates = new Map<string, Migrate>();\n\n/**\n * Remember `migrate` for `key` if it is the first one, and return the key's migrate. A\n * snapshot parsed before the key had one is dropped and re-parsed, and the key's readers\n * hear of it after this render (never from inside one).\n * @internal Not part of the barrel; `TourProvider` registers its `progress` with it.\n */\nexport function rememberMigrate(key: string, migrate: UseTourProgressOptions[\"migrate\"]): Migrate | undefined {\n if (migrate && !migrates.has(key)) {\n migrates.set(key, migrate);\n if (cache.delete(key)) queueMicrotask(() => emit(key));\n }\n return migrates.get(key);\n}\n\nfunction emit(key: string) {\n listeners.get(key)?.forEach((listener) => listener());\n}\n\nfunction rawOf(key: string): string | null {\n return memory.get(key) ?? readStored(key);\n}\n\n/** Is this the kit's own shape already: a record whose every value is a string or null? */\nfunction isKitShape(value: unknown): boolean {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return false;\n return Object.values(value as Record<string, unknown>).every((at) => at === null || typeof at === \"string\");\n}\n\n/** The kit's own shape: a record of string or null; anything else is dropped. */\nfunction fromRecord(value: unknown): TourCompletions | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n const out: Record<string, string | null> = {};\n for (const [id, at] of Object.entries(value as Record<string, unknown>)) {\n if (at === null || at === \"\") out[id] = null;\n else if (typeof at === \"string\") out[id] = at;\n }\n return out;\n}\n\n/**\n * The record, and whether the text was READ (empty, the kit's shape, or a shape `migrate`\n * knows) rather than dropped: a write over an unread text would lose it (B-1).\n */\nfunction parse(raw: string | null, migrate: UseTourProgressOptions[\"migrate\"]): { value: TourCompletions; readable: boolean } {\n if (raw === null || raw.trim() === \"\") return { value: EMPTY, readable: true };\n let value: unknown = raw;\n try {\n value = JSON.parse(raw);\n } catch {\n // Not JSON: `migrate` still sees the text.\n }\n if (migrate && !isKitShape(value)) {\n try {\n const migrated = fromRecord(migrate(value));\n if (migrated !== undefined) return { value: migrated, readable: true };\n } catch {\n // A migrate that throws reads as \"not a shape it knows\".\n }\n }\n return { value: fromRecord(value) ?? EMPTY, readable: isKitShape(value) };\n}\n\nfunction snapshot(key: string, migrate: UseTourProgressOptions[\"migrate\"]): TourCompletions {\n const known = rememberMigrate(key, migrate);\n const raw = rawOf(key);\n const hit = cache.get(key);\n if (hit && hit.raw === raw) return hit.value;\n const parsed = parse(raw, known);\n cache.set(key, { raw, ...parsed });\n return parsed.value;\n}\n\n/** The record a write builds on; throws rather than overwrite a text nobody could read (B-1). */\nfunction readForWrite(key: string, migrate: UseTourProgressOptions[\"migrate\"], own = false): TourCompletions {\n // `own`: this migrate, even where the key registered another (completeTour's `{migrate}`).\n const known = rememberMigrate(key, migrate);\n const { value, readable } =\n own && migrate && known !== migrate\n ? parse(rawOf(key), migrate)\n : { value: snapshot(key, migrate), readable: cache.get(key)?.readable };\n if (!readable) {\n throw new Error(\n `[ui-kit] tour progress: the text stored under \"${key}\" is not the kit's shape and no migrate reads it; ` +\n \"refusing to overwrite it. Pass the key's migrate (the shared useTourProgress / TourProvider options, \" +\n \"or completeTour's {migrate}).\",\n );\n }\n return value;\n}\n\n/** The one written form (§9.6.6): a completion without a date is stored as `\"\"`, never `null`. */\nfunction write(key: string, record: TourCompletions) {\n const stored: Record<string, string> = {};\n for (const [id, at] of Object.entries(record)) stored[id] = at ?? \"\";\n const raw = JSON.stringify(stored);\n writeStored(key, raw);\n // Storage refused it (blocked, full): keep it for this page's life instead.\n if (readStored(key) === raw) memory.delete(key);\n else memory.set(key, raw);\n emit(key);\n}\n\n/* ── The account behind the device key (0.45, §10.2.4–§10.2.8) ─────────────── */\n\n/** The key's account: the first one any reader or the provider passes (as `migrate`). */\nconst accounts = new Map<string, TourProgressAccount>();\n/** Keys whose account was loaded (or is loading) on this page: the first reader loads. */\nconst loaded = new Set<string>();\n/** Keys with a sync running, and those asked for another while it ran. */\nconst running = new Set<string>();\nconst rerun = new Set<string>();\n/** Keys with a sync queued for the end of this task: two writes in one handler, one PATCH. */\nconst queued = new Set<string>();\n/** Bumped by a sign-out: an answer that comes back after it never refills the cache. */\nconst epochs = new Map<string, number>();\n/** Keys whose PATCH got a 402 (a lapsed plan): the pending record is kept but not sent\n * again on this page; the next session (or page load) tries once more. */\nconst parked = new Set<string>();\n/** The pending record where storage refused to keep it. */\nconst pendingMemory = new Map<string, string>();\nlet onlineListening = false;\n\n/** The pending record (§10.2.4): what waits to be sent, and whether the device key was\n * ever synced. */\ninterface PendingRecord {\n synced: boolean;\n completed: TourProgressWire;\n forget: string[];\n}\n\nconst pendingKey = (key: string) => `${key}:pending`;\n\nfunction devWarn(message: string) {\n if (import.meta.env?.DEV) console.warn(message);\n}\n\n/**\n * Remember `account` for `key` if it is the first one, and return the key's account.\n * @internal Not part of the barrel; `TourProvider` registers its `progress` with it.\n */\nexport function rememberAccount(key: string, account: TourProgressAccount | undefined): TourProgressAccount | undefined {\n if (account && !accounts.has(key)) {\n accounts.set(key, account);\n if (account.adopt && account.adopt.storageKey === key) {\n devWarn(\n `[ui-kit] tour progress: the account key \"${key}\" is the key \\`adopt\\` takes over. ` +\n \"With an account the key must be per account (a prefix plus the user id), apart from the old device-wide key.\",\n );\n }\n if (!onlineListening && typeof window !== \"undefined\") {\n onlineListening = true;\n window.addEventListener(\"online\", () => {\n for (const known of loaded) void sync(known);\n });\n }\n }\n return accounts.get(key);\n}\n\nfunction readPending(key: string): PendingRecord | null {\n const raw = pendingMemory.get(key) ?? readStored(pendingKey(key));\n if (raw === null) return null;\n try {\n const value = JSON.parse(raw) as Partial<PendingRecord> | null;\n if (!value || typeof value !== \"object\") return null;\n const completed: TourProgressWire = {};\n for (const [id, at] of Object.entries(value.completed ?? {})) {\n if (typeof at === \"string\") completed[id] = at;\n else if (at === null) completed[id] = \"\";\n }\n const forget = Array.isArray(value.forget) ? value.forget.filter((id): id is string => typeof id === \"string\") : [];\n return { synced: value.synced === true, completed, forget };\n } catch {\n return null;\n }\n}\n\nfunction writePending(key: string, record: PendingRecord) {\n const raw = JSON.stringify(record);\n writeStored(pendingKey(key), raw);\n if (readStored(pendingKey(key)) === raw) pendingMemory.delete(key);\n else pendingMemory.set(key, raw);\n}\n\nconst hasWork = (record: PendingRecord) => Object.keys(record.completed).length > 0 || record.forget.length > 0;\n\n/** \"Later completion wins\" (§10.2.3), as the server merges: an instant beats `\"\"`, `\"\"`\n * beats nothing, and between two instants the later is kept. */\nfunction later(a: string | undefined, b: string): string {\n if (a === undefined || a === \"\") return b;\n if (b === \"\") return a;\n return Date.parse(b) > Date.parse(a) ? b : a;\n}\n\n/** Note a completion (or, with `forget`, its removal) in the key's pending record. */\nfunction markPending(key: string, id: string, at: string | null | undefined, forget = false) {\n const record = readPending(key) ?? { synced: false, completed: {}, forget: [] };\n if (forget) {\n delete record.completed[id];\n if (!record.forget.includes(id)) record.forget.push(id);\n } else {\n record.completed[id] = at ?? \"\";\n record.forget = record.forget.filter((other) => other !== id);\n }\n writePending(key, record);\n}\n\n/** After an answer (or a 403/422): take what was sent out of the pending record, keeping\n * what was written while the request ran. */\nfunction clearSent(key: string, sent: TourProgressBody, synced: boolean) {\n const record = readPending(key) ?? { synced: false, completed: {}, forget: [] };\n for (const [id, at] of Object.entries(sent.completed ?? {})) {\n if (record.completed[id] === at) delete record.completed[id];\n }\n const forgotten = new Set(sent.forget ?? []);\n record.forget = record.forget.filter((id) => !forgotten.has(id));\n record.synced = record.synced || synced;\n writePending(key, record);\n}\n\n/** The account's answer as the cache, with what still waits to be sent laid over it. */\nfunction refill(key: string, answer: { completed: TourProgressWire } | null | undefined) {\n const next: Record<string, string> = {};\n for (const [id, at] of Object.entries(fromRecord(answer?.completed) ?? {})) next[id] = at ?? \"\";\n const waiting = readPending(key);\n if (waiting) {\n for (const [id, at] of Object.entries(waiting.completed)) next[id] = later(next[id], at);\n for (const id of waiting.forget) delete next[id];\n }\n const record: Record<string, string | null> = {};\n for (const [id, at] of Object.entries(next)) record[id] = at === \"\" ? null : at;\n write(key, record);\n}\n\n/** The first load of an account key on this page takes over the old device-wide key\n * (§10.2.5). */\nfunction adoptOldRecord(key: string, account: TourProgressAccount) {\n const adopt = account.adopt;\n if (!adopt || adopt.storageKey === key) return;\n const old = adopt.storageKey;\n if (!adopt.discard) {\n const { value } = parse(rawOf(old), adopt.migrate ?? migrates.get(old));\n const entries = Object.entries(value);\n if (entries.length > 0) {\n const record = readPending(key) ?? { synced: false, completed: {}, forget: [] };\n for (const [id, at] of entries) record.completed[id] = later(record.completed[id], at ?? \"\");\n writePending(key, record);\n }\n }\n // Removed either way, at once: no second account on this device gets it.\n removeStored(old);\n memory.delete(old);\n cache.delete(old);\n emit(old);\n}\n\n/** A device record never synced is uploaded whole on its first load (§10.2.5). */\nfunction uploadUnsynced(key: string) {\n const record = readPending(key);\n if (record?.synced) return;\n const device = snapshot(key, migrates.get(key));\n const entries = Object.entries(device);\n const next = record ?? { synced: false, completed: {}, forget: [] };\n if (entries.length === 0 && record) return;\n for (const [id, at] of entries) {\n if (!next.forget.includes(id)) next.completed[id] = later(next.completed[id], at ?? \"\");\n }\n writePending(key, next);\n}\n\n/** The key's first sync on this page: take over the old record, upload a never-synced one. */\nfunction prepare(key: string) {\n loaded.add(key);\n adoptOldRecord(key, accounts.get(key)!);\n uploadUnsynced(key);\n}\n\n/** Queue a sync for the end of this task, so a handler's writes go as one PATCH. */\nfunction queueSync(key: string) {\n if (!loaded.has(key)) prepare(key);\n if (queued.has(key)) return;\n queued.add(key);\n queueMicrotask(() => {\n queued.delete(key);\n void sync(key);\n });\n}\n\n/** The first reader of an account key on this page loads it. */\nfunction ensureLoaded(key: string) {\n if (!accounts.has(key) || loaded.has(key)) return;\n prepare(key);\n void sync(key);\n}\n\n/**\n * Send what waits (one PATCH) or, with nothing waiting, load; the answer replaces the\n * cache. One at a time per key; a request asked for while one runs follows it.\n */\nasync function sync(key: string): Promise<void> {\n const account = accounts.get(key);\n if (!account || !loaded.has(key)) return;\n if (running.has(key)) {\n rerun.add(key);\n return;\n }\n if (typeof navigator !== \"undefined\" && navigator.onLine === false) return;\n running.add(key);\n const epoch = epochs.get(key) ?? 0;\n try {\n const waiting = readPending(key);\n if (waiting && hasWork(waiting)) {\n if (parked.has(key)) return;\n const body: TourProgressBody = {};\n if (Object.keys(waiting.completed).length > 0) body.completed = { ...waiting.completed };\n if (waiting.forget.length > 0) body.forget = [...waiting.forget];\n let answer: { completed: TourProgressWire };\n try {\n answer = await account.save(body);\n } catch (error) {\n const status = httpStatusOf(error);\n if (status === 403 || status === 422) {\n // Refused for good (a demo, a malformed or full record): dropped, not retried.\n if ((epochs.get(key) ?? 0) === epoch) clearSent(key, body, false);\n console.warn(`[ui-kit] tour progress: the account refused the pending record (${status}); dropped.`, error);\n } else if (status === 402) {\n // A lapsed plan refuses every write: kept, but not re-sent after each finish\n // and each `online` — the next session or page load tries again.\n parked.add(key);\n }\n // No answer (offline, 5xx, 401): it stays for the next load or `online`.\n return;\n }\n if ((epochs.get(key) ?? 0) !== epoch) return;\n clearSent(key, body, true);\n refill(key, answer);\n } else {\n const answer = await account.load();\n if ((epochs.get(key) ?? 0) !== epoch) return;\n if (waiting && !waiting.synced) writePending(key, { ...waiting, synced: true });\n else if (!waiting) writePending(key, { synced: true, completed: {}, forget: [] });\n refill(key, answer);\n }\n } catch {\n // A load that failed keeps the cache as it is.\n } finally {\n running.delete(key);\n if (rerun.delete(key)) void sync(key);\n }\n}\n\n/**\n * The end of a session (§10.2.6) — sign-out, a 401 nothing could renew, the account's\n * deletion, every path: removes the CACHE only (the device key and the page's snapshot;\n * every reader in this tab moves), synchronously and with no request, and KEEPS\n * `<storageKey>:pending`. That record sits under this person's own key, holds tour ids and\n * dates only, and goes out on their next sign-in. An answer still on its way is ignored.\n *\n * function logout() { …; signOutTourProgress(`app.tours:${user.id}`); }\n */\nexport function signOutTourProgress(storageKey: string): void {\n epochs.set(storageKey, (epochs.get(storageKey) ?? 0) + 1);\n loaded.delete(storageKey);\n rerun.delete(storageKey);\n parked.delete(storageKey);\n removeStored(storageKey);\n memory.delete(storageKey);\n cache.delete(storageKey);\n emit(storageKey);\n}\n\nfunction subscribe(key: string, listener: () => void): () => void {\n let set = listeners.get(key);\n if (!set) listeners.set(key, (set = new Set()));\n set.add(listener);\n // Another tab wrote (or cleared all storage: `key` null).\n const onStorage = (event: StorageEvent) => {\n if (event.key !== null && event.key !== key) return;\n memory.delete(key);\n listener();\n };\n if (typeof window !== \"undefined\") window.addEventListener(\"storage\", onStorage);\n return () => {\n set.delete(listener);\n if (typeof window !== \"undefined\") window.removeEventListener(\"storage\", onStorage);\n };\n}\n\n/**\n * Forget every tour completion under `storageKey` on this device: the stored key, what the\n * page kept in memory where storage refused it, and every reader in this tab moves at\n * once. For \"Reset this device's settings\" (settings §11):\n *\n * registerDeviceSetting({ key: TOUR_KEY, kind: \"view\", reset: () => resetTourProgress(TOUR_KEY) });\n *\n * With an account (0.45) it clears the cache only: the account is never written, the\n * pending record stays (\"anything waiting to be sent\", settings §11.2.2) and the next load\n * refills the cache. Tour progress is never registered with the device reset (§10.2.8).\n */\nexport function resetTourProgress(storageKey: string): void {\n if (accounts.has(storageKey)) loaded.delete(storageKey);\n removeStored(storageKey);\n memory.delete(storageKey);\n cache.delete(storageKey);\n emit(storageKey);\n}\n\n/**\n * Record a completion under `storageKey`, as {@link TourProgress.complete} does: through\n * the one shared store and snapshot (never a copy), so every reader of the key moves.\n * Returns `true` when it counts anew. `TourProvider` calls this on a tour's last step.\n * @internal Not part of the barrel.\n */\nexport function completeStored(\n storageKey: string,\n migrate: UseTourProgressOptions[\"migrate\"],\n tour: CompletedTour,\n): boolean {\n const subject = typeof tour === \"string\" ? { id: tour } : tour;\n // Read now rather than from a render: two completions in one handler both count.\n const current = readForWrite(storageKey, migrate);\n if (isTourDone(subject, current[subject.id])) return false;\n const at = new Date().toISOString();\n write(storageKey, { ...current, [subject.id]: at });\n if (accounts.has(storageKey)) {\n markPending(storageKey, subject.id, at);\n queueSync(storageKey);\n }\n return true;\n}\n\n/** \"Mark as not done\" (§10.2.8): off the cache, and `forget` to the account. @internal */\nfunction resetStored(storageKey: string, migrate: UseTourProgressOptions[\"migrate\"], id: string) {\n const current = snapshot(storageKey, migrate);\n if (current[id] === undefined) return;\n const next = { ...current };\n delete next[id];\n write(storageKey, next);\n if (accounts.has(storageKey)) {\n markPending(storageKey, id, undefined, true);\n queueSync(storageKey);\n }\n}\n\n/**\n * Write a completion from outside a page (§9.2): for an app's tests, seeds and scripts,\n * beside {@link resetTourProgress}. It writes the kit's stored shape (`{[id]: ISO | \"\"}`)\n * under `storageKey`, keeps the other tours' marks, and notifies every reader of the key\n * in this tab directly (the `storage` event reaches only OTHER tabs), so a mounted\n * `useTourProgress` or `TourCatalogue` moves at once.\n *\n * Unlike `complete`, it always writes (over a readable text; see below): `at` is the\n * completion's instant (an ISO string or a `Date`, default now), `null` a completion\n * without a date (stored as `\"\"`, read back as `null`).\n *\n * completeTour(TOUR_KEY, \"budget\", \"2026-09-01T10:00:00Z\");\n *\n * A stored text in an app's older shape is read through `opts.migrate`, or else the\n * migrate a reader or the provider registered for the key. If the text is neither empty\n * nor the kit's shape and no migrate reads it, this THROWS and writes nothing (B-1):\n *\n * completeTour(TOUR_KEY, \"budget\", undefined, { migrate: fromZustand });\n *\n * With an account on the key (0.45) it stays a device write — no request — and is marked\n * pending, so the next load sends it.\n */\nexport function completeTour(\n storageKey: string,\n tour: string | { id: string },\n at?: string | Date | null,\n opts?: { migrate?: UseTourProgressOptions[\"migrate\"] },\n): void {\n const id = typeof tour === \"string\" ? tour : tour.id;\n const when = at === null ? null : at instanceof Date ? at.toISOString() : (at ?? new Date().toISOString());\n write(storageKey, { ...readForWrite(storageKey, opts?.migrate, true), [id]: when });\n if (accounts.has(storageKey)) markPending(storageKey, id, when);\n}\n\n/** The device's tour completions under `storageKey`; see {@link UseTourProgressOptions}. */\nexport function useTourProgress({ storageKey, migrate, account }: UseTourProgressOptions): TourProgress {\n // Held, not depended on: an inline arrow is a new function every render, and neither the\n // snapshot nor the callbacks may change with it.\n const migrateRef = useRef(migrate);\n useEffect(() => {\n migrateRef.current = migrate;\n });\n // The first account for the key wins (as `migrate`); its first reader on the page loads.\n const accountRef = useRef(account);\n useEffect(() => {\n accountRef.current = account;\n });\n useEffect(() => {\n if (rememberAccount(storageKey, accountRef.current)) ensureLoaded(storageKey);\n }, [storageKey]);\n const completed = useSyncExternalStore(\n useCallback((listener: () => void) => subscribe(storageKey, listener), [storageKey]),\n () => snapshot(storageKey, migrateRef.current),\n () => EMPTY,\n );\n\n const complete = useCallback(\n (tour: CompletedTour) => completeStored(storageKey, migrateRef.current, tour),\n [storageKey],\n );\n\n const reset = useCallback((id: string) => resetStored(storageKey, migrateRef.current, id), [storageKey]);\n\n return useMemo(() => ({ completed, complete, reset }), [completed, complete, reset]);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/tour/use-tour-progress.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from \"react\";\nimport { readStored, removeStored, writeStored } from \"../lib/safe-storage\";\nimport { httpStatusOf } from \"../auth/auth-errors\";\nimport { isTourDone, type TourCompletions } from \"./tour-info\";\n\n/**\n * Which tours this device has completed (docs/tours-harmonization.md §2.6, §6.2) —\n * keksdose's `tour-progress-store` and kastlan's `completions.ts`, as one hook.\n *\n * - **Device storage, the app's key.** `storageKey` is always the app's: an app on shared\n * devices scopes it per account (KS, bench PCs), and an app that lists its keys\n * (KS `storage-keys.ts`) spells it there. Every access is guarded: a blocked or full\n * storage keeps the marks for this page's life instead of throwing.\n * - **One store, every reader.** A change notifies every hook on the same key in this tab\n * (an in-tab emitter) and in other tabs (the `storage` event), so the tours page and\n * `TourMenu` agree without a reload (ka F3).\n * - **`migrate(raw)`** reads an older shape, so nobody loses their done marks at adoption:\n * it gets the stored JSON (or the raw text, if it is no JSON) and returns the record for\n * a shape it recognises, `undefined` for any other — the kit's own shape included,\n * which it then reads itself. keksdose: zustand's `{state: {completed}, version: 1}`;\n * kastlan: an array of ids, completed without a date (`null`). It runs only on a stored\n * text that is not the kit's shape already, once per text and key. The FIRST `migrate`\n * any reader, `TourProvider` or {@link completeTour} passes for a key is remembered for\n * that key (later identities are ignored), so a reader without one, mounted first, no\n * longer hides the old marks from the rest (keksdose B-2). **Still, define the options\n * once, at module level, and hand the provider and every reader the same object.**\n * - **Never overwritten unread** (keksdose B-1): a write (`complete`, the provider's,\n * `completeTour`) on a stored text that is neither empty nor the kit's shape, and that no\n * known `migrate` reads, THROWS instead of replacing the old marks.\n * - **Resetting the device** (settings §11): register the key with `reset: () =>\n * resetTourProgress(storageKey)`, which clears storage and the page's memory and tells\n * every reader in this tab.\n *\n * **The stored shape** (§9.3, §9.6.6), for an app's tests and seeds: the JSON record\n * `{ [id]: ISO instant | \"\" }` under `storageKey`, where `\"\"` is a completion without a\n * date (a migrated record: kastlan's id list). That is the one written form; the hooks\n * and callbacks (`completed`, `TourCompletions`) give `null` for `\"\"`. A `null` read from\n * storage (or returned by `migrate`) is accepted as the same thing and written back as\n * `\"\"`. To write a completion from outside a page, use {@link completeTour}.\n *\n * **On the account** (0.45, §10.2–§10.3): with `account`, the device key becomes the\n * account's CACHE — see {@link TourProgressAccount}. Without it, nothing changes from 0.44.\n */\nexport interface UseTourProgressOptions {\n storageKey: string;\n migrate?: (raw: unknown) => Record<string, string | null> | undefined;\n /**\n * 0.45: the account's record behind the device key (§10.2.4). Remembered per key like\n * `migrate` (the FIRST one any reader or the provider passes wins). With it,\n * `storageKey` must be per account (a prefix plus the user id), so build the options per\n * account and keep ONE stable object per key — memoise them in a `Map` by user id — and\n * let `load`/`save` read the current token when they run, never capture one:\n *\n * const byUser = new Map<string, UseTourProgressOptions>();\n * function tourProgressFor(user: { id: string; isDemo: boolean } | null) {\n * if (!user || user.isDemo) return { storageKey: \"app.tours.completions\" }; // no account\n * let options = byUser.get(user.id);\n * if (!options) byUser.set(user.id, (options = { storageKey: `app.tours.completions:${user.id}`, account }));\n * return options;\n * }\n *\n * Signed out, and in a demo, pass none (and a fixed key): the device record works as in\n * 0.44, and a demo neither PATCHes into a 403 nor leaves an orphan key per demo id.\n */\n account?: TourProgressAccount;\n}\n\n/** The record on the wire (§10.2.2): tour id → ISO instant, or `\"\"` (done, no date). */\ntype TourProgressWire = Record<string, string>;\n\n/** The `PATCH /tours/progress` body (§10.2.2): what to mark done, and what to forget.\n * An id is never in both. Either may be left out. */\nexport interface TourProgressBody {\n completed?: TourProgressWire;\n forget?: string[];\n}\n\n/**\n * Taking over a device-wide record from before the account (§10.2.5), on the FIRST load\n * of the account key on this page:\n * - `{storageKey, discard: true}` (kastlan): removes the old key and uploads nothing — a\n * shared PC must not hand one person another's marks. Removing an absent key is a no-op.\n * - `{storageKey, migrate?}`: reads the old key once (through `migrate` for an older\n * shape), uploads it to the first account that signs in on this device, then removes it.\n */\nexport type TourProgressAdopt =\n | { storageKey: string; discard: true; migrate?: never }\n | { storageKey: string; discard?: false; migrate?: UseTourProgressOptions[\"migrate\"] };\n\n/**\n * The app's account record (§10.3): `load()` is its `GET /tours/progress`, `save(body)`\n * its `PATCH`, each resolving to the parsed answer, `{completed}` — the whole record.\n *\n * - The page paints from the device key at once (and offline); the account is loaded on\n * the first reader of the key on this page, on `online`, and after each save, and its\n * answer replaces the cache.\n * - A write (`complete`, the provider's finish, `reset`) goes to the cache at once and to\n * a PENDING record beside it, `<storageKey>:pending` (`{synced, completed, forget}`),\n * sent as one PATCH and cleared by its answer. A failure with no answer (offline, 5xx,\n * 401) keeps it for the next load or `online`; a 402 (a lapsed plan) keeps it but sends it\n * no more on this page (the next session or page load tries again); a 403 or 422 drops\n * it and logs.\n * - `awarded` stays the device's answer against the cache (§10.2.4).\n * - The pending record is NOT the app's outbox: leave it out of any \"N waiting\" count\n * and any sign-out guard (losing a tour mark is acceptable).\n * - A device record never synced (no `:pending` with `synced: true`) is uploaded whole on\n * its first load, by the server's merge rule (later completion wins).\n * - On every end of a session call {@link signOutTourProgress}.\n */\nexport interface TourProgressAccount {\n load: () => Promise<{ completed: TourProgressWire }>;\n save: (body: TourProgressBody) => Promise<{ completed: TourProgressWire }>;\n adopt?: TourProgressAdopt;\n}\n\n/** What `complete` takes: a tour id, or the tour (`{id, updated}`, a `TourInfo` fits). */\ntype CompletedTour = string | { id: string; updated?: { date: string } | null };\n\nexport interface TourProgress {\n /** Tour id → the completion's ISO timestamp, or `null` when recorded without a date. */\n completed: TourCompletions;\n /**\n * Record a completion. Keeps the FIRST completion's date, so a replay changes nothing —\n * unless the tour was reset by an update since (§8): pass the tour (`{id, updated}`) and\n * a completion from before `updated.date` is replaced by today's.\n *\n * Returns `true` when this counts as a new completion — the moment to award the tour's\n * points and show the app's toast (§6.1) — and `false` for a replay.\n *\n * A tour started with `start(steps, {tour})` under a `TourProvider` that has `progress`\n * is recorded by the provider (§9.6.2): do not call this from that start's `onFinish`\n * too, or the second write answers `false`.\n */\n complete: (tour: CompletedTour) => boolean;\n /** \"Mark as not done\": forget the completion. */\n reset: (id: string) => void;\n}\n\n/* ── The store: one per key, shared by every hook ───────────────────────────── */\n\nconst EMPTY: TourCompletions = Object.freeze({});\n/** In-tab listeners per key. */\nconst listeners = new Map<string, Set<() => void>>();\n/** What this page wrote where storage refused to keep it (blocked, full): per key. */\nconst memory = new Map<string, string>();\n/**\n * The last raw text read per key and what it parsed to, so a snapshot keeps its identity:\n * the same text gives the same object, whichever reader asks and whatever `migrate` it\n * holds. Several readers of one key with inline `migrate` arrows used to evict each other\n * here and loop (kastlan B1, 0.43).\n */\nconst cache = new Map<string, { raw: string | null; value: TourCompletions; readable: boolean }>();\ntype Migrate = NonNullable<UseTourProgressOptions[\"migrate\"]>;\n/** Each key's migrate: the first one any reader, the provider or `completeTour` passes (B-2). */\nconst migrates = new Map<string, Migrate>();\n\n/**\n * Remember `migrate` for `key` if it is the first one, and return the key's migrate. A\n * snapshot parsed before the key had one is dropped and re-parsed, and the key's readers\n * hear of it after this render (never from inside one).\n * @internal Not part of the barrel; `TourProvider` registers its `progress` with it.\n */\nexport function rememberMigrate(key: string, migrate: UseTourProgressOptions[\"migrate\"]): Migrate | undefined {\n if (migrate && !migrates.has(key)) {\n migrates.set(key, migrate);\n if (cache.delete(key)) queueMicrotask(() => emit(key));\n }\n return migrates.get(key);\n}\n\nfunction emit(key: string) {\n listeners.get(key)?.forEach((listener) => listener());\n}\n\nfunction rawOf(key: string): string | null {\n return memory.get(key) ?? readStored(key);\n}\n\n/** Is this the kit's own shape already: a record whose every value is a string or null? */\nfunction isKitShape(value: unknown): boolean {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return false;\n return Object.values(value as Record<string, unknown>).every((at) => at === null || typeof at === \"string\");\n}\n\n/** The kit's own shape: a record of string or null; anything else is dropped. */\nfunction fromRecord(value: unknown): TourCompletions | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n const out: Record<string, string | null> = {};\n for (const [id, at] of Object.entries(value as Record<string, unknown>)) {\n if (at === null || at === \"\") out[id] = null;\n else if (typeof at === \"string\") out[id] = at;\n }\n return out;\n}\n\n/**\n * The record, and whether the text was READ (empty, the kit's shape, or a shape `migrate`\n * knows) rather than dropped: a write over an unread text would lose it (B-1).\n */\nfunction parse(\n raw: string | null,\n migrate: UseTourProgressOptions[\"migrate\"],\n): { value: TourCompletions; readable: boolean } {\n if (raw === null || raw.trim() === \"\") return { value: EMPTY, readable: true };\n let value: unknown = raw;\n try {\n value = JSON.parse(raw);\n } catch {\n // Not JSON: `migrate` still sees the text.\n }\n if (migrate && !isKitShape(value)) {\n try {\n const migrated = fromRecord(migrate(value));\n if (migrated !== undefined) return { value: migrated, readable: true };\n } catch {\n // A migrate that throws reads as \"not a shape it knows\".\n }\n }\n return { value: fromRecord(value) ?? EMPTY, readable: isKitShape(value) };\n}\n\nfunction snapshot(key: string, migrate: UseTourProgressOptions[\"migrate\"]): TourCompletions {\n const known = rememberMigrate(key, migrate);\n const raw = rawOf(key);\n const hit = cache.get(key);\n if (hit && hit.raw === raw) return hit.value;\n const parsed = parse(raw, known);\n cache.set(key, { raw, ...parsed });\n return parsed.value;\n}\n\n/** The record a write builds on; throws rather than overwrite a text nobody could read (B-1). */\nfunction readForWrite(key: string, migrate: UseTourProgressOptions[\"migrate\"], own = false): TourCompletions {\n // `own`: this migrate, even where the key registered another (completeTour's `{migrate}`).\n const known = rememberMigrate(key, migrate);\n const { value, readable } =\n own && migrate && known !== migrate\n ? parse(rawOf(key), migrate)\n : { value: snapshot(key, migrate), readable: cache.get(key)?.readable };\n if (!readable) {\n throw new Error(\n `[ui-kit] tour progress: the text stored under \"${key}\" is not the kit's shape and no migrate reads it; ` +\n \"refusing to overwrite it. Pass the key's migrate (the shared useTourProgress / TourProvider options, \" +\n \"or completeTour's {migrate}).\",\n );\n }\n return value;\n}\n\n/** The one written form (§9.6.6): a completion without a date is stored as `\"\"`, never `null`. */\nfunction write(key: string, record: TourCompletions) {\n const stored: Record<string, string> = {};\n for (const [id, at] of Object.entries(record)) stored[id] = at ?? \"\";\n const raw = JSON.stringify(stored);\n writeStored(key, raw);\n // Storage refused it (blocked, full): keep it for this page's life instead.\n if (readStored(key) === raw) memory.delete(key);\n else memory.set(key, raw);\n emit(key);\n}\n\n/* ── The account behind the device key (0.45, §10.2.4–§10.2.8) ─────────────── */\n\n/** The key's account: the first one any reader or the provider passes (as `migrate`). */\nconst accounts = new Map<string, TourProgressAccount>();\n/** Keys whose account was loaded (or is loading) on this page: the first reader loads. */\nconst loaded = new Set<string>();\n/** Keys with a sync running, and those asked for another while it ran. */\nconst running = new Set<string>();\nconst rerun = new Set<string>();\n/** Keys with a sync queued for the end of this task: two writes in one handler, one PATCH. */\nconst queued = new Set<string>();\n/** Bumped by a sign-out: an answer that comes back after it never refills the cache. */\nconst epochs = new Map<string, number>();\n/** Keys whose PATCH got a 402 (a lapsed plan): the pending record is kept but not sent\n * again on this page; the next session (or page load) tries once more. */\nconst parked = new Set<string>();\n/** The pending record where storage refused to keep it. */\nconst pendingMemory = new Map<string, string>();\nlet onlineListening = false;\n\n/** The pending record (§10.2.4): what waits to be sent, and whether the device key was\n * ever synced. */\ninterface PendingRecord {\n synced: boolean;\n completed: TourProgressWire;\n forget: string[];\n}\n\nconst pendingKey = (key: string) => `${key}:pending`;\n\nfunction devWarn(message: string) {\n if (import.meta.env?.DEV) console.warn(message);\n}\n\n/**\n * Remember `account` for `key` if it is the first one, and return the key's account.\n * @internal Not part of the barrel; `TourProvider` registers its `progress` with it.\n */\nexport function rememberAccount(\n key: string,\n account: TourProgressAccount | undefined,\n): TourProgressAccount | undefined {\n if (account && !accounts.has(key)) {\n accounts.set(key, account);\n if (account.adopt && account.adopt.storageKey === key) {\n devWarn(\n `[ui-kit] tour progress: the account key \"${key}\" is the key \\`adopt\\` takes over. ` +\n \"With an account the key must be per account (a prefix plus the user id), apart from the old device-wide key.\",\n );\n }\n if (!onlineListening && typeof window !== \"undefined\") {\n onlineListening = true;\n window.addEventListener(\"online\", () => {\n for (const known of loaded) void sync(known);\n });\n }\n }\n return accounts.get(key);\n}\n\nfunction readPending(key: string): PendingRecord | null {\n const raw = pendingMemory.get(key) ?? readStored(pendingKey(key));\n if (raw === null) return null;\n try {\n const value = JSON.parse(raw) as Partial<PendingRecord> | null;\n if (!value || typeof value !== \"object\") return null;\n const completed: TourProgressWire = {};\n for (const [id, at] of Object.entries(value.completed ?? {})) {\n if (typeof at === \"string\") completed[id] = at;\n else if (at === null) completed[id] = \"\";\n }\n const forget = Array.isArray(value.forget) ? value.forget.filter((id): id is string => typeof id === \"string\") : [];\n return { synced: value.synced === true, completed, forget };\n } catch {\n return null;\n }\n}\n\nfunction writePending(key: string, record: PendingRecord) {\n const raw = JSON.stringify(record);\n writeStored(pendingKey(key), raw);\n if (readStored(pendingKey(key)) === raw) pendingMemory.delete(key);\n else pendingMemory.set(key, raw);\n}\n\nconst hasWork = (record: PendingRecord) => Object.keys(record.completed).length > 0 || record.forget.length > 0;\n\n/** \"Later completion wins\" (§10.2.3), as the server merges: an instant beats `\"\"`, `\"\"`\n * beats nothing, and between two instants the later is kept. */\nfunction later(a: string | undefined, b: string): string {\n if (a === undefined || a === \"\") return b;\n if (b === \"\") return a;\n return Date.parse(b) > Date.parse(a) ? b : a;\n}\n\n/** Note a completion (or, with `forget`, its removal) in the key's pending record. */\nfunction markPending(key: string, id: string, at: string | null | undefined, forget = false) {\n const record = readPending(key) ?? { synced: false, completed: {}, forget: [] };\n if (forget) {\n delete record.completed[id];\n if (!record.forget.includes(id)) record.forget.push(id);\n } else {\n record.completed[id] = at ?? \"\";\n record.forget = record.forget.filter((other) => other !== id);\n }\n writePending(key, record);\n}\n\n/** After an answer (or a 403/422): take what was sent out of the pending record, keeping\n * what was written while the request ran. */\nfunction clearSent(key: string, sent: TourProgressBody, synced: boolean) {\n const record = readPending(key) ?? { synced: false, completed: {}, forget: [] };\n for (const [id, at] of Object.entries(sent.completed ?? {})) {\n if (record.completed[id] === at) delete record.completed[id];\n }\n const forgotten = new Set(sent.forget ?? []);\n record.forget = record.forget.filter((id) => !forgotten.has(id));\n record.synced = record.synced || synced;\n writePending(key, record);\n}\n\n/** The account's answer as the cache, with what still waits to be sent laid over it. */\nfunction refill(key: string, answer: { completed: TourProgressWire } | null | undefined) {\n const next: Record<string, string> = {};\n for (const [id, at] of Object.entries(fromRecord(answer?.completed) ?? {})) next[id] = at ?? \"\";\n const waiting = readPending(key);\n if (waiting) {\n for (const [id, at] of Object.entries(waiting.completed)) next[id] = later(next[id], at);\n for (const id of waiting.forget) delete next[id];\n }\n const record: Record<string, string | null> = {};\n for (const [id, at] of Object.entries(next)) record[id] = at === \"\" ? null : at;\n write(key, record);\n}\n\n/** The first load of an account key on this page takes over the old device-wide key\n * (§10.2.5). */\nfunction adoptOldRecord(key: string, account: TourProgressAccount) {\n const adopt = account.adopt;\n if (!adopt || adopt.storageKey === key) return;\n const old = adopt.storageKey;\n if (!adopt.discard) {\n const { value } = parse(rawOf(old), adopt.migrate ?? migrates.get(old));\n const entries = Object.entries(value);\n if (entries.length > 0) {\n const record = readPending(key) ?? { synced: false, completed: {}, forget: [] };\n for (const [id, at] of entries) record.completed[id] = later(record.completed[id], at ?? \"\");\n writePending(key, record);\n }\n }\n // Removed either way, at once: no second account on this device gets it.\n removeStored(old);\n memory.delete(old);\n cache.delete(old);\n emit(old);\n}\n\n/** A device record never synced is uploaded whole on its first load (§10.2.5). */\nfunction uploadUnsynced(key: string) {\n const record = readPending(key);\n if (record?.synced) return;\n const device = snapshot(key, migrates.get(key));\n const entries = Object.entries(device);\n const next = record ?? { synced: false, completed: {}, forget: [] };\n if (entries.length === 0 && record) return;\n for (const [id, at] of entries) {\n if (!next.forget.includes(id)) next.completed[id] = later(next.completed[id], at ?? \"\");\n }\n writePending(key, next);\n}\n\n/** The key's first sync on this page: take over the old record, upload a never-synced one. */\nfunction prepare(key: string) {\n loaded.add(key);\n adoptOldRecord(key, accounts.get(key)!);\n uploadUnsynced(key);\n}\n\n/** Queue a sync for the end of this task, so a handler's writes go as one PATCH. */\nfunction queueSync(key: string) {\n if (!loaded.has(key)) prepare(key);\n if (queued.has(key)) return;\n queued.add(key);\n queueMicrotask(() => {\n queued.delete(key);\n void sync(key);\n });\n}\n\n/** The first reader of an account key on this page loads it. */\nfunction ensureLoaded(key: string) {\n if (!accounts.has(key) || loaded.has(key)) return;\n prepare(key);\n void sync(key);\n}\n\n/**\n * Send what waits (one PATCH) or, with nothing waiting, load; the answer replaces the\n * cache. One at a time per key; a request asked for while one runs follows it.\n */\nasync function sync(key: string): Promise<void> {\n const account = accounts.get(key);\n if (!account || !loaded.has(key)) return;\n if (running.has(key)) {\n rerun.add(key);\n return;\n }\n if (typeof navigator !== \"undefined\" && navigator.onLine === false) return;\n running.add(key);\n const epoch = epochs.get(key) ?? 0;\n try {\n const waiting = readPending(key);\n if (waiting && hasWork(waiting)) {\n if (parked.has(key)) return;\n const body: TourProgressBody = {};\n if (Object.keys(waiting.completed).length > 0) body.completed = { ...waiting.completed };\n if (waiting.forget.length > 0) body.forget = [...waiting.forget];\n let answer: { completed: TourProgressWire };\n try {\n answer = await account.save(body);\n } catch (error) {\n const status = httpStatusOf(error);\n if (status === 403 || status === 422) {\n // Refused for good (a demo, a malformed or full record): dropped, not retried.\n if ((epochs.get(key) ?? 0) === epoch) clearSent(key, body, false);\n console.warn(`[ui-kit] tour progress: the account refused the pending record (${status}); dropped.`, error);\n } else if (status === 402) {\n // A lapsed plan refuses every write: kept, but not re-sent after each finish\n // and each `online` — the next session or page load tries again.\n parked.add(key);\n }\n // No answer (offline, 5xx, 401): it stays for the next load or `online`.\n return;\n }\n if ((epochs.get(key) ?? 0) !== epoch) return;\n clearSent(key, body, true);\n refill(key, answer);\n } else {\n const answer = await account.load();\n if ((epochs.get(key) ?? 0) !== epoch) return;\n if (waiting && !waiting.synced) writePending(key, { ...waiting, synced: true });\n else if (!waiting) writePending(key, { synced: true, completed: {}, forget: [] });\n refill(key, answer);\n }\n } catch {\n // A load that failed keeps the cache as it is.\n } finally {\n running.delete(key);\n if (rerun.delete(key)) void sync(key);\n }\n}\n\n/**\n * The end of a session (§10.2.6) — sign-out, a 401 nothing could renew, the account's\n * deletion, every path: removes the CACHE only (the device key and the page's snapshot;\n * every reader in this tab moves), synchronously and with no request, and KEEPS\n * `<storageKey>:pending`. That record sits under this person's own key, holds tour ids and\n * dates only, and goes out on their next sign-in. An answer still on its way is ignored.\n *\n * function logout() { …; signOutTourProgress(`app.tours:${user.id}`); }\n */\nexport function signOutTourProgress(storageKey: string): void {\n epochs.set(storageKey, (epochs.get(storageKey) ?? 0) + 1);\n loaded.delete(storageKey);\n rerun.delete(storageKey);\n parked.delete(storageKey);\n removeStored(storageKey);\n memory.delete(storageKey);\n cache.delete(storageKey);\n emit(storageKey);\n}\n\nfunction subscribe(key: string, listener: () => void): () => void {\n let set = listeners.get(key);\n if (!set) listeners.set(key, (set = new Set()));\n set.add(listener);\n // Another tab wrote (or cleared all storage: `key` null).\n const onStorage = (event: StorageEvent) => {\n if (event.key !== null && event.key !== key) return;\n memory.delete(key);\n listener();\n };\n if (typeof window !== \"undefined\") window.addEventListener(\"storage\", onStorage);\n return () => {\n set.delete(listener);\n if (typeof window !== \"undefined\") window.removeEventListener(\"storage\", onStorage);\n };\n}\n\n/**\n * Forget every tour completion under `storageKey` on this device: the stored key, what the\n * page kept in memory where storage refused it, and every reader in this tab moves at\n * once. For \"Reset this device's settings\" (settings §11):\n *\n * registerDeviceSetting({ key: TOUR_KEY, kind: \"view\", reset: () => resetTourProgress(TOUR_KEY) });\n *\n * With an account (0.45) it clears the cache only: the account is never written, the\n * pending record stays (\"anything waiting to be sent\", settings §11.2.2) and the next load\n * refills the cache. Tour progress is never registered with the device reset (§10.2.8).\n */\nexport function resetTourProgress(storageKey: string): void {\n if (accounts.has(storageKey)) loaded.delete(storageKey);\n removeStored(storageKey);\n memory.delete(storageKey);\n cache.delete(storageKey);\n emit(storageKey);\n}\n\n/**\n * Record a completion under `storageKey`, as {@link TourProgress.complete} does: through\n * the one shared store and snapshot (never a copy), so every reader of the key moves.\n * Returns `true` when it counts anew. `TourProvider` calls this on a tour's last step.\n * @internal Not part of the barrel.\n */\nexport function completeStored(\n storageKey: string,\n migrate: UseTourProgressOptions[\"migrate\"],\n tour: CompletedTour,\n): boolean {\n const subject = typeof tour === \"string\" ? { id: tour } : tour;\n // Read now rather than from a render: two completions in one handler both count.\n const current = readForWrite(storageKey, migrate);\n if (isTourDone(subject, current[subject.id])) return false;\n const at = new Date().toISOString();\n write(storageKey, { ...current, [subject.id]: at });\n if (accounts.has(storageKey)) {\n markPending(storageKey, subject.id, at);\n queueSync(storageKey);\n }\n return true;\n}\n\n/** \"Mark as not done\" (§10.2.8): off the cache, and `forget` to the account. @internal */\nfunction resetStored(storageKey: string, migrate: UseTourProgressOptions[\"migrate\"], id: string) {\n const current = snapshot(storageKey, migrate);\n if (current[id] === undefined) return;\n const next = { ...current };\n delete next[id];\n write(storageKey, next);\n if (accounts.has(storageKey)) {\n markPending(storageKey, id, undefined, true);\n queueSync(storageKey);\n }\n}\n\n/**\n * Write a completion from outside a page (§9.2): for an app's tests, seeds and scripts,\n * beside {@link resetTourProgress}. It writes the kit's stored shape (`{[id]: ISO | \"\"}`)\n * under `storageKey`, keeps the other tours' marks, and notifies every reader of the key\n * in this tab directly (the `storage` event reaches only OTHER tabs), so a mounted\n * `useTourProgress` or `TourCatalogue` moves at once.\n *\n * Unlike `complete`, it always writes (over a readable text; see below): `at` is the\n * completion's instant (an ISO string or a `Date`, default now), `null` a completion\n * without a date (stored as `\"\"`, read back as `null`).\n *\n * completeTour(TOUR_KEY, \"budget\", \"2026-09-01T10:00:00Z\");\n *\n * A stored text in an app's older shape is read through `opts.migrate`, or else the\n * migrate a reader or the provider registered for the key. If the text is neither empty\n * nor the kit's shape and no migrate reads it, this THROWS and writes nothing (B-1):\n *\n * completeTour(TOUR_KEY, \"budget\", undefined, { migrate: fromZustand });\n *\n * With an account on the key (0.45) it stays a device write — no request — and is marked\n * pending, so the next load sends it.\n */\nexport function completeTour(\n storageKey: string,\n tour: string | { id: string },\n at?: string | Date | null,\n opts?: { migrate?: UseTourProgressOptions[\"migrate\"] },\n): void {\n const id = typeof tour === \"string\" ? tour : tour.id;\n const when = at === null ? null : at instanceof Date ? at.toISOString() : (at ?? new Date().toISOString());\n write(storageKey, { ...readForWrite(storageKey, opts?.migrate, true), [id]: when });\n if (accounts.has(storageKey)) markPending(storageKey, id, when);\n}\n\n/** The device's tour completions under `storageKey`; see {@link UseTourProgressOptions}. */\nexport function useTourProgress({ storageKey, migrate, account }: UseTourProgressOptions): TourProgress {\n // Held, not depended on: an inline arrow is a new function every render, and neither the\n // snapshot nor the callbacks may change with it.\n const migrateRef = useRef(migrate);\n useEffect(() => {\n migrateRef.current = migrate;\n });\n // The first account for the key wins (as `migrate`); its first reader on the page loads.\n const accountRef = useRef(account);\n useEffect(() => {\n accountRef.current = account;\n });\n useEffect(() => {\n if (rememberAccount(storageKey, accountRef.current)) ensureLoaded(storageKey);\n }, [storageKey]);\n const completed = useSyncExternalStore(\n useCallback((listener: () => void) => subscribe(storageKey, listener), [storageKey]),\n () => snapshot(storageKey, migrateRef.current),\n () => EMPTY,\n );\n\n const complete = useCallback(\n (tour: CompletedTour) => completeStored(storageKey, migrateRef.current, tour),\n [storageKey],\n );\n\n const reset = useCallback((id: string) => resetStored(storageKey, migrateRef.current, id), [storageKey]);\n\n return useMemo(() => ({ completed, complete, reset }), [completed, complete, reset]);\n}\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tour/use-tour-url-sync.ts"],"sourcesContent":["import { createContext, useContext, useEffect, useLayoutEffect, useRef, useState, type Context } from \"react\";\nimport * as ReactRouter from \"react-router\";\nimport { useLocation, useNavigate, type Navigator } from \"react-router\";\nimport { useSearchParamsState } from \"../hooks/use-search-param-state\";\nimport { useTourOptional, type TourEngine, type TourStart } from \"./tour\";\nimport { liveEngine } from \"./tour-engine\";\nimport { devWarnings } from \"./tour-gates\";\n\n/* ── useTourUrlSync ─────────────────────────────────────────────────────────── */\n\nexport interface UseTourUrlSyncOptions {\n /**\n * Start the tour `id` at `startIndex` (0-based) — the app's launcher, which builds the\n * steps, awaits its own `prepare()` and passes `onFinish` (the completion, the points\n * toast) to `useTour().start`. An unknown id should do nothing. May return a promise:\n * the parameters are kept until it settles. Under a `TourProvider` the third argument is\n * that provider's `start` (0.50, as `TourProvider launch` gets it), so one launcher serves\n * both; the fourth is its whole engine, read live (0.50.1, `TourEngine`: `stop`, `next`,\n * `active`, …). Without a provider `start` and the engine's methods do nothing (a\n * development build warns) and the engine reads as idle.\n */\n launch: (\n id: string,\n options: { startIndex: number },\n start: TourStart,\n engine: TourEngine,\n ) => void | Promise<unknown>;\n /**\n * Are the facts a tour's steps are built from loaded yet (keksdose's `ready`: the\n * caches a branching tour reads)? A deep link waits for it, keeping its parameters, so\n * an F5 mid-tour resumes the right variant at the right step. Default `true`.\n */\n ready?: boolean;\n /**\n * Older names of `tourStep`, read when it is absent and removed with it — kastlan's\n * `ts` (§2.7, for one minor). Their value is a 0-based step index, as kastlan wrote it.\n */\n stepAliases?: readonly string[];\n}\n\ntype Place = { pathname: string; search: string; hash: string };\n\n/**\n * Has the router already moved on from the location React is still rendering?\n * keksdose's check (tour-url-sync.tsx, tours audit 2026-10-02): a mirror write built\n * from the page being LEFT is a replace back onto it, which cancels a step's `go()`.\n *\n * - A data router (`createBrowserRouter`, kastlan): its navigator has no location (build\n * review 0.43, kastlan A2), so the router's own state answers — a navigation still\n * loading, or a location it has committed that React has not rendered yet.\n * - A declarative router (`BrowserRouter`, keksdose): its navigator is its history and\n * carries where it is.\n * - Anything else answers \"not in flight\".\n */\nfunction navigationInFlight(history: Navigator | undefined, dataRouter: DataRouterProbe, rendered: Place) {\n const state = dataRouter?.router?.state;\n if (state && (state.navigation?.state ?? \"idle\") !== \"idle\") return true;\n const live = state?.location ?? (history as (Navigator & { location?: Partial<Place> }) | undefined)?.location;\n if (!live) return false;\n return (\n live.pathname !== rendered.pathname || (live.search ?? \"\") !== rendered.search || (live.hash ?? \"\") !== rendered.hash\n );\n}\n\ntype NavigationProbe = { navigator?: Navigator } | null;\nconst NO_NAVIGATION = createContext<NavigationProbe>(null);\n/** The data router's state, as `useSearchParamState` reads it. */\ntype DataRouterProbe = {\n router?: { state?: { navigation?: { state?: string }; location?: Partial<Place> } };\n} | null;\nconst NO_DATA_ROUTER = createContext<DataRouterProbe>(null);\nconst DataRouterContext: Context<DataRouterProbe> = (() => {\n try {\n return (\n (ReactRouter as unknown as { UNSAFE_DataRouterContext?: Context<DataRouterProbe> }).UNSAFE_DataRouterContext ??\n NO_DATA_ROUTER\n );\n } catch {\n return NO_DATA_ROUTER;\n }\n})();\n/** Read off the namespace inside a `try`, as `useSearchParamState` reads the data router:\n * a test's partial `vi.mock(\"react-router\")` throws on an export it does not define. */\nconst NavigationContext: Context<NavigationProbe> = (() => {\n try {\n return (\n (ReactRouter as unknown as { UNSAFE_NavigationContext?: Context<NavigationProbe> }).UNSAFE_NavigationContext ??\n NO_NAVIGATION\n );\n } catch {\n return NO_NAVIGATION;\n }\n})();\n\n/** The step a link asks for, 0-based: `tourStep` is 1-based (keksdose), an alias 0-based. */\nfunction stepOf(params: URLSearchParams, aliases: readonly string[]): number {\n const step = Number(params.get(\"tourStep\"));\n if (params.has(\"tourStep\")) return Number.isFinite(step) && step > 0 ? step - 1 : 0;\n for (const alias of aliases) {\n const value = Number(params.get(alias));\n if (params.has(alias)) return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;\n }\n return 0;\n}\n\n/**\n * Tours and the URL (docs/tours-harmonization.md §2.7, §6.4) — keksdose's `TourUrlSync`,\n * on the kit's engine (`useTourOptional`):\n *\n * - **`?tour=<id>` starts that tour whenever it APPEARS** — on load, and on in-app\n * navigation too (kastlan's assistant links to `/help/tours?tour=<id>`, ka F4) —\n * resuming at `&tourStep=<n>` (1-based). It waits for `ready`.\n * - **While a tour runs, `?tour=&tourStep=` mirror where it is**, so a reload or a copied\n * link pins the step (kk K-3). A step that navigates drops them; this puts them back.\n * - **At the end it strips only its own parameters**; `?q=`, `?state=` and the hash stay.\n *\n * Mount it once inside the router and the `TourProvider`. Without a provider it still\n * launches through `launch` and never mirrors.\n */\nexport function useTourUrlSync({ launch, ready = true, stepAliases = [] }: UseTourUrlSyncOptions): void {\n const navigate = useNavigate();\n const location = useLocation();\n const { pathname, search, hash } = location;\n const history = useContext(NavigationContext)?.navigator;\n const dataRouter = useContext(DataRouterContext);\n const engine = useTourOptional();\n const active = engine?.active ?? false;\n const activeId = engine?.activeId ?? null;\n const index = engine?.index ?? 0;\n const urlTour = new URLSearchParams(search).get(\"tour\");\n const aliases = stepAliases.join(\"\\n\");\n\n const launchRef = useRef(launch);\n const startRef = useRef<TourStart>(engine?.start ?? noProviderStart);\n const [live] = useState(() => liveEngine<TourEngine>(ENGINE_KEYS, NO_PROVIDER_ENGINE));\n const activeIdRef = useRef(activeId);\n useLayoutEffect(() => {\n launchRef.current = launch;\n startRef.current = engine?.start ?? noProviderStart;\n live.set(engine ?? NO_PROVIDER_ENGINE);\n activeIdRef.current = active ? activeId : null;\n });\n /** The `tour` value last seen in the URL: a tour starts when it CHANGES to an id. */\n const seenRef = useRef<string | null>(null);\n /** A deep link waiting for `ready`. */\n const pendingRef = useRef<{ id: string; startIndex: number } | null>(null);\n /** A launch under way (an app's `prepare()`). */\n const launchingRef = useRef(false);\n /** Bumped when a launch settles, so the mirror looks again. */\n const [settled, setSettled] = useState(0);\n\n useEffect(() => {\n if (urlTour !== seenRef.current) {\n seenRef.current = urlTour;\n // Its own mirror writing the running tour back is no new link.\n pendingRef.current =\n urlTour && urlTour !== activeIdRef.current\n ? { id: urlTour, startIndex: stepOf(new URLSearchParams(search), aliases ? aliases.split(\"\\n\") : []) }\n : null;\n }\n const pending = pendingRef.current;\n if (!pending || !ready) return;\n pendingRef.current = null;\n launchingRef.current = true;\n void Promise.resolve()\n .then(() => launchRef.current(pending.id, { startIndex: pending.startIndex }, startRef.current, live.engine))\n .catch(() => undefined)\n .finally(() => {\n launchingRef.current = false;\n setSettled((n) => n + 1);\n });\n // Only a change of the parameter, or readiness, starts a tour.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [urlTour, ready]);\n\n useEffect(() => {\n if (navigationInFlight(history, dataRouter, { pathname, search, hash })) return;\n // A link not yet used keeps its parameters: a reload in that window resumes.\n if (pendingRef.current || launchingRef.current) return;\n const tourId = active && activeId ? activeId : null;\n const own = [\"tour\", \"tourStep\", ...(aliases ? aliases.split(\"\\n\") : [])];\n const current = new URLSearchParams(search);\n // Nothing to mirror and nothing to strip: never touch the URL (a no-op replace\n // clobbers a page's own just-issued navigation, keksdose feedback #4).\n if (!tourId && !own.some((key) => current.has(key))) return;\n const next = new URLSearchParams(search);\n // `set` keeps a parameter where it stands; only the aliases (and, at the end, the\n // tour's own) go.\n for (const key of tourId ? own.slice(2) : own) next.delete(key);\n if (tourId) {\n next.set(\"tour\", tourId);\n next.set(\"tourStep\", String(index + 1));\n }\n if (next.toString() === current.toString()) return;\n // Spelled out, not `setSearchParams` (\"?…\" drops the hash, keksdose dev#495).\n void navigate({ pathname, search: next.toString(), hash }, { replace: true, state: location.state });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [active, activeId, index, pathname, search, hash, settled, aliases]);\n}\n\n/** `start` for a launcher run with no `TourProvider` above `useTourUrlSync`. */\nconst noProviderStart: TourStart = () => {\n if (devWarnings()) console.warn(\"[ui-kit] useTourUrlSync: the launcher's start needs a TourProvider above it.\");\n};\n\nconst noProviderCall = () => {\n if (devWarnings()) console.warn(\"[ui-kit] useTourUrlSync: the launcher's engine needs a TourProvider above it.\");\n};\n/** The engine for a launcher run with no `TourProvider` above `useTourUrlSync`: idle. */\nconst NO_PROVIDER_ENGINE: TourEngine = {\n active: false,\n activeId: null,\n start: noProviderStart,\n stop: noProviderCall,\n next: noProviderCall,\n prev: noProviderCall,\n goToStep: noProviderCall,\n index: 0,\n total: 0,\n position: 0,\n count: 0,\n};\nconst ENGINE_KEYS = Object.keys(NO_PROVIDER_ENGINE) as (keyof TourEngine)[];\n\n/* ── useTourCatalogueUrlState ───────────────────────────────────────────────── */\n\n/** The catalogue's state filter. */\nexport type TourStateFilter = \"all\" | \"todo\" | \"done\";\n\n/** The catalogue's filters: the search, the group (`null`: every group), the state. */\nexport interface TourCatalogueFilter {\n query: string;\n group: string | null;\n state: TourStateFilter;\n}\n\nconst STATES: readonly TourStateFilter[] = [\"all\", \"todo\", \"done\"];\n\n/**\n * The catalogue's filters in the URL (§2.8) — keksdose's `?q`, `?topic`, `?state`, each\n * written with `replace` (typing does not bury the Back button), the defaults as the\n * clean URL. Spread it onto the catalogue: `<TourCatalogue {...useTourCatalogueUrlState()} … />`.\n * `params` renames them (kastlan's `?group`). Requires a react-router Router.\n */\nexport function useTourCatalogueUrlState(\n params: { query?: string; group?: string; state?: string } = {},\n): TourCatalogueFilter & { onChange: (patch: Partial<TourCatalogueFilter>) => void } {\n const [filter, setFilter] = useSearchParamsState<TourCatalogueFilter>({\n query: { param: params.query ?? \"q\", default: \"\", history: \"replace\" },\n group: { param: params.group ?? \"topic\", default: null, history: \"replace\", parse: (raw) => raw || undefined },\n state: {\n param: params.state ?? \"state\",\n default: \"all\",\n history: \"replace\",\n parse: (raw) => STATES.find((x) => x === raw),\n },\n });\n // `group: null` is a value to the setter (it clears `?topic`); `undefined` leaves it.\n return { ...filter, onChange: setFilter };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/tour/use-tour-url-sync.ts"],"sourcesContent":["import { createContext, useContext, useEffect, useLayoutEffect, useRef, useState, type Context } from \"react\";\nimport * as ReactRouter from \"react-router\";\nimport { useLocation, useNavigate, type Navigator } from \"react-router\";\nimport { useSearchParamsState } from \"../hooks/use-search-param-state\";\nimport { useTourOptional, type TourEngine, type TourStart } from \"./tour\";\nimport { liveEngine } from \"./tour-engine\";\nimport { devWarnings } from \"./tour-gates\";\n\n/* ── useTourUrlSync ─────────────────────────────────────────────────────────── */\n\nexport interface UseTourUrlSyncOptions {\n /**\n * Start the tour `id` at `startIndex` (0-based) — the app's launcher, which builds the\n * steps, awaits its own `prepare()` and passes `onFinish` (the completion, the points\n * toast) to `useTour().start`. An unknown id should do nothing. May return a promise:\n * the parameters are kept until it settles. Under a `TourProvider` the third argument is\n * that provider's `start` (0.50, as `TourProvider launch` gets it), so one launcher serves\n * both; the fourth is its whole engine, read live (0.50.1, `TourEngine`: `stop`, `next`,\n * `active`, …). Without a provider `start` and the engine's methods do nothing (a\n * development build warns) and the engine reads as idle.\n */\n launch: (\n id: string,\n options: { startIndex: number },\n start: TourStart,\n engine: TourEngine,\n ) => void | Promise<unknown>;\n /**\n * Are the facts a tour's steps are built from loaded yet (keksdose's `ready`: the\n * caches a branching tour reads)? A deep link waits for it, keeping its parameters, so\n * an F5 mid-tour resumes the right variant at the right step. Default `true`.\n */\n ready?: boolean;\n /**\n * Older names of `tourStep`, read when it is absent and removed with it — kastlan's\n * `ts` (§2.7, for one minor). Their value is a 0-based step index, as kastlan wrote it.\n */\n stepAliases?: readonly string[];\n}\n\ntype Place = { pathname: string; search: string; hash: string };\n\n/**\n * Has the router already moved on from the location React is still rendering?\n * keksdose's check (tour-url-sync.tsx, tours audit 2026-10-02): a mirror write built\n * from the page being LEFT is a replace back onto it, which cancels a step's `go()`.\n *\n * - A data router (`createBrowserRouter`, kastlan): its navigator has no location (build\n * review 0.43, kastlan A2), so the router's own state answers — a navigation still\n * loading, or a location it has committed that React has not rendered yet.\n * - A declarative router (`BrowserRouter`, keksdose): its navigator is its history and\n * carries where it is.\n * - Anything else answers \"not in flight\".\n */\nfunction navigationInFlight(history: Navigator | undefined, dataRouter: DataRouterProbe, rendered: Place) {\n const state = dataRouter?.router?.state;\n if (state && (state.navigation?.state ?? \"idle\") !== \"idle\") return true;\n const live = state?.location ?? (history as (Navigator & { location?: Partial<Place> }) | undefined)?.location;\n if (!live) return false;\n return (\n live.pathname !== rendered.pathname ||\n (live.search ?? \"\") !== rendered.search ||\n (live.hash ?? \"\") !== rendered.hash\n );\n}\n\ntype NavigationProbe = { navigator?: Navigator } | null;\nconst NO_NAVIGATION = createContext<NavigationProbe>(null);\n/** The data router's state, as `useSearchParamState` reads it. */\ntype DataRouterProbe = {\n router?: { state?: { navigation?: { state?: string }; location?: Partial<Place> } };\n} | null;\nconst NO_DATA_ROUTER = createContext<DataRouterProbe>(null);\nconst DataRouterContext: Context<DataRouterProbe> = (() => {\n try {\n return (\n (ReactRouter as unknown as { UNSAFE_DataRouterContext?: Context<DataRouterProbe> }).UNSAFE_DataRouterContext ??\n NO_DATA_ROUTER\n );\n } catch {\n return NO_DATA_ROUTER;\n }\n})();\n/** Read off the namespace inside a `try`, as `useSearchParamState` reads the data router:\n * a test's partial `vi.mock(\"react-router\")` throws on an export it does not define. */\nconst NavigationContext: Context<NavigationProbe> = (() => {\n try {\n return (\n (ReactRouter as unknown as { UNSAFE_NavigationContext?: Context<NavigationProbe> }).UNSAFE_NavigationContext ??\n NO_NAVIGATION\n );\n } catch {\n return NO_NAVIGATION;\n }\n})();\n\n/** The step a link asks for, 0-based: `tourStep` is 1-based (keksdose), an alias 0-based. */\nfunction stepOf(params: URLSearchParams, aliases: readonly string[]): number {\n const step = Number(params.get(\"tourStep\"));\n if (params.has(\"tourStep\")) return Number.isFinite(step) && step > 0 ? step - 1 : 0;\n for (const alias of aliases) {\n const value = Number(params.get(alias));\n if (params.has(alias)) return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;\n }\n return 0;\n}\n\n/**\n * Tours and the URL (docs/tours-harmonization.md §2.7, §6.4) — keksdose's `TourUrlSync`,\n * on the kit's engine (`useTourOptional`):\n *\n * - **`?tour=<id>` starts that tour whenever it APPEARS** — on load, and on in-app\n * navigation too (kastlan's assistant links to `/help/tours?tour=<id>`, ka F4) —\n * resuming at `&tourStep=<n>` (1-based). It waits for `ready`.\n * - **While a tour runs, `?tour=&tourStep=` mirror where it is**, so a reload or a copied\n * link pins the step (kk K-3). A step that navigates drops them; this puts them back.\n * - **At the end it strips only its own parameters**; `?q=`, `?state=` and the hash stay.\n *\n * Mount it once inside the router and the `TourProvider`. Without a provider it still\n * launches through `launch` and never mirrors.\n */\nexport function useTourUrlSync({ launch, ready = true, stepAliases = [] }: UseTourUrlSyncOptions): void {\n const navigate = useNavigate();\n const location = useLocation();\n const { pathname, search, hash } = location;\n const history = useContext(NavigationContext)?.navigator;\n const dataRouter = useContext(DataRouterContext);\n const engine = useTourOptional();\n const active = engine?.active ?? false;\n const activeId = engine?.activeId ?? null;\n const index = engine?.index ?? 0;\n const urlTour = new URLSearchParams(search).get(\"tour\");\n const aliases = stepAliases.join(\"\\n\");\n\n const launchRef = useRef(launch);\n const startRef = useRef<TourStart>(engine?.start ?? noProviderStart);\n const [live] = useState(() => liveEngine<TourEngine>(ENGINE_KEYS, NO_PROVIDER_ENGINE));\n const activeIdRef = useRef(activeId);\n useLayoutEffect(() => {\n launchRef.current = launch;\n startRef.current = engine?.start ?? noProviderStart;\n live.set(engine ?? NO_PROVIDER_ENGINE);\n activeIdRef.current = active ? activeId : null;\n });\n /** The `tour` value last seen in the URL: a tour starts when it CHANGES to an id. */\n const seenRef = useRef<string | null>(null);\n /** A deep link waiting for `ready`. */\n const pendingRef = useRef<{ id: string; startIndex: number } | null>(null);\n /** A launch under way (an app's `prepare()`). */\n const launchingRef = useRef(false);\n /** Bumped when a launch settles, so the mirror looks again. */\n const [settled, setSettled] = useState(0);\n\n useEffect(() => {\n if (urlTour !== seenRef.current) {\n seenRef.current = urlTour;\n // Its own mirror writing the running tour back is no new link.\n pendingRef.current =\n urlTour && urlTour !== activeIdRef.current\n ? { id: urlTour, startIndex: stepOf(new URLSearchParams(search), aliases ? aliases.split(\"\\n\") : []) }\n : null;\n }\n const pending = pendingRef.current;\n if (!pending || !ready) return;\n pendingRef.current = null;\n launchingRef.current = true;\n void Promise.resolve()\n .then(() => launchRef.current(pending.id, { startIndex: pending.startIndex }, startRef.current, live.engine))\n .catch(() => undefined)\n .finally(() => {\n launchingRef.current = false;\n setSettled((n) => n + 1);\n });\n // Only a change of the parameter, or readiness, starts a tour.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [urlTour, ready]);\n\n useEffect(() => {\n if (navigationInFlight(history, dataRouter, { pathname, search, hash })) return;\n // A link not yet used keeps its parameters: a reload in that window resumes.\n if (pendingRef.current || launchingRef.current) return;\n const tourId = active && activeId ? activeId : null;\n const own = [\"tour\", \"tourStep\", ...(aliases ? aliases.split(\"\\n\") : [])];\n const current = new URLSearchParams(search);\n // Nothing to mirror and nothing to strip: never touch the URL (a no-op replace\n // clobbers a page's own just-issued navigation, keksdose feedback #4).\n if (!tourId && !own.some((key) => current.has(key))) return;\n const next = new URLSearchParams(search);\n // `set` keeps a parameter where it stands; only the aliases (and, at the end, the\n // tour's own) go.\n for (const key of tourId ? own.slice(2) : own) next.delete(key);\n if (tourId) {\n next.set(\"tour\", tourId);\n next.set(\"tourStep\", String(index + 1));\n }\n if (next.toString() === current.toString()) return;\n // Spelled out, not `setSearchParams` (\"?…\" drops the hash, keksdose dev#495).\n void navigate({ pathname, search: next.toString(), hash }, { replace: true, state: location.state });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [active, activeId, index, pathname, search, hash, settled, aliases]);\n}\n\n/** `start` for a launcher run with no `TourProvider` above `useTourUrlSync`. */\nconst noProviderStart: TourStart = () => {\n if (devWarnings()) console.warn(\"[ui-kit] useTourUrlSync: the launcher's start needs a TourProvider above it.\");\n};\n\nconst noProviderCall = () => {\n if (devWarnings()) console.warn(\"[ui-kit] useTourUrlSync: the launcher's engine needs a TourProvider above it.\");\n};\n/** The engine for a launcher run with no `TourProvider` above `useTourUrlSync`: idle. */\nconst NO_PROVIDER_ENGINE: TourEngine = {\n active: false,\n activeId: null,\n start: noProviderStart,\n stop: noProviderCall,\n next: noProviderCall,\n prev: noProviderCall,\n goToStep: noProviderCall,\n index: 0,\n total: 0,\n position: 0,\n count: 0,\n};\nconst ENGINE_KEYS = Object.keys(NO_PROVIDER_ENGINE) as (keyof TourEngine)[];\n\n/* ── useTourCatalogueUrlState ───────────────────────────────────────────────── */\n\n/** The catalogue's state filter. */\nexport type TourStateFilter = \"all\" | \"todo\" | \"done\";\n\n/** The catalogue's filters: the search, the group (`null`: every group), the state. */\nexport interface TourCatalogueFilter {\n query: string;\n group: string | null;\n state: TourStateFilter;\n}\n\nconst STATES: readonly TourStateFilter[] = [\"all\", \"todo\", \"done\"];\n\n/**\n * The catalogue's filters in the URL (§2.8) — keksdose's `?q`, `?topic`, `?state`, each\n * written with `replace` (typing does not bury the Back button), the defaults as the\n * clean URL. Spread it onto the catalogue: `<TourCatalogue {...useTourCatalogueUrlState()} … />`.\n * `params` renames them (kastlan's `?group`). Requires a react-router Router.\n */\nexport function useTourCatalogueUrlState(\n params: { query?: string; group?: string; state?: string } = {},\n): TourCatalogueFilter & { onChange: (patch: Partial<TourCatalogueFilter>) => void } {\n const [filter, setFilter] = useSearchParamsState<TourCatalogueFilter>({\n query: { param: params.query ?? \"q\", default: \"\", history: \"replace\" },\n group: { param: params.group ?? \"topic\", default: null, history: \"replace\", parse: (raw) => raw || undefined },\n state: {\n param: params.state ?? \"state\",\n default: \"all\",\n history: \"replace\",\n parse: (raw) => STATES.find((x) => x === raw),\n },\n });\n // `group: null` is a value to the setter (it clears `?topic`); `undefined` leaves it.\n return { ...filter, onChange: setFilter };\n}\n"],"mappings":"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","names":[]}
@@ -149,10 +149,7 @@ function StepperNav({
149
149
  !isLast && /* @__PURE__ */ jsx(
150
150
  "div",
151
151
  {
152
- className: cn(
153
- "mx-2 h-0.5 flex-1",
154
- status === "completed" ? "bg-brand" : "bg-[var(--border)]"
155
- )
152
+ className: cn("mx-2 h-0.5 flex-1", status === "completed" ? "bg-brand" : "bg-[var(--border)]")
156
153
  }
157
154
  )
158
155
  ] }, step.id);
@@ -164,32 +161,14 @@ function StepperNav({
164
161
  ] })
165
162
  ] }),
166
163
  /* @__PURE__ */ jsx(WizardContextProvider, { value: contextValue, children: /* @__PURE__ */ jsx("div", { style: contentFloor > 0 ? { minHeight: contentFloor } : void 0, children: /* @__PURE__ */ jsx("div", { ref: contentRef, children }) }) }),
167
- wizard.error && /* @__PURE__ */ jsx(
168
- "div",
169
- {
170
- role: "alert",
171
- className: cn(
172
- "text-sm text-danger",
173
- alertFrameClass("danger")
174
- ),
175
- children: wizard.error
176
- }
177
- ),
164
+ wizard.error && /* @__PURE__ */ jsx("div", { role: "alert", className: cn("text-sm text-danger", alertFrameClass("danger")), children: wizard.error }),
178
165
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-t border-subtle pt-4", children: [
179
166
  wizard.canCancel ? /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: wizard.cancel, children: l.cancel }) : /* @__PURE__ */ jsx("span", {}),
180
167
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
181
168
  wizard.canGoBack && // Disabled, not hidden, while the commit runs: it comes back if the commit
182
169
  // fails, and a button that vanishes mid-click is its own kind of surprise.
183
170
  /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: wizard.goBack, disabled: wizard.isSubmitting, children: l.back }),
184
- wizard.currentStep.optional && !wizard.isLastStep && !wizard.isCommitStep && /* @__PURE__ */ jsx(
185
- Button,
186
- {
187
- variant: "ghost",
188
- onClick: wizard.skip,
189
- disabled: wizard.nextBlocked || wizard.isValidating,
190
- children: l.skip
191
- }
192
- ),
171
+ wizard.currentStep.optional && !wizard.isLastStep && !wizard.isCommitStep && /* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: wizard.skip, disabled: wizard.nextBlocked || wizard.isValidating, children: l.skip }),
193
172
  forwardButton
194
173
  ] })
195
174
  ] }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/wizard/stepper-nav.tsx"],"sourcesContent":["import { useId, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { Button } from \"../components/button\";\nimport { Spinner } from \"../components/spinner\";\nimport type { ButtonVariant } from \"../components/button-look\";\nimport { Modal } from \"../components/modal\";\nimport { alertFrameClass } from \"../components/alert-banner\";\nimport { WizardContextProvider } from \"./wizard-context\";\nimport { DEFAULT_WIZARD_LABELS } from \"./types\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport type { UseWizardReturn, WizardFinishResult, WizardLabels } from \"./types\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport { useTourGateRelease } from \"../tour/tour-gates\";\n\n/**\n * The chrome around a wizard: the step indicator, the step content, and the\n * Cancel / Back / Skip / Next / Finish bar — plus the cancel-confirmation\n * dialog, because \"are you sure you want to lose this\" is part of the wizard's\n * contract rather than something each app should re-decide.\n *\n * It is a component and not a set of parts for one reason: which button shows,\n * and whether it is enabled, is a function of `wizard` state that was getting\n * re-derived (and re-derived slightly differently) at every call site. Finish is\n * gated on `canFinish`, Next and Skip on `nextBlocked`, and Skip only appears on\n * an `optional` step that is not the last — that policy lives here now.\n *\n * The `<StepperNav>` is also what publishes {@link WizardContextProvider}, which\n * is how a mounted step registers its own validator. A step's\n * `useRhfWizardStep` / `useWizardStepValidate` therefore only works inside one.\n *\n * Which buttons show is read off `wizard` (`canCancel`, `canGoBack`, `canDone`,\n * `isCommitStep`), so the policy options — `cancellable`, `onExit`, `commits`,\n * `onDone` — are all set on `useWizard`. What is set HERE is how the Finish button\n * looks and what wraps it, because that is presentation and usually depends on\n * state the app holds (a \"replace everything\" toggle, a write lock — `finishCommit`).\n */\nexport function StepperNav<TData extends Record<string, unknown>>({\n wizard,\n children,\n title,\n description,\n className,\n labels,\n finishVariant = \"accent\",\n finishDisabled = false,\n finishCommit,\n finishDisabledReason,\n doneDisabled = false,\n renderFinish,\n}: {\n wizard: UseWizardReturn<TData>;\n children: ReactNode;\n title?: ReactNode;\n description?: ReactNode;\n className?: string;\n labels?: LabelOverride<WizardLabels>;\n /** The Finish button's variant — `\"danger\"` for a destructive commit (an import\n * that replaces what is there). Default `\"accent\"`. Next keeps `\"accent\"`. */\n finishVariant?: ButtonVariant;\n /** Disable Finish for a reason of the app's own, on top of the kit's gates\n * (`canFinish`, submitting, validating). */\n finishDisabled?: boolean;\n /**\n * Finish COMMITS — {@link Button}'s `commit`, handed to the Finish button. Under a\n * locked {@link WriteLockProvider} it is `aria-disabled` but focusable, with the lock's\n * reason in the kit Tooltip, and a click (or Enter) does not run `wizard.finish`.\n * Every other button of the bar stays live: the steps before the commit write nothing,\n * so the whole wizard can still be walked through on a read-only page.\n *\n * keksdose G1: its YNAB import runs the mapping wizard on the read-only demo (dev#497)\n * and locks only the confirm step's Finish — by cloning `commit` into the element\n * `renderFinish` hands it, because StepperNav took no `commit` of its own. That clone\n * still works; this is the same thing without one. Named `finishCommit` beside\n * `finishVariant` / `finishDisabled` rather than a bare `commit`, which on a component\n * with five buttons would not say which one saves — and would read like `useWizard`'s\n * `commits`, which is a step's policy, not a lock.\n */\n finishCommit?: CommitScope;\n /**\n * Why Finish is not available — {@link Button}'s `disabledReason` on the Finish button:\n * `aria-disabled` and focusable, the reason in the kit Tooltip and on the button's\n * `aria-describedby`, `wizard.finish` not run. The explained form of `finishDisabled`\n * (a native `disabled` takes the button out of the tab order, so its reason never\n * reaches a keyboard). Under a lock with `finishCommit`, the lock's reason wins.\n */\n finishDisabledReason?: ReactNode;\n /**\n * Disable Done — the button of the last step after a commit — while the step still\n * has work of its own running. keksdose's YNAB import ends on a recurring step whose\n * convert loop writes one rule at a time (ynab-import-panel.tsx): leaving mid-loop\n * abandons the rest. Until now the panel dropped `onDone` for the duration, which\n * takes the button away instead of saying \"not yet\": disabled, it stays where the\n * eye left it and comes back to life when the loop ends.\n */\n doneDisabled?: boolean;\n /**\n * Wrap the Finish button: receives the kit's button element — already labelled,\n * gated and wired to `wizard.finish` — and returns what to render in its place.\n *\n * ```tsx\n * renderFinish={(button) => <SaveGuard lock={lock}>{button}</SaveGuard>}\n * ```\n *\n * A wrapper and not a replacement (`finishButton: (props) => …`) or a hook\n * (`onBeforeFinish`): the app's need is to put something AROUND the commit — a\n * write-lock tooltip, which has to sit on a wrapping element because a disabled\n * button receives no pointer events — while the kit keeps owning what the button\n * says and when it is enabled. A replacement would hand every app the gating to\n * re-derive (the problem this component exists to end); `onBeforeFinish` can veto\n * a click but cannot explain a disabled button. The element is a plain `<Button>`,\n * so a wrapper that clones it with `disabled` works as expected.\n *\n * For the write lock itself there is no need to wrap any more: `finishCommit` (and\n * `finishDisabledReason`) put the kit Button's own Tooltip on Finish. An element\n * cloned with `commit` here keeps working the same way.\n */\n renderFinish?: (button: ReactElement) => ReactNode;\n}) {\n const l = useKitLabels(\"wizard\", DEFAULT_WIZARD_LABELS, labels);\n const cancelTitleId = useId();\n\n // The step area never shrinks below the tallest step shown so far, so the button\n // bar does not jump up and down as Next and Back move between a long step and a\n // short one. This replaces a fixed `min-h-[300px]` (carried over from Kastlan,\n // whose steps are all long forms), which did the same for steps under 300px but\n // left a short wizard — two fields a step — with a ~200px hole above its buttons.\n // Measured rather than guessed: the floor is whatever the app's own steps need.\n const contentRef = useRef<HTMLDivElement>(null);\n const [contentFloor, setContentFloor] = useState(0);\n useLayoutEffect(() => {\n const el = contentRef.current;\n if (!el || typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => {\n const h = el.offsetHeight;\n setContentFloor((floor) => (h > floor ? h : floor));\n });\n observer.observe(el);\n return () => observer.disconnect();\n }, []);\n\n const contextValue = useMemo(\n () => ({\n registerStepValidate: wizard.registerStepValidate,\n setNextBlocked: wizard.setNextBlocked,\n }),\n [wizard.registerStepValidate, wizard.setNextBlocked],\n );\n\n // Finish is only valid once every step before the last has been completed or\n // skipped (derived once in useWizard). Prevents a restored last-step index\n // (empty data after a reload) from firing an empty submit.\n const canFinish = wizard.canFinish;\n\n // A running tour's `awaitGate` steps (0.50, docs/tours-harmonization.md §11.9): Next\n // releases \"wizard-next\" when the wizard moved and refuses it when a validator kept it\n // in place; Finish releases \"wizard-finish\" once `onComplete` resolved and refuses it,\n // with the error's message, when it threw. No-ops without a TourProvider or a tour.\n const tourGates = useTourGateRelease();\n // `useWizard`'s goNext/finish resolve their outcome at run time (see their types); a\n // wizard object built some other way resolves nothing, and nothing is signalled.\n const goNext = async () => {\n const moved = (await wizard.goNext()) as unknown as boolean | undefined;\n if (moved === true) tourGates.release(\"wizard-next\");\n else if (moved === false) tourGates.refuse(\"wizard-next\");\n };\n const finish = async () => {\n const result = (await wizard.finish()) as unknown as WizardFinishResult | undefined;\n if (!result) return;\n if (result.ok) tourGates.release(\"wizard-finish\");\n else tourGates.refuse(\"wizard-finish\", result.error);\n };\n\n // Pending while the step's validators run (an async server check): a spinner and\n // `aria-busy`, and disabled — the hook's own re-entry guard still refuses a second\n // press, this just stops it looking like the first one did nothing.\n const pending = wizard.isValidating;\n const stepNextLabel = wizard.currentStep.nextLabel;\n\n // Every branch below assigns it; no initial value to overwrite.\n let forwardButton: ReactNode;\n if (wizard.isCommitStep) {\n const finishButton = (\n <Button\n variant={finishVariant}\n data-tour=\"wizard-finish\"\n onClick={() => void finish()}\n // The kit Button's lock: a reason (its own or, with `commit`, the provider's)\n // wins over `disabled` and keeps the button reachable, saying why.\n commit={finishCommit}\n disabledReason={finishDisabledReason}\n disabled={wizard.isSubmitting || pending || !canFinish || finishDisabled}\n aria-busy={wizard.isSubmitting || pending || undefined}\n >\n {/* Decorative: the button's text is what a reader hears. */}\n {pending && <Spinner label={null} className=\"size-4\" />}\n {wizard.isSubmitting ? l.submitting : (stepNextLabel ?? l.finish)}\n </Button>\n );\n forwardButton = renderFinish ? renderFinish(finishButton) : finishButton;\n } else if (wizard.isLastStep) {\n // The last step after a commit. Nothing to finish; Done only when the app gave\n // an `onDone` — otherwise the step carries its own way out.\n forwardButton = wizard.canDone ? (\n <Button variant=\"accent\" data-tour=\"wizard-done\" onClick={wizard.done} disabled={doneDisabled}>\n {stepNextLabel ?? l.done ?? DEFAULT_WIZARD_LABELS.done}\n </Button>\n ) : null;\n } else {\n forwardButton = (\n <Button\n variant=\"accent\"\n data-tour=\"wizard-next\"\n onClick={() => void goNext()}\n disabled={wizard.nextBlocked || pending}\n aria-busy={pending || undefined}\n >\n {pending && <Spinner label={null} className=\"size-4\" />}\n {stepNextLabel ?? l.next}\n </Button>\n );\n }\n\n return (\n <div className={cn(\"flex flex-col gap-6\", className)}>\n {(title || description) && (\n <div>\n {title && (\n <h1 className=\"text-2xl font-bold tracking-tight text-primary\">\n {title}\n </h1>\n )}\n {description && (\n <p className=\"mt-1 text-sm text-muted\">{description}</p>\n )}\n </div>\n )}\n\n {/* Step indicator */}\n <nav aria-label={l.steps}>\n <ol className=\"flex w-full items-center\">\n {wizard.steps.map((step, index) => {\n const status = wizard.stepStatus(index);\n const isLast = index === wizard.steps.length - 1;\n\n return (\n <li key={step.id} className={cn(\"flex items-center\", !isLast && \"flex-1\")}>\n <button\n type=\"button\"\n onClick={() => wizard.goToStep(index)}\n // `canGoToStep` adds the commit lock: once past a committing step,\n // the steps up to it are done with and cannot be reopened.\n disabled={status === \"upcoming\" || !wizard.canGoToStep(index)}\n aria-current={status === \"active\" ? \"step\" : undefined}\n className={cn(\n \"group flex flex-col items-center gap-1.5\",\n status === \"upcoming\" ? \"cursor-default\" : \"cursor-pointer\",\n )}\n >\n {/* Circle */}\n <span\n className={cn(\n \"flex size-8 shrink-0 items-center justify-center rounded-full border-2 text-xs font-semibold transition-colors\",\n status === \"completed\" &&\n \"border-brand bg-brand text-brand-contrast\",\n status === \"active\" &&\n \"border-brand bg-surface text-brand\",\n status === \"upcoming\" &&\n \"border-subtle bg-surface text-placeholder\",\n status === \"skipped\" &&\n \"border-subtle bg-surface-2 text-muted\",\n )}\n >\n {status === \"completed\" ? <Check className=\"size-4\" /> : index + 1}\n </span>\n {/* Label — hidden on mobile, where the line below carries it */}\n <span\n className={cn(\n \"hidden text-xs font-medium md:block\",\n status === \"active\" || status === \"completed\"\n ? \"text-primary\"\n : \"text-muted\",\n )}\n >\n {step.label}\n </span>\n </button>\n\n {/* Connector line */}\n {!isLast && (\n <div\n className={cn(\n \"mx-2 h-0.5 flex-1\",\n status === \"completed\" ? \"bg-brand\" : \"bg-[var(--border)]\",\n )}\n />\n )}\n </li>\n );\n })}\n </ol>\n {/* Mobile: the labels above are hidden, so name the current step here */}\n <p className=\"mt-2 text-center text-sm font-medium text-primary md:hidden\">\n {l.step(wizard.currentStepIndex + 1, wizard.steps.length)}\n {\" — \"}\n {wizard.currentStep.label}\n </p>\n </nav>\n\n {/* Step content */}\n <WizardContextProvider value={contextValue}>\n <div style={contentFloor > 0 ? { minHeight: contentFloor } : undefined}>\n <div ref={contentRef}>{children}</div>\n </div>\n </WizardContextProvider>\n\n {/* Submit error */}\n {wizard.error && (\n <div\n role=\"alert\"\n className={cn(\n \"text-sm text-danger\",\n alertFrameClass(\"danger\"),\n )}\n >\n {wizard.error}\n </div>\n )}\n\n {/* Navigation bar */}\n <div className=\"flex items-center justify-between border-t border-subtle pt-4\">\n {/* The empty span keeps the right-hand group on the right when Cancel is off. */}\n {wizard.canCancel ? (\n <Button variant=\"secondary\" onClick={wizard.cancel}>\n {l.cancel}\n </Button>\n ) : (\n <span />\n )}\n <div className=\"flex items-center gap-2\">\n {wizard.canGoBack && (\n // Disabled, not hidden, while the commit runs: it comes back if the commit\n // fails, and a button that vanishes mid-click is its own kind of surprise.\n <Button variant=\"secondary\" onClick={wizard.goBack} disabled={wizard.isSubmitting}>\n {l.back}\n </Button>\n )}\n {wizard.currentStep.optional && !wizard.isLastStep && !wizard.isCommitStep && (\n <Button\n variant=\"ghost\"\n onClick={wizard.skip}\n disabled={wizard.nextBlocked || wizard.isValidating}\n >\n {l.skip}\n </Button>\n )}\n {forwardButton}\n </div>\n </div>\n\n {/* Cancel confirmation */}\n {wizard.showCancelDialog && (\n <Modal onClose={wizard.dismissCancel} labelledBy={cancelTitleId} className=\"space-y-4\">\n <div className=\"space-y-1.5\">\n <h2 id={cancelTitleId} className=\"text-lg font-semibold text-primary\">\n {l.cancelTitle}\n </h2>\n <p className=\"text-sm text-muted\">{l.confirmCancel}</p>\n </div>\n <div className=\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\">\n <Button variant=\"secondary\" onClick={wizard.dismissCancel}>\n {l.cancelDismissLabel}\n </Button>\n <Button variant=\"danger\" onClick={wizard.confirmCancel}>\n {l.cancelConfirmLabel}\n </Button>\n </div>\n </Modal>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/wizard/stepper-nav.tsx"],"sourcesContent":["import { useId, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { Check } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { Button } from \"../components/button\";\nimport { Spinner } from \"../components/spinner\";\nimport type { ButtonVariant } from \"../components/button-look\";\nimport { Modal } from \"../components/modal\";\nimport { alertFrameClass } from \"../components/alert-banner\";\nimport { WizardContextProvider } from \"./wizard-context\";\nimport { DEFAULT_WIZARD_LABELS } from \"./types\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport type { UseWizardReturn, WizardFinishResult, WizardLabels } from \"./types\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport { useTourGateRelease } from \"../tour/tour-gates\";\n\n/**\n * The chrome around a wizard: the step indicator, the step content, and the\n * Cancel / Back / Skip / Next / Finish bar — plus the cancel-confirmation\n * dialog, because \"are you sure you want to lose this\" is part of the wizard's\n * contract rather than something each app should re-decide.\n *\n * It is a component and not a set of parts for one reason: which button shows,\n * and whether it is enabled, is a function of `wizard` state that was getting\n * re-derived (and re-derived slightly differently) at every call site. Finish is\n * gated on `canFinish`, Next and Skip on `nextBlocked`, and Skip only appears on\n * an `optional` step that is not the last — that policy lives here now.\n *\n * The `<StepperNav>` is also what publishes {@link WizardContextProvider}, which\n * is how a mounted step registers its own validator. A step's\n * `useRhfWizardStep` / `useWizardStepValidate` therefore only works inside one.\n *\n * Which buttons show is read off `wizard` (`canCancel`, `canGoBack`, `canDone`,\n * `isCommitStep`), so the policy options — `cancellable`, `onExit`, `commits`,\n * `onDone` — are all set on `useWizard`. What is set HERE is how the Finish button\n * looks and what wraps it, because that is presentation and usually depends on\n * state the app holds (a \"replace everything\" toggle, a write lock — `finishCommit`).\n */\nexport function StepperNav<TData extends Record<string, unknown>>({\n wizard,\n children,\n title,\n description,\n className,\n labels,\n finishVariant = \"accent\",\n finishDisabled = false,\n finishCommit,\n finishDisabledReason,\n doneDisabled = false,\n renderFinish,\n}: {\n wizard: UseWizardReturn<TData>;\n children: ReactNode;\n title?: ReactNode;\n description?: ReactNode;\n className?: string;\n labels?: LabelOverride<WizardLabels>;\n /** The Finish button's variant — `\"danger\"` for a destructive commit (an import\n * that replaces what is there). Default `\"accent\"`. Next keeps `\"accent\"`. */\n finishVariant?: ButtonVariant;\n /** Disable Finish for a reason of the app's own, on top of the kit's gates\n * (`canFinish`, submitting, validating). */\n finishDisabled?: boolean;\n /**\n * Finish COMMITS — {@link Button}'s `commit`, handed to the Finish button. Under a\n * locked {@link WriteLockProvider} it is `aria-disabled` but focusable, with the lock's\n * reason in the kit Tooltip, and a click (or Enter) does not run `wizard.finish`.\n * Every other button of the bar stays live: the steps before the commit write nothing,\n * so the whole wizard can still be walked through on a read-only page.\n *\n * keksdose G1: its YNAB import runs the mapping wizard on the read-only demo (dev#497)\n * and locks only the confirm step's Finish — by cloning `commit` into the element\n * `renderFinish` hands it, because StepperNav took no `commit` of its own. That clone\n * still works; this is the same thing without one. Named `finishCommit` beside\n * `finishVariant` / `finishDisabled` rather than a bare `commit`, which on a component\n * with five buttons would not say which one saves — and would read like `useWizard`'s\n * `commits`, which is a step's policy, not a lock.\n */\n finishCommit?: CommitScope;\n /**\n * Why Finish is not available — {@link Button}'s `disabledReason` on the Finish button:\n * `aria-disabled` and focusable, the reason in the kit Tooltip and on the button's\n * `aria-describedby`, `wizard.finish` not run. The explained form of `finishDisabled`\n * (a native `disabled` takes the button out of the tab order, so its reason never\n * reaches a keyboard). Under a lock with `finishCommit`, the lock's reason wins.\n */\n finishDisabledReason?: ReactNode;\n /**\n * Disable Done — the button of the last step after a commit — while the step still\n * has work of its own running. keksdose's YNAB import ends on a recurring step whose\n * convert loop writes one rule at a time (ynab-import-panel.tsx): leaving mid-loop\n * abandons the rest. Until now the panel dropped `onDone` for the duration, which\n * takes the button away instead of saying \"not yet\": disabled, it stays where the\n * eye left it and comes back to life when the loop ends.\n */\n doneDisabled?: boolean;\n /**\n * Wrap the Finish button: receives the kit's button element — already labelled,\n * gated and wired to `wizard.finish` — and returns what to render in its place.\n *\n * ```tsx\n * renderFinish={(button) => <SaveGuard lock={lock}>{button}</SaveGuard>}\n * ```\n *\n * A wrapper and not a replacement (`finishButton: (props) => …`) or a hook\n * (`onBeforeFinish`): the app's need is to put something AROUND the commit — a\n * write-lock tooltip, which has to sit on a wrapping element because a disabled\n * button receives no pointer events — while the kit keeps owning what the button\n * says and when it is enabled. A replacement would hand every app the gating to\n * re-derive (the problem this component exists to end); `onBeforeFinish` can veto\n * a click but cannot explain a disabled button. The element is a plain `<Button>`,\n * so a wrapper that clones it with `disabled` works as expected.\n *\n * For the write lock itself there is no need to wrap any more: `finishCommit` (and\n * `finishDisabledReason`) put the kit Button's own Tooltip on Finish. An element\n * cloned with `commit` here keeps working the same way.\n */\n renderFinish?: (button: ReactElement) => ReactNode;\n}) {\n const l = useKitLabels(\"wizard\", DEFAULT_WIZARD_LABELS, labels);\n const cancelTitleId = useId();\n\n // The step area never shrinks below the tallest step shown so far, so the button\n // bar does not jump up and down as Next and Back move between a long step and a\n // short one. This replaces a fixed `min-h-[300px]` (carried over from Kastlan,\n // whose steps are all long forms), which did the same for steps under 300px but\n // left a short wizard — two fields a step — with a ~200px hole above its buttons.\n // Measured rather than guessed: the floor is whatever the app's own steps need.\n const contentRef = useRef<HTMLDivElement>(null);\n const [contentFloor, setContentFloor] = useState(0);\n useLayoutEffect(() => {\n const el = contentRef.current;\n if (!el || typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(() => {\n const h = el.offsetHeight;\n setContentFloor((floor) => (h > floor ? h : floor));\n });\n observer.observe(el);\n return () => observer.disconnect();\n }, []);\n\n const contextValue = useMemo(\n () => ({\n registerStepValidate: wizard.registerStepValidate,\n setNextBlocked: wizard.setNextBlocked,\n }),\n [wizard.registerStepValidate, wizard.setNextBlocked],\n );\n\n // Finish is only valid once every step before the last has been completed or\n // skipped (derived once in useWizard). Prevents a restored last-step index\n // (empty data after a reload) from firing an empty submit.\n const canFinish = wizard.canFinish;\n\n // A running tour's `awaitGate` steps (0.50, docs/tours-harmonization.md §11.9): Next\n // releases \"wizard-next\" when the wizard moved and refuses it when a validator kept it\n // in place; Finish releases \"wizard-finish\" once `onComplete` resolved and refuses it,\n // with the error's message, when it threw. No-ops without a TourProvider or a tour.\n const tourGates = useTourGateRelease();\n // `useWizard`'s goNext/finish resolve their outcome at run time (see their types); a\n // wizard object built some other way resolves nothing, and nothing is signalled.\n const goNext = async () => {\n const moved = (await wizard.goNext()) as unknown as boolean | undefined;\n if (moved === true) tourGates.release(\"wizard-next\");\n else if (moved === false) tourGates.refuse(\"wizard-next\");\n };\n const finish = async () => {\n const result = (await wizard.finish()) as unknown as WizardFinishResult | undefined;\n if (!result) return;\n if (result.ok) tourGates.release(\"wizard-finish\");\n else tourGates.refuse(\"wizard-finish\", result.error);\n };\n\n // Pending while the step's validators run (an async server check): a spinner and\n // `aria-busy`, and disabled — the hook's own re-entry guard still refuses a second\n // press, this just stops it looking like the first one did nothing.\n const pending = wizard.isValidating;\n const stepNextLabel = wizard.currentStep.nextLabel;\n\n // Every branch below assigns it; no initial value to overwrite.\n let forwardButton: ReactNode;\n if (wizard.isCommitStep) {\n const finishButton = (\n <Button\n variant={finishVariant}\n data-tour=\"wizard-finish\"\n onClick={() => void finish()}\n // The kit Button's lock: a reason (its own or, with `commit`, the provider's)\n // wins over `disabled` and keeps the button reachable, saying why.\n commit={finishCommit}\n disabledReason={finishDisabledReason}\n disabled={wizard.isSubmitting || pending || !canFinish || finishDisabled}\n aria-busy={wizard.isSubmitting || pending || undefined}\n >\n {/* Decorative: the button's text is what a reader hears. */}\n {pending && <Spinner label={null} className=\"size-4\" />}\n {wizard.isSubmitting ? l.submitting : (stepNextLabel ?? l.finish)}\n </Button>\n );\n forwardButton = renderFinish ? renderFinish(finishButton) : finishButton;\n } else if (wizard.isLastStep) {\n // The last step after a commit. Nothing to finish; Done only when the app gave\n // an `onDone` — otherwise the step carries its own way out.\n forwardButton = wizard.canDone ? (\n <Button variant=\"accent\" data-tour=\"wizard-done\" onClick={wizard.done} disabled={doneDisabled}>\n {stepNextLabel ?? l.done ?? DEFAULT_WIZARD_LABELS.done}\n </Button>\n ) : null;\n } else {\n forwardButton = (\n <Button\n variant=\"accent\"\n data-tour=\"wizard-next\"\n onClick={() => void goNext()}\n disabled={wizard.nextBlocked || pending}\n aria-busy={pending || undefined}\n >\n {pending && <Spinner label={null} className=\"size-4\" />}\n {stepNextLabel ?? l.next}\n </Button>\n );\n }\n\n return (\n <div className={cn(\"flex flex-col gap-6\", className)}>\n {(title || description) && (\n <div>\n {title && <h1 className=\"text-2xl font-bold tracking-tight text-primary\">{title}</h1>}\n {description && <p className=\"mt-1 text-sm text-muted\">{description}</p>}\n </div>\n )}\n\n {/* Step indicator */}\n <nav aria-label={l.steps}>\n <ol className=\"flex w-full items-center\">\n {wizard.steps.map((step, index) => {\n const status = wizard.stepStatus(index);\n const isLast = index === wizard.steps.length - 1;\n\n return (\n <li key={step.id} className={cn(\"flex items-center\", !isLast && \"flex-1\")}>\n <button\n type=\"button\"\n onClick={() => wizard.goToStep(index)}\n // `canGoToStep` adds the commit lock: once past a committing step,\n // the steps up to it are done with and cannot be reopened.\n disabled={status === \"upcoming\" || !wizard.canGoToStep(index)}\n aria-current={status === \"active\" ? \"step\" : undefined}\n className={cn(\n \"group flex flex-col items-center gap-1.5\",\n status === \"upcoming\" ? \"cursor-default\" : \"cursor-pointer\",\n )}\n >\n {/* Circle */}\n <span\n className={cn(\n \"flex size-8 shrink-0 items-center justify-center rounded-full border-2 text-xs font-semibold transition-colors\",\n status === \"completed\" && \"border-brand bg-brand text-brand-contrast\",\n status === \"active\" && \"border-brand bg-surface text-brand\",\n status === \"upcoming\" && \"border-subtle bg-surface text-placeholder\",\n status === \"skipped\" && \"border-subtle bg-surface-2 text-muted\",\n )}\n >\n {status === \"completed\" ? <Check className=\"size-4\" /> : index + 1}\n </span>\n {/* Label — hidden on mobile, where the line below carries it */}\n <span\n className={cn(\n \"hidden text-xs font-medium md:block\",\n status === \"active\" || status === \"completed\" ? \"text-primary\" : \"text-muted\",\n )}\n >\n {step.label}\n </span>\n </button>\n\n {/* Connector line */}\n {!isLast && (\n <div\n className={cn(\"mx-2 h-0.5 flex-1\", status === \"completed\" ? \"bg-brand\" : \"bg-[var(--border)]\")}\n />\n )}\n </li>\n );\n })}\n </ol>\n {/* Mobile: the labels above are hidden, so name the current step here */}\n <p className=\"mt-2 text-center text-sm font-medium text-primary md:hidden\">\n {l.step(wizard.currentStepIndex + 1, wizard.steps.length)}\n {\" — \"}\n {wizard.currentStep.label}\n </p>\n </nav>\n\n {/* Step content */}\n <WizardContextProvider value={contextValue}>\n <div style={contentFloor > 0 ? { minHeight: contentFloor } : undefined}>\n <div ref={contentRef}>{children}</div>\n </div>\n </WizardContextProvider>\n\n {/* Submit error */}\n {wizard.error && (\n <div role=\"alert\" className={cn(\"text-sm text-danger\", alertFrameClass(\"danger\"))}>\n {wizard.error}\n </div>\n )}\n\n {/* Navigation bar */}\n <div className=\"flex items-center justify-between border-t border-subtle pt-4\">\n {/* The empty span keeps the right-hand group on the right when Cancel is off. */}\n {wizard.canCancel ? (\n <Button variant=\"secondary\" onClick={wizard.cancel}>\n {l.cancel}\n </Button>\n ) : (\n <span />\n )}\n <div className=\"flex items-center gap-2\">\n {wizard.canGoBack && (\n // Disabled, not hidden, while the commit runs: it comes back if the commit\n // fails, and a button that vanishes mid-click is its own kind of surprise.\n <Button variant=\"secondary\" onClick={wizard.goBack} disabled={wizard.isSubmitting}>\n {l.back}\n </Button>\n )}\n {wizard.currentStep.optional && !wizard.isLastStep && !wizard.isCommitStep && (\n <Button variant=\"ghost\" onClick={wizard.skip} disabled={wizard.nextBlocked || wizard.isValidating}>\n {l.skip}\n </Button>\n )}\n {forwardButton}\n </div>\n </div>\n\n {/* Cancel confirmation */}\n {wizard.showCancelDialog && (\n <Modal onClose={wizard.dismissCancel} labelledBy={cancelTitleId} className=\"space-y-4\">\n <div className=\"space-y-1.5\">\n <h2 id={cancelTitleId} className=\"text-lg font-semibold text-primary\">\n {l.cancelTitle}\n </h2>\n <p className=\"text-sm text-muted\">{l.confirmCancel}</p>\n </div>\n <div className=\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\">\n <Button variant=\"secondary\" onClick={wizard.dismissCancel}>\n {l.cancelDismissLabel}\n </Button>\n <Button variant=\"danger\" onClick={wizard.confirmCancel}>\n {l.cancelConfirmLabel}\n </Button>\n </div>\n </Modal>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/wizard/types.ts"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { DEFAULT_WIZARD_LABELS } from \"./wizard-labels\";\n\nexport type FieldErrors = Record<string, string>;\n\nexport type ValidateResult = boolean | { ok: boolean; errors?: FieldErrors };\n\nexport interface WizardStepConfig {\n /** Unique step identifier. */\n id: string;\n /**\n * What the step is called in the indicator.\n *\n * A `ReactNode`, not a translation key: the engine never resolves strings.\n * An app with i18n maps its own keys at the `useWizard` call — the same split\n * `TourStep` and `DataTableLabels` already use.\n */\n label: ReactNode;\n /** Whether the step can be skipped. */\n optional?: boolean;\n /**\n * Validation gate before advancing.\n * - Return `true` / `false` to just block.\n * - Return `{ ok: false, errors: { fieldName: \"message\" } }` to also surface\n * per-field error messages via `wizard.fieldErrors`.\n */\n validate?: () => ValidateResult | Promise<ValidateResult>;\n /**\n * The forward button's label on THIS step, in place of `labels.next` — \"Analyse\",\n * \"Import\", \"Send code\". On the committing step (see {@link commits}) it labels\n * Finish instead of `labels.finish`; `labels.submitting` still replaces it while\n * the commit runs. Like `label`, a node the app has already translated.\n */\n nextLabel?: ReactNode;\n /**\n * This step's forward button COMMITS: it runs `onComplete` (via `finish`) instead\n * of just advancing. Without the flag the last step commits, as it always has.\n *\n * Flag a step before the last and every step after it becomes a post-commit step —\n * an import's \"here is what happened, set up recurring rules?\" page:\n * - a successful `onComplete` advances onto the next step (a failed one stays, with\n * `wizard.error`, exactly like a last-step Finish);\n * - from there Back, the indicator and `goBack`/`goToStep` cannot cross back over\n * the commit, and Cancel is hidden — there is nothing left to discard;\n * - the last post-commit step's button is Done, which calls `onDone` (and is not\n * shown when there is no `onDone`: the step then carries its own way out).\n *\n * Only the FIRST flagged step counts. Steps after it may still validate, skip and\n * use `nextLabel` like any other.\n */\n commits?: boolean;\n}\n\n/**\n * Where the active step lives. `true` (the default) mirrors it to `?step=N` through\n * react-router, as every wizard before 0.8 did; `{ param }` renames that param (two\n * wizards on one page, or a page that already uses `step`); `false` keeps it in\n * memory only and needs no router at all.\n *\n * Read once, at mount: switching it on a mounted wizard has no effect.\n */\nexport type WizardUrlSync = boolean | { param?: string };\n\nexport interface UseWizardOptions<TData extends Record<string, unknown>> {\n steps: WizardStepConfig[];\n initialData?: Partial<TData>;\n onComplete?: (data: TData) => void | Promise<void>;\n onCancel?: () => void;\n /**\n * Leave the wizard BACKWARDS: when given, Back is shown on the first step too and\n * calls this — keksdose's import panel, whose first step's Back returns to the\n * dropzone the file came from. No confirm dialog: going back is not discarding.\n */\n onExit?: () => void;\n /**\n * The Done button on the last step after a committing step (see\n * {@link WizardStepConfig.commits}). Without it no Done button is rendered, and the\n * post-commit step is expected to carry its own way out.\n */\n onDone?: () => void;\n /**\n * Whether the chrome offers Cancel at all. Default `true`. `false` hides the button\n * (`wizard.canCancel` is false); `wizard.cancel()` still works if the app calls it.\n */\n cancellable?: boolean;\n /**\n * Whether Cancel asks first (\"Discard this form?\"). Default `true`. `false` makes\n * `cancel()` call `onCancel` straight away — for a wizard that has nothing to lose\n * yet, or one whose data is kept elsewhere.\n */\n confirmCancel?: boolean;\n /**\n * Mirror the active step to the URL. Default `true` (`?step=N`, needs a router).\n * See {@link WizardUrlSync}.\n *\n * The param is WRITTEN only, never read back (see the note in `use-wizard.ts`). It\n * is removed again when the wizard is left through the kit: a confirmed Cancel,\n * `onExit` from the first step's Back, and Done — each before the app's callback\n * runs, so an app that navigates away navigates from a clean URL. It is NOT removed\n * after `onComplete` on a last step: an app navigating away there would otherwise\n * be pulled back by the kit's own URL write. An app that stays on the page after a\n * final Finish drops the param itself, or uses `urlSync: false`.\n */\n urlSync?: WizardUrlSync;\n /**\n * Shown when a step blocks with no per-field detail — an RHF step surfacing\n * its own inline messages, say — so the user learns why the wizard did not\n * advance. Falls back to `wizard.missingRequired` from the {@link UiKitProvider},\n * then to English.\n */\n missingRequiredMessage?: string;\n /**\n * How to surface {@link missingRequiredMessage}. Defaults to the kit's `toast.error`\n * (sonner, an optional peer, is loaded only when a toast is shown). Pass your own to\n * route it somewhere else, or to a no-op to silence it.\n */\n onValidationFailed?: (message: string) => void;\n}\n\nexport type StepStatus = \"upcoming\" | \"active\" | \"completed\" | \"skipped\";\n\n\n/** What `finish` resolves to at run time (0.50): whether the commit went through, and the\n * error's message when `onComplete` threw. `undefined` when there was nothing to run. */\nexport interface WizardFinishResult {\n ok: boolean;\n error?: string;\n}\nexport interface UseWizardReturn<TData extends Record<string, unknown>> {\n // Navigation\n currentStepIndex: number;\n currentStep: WizardStepConfig;\n steps: WizardStepConfig[];\n isFirstStep: boolean;\n isLastStep: boolean;\n /** True once every step before the committing one (the last, unless a step is\n * flagged `commits`) has been completed or skipped. */\n canFinish: boolean;\n /** The step whose forward button commits: the first `commits` step, else the last. */\n commitStepIndex: number;\n /** The active step is {@link commitStepIndex}: the chrome shows Finish, not Next. */\n isCommitStep: boolean;\n /** `onComplete` has resolved. Once the active step is past the committing one, Back\n * cannot cross it and Cancel is hidden. */\n committed: boolean;\n /** Whether Back is offered here: not on the first step unless `onExit` was given,\n * and never back across a commit. (Disabled, not hidden, while `isSubmitting`.) */\n canGoBack: boolean;\n /** Whether Cancel is offered: `cancellable`, and not after a commit. */\n canCancel: boolean;\n /** Whether Done is offered: the last step, past a commit, with an `onDone`. */\n canDone: boolean;\n /** Whether the indicator may jump to `index` — a completed/skipped/active step on\n * the right side of any commit. `goToStep` refuses anything this rejects. */\n canGoToStep: (index: number) => boolean;\n /** Validates the step and moves on. (0.50: at run time it resolves `true` when it\n * moved, `false` when a validator kept it in place — what StepperNav tells a running\n * tour, docs/tours-harmonization.md §11.9. The declared type stays `Promise<void>`.) */\n goNext: () => Promise<void>;\n /** One step back — or, on the first step, `onExit`. Refused while submitting. */\n goBack: () => void;\n goToStep: (index: number) => void;\n skip: () => void;\n cancel: () => void;\n /** Validates and commits (`onComplete`). (0.50: at run time it resolves `{ ok, error? }`\n * — see {@link WizardFinishResult}; the declared type stays `Promise<void>`.) */\n finish: () => Promise<void>;\n /** Leave after the post-commit steps: drops the URL param, then calls `onDone`. */\n done: () => void;\n /** Register a validator for the mounted step (used by useRhfWizardStep). */\n registerStepValidate: (\n fn: () => ValidateResult | Promise<ValidateResult>,\n ) => () => void;\n /** True when the mounted step has blocked forward navigation (Next/Skip disabled). */\n nextBlocked: boolean;\n /** Set by the active step (via useWizardNextGate) to disable/enable Next+Skip. */\n setNextBlocked: (blocked: boolean) => void;\n\n // Step state\n stepStatus: (index: number) => StepStatus;\n completedSteps: Set<number>;\n\n // Data\n data: TData;\n updateData: (partial: Partial<TData>) => void;\n\n // Loading / error\n isSubmitting: boolean;\n /** A `goNext`/`finish` is running the step's validators (an async server check,\n * say). The chrome shows Next pending — spinner, `aria-busy`, disabled. */\n isValidating: boolean;\n error: string | null;\n clearError: () => void;\n\n // Per-field validation errors (populated by the last failed goNext)\n fieldErrors: FieldErrors;\n clearFieldErrors: () => void;\n\n // Cancel dialog\n showCancelDialog: boolean;\n confirmCancel: () => void;\n dismissCancel: () => void;\n}\n\nexport interface SummaryItem {\n label: ReactNode;\n value: ReactNode;\n}\n\nexport interface SummarySection {\n label: ReactNode;\n /**\n * Which step the section's edit button jumps back to. Leave it out for a section\n * that belongs to no step — file-level counts, a total — and the card has no edit\n * button, instead of borrowing one from whichever step it was parked under.\n */\n stepIndex?: number;\n items: SummaryItem[];\n}\n\n/**\n * Every string the wizard chrome shows. All optional with English defaults, so a\n * monolingual app can mount `StepperNav` bare; a translated app passes the whole\n * object once (typically from a thin app-side wrapper).\n */\nexport interface WizardLabels {\n cancel: string;\n back: string;\n next: string;\n skip: string;\n finish: string;\n /** Shown on Finish while `isSubmitting`. */\n submitting: string;\n /** `aria-label` for the step indicator's `<nav>`. */\n steps: string;\n /** The mobile \"Step 2 of 5\" line. */\n step: (current: number, total: number) => string;\n /** Heading of the cancel-confirmation dialog. */\n cancelTitle: string;\n /** Body of the cancel-confirmation dialog. */\n confirmCancel: string;\n /** Confirm button in the cancel dialog. */\n cancelConfirmLabel: string;\n /** Dismiss button in the cancel dialog (\"keep editing\"). */\n cancelDismissLabel: string;\n /** Heading above a {@link WizardSummary}. */\n reviewTitle: string;\n /** `aria-label` on a summary section's edit button. */\n edit: string;\n /**\n * Toasted (or handed to `onValidationFailed`) when a step blocks Next without\n * saying which field is at fault. `useWizard`'s `missingRequiredMessage` wins.\n *\n * This and `genericError` are OPTIONAL, unlike every key above, for the reason\n * `TwoFactorSettingLabels.qrAlt` is: apps annotate a full translation as\n * `const DE: WizardLabels = {…}`, and a new required key would be a compile error\n * in each of them on the next update. The defaults below always carry both.\n */\n missingRequired?: string;\n /** `wizard.error` when `onComplete` rejects with nothing readable — a thrown\n * non-Error, or an Error whose message is empty. Optional: see above. */\n genericError?: string;\n /** The button on the last step after a committing step (`onDone`). Optional: see\n * `missingRequired`. */\n done?: string;\n}\n\n// The English words live in ./wizard-labels, a module of words (round 0.34).\nexport { DEFAULT_WIZARD_LABELS };\n"],"mappings":";AACA,SAAS,6BAA6B;","names":[]}
1
+ {"version":3,"sources":["../../src/wizard/types.ts"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { DEFAULT_WIZARD_LABELS } from \"./wizard-labels\";\n\nexport type FieldErrors = Record<string, string>;\n\nexport type ValidateResult = boolean | { ok: boolean; errors?: FieldErrors };\n\nexport interface WizardStepConfig {\n /** Unique step identifier. */\n id: string;\n /**\n * What the step is called in the indicator.\n *\n * A `ReactNode`, not a translation key: the engine never resolves strings.\n * An app with i18n maps its own keys at the `useWizard` call — the same split\n * `TourStep` and `DataTableLabels` already use.\n */\n label: ReactNode;\n /** Whether the step can be skipped. */\n optional?: boolean;\n /**\n * Validation gate before advancing.\n * - Return `true` / `false` to just block.\n * - Return `{ ok: false, errors: { fieldName: \"message\" } }` to also surface\n * per-field error messages via `wizard.fieldErrors`.\n */\n validate?: () => ValidateResult | Promise<ValidateResult>;\n /**\n * The forward button's label on THIS step, in place of `labels.next` — \"Analyse\",\n * \"Import\", \"Send code\". On the committing step (see {@link commits}) it labels\n * Finish instead of `labels.finish`; `labels.submitting` still replaces it while\n * the commit runs. Like `label`, a node the app has already translated.\n */\n nextLabel?: ReactNode;\n /**\n * This step's forward button COMMITS: it runs `onComplete` (via `finish`) instead\n * of just advancing. Without the flag the last step commits, as it always has.\n *\n * Flag a step before the last and every step after it becomes a post-commit step —\n * an import's \"here is what happened, set up recurring rules?\" page:\n * - a successful `onComplete` advances onto the next step (a failed one stays, with\n * `wizard.error`, exactly like a last-step Finish);\n * - from there Back, the indicator and `goBack`/`goToStep` cannot cross back over\n * the commit, and Cancel is hidden — there is nothing left to discard;\n * - the last post-commit step's button is Done, which calls `onDone` (and is not\n * shown when there is no `onDone`: the step then carries its own way out).\n *\n * Only the FIRST flagged step counts. Steps after it may still validate, skip and\n * use `nextLabel` like any other.\n */\n commits?: boolean;\n}\n\n/**\n * Where the active step lives. `true` (the default) mirrors it to `?step=N` through\n * react-router, as every wizard before 0.8 did; `{ param }` renames that param (two\n * wizards on one page, or a page that already uses `step`); `false` keeps it in\n * memory only and needs no router at all.\n *\n * Read once, at mount: switching it on a mounted wizard has no effect.\n */\nexport type WizardUrlSync = boolean | { param?: string };\n\nexport interface UseWizardOptions<TData extends Record<string, unknown>> {\n steps: WizardStepConfig[];\n initialData?: Partial<TData>;\n onComplete?: (data: TData) => void | Promise<void>;\n onCancel?: () => void;\n /**\n * Leave the wizard BACKWARDS: when given, Back is shown on the first step too and\n * calls this — keksdose's import panel, whose first step's Back returns to the\n * dropzone the file came from. No confirm dialog: going back is not discarding.\n */\n onExit?: () => void;\n /**\n * The Done button on the last step after a committing step (see\n * {@link WizardStepConfig.commits}). Without it no Done button is rendered, and the\n * post-commit step is expected to carry its own way out.\n */\n onDone?: () => void;\n /**\n * Whether the chrome offers Cancel at all. Default `true`. `false` hides the button\n * (`wizard.canCancel` is false); `wizard.cancel()` still works if the app calls it.\n */\n cancellable?: boolean;\n /**\n * Whether Cancel asks first (\"Discard this form?\"). Default `true`. `false` makes\n * `cancel()` call `onCancel` straight away — for a wizard that has nothing to lose\n * yet, or one whose data is kept elsewhere.\n */\n confirmCancel?: boolean;\n /**\n * Mirror the active step to the URL. Default `true` (`?step=N`, needs a router).\n * See {@link WizardUrlSync}.\n *\n * The param is WRITTEN only, never read back (see the note in `use-wizard.ts`). It\n * is removed again when the wizard is left through the kit: a confirmed Cancel,\n * `onExit` from the first step's Back, and Done — each before the app's callback\n * runs, so an app that navigates away navigates from a clean URL. It is NOT removed\n * after `onComplete` on a last step: an app navigating away there would otherwise\n * be pulled back by the kit's own URL write. An app that stays on the page after a\n * final Finish drops the param itself, or uses `urlSync: false`.\n */\n urlSync?: WizardUrlSync;\n /**\n * Shown when a step blocks with no per-field detail — an RHF step surfacing\n * its own inline messages, say — so the user learns why the wizard did not\n * advance. Falls back to `wizard.missingRequired` from the {@link UiKitProvider},\n * then to English.\n */\n missingRequiredMessage?: string;\n /**\n * How to surface {@link missingRequiredMessage}. Defaults to the kit's `toast.error`\n * (sonner, an optional peer, is loaded only when a toast is shown). Pass your own to\n * route it somewhere else, or to a no-op to silence it.\n */\n onValidationFailed?: (message: string) => void;\n}\n\nexport type StepStatus = \"upcoming\" | \"active\" | \"completed\" | \"skipped\";\n\n/** What `finish` resolves to at run time (0.50): whether the commit went through, and the\n * error's message when `onComplete` threw. `undefined` when there was nothing to run. */\nexport interface WizardFinishResult {\n ok: boolean;\n error?: string;\n}\nexport interface UseWizardReturn<TData extends Record<string, unknown>> {\n // Navigation\n currentStepIndex: number;\n currentStep: WizardStepConfig;\n steps: WizardStepConfig[];\n isFirstStep: boolean;\n isLastStep: boolean;\n /** True once every step before the committing one (the last, unless a step is\n * flagged `commits`) has been completed or skipped. */\n canFinish: boolean;\n /** The step whose forward button commits: the first `commits` step, else the last. */\n commitStepIndex: number;\n /** The active step is {@link commitStepIndex}: the chrome shows Finish, not Next. */\n isCommitStep: boolean;\n /** `onComplete` has resolved. Once the active step is past the committing one, Back\n * cannot cross it and Cancel is hidden. */\n committed: boolean;\n /** Whether Back is offered here: not on the first step unless `onExit` was given,\n * and never back across a commit. (Disabled, not hidden, while `isSubmitting`.) */\n canGoBack: boolean;\n /** Whether Cancel is offered: `cancellable`, and not after a commit. */\n canCancel: boolean;\n /** Whether Done is offered: the last step, past a commit, with an `onDone`. */\n canDone: boolean;\n /** Whether the indicator may jump to `index` — a completed/skipped/active step on\n * the right side of any commit. `goToStep` refuses anything this rejects. */\n canGoToStep: (index: number) => boolean;\n /** Validates the step and moves on. (0.50: at run time it resolves `true` when it\n * moved, `false` when a validator kept it in place — what StepperNav tells a running\n * tour, docs/tours-harmonization.md §11.9. The declared type stays `Promise<void>`.) */\n goNext: () => Promise<void>;\n /** One step back — or, on the first step, `onExit`. Refused while submitting. */\n goBack: () => void;\n goToStep: (index: number) => void;\n skip: () => void;\n cancel: () => void;\n /** Validates and commits (`onComplete`). (0.50: at run time it resolves `{ ok, error? }`\n * — see {@link WizardFinishResult}; the declared type stays `Promise<void>`.) */\n finish: () => Promise<void>;\n /** Leave after the post-commit steps: drops the URL param, then calls `onDone`. */\n done: () => void;\n /** Register a validator for the mounted step (used by useRhfWizardStep). */\n registerStepValidate: (fn: () => ValidateResult | Promise<ValidateResult>) => () => void;\n /** True when the mounted step has blocked forward navigation (Next/Skip disabled). */\n nextBlocked: boolean;\n /** Set by the active step (via useWizardNextGate) to disable/enable Next+Skip. */\n setNextBlocked: (blocked: boolean) => void;\n\n // Step state\n stepStatus: (index: number) => StepStatus;\n completedSteps: Set<number>;\n\n // Data\n data: TData;\n updateData: (partial: Partial<TData>) => void;\n\n // Loading / error\n isSubmitting: boolean;\n /** A `goNext`/`finish` is running the step's validators (an async server check,\n * say). The chrome shows Next pending — spinner, `aria-busy`, disabled. */\n isValidating: boolean;\n error: string | null;\n clearError: () => void;\n\n // Per-field validation errors (populated by the last failed goNext)\n fieldErrors: FieldErrors;\n clearFieldErrors: () => void;\n\n // Cancel dialog\n showCancelDialog: boolean;\n confirmCancel: () => void;\n dismissCancel: () => void;\n}\n\nexport interface SummaryItem {\n label: ReactNode;\n value: ReactNode;\n}\n\nexport interface SummarySection {\n label: ReactNode;\n /**\n * Which step the section's edit button jumps back to. Leave it out for a section\n * that belongs to no step — file-level counts, a total — and the card has no edit\n * button, instead of borrowing one from whichever step it was parked under.\n */\n stepIndex?: number;\n items: SummaryItem[];\n}\n\n/**\n * Every string the wizard chrome shows. All optional with English defaults, so a\n * monolingual app can mount `StepperNav` bare; a translated app passes the whole\n * object once (typically from a thin app-side wrapper).\n */\nexport interface WizardLabels {\n cancel: string;\n back: string;\n next: string;\n skip: string;\n finish: string;\n /** Shown on Finish while `isSubmitting`. */\n submitting: string;\n /** `aria-label` for the step indicator's `<nav>`. */\n steps: string;\n /** The mobile \"Step 2 of 5\" line. */\n step: (current: number, total: number) => string;\n /** Heading of the cancel-confirmation dialog. */\n cancelTitle: string;\n /** Body of the cancel-confirmation dialog. */\n confirmCancel: string;\n /** Confirm button in the cancel dialog. */\n cancelConfirmLabel: string;\n /** Dismiss button in the cancel dialog (\"keep editing\"). */\n cancelDismissLabel: string;\n /** Heading above a {@link WizardSummary}. */\n reviewTitle: string;\n /** `aria-label` on a summary section's edit button. */\n edit: string;\n /**\n * Toasted (or handed to `onValidationFailed`) when a step blocks Next without\n * saying which field is at fault. `useWizard`'s `missingRequiredMessage` wins.\n *\n * This and `genericError` are OPTIONAL, unlike every key above, for the reason\n * `TwoFactorSettingLabels.qrAlt` is: apps annotate a full translation as\n * `const DE: WizardLabels = {…}`, and a new required key would be a compile error\n * in each of them on the next update. The defaults below always carry both.\n */\n missingRequired?: string;\n /** `wizard.error` when `onComplete` rejects with nothing readable — a thrown\n * non-Error, or an Error whose message is empty. Optional: see above. */\n genericError?: string;\n /** The button on the last step after a committing step (`onDone`). Optional: see\n * `missingRequired`. */\n done?: string;\n}\n\n// The English words live in ./wizard-labels, a module of words (round 0.34).\nexport { DEFAULT_WIZARD_LABELS };\n"],"mappings":";AACA,SAAS,6BAA6B;","names":[]}
@@ -99,23 +99,16 @@ function useWizard(options) {
99
99
  });
100
100
  const missingRequired = wizardLabels.missingRequired ?? DEFAULT_WIZARD_LABELS.missingRequired;
101
101
  const genericError = wizardLabels.genericError ?? DEFAULT_WIZARD_LABELS.genericError;
102
- const validatorsRef = useRef(
103
- /* @__PURE__ */ new Map()
104
- );
105
- const registerStepValidate = useCallback(
106
- (fn) => {
107
- const token = /* @__PURE__ */ Symbol();
108
- validatorsRef.current.set(token, fn);
109
- return () => {
110
- validatorsRef.current.delete(token);
111
- };
112
- },
113
- []
114
- );
102
+ const validatorsRef = useRef(/* @__PURE__ */ new Map());
103
+ const registerStepValidate = useCallback((fn) => {
104
+ const token = /* @__PURE__ */ Symbol();
105
+ validatorsRef.current.set(token, fn);
106
+ return () => {
107
+ validatorsRef.current.delete(token);
108
+ };
109
+ }, []);
115
110
  const [useStepUrl] = useState(() => urlSync === false ? useNoStepUrl : useSearchParamStepUrl);
116
- const [urlParam] = useState(
117
- () => typeof urlSync === "object" && urlSync.param ? urlSync.param : "step"
118
- );
111
+ const [urlParam] = useState(() => typeof urlSync === "object" && urlSync.param ? urlSync.param : "step");
119
112
  const initialStep = 0;
120
113
  const reducer = createReducer(steps.length);
121
114
  const [state, dispatch] = useReducer(reducer, {
@@ -131,10 +124,7 @@ function useWizard(options) {
131
124
  committedStepIndex: null
132
125
  });
133
126
  const [nextBlocked, setNextBlockedState] = useState(false);
134
- const setNextBlocked = useCallback(
135
- (blocked) => setNextBlockedState(blocked),
136
- []
137
- );
127
+ const setNextBlocked = useCallback((blocked) => setNextBlockedState(blocked), []);
138
128
  const clearStepUrl = useStepUrl(urlParam, state.currentStepIndex);
139
129
  const currentStep = steps[state.currentStepIndex];
140
130
  const isFirstStep = state.currentStepIndex === 0;
@@ -350,18 +340,24 @@ function useSearchParamStepUrl(param, index) {
350
340
  const leftRef = useRef(false);
351
341
  useEffect(() => {
352
342
  if (leftRef.current) return;
353
- setUrlParams((prev) => {
354
- prev.set(param, String(index));
355
- return prev;
356
- }, { replace: true });
343
+ setUrlParams(
344
+ (prev) => {
345
+ prev.set(param, String(index));
346
+ return prev;
347
+ },
348
+ { replace: true }
349
+ );
357
350
  }, [param, index, setUrlParams]);
358
351
  return useCallback(() => {
359
352
  if (leftRef.current) return;
360
353
  leftRef.current = true;
361
- setUrlParams((prev) => {
362
- prev.delete(param);
363
- return prev;
364
- }, { replace: true });
354
+ setUrlParams(
355
+ (prev) => {
356
+ prev.delete(param);
357
+ return prev;
358
+ },
359
+ { replace: true }
360
+ );
365
361
  }, [param, setUrlParams]);
366
362
  }
367
363
  function useNoStepUrl(_param, _index) {