@eifi1/ui-kit 0.54.0 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (847) hide show
  1. package/README.md +90 -76
  2. package/dist/admin/plan-change.js +2 -2
  3. package/dist/admin/plan-change.js.map +1 -1
  4. package/dist/admin/user-roster.d.ts +6 -0
  5. package/dist/admin/user-roster.js +4 -0
  6. package/dist/admin/user-roster.js.map +1 -1
  7. package/dist/billing/billing-labels.d.ts +26 -0
  8. package/dist/billing/billing-labels.js +9 -0
  9. package/dist/billing/billing-labels.js.map +1 -1
  10. package/dist/billing/subscription-standing.d.ts +61 -0
  11. package/dist/billing/subscription-standing.js +51 -0
  12. package/dist/billing/subscription-standing.js.map +1 -0
  13. package/dist/billing/usage-meter.d.ts +52 -0
  14. package/dist/billing/usage-meter.js +52 -0
  15. package/dist/billing/usage-meter.js.map +1 -0
  16. package/dist/chart.d.ts +1 -0
  17. package/dist/chart.js +4 -0
  18. package/dist/chart.js.map +1 -1
  19. package/dist/components/alert-banner.js +2 -3
  20. package/dist/components/alert-banner.js.map +1 -1
  21. package/dist/components/amount-input.js +3 -15
  22. package/dist/components/amount-input.js.map +1 -1
  23. package/dist/components/autocomplete.js +2 -2
  24. package/dist/components/autocomplete.js.map +1 -1
  25. package/dist/components/bulk-action-bar.js +2 -1
  26. package/dist/components/bulk-action-bar.js.map +1 -1
  27. package/dist/components/button.d.ts +35 -1
  28. package/dist/components/button.js +6 -9
  29. package/dist/components/button.js.map +1 -1
  30. package/dist/components/calendar-heatmap.js +2 -2
  31. package/dist/components/calendar-heatmap.js.map +1 -1
  32. package/dist/components/camera-button.d.ts +89 -0
  33. package/dist/components/camera-button.js +122 -0
  34. package/dist/components/camera-button.js.map +1 -0
  35. package/dist/components/camera-capture.d.ts +66 -0
  36. package/dist/components/camera-capture.js +190 -0
  37. package/dist/components/camera-capture.js.map +1 -0
  38. package/dist/components/camera-labels.d.ts +20 -0
  39. package/dist/components/camera-labels.js +15 -0
  40. package/dist/components/camera-labels.js.map +1 -0
  41. package/dist/components/chart-edge.d.ts +9 -0
  42. package/dist/components/chart-edge.js +14 -3
  43. package/dist/components/chart-edge.js.map +1 -1
  44. package/dist/components/chart-zoom-core.js +34 -29
  45. package/dist/components/chart-zoom-core.js.map +1 -1
  46. package/dist/components/checkbox-group.js +1 -1
  47. package/dist/components/checkbox-group.js.map +1 -1
  48. package/dist/components/combobox-core.js +3 -2
  49. package/dist/components/combobox-core.js.map +1 -1
  50. package/dist/components/combobox.js +2 -2
  51. package/dist/components/combobox.js.map +1 -1
  52. package/dist/components/consent-again-notice-labels.d.ts +25 -0
  53. package/dist/components/consent-again-notice-labels.js +15 -0
  54. package/dist/components/consent-again-notice-labels.js.map +1 -0
  55. package/dist/components/consent-again-notice.d.ts +85 -0
  56. package/dist/components/consent-again-notice.js +94 -0
  57. package/dist/components/consent-again-notice.js.map +1 -0
  58. package/dist/components/country-select.js +2 -7
  59. package/dist/components/country-select.js.map +1 -1
  60. package/dist/components/currency-select.js +2 -2
  61. package/dist/components/currency-select.js.map +1 -1
  62. package/dist/components/danger-confirm.js +2 -2
  63. package/dist/components/danger-confirm.js.map +1 -1
  64. package/dist/components/data-table-column-settings.js +2 -2
  65. package/dist/components/data-table-column-settings.js.map +1 -1
  66. package/dist/components/data-table-fit.js.map +1 -1
  67. package/dist/components/data-table-virtual.js +13 -3
  68. package/dist/components/data-table-virtual.js.map +1 -1
  69. package/dist/components/date-field.js +2 -2
  70. package/dist/components/date-field.js.map +1 -1
  71. package/dist/components/date-picker.js +2 -2
  72. package/dist/components/date-picker.js.map +1 -1
  73. package/dist/components/date-range-picker.js +2 -6
  74. package/dist/components/date-range-picker.js.map +1 -1
  75. package/dist/components/dialog-frame.d.ts +3 -1
  76. package/dist/components/dialog-frame.js.map +1 -1
  77. package/dist/components/dropdown.js +7 -3
  78. package/dist/components/dropdown.js.map +1 -1
  79. package/dist/components/entity-combobox.js +2 -2
  80. package/dist/components/entity-combobox.js.map +1 -1
  81. package/dist/components/field-parts.d.ts +108 -6
  82. package/dist/components/field-parts.js +59 -6
  83. package/dist/components/field-parts.js.map +1 -1
  84. package/dist/components/field-suggestion-labels.d.ts +2 -0
  85. package/dist/components/field-suggestion-labels.js +10 -0
  86. package/dist/components/field-suggestion-labels.js.map +1 -0
  87. package/dist/components/field.js +1 -1
  88. package/dist/components/field.js.map +1 -1
  89. package/dist/components/file-button.d.ts +1 -1
  90. package/dist/components/file-button.js.map +1 -1
  91. package/dist/components/floating-panel.d.ts +7 -6
  92. package/dist/components/floating-panel.js +3 -17
  93. package/dist/components/floating-panel.js.map +1 -1
  94. package/dist/components/form-actions.d.ts +3 -2
  95. package/dist/components/form-actions.js.map +1 -1
  96. package/dist/components/hover-menu.js +3 -2
  97. package/dist/components/hover-menu.js.map +1 -1
  98. package/dist/components/icon-button.d.ts +23 -5
  99. package/dist/components/icon-button.js +7 -8
  100. package/dist/components/icon-button.js.map +1 -1
  101. package/dist/components/inline-confirm.js +2 -1
  102. package/dist/components/inline-confirm.js.map +1 -1
  103. package/dist/components/inline-entity-combobox.js +2 -2
  104. package/dist/components/inline-entity-combobox.js.map +1 -1
  105. package/dist/components/input.js +2 -2
  106. package/dist/components/input.js.map +1 -1
  107. package/dist/components/jump-calendar.js +3 -3
  108. package/dist/components/jump-calendar.js.map +1 -1
  109. package/dist/components/legal.d.ts +13 -1
  110. package/dist/components/legal.js +12 -0
  111. package/dist/components/legal.js.map +1 -1
  112. package/dist/components/legended-series-chart.d.ts +83 -0
  113. package/dist/components/legended-series-chart.js +77 -0
  114. package/dist/components/legended-series-chart.js.map +1 -0
  115. package/dist/components/lightbox.js +2 -1
  116. package/dist/components/lightbox.js.map +1 -1
  117. package/dist/components/mini-calendar.js +13 -5
  118. package/dist/components/mini-calendar.js.map +1 -1
  119. package/dist/components/modal.d.ts +23 -2
  120. package/dist/components/modal.js +24 -6
  121. package/dist/components/modal.js.map +1 -1
  122. package/dist/components/month-picker.d.ts +1 -1
  123. package/dist/components/month-picker.js +6 -6
  124. package/dist/components/month-picker.js.map +1 -1
  125. package/dist/components/multi-entity-combobox.js +2 -2
  126. package/dist/components/multi-entity-combobox.js.map +1 -1
  127. package/dist/components/multi-select.js +2 -6
  128. package/dist/components/multi-select.js.map +1 -1
  129. package/dist/components/number-input.js +2 -2
  130. package/dist/components/number-input.js.map +1 -1
  131. package/dist/components/page-header-layout.d.ts +18 -0
  132. package/dist/components/page-header-layout.js +23 -0
  133. package/dist/components/page-header-layout.js.map +1 -0
  134. package/dist/components/page-header-skeleton.d.ts +41 -0
  135. package/dist/components/page-header-skeleton.js +52 -0
  136. package/dist/components/page-header-skeleton.js.map +1 -0
  137. package/dist/components/page-header.js +4 -17
  138. package/dist/components/page-header.js.map +1 -1
  139. package/dist/components/popover.js +7 -2
  140. package/dist/components/popover.js.map +1 -1
  141. package/dist/components/release-notes-labels.d.ts +16 -0
  142. package/dist/components/release-notes-labels.js +18 -0
  143. package/dist/components/release-notes-labels.js.map +1 -0
  144. package/dist/components/release-notes.d.ts +65 -0
  145. package/dist/components/release-notes.js +65 -0
  146. package/dist/components/release-notes.js.map +1 -0
  147. package/dist/components/select.js +2 -2
  148. package/dist/components/select.js.map +1 -1
  149. package/dist/components/series-chart-labels.d.ts +9 -0
  150. package/dist/components/series-chart-labels.js +3 -1
  151. package/dist/components/series-chart-labels.js.map +1 -1
  152. package/dist/components/series-chart.d.ts +5 -0
  153. package/dist/components/series-chart.js +89 -36
  154. package/dist/components/series-chart.js.map +1 -1
  155. package/dist/components/toggle-group.js +3 -2
  156. package/dist/components/toggle-group.js.map +1 -1
  157. package/dist/components/treemap-layout.js.map +1 -1
  158. package/dist/components/treemap.d.ts +17 -15
  159. package/dist/components/treemap.js +167 -85
  160. package/dist/components/treemap.js.map +1 -1
  161. package/dist/eslint/index.js +1 -1
  162. package/dist/eslint/role-utilities.d.ts +2 -2
  163. package/dist/eslint/role-utilities.js.map +1 -1
  164. package/dist/feedback/feedback-inbox-page.d.ts +260 -0
  165. package/dist/feedback/feedback-inbox-page.js +207 -0
  166. package/dist/feedback/feedback-inbox-page.js.map +1 -0
  167. package/dist/feedback/feedback-vocabulary.js +2 -2
  168. package/dist/feedback/feedback-vocabulary.js.map +1 -1
  169. package/dist/feedback.d.ts +1 -0
  170. package/dist/feedback.js +6 -0
  171. package/dist/feedback.js.map +1 -1
  172. package/dist/hooks/use-debounce.d.ts +32 -5
  173. package/dist/hooks/use-debounce.js +6 -5
  174. package/dist/hooks/use-debounce.js.map +1 -1
  175. package/dist/hooks/use-file-picker.d.ts +3 -1
  176. package/dist/hooks/use-file-picker.js.map +1 -1
  177. package/dist/hooks/use-focus-trap.d.ts +19 -3
  178. package/dist/hooks/use-focus-trap.js +15 -5
  179. package/dist/hooks/use-focus-trap.js.map +1 -1
  180. package/dist/hooks/use-has-camera.d.ts +36 -0
  181. package/dist/hooks/use-has-camera.js +53 -0
  182. package/dist/hooks/use-has-camera.js.map +1 -0
  183. package/dist/hooks/use-hotkey.js +1 -10
  184. package/dist/hooks/use-hotkey.js.map +1 -1
  185. package/dist/hooks/use-overlay-history.d.ts +6 -1
  186. package/dist/hooks/use-overlay-history.js +10 -2
  187. package/dist/hooks/use-overlay-history.js.map +1 -1
  188. package/dist/i18n/defaults.js +11 -1
  189. package/dist/i18n/defaults.js.map +1 -1
  190. package/dist/i18n/german.js +75 -3
  191. package/dist/i18n/german.js.map +1 -1
  192. package/dist/i18n/kit-labels.d.ts +20 -0
  193. package/dist/i18n/kit-labels.js.map +1 -1
  194. package/dist/i18n/locales/en.js +92 -4
  195. package/dist/i18n/locales/en.js.map +3 -3
  196. package/dist/i18n/locales/es.js +71 -3
  197. package/dist/i18n/locales/es.js.map +1 -1
  198. package/dist/i18n/locales/fr.js +72 -2
  199. package/dist/i18n/locales/fr.js.map +1 -1
  200. package/dist/i18n/locales/hu.js +74 -3
  201. package/dist/i18n/locales/hu.js.map +1 -1
  202. package/dist/i18n/locales/it.js +74 -3
  203. package/dist/i18n/locales/it.js.map +1 -1
  204. package/dist/i18n/locales/zh.js +71 -3
  205. package/dist/i18n/locales/zh.js.map +1 -1
  206. package/dist/i18n/review.js +22 -1
  207. package/dist/i18n/review.js.map +1 -1
  208. package/dist/index.d.ts +24 -5
  209. package/dist/index.js +35 -1
  210. package/dist/index.js.map +1 -1
  211. package/dist/landing/landing-labels.d.ts +8 -0
  212. package/dist/landing/landing-labels.js +2 -1
  213. package/dist/landing/landing-labels.js.map +1 -1
  214. package/dist/lib/changelog.d.ts +130 -0
  215. package/dist/lib/changelog.js +137 -0
  216. package/dist/lib/changelog.js.map +1 -0
  217. package/dist/lib/clipboard-minus.js +2 -2
  218. package/dist/lib/clipboard-minus.js.map +1 -1
  219. package/dist/lib/dates.d.ts +29 -3
  220. package/dist/lib/dates.js +41 -12
  221. package/dist/lib/dates.js.map +1 -1
  222. package/dist/lib/editable-content.d.ts +33 -0
  223. package/dist/lib/editable-content.js +15 -0
  224. package/dist/lib/editable-content.js.map +1 -0
  225. package/dist/lib/focus-left.d.ts +47 -1
  226. package/dist/lib/focus-left.js +11 -2
  227. package/dist/lib/focus-left.js.map +1 -1
  228. package/dist/lib/server-wake.d.ts +49 -9
  229. package/dist/lib/server-wake.js +25 -14
  230. package/dist/lib/server-wake.js.map +1 -1
  231. package/dist/lib/session-refresh.d.ts +53 -0
  232. package/dist/lib/session-refresh.js +27 -1
  233. package/dist/lib/session-refresh.js.map +1 -1
  234. package/dist/rhf/fields.js +3 -3
  235. package/dist/rhf/fields.js.map +1 -1
  236. package/dist/shell/app-shell.d.ts +25 -1
  237. package/dist/shell/app-shell.js +3 -1
  238. package/dist/shell/app-shell.js.map +1 -1
  239. package/dist/shell/dev-menu.d.ts +3 -0
  240. package/dist/shell/dev-menu.js +4 -2
  241. package/dist/shell/dev-menu.js.map +1 -1
  242. package/dist/shell/held-changes-banner-labels.d.ts +23 -0
  243. package/dist/shell/held-changes-banner-labels.js +14 -0
  244. package/dist/shell/held-changes-banner-labels.js.map +1 -0
  245. package/dist/shell/held-changes-banner.d.ts +75 -0
  246. package/dist/shell/held-changes-banner.js +106 -0
  247. package/dist/shell/held-changes-banner.js.map +1 -0
  248. package/dist/shell/sync-status-indicator.d.ts +27 -12
  249. package/dist/shell/sync-status-indicator.js +13 -4
  250. package/dist/shell/sync-status-indicator.js.map +1 -1
  251. package/dist/shell/top-bar-parts-labels.d.ts +5 -0
  252. package/dist/shell/top-bar-parts-labels.js +3 -1
  253. package/dist/shell/top-bar-parts-labels.js.map +1 -1
  254. package/dist/shell.d.ts +2 -0
  255. package/dist/shell.js +3 -0
  256. package/dist/shell.js.map +1 -1
  257. package/dist/sync/engine.d.ts +11 -2
  258. package/dist/sync/engine.js +13 -5
  259. package/dist/sync/engine.js.map +1 -1
  260. package/dist/sync/hooks.d.ts +3 -2
  261. package/dist/sync/hooks.js +4 -2
  262. package/dist/sync/hooks.js.map +1 -1
  263. package/dist/testing/css-variables.generated.d.ts +2 -2
  264. package/dist/testing/css-variables.generated.js +1 -1
  265. package/dist/testing/css-variables.generated.js.map +1 -1
  266. package/dist/theme/chart-palette.d.ts +18 -0
  267. package/dist/theme/chart-palette.js +17 -1
  268. package/dist/theme/chart-palette.js.map +1 -1
  269. package/dist/theme/contrast-tokens.d.ts +3 -3
  270. package/dist/theme/contrast-tokens.js.map +1 -1
  271. package/dist/theme/contrast.d.ts +4 -4
  272. package/dist/theme/contrast.js.map +1 -1
  273. package/dist/tour/tour-overlay.js +2 -1
  274. package/dist/tour/tour-overlay.js.map +1 -1
  275. package/package.json +9 -12
  276. package/tokens.css +19 -54
  277. package/src/account/account-parts.ts +0 -47
  278. package/src/account/account-settings-labels.ts +0 -240
  279. package/src/account/account-settings.tsx +0 -698
  280. package/src/account/data-export-setting-labels.ts +0 -20
  281. package/src/account/data-export-setting.tsx +0 -200
  282. package/src/account/delete-account-setting-labels.ts +0 -33
  283. package/src/account/delete-account-setting.tsx +0 -342
  284. package/src/account/email-change-setting-labels.ts +0 -31
  285. package/src/account/email-change-setting.tsx +0 -443
  286. package/src/account/passkeys-setting.tsx +0 -445
  287. package/src/account/sessions-setting-labels.ts +0 -24
  288. package/src/account/sessions-setting.tsx +0 -283
  289. package/src/admin/admin-action-confirm-labels.ts +0 -19
  290. package/src/admin/admin-action-confirm.tsx +0 -509
  291. package/src/admin/admin-action-log-labels.ts +0 -83
  292. package/src/admin/admin-action-log.tsx +0 -464
  293. package/src/admin/admin-area-labels.ts +0 -114
  294. package/src/admin/admin-forbidden.tsx +0 -75
  295. package/src/admin/admin-overview.tsx +0 -258
  296. package/src/admin/admin-parts.ts +0 -68
  297. package/src/admin/admin-sections.tsx +0 -208
  298. package/src/admin/admin-tool-links.tsx +0 -90
  299. package/src/admin/invitations-panel-labels.ts +0 -31
  300. package/src/admin/invitations-panel.tsx +0 -516
  301. package/src/admin/mail-test-card-labels.ts +0 -19
  302. package/src/admin/mail-test-card.tsx +0 -189
  303. package/src/admin/plan-change-labels.ts +0 -5
  304. package/src/admin/plan-change.tsx +0 -474
  305. package/src/admin/reviewer-scope-editor-labels.ts +0 -16
  306. package/src/admin/reviewer-scope-editor.tsx +0 -224
  307. package/src/admin/role-select-labels.ts +0 -16
  308. package/src/admin/role-select.tsx +0 -282
  309. package/src/admin/transfer-ownership-dialog-labels.ts +0 -20
  310. package/src/admin/transfer-ownership-dialog.tsx +0 -162
  311. package/src/admin/user-roster-labels.ts +0 -17
  312. package/src/admin/user-roster.tsx +0 -559
  313. package/src/auth/accept-invitation-labels.ts +0 -21
  314. package/src/auth/accept-invitation.tsx +0 -210
  315. package/src/auth/auth-errors-labels.ts +0 -23
  316. package/src/auth/auth-errors.ts +0 -439
  317. package/src/auth/complete-name-dialog-labels.ts +0 -16
  318. package/src/auth/complete-name-dialog.tsx +0 -253
  319. package/src/auth/email-tag.ts +0 -75
  320. package/src/auth/forgot-password-form-labels.ts +0 -20
  321. package/src/auth/forgot-password-form.tsx +0 -221
  322. package/src/auth/form-rules.ts +0 -40
  323. package/src/auth/new-password-fields.tsx +0 -85
  324. package/src/auth/not-found-page-labels.ts +0 -12
  325. package/src/auth/not-found-page.tsx +0 -95
  326. package/src/auth/register-form-labels.ts +0 -31
  327. package/src/auth/register-form.tsx +0 -456
  328. package/src/auth/reset-password-form-labels.ts +0 -27
  329. package/src/auth/reset-password-form.tsx +0 -341
  330. package/src/auth/sign-in-form-labels.ts +0 -44
  331. package/src/auth/sign-in-form.tsx +0 -858
  332. package/src/auth/status-parts.tsx +0 -132
  333. package/src/auth/verify-email-labels.ts +0 -26
  334. package/src/auth/verify-email.tsx +0 -445
  335. package/src/billing/billing-banner.tsx +0 -258
  336. package/src/billing/billing-labels.ts +0 -381
  337. package/src/billing/billing-lock.ts +0 -108
  338. package/src/billing/billing-overview.ts +0 -157
  339. package/src/billing/billing-standing.ts +0 -80
  340. package/src/billing/cancel-request.ts +0 -97
  341. package/src/billing/cancel-subscription.tsx +0 -98
  342. package/src/billing/checkout-processing.ts +0 -394
  343. package/src/billing/checkout-return.ts +0 -90
  344. package/src/billing/pay-page.ts +0 -330
  345. package/src/billing/plan-card.tsx +0 -562
  346. package/src/billing/plan-change-labels.ts +0 -66
  347. package/src/billing/plan-limit-notice.tsx +0 -148
  348. package/src/billing/plan-limit-toast.tsx +0 -90
  349. package/src/billing/plan-limit.ts +0 -52
  350. package/src/billing/plan-price.ts +0 -98
  351. package/src/billing/plan-switch.tsx +0 -216
  352. package/src/billing/provider-page.ts +0 -84
  353. package/src/billing/subscription-actions.tsx +0 -247
  354. package/src/billing/subscription-status-chip.tsx +0 -95
  355. package/src/chart.ts +0 -103
  356. package/src/components/account-chips-labels.ts +0 -16
  357. package/src/components/account-chips.tsx +0 -474
  358. package/src/components/alert-banner.tsx +0 -670
  359. package/src/components/amount-input.tsx +0 -816
  360. package/src/components/appearance-labels.ts +0 -63
  361. package/src/components/authed-image-labels.ts +0 -11
  362. package/src/components/authed-image.tsx +0 -209
  363. package/src/components/autocomplete.tsx +0 -645
  364. package/src/components/breadcrumbs-labels.ts +0 -10
  365. package/src/components/breadcrumbs.tsx +0 -205
  366. package/src/components/bulk-action-bar-labels.ts +0 -11
  367. package/src/components/bulk-action-bar.tsx +0 -443
  368. package/src/components/button-group.tsx +0 -223
  369. package/src/components/button-look.ts +0 -256
  370. package/src/components/button.tsx +0 -339
  371. package/src/components/calculator.tsx +0 -341
  372. package/src/components/calendar-heatmap-labels.ts +0 -15
  373. package/src/components/calendar-heatmap.tsx +0 -600
  374. package/src/components/card.tsx +0 -377
  375. package/src/components/chart-edge.ts +0 -106
  376. package/src/components/chart-figure.tsx +0 -35
  377. package/src/components/chart-height.ts +0 -30
  378. package/src/components/chart-zoom-core.tsx +0 -627
  379. package/src/components/chart-zoom.tsx +0 -22
  380. package/src/components/chart.tsx +0 -554
  381. package/src/components/checkbox-group.tsx +0 -247
  382. package/src/components/checkbox.tsx +0 -346
  383. package/src/components/chip-input.tsx +0 -370
  384. package/src/components/chip-labels.ts +0 -13
  385. package/src/components/chip.tsx +0 -843
  386. package/src/components/choice-card.tsx +0 -544
  387. package/src/components/column-mapper-labels.ts +0 -38
  388. package/src/components/column-mapper.tsx +0 -655
  389. package/src/components/combobox-core.tsx +0 -1321
  390. package/src/components/combobox-suggestions.tsx +0 -131
  391. package/src/components/combobox.tsx +0 -716
  392. package/src/components/confirm-context.ts +0 -7
  393. package/src/components/confirm-dialog-labels.ts +0 -13
  394. package/src/components/confirm-dialog.tsx +0 -341
  395. package/src/components/copy-button-labels.ts +0 -13
  396. package/src/components/copy-button.tsx +0 -192
  397. package/src/components/country-select-labels.ts +0 -11
  398. package/src/components/country-select.tsx +0 -539
  399. package/src/components/currency-option-list.tsx +0 -188
  400. package/src/components/currency-select.tsx +0 -343
  401. package/src/components/danger-confirm-labels.ts +0 -20
  402. package/src/components/danger-confirm.tsx +0 -987
  403. package/src/components/data-table-cells.tsx +0 -118
  404. package/src/components/data-table-column-settings.tsx +0 -198
  405. package/src/components/data-table-filter-popover.tsx +0 -289
  406. package/src/components/data-table-filters.ts +0 -315
  407. package/src/components/data-table-fit.ts +0 -301
  408. package/src/components/data-table-groups.tsx +0 -288
  409. package/src/components/data-table-head.tsx +0 -280
  410. package/src/components/data-table-labels.ts +0 -198
  411. package/src/components/data-table-leading-row.tsx +0 -58
  412. package/src/components/data-table-mobile.tsx +0 -1117
  413. package/src/components/data-table-pagination.tsx +0 -202
  414. package/src/components/data-table-resize.ts +0 -188
  415. package/src/components/data-table-row-actions.tsx +0 -302
  416. package/src/components/data-table-row.tsx +0 -496
  417. package/src/components/data-table-selection.ts +0 -71
  418. package/src/components/data-table-shared.tsx +0 -287
  419. package/src/components/data-table-sort.ts +0 -163
  420. package/src/components/data-table-virtual.ts +0 -576
  421. package/src/components/data-table.tsx +0 -2598
  422. package/src/components/date-field.tsx +0 -684
  423. package/src/components/date-format-setting.tsx +0 -64
  424. package/src/components/date-picker.tsx +0 -404
  425. package/src/components/date-range-picker.tsx +0 -518
  426. package/src/components/description-list-labels.ts +0 -9
  427. package/src/components/description-list.tsx +0 -386
  428. package/src/components/dialog-frame-labels.ts +0 -7
  429. package/src/components/dialog-frame.tsx +0 -284
  430. package/src/components/disclosure.tsx +0 -476
  431. package/src/components/dropdown.tsx +0 -461
  432. package/src/components/empty-state.tsx +0 -136
  433. package/src/components/entity-combobox.tsx +0 -422
  434. package/src/components/error-boundary-labels.ts +0 -20
  435. package/src/components/error-boundary.tsx +0 -675
  436. package/src/components/facing-pair.ts +0 -93
  437. package/src/components/field-base-labels.ts +0 -20
  438. package/src/components/field-base.tsx +0 -656
  439. package/src/components/field-parts.tsx +0 -825
  440. package/src/components/field-strip.tsx +0 -129
  441. package/src/components/field.tsx +0 -206
  442. package/src/components/file-button.tsx +0 -295
  443. package/src/components/file-dropzone.tsx +0 -559
  444. package/src/components/floating-panel-labels.ts +0 -10
  445. package/src/components/floating-panel.tsx +0 -828
  446. package/src/components/focus-ring.ts +0 -16
  447. package/src/components/form-actions-labels.ts +0 -11
  448. package/src/components/form-actions.tsx +0 -578
  449. package/src/components/full-bleed-dialog.tsx +0 -294
  450. package/src/components/grouped-picker.tsx +0 -184
  451. package/src/components/help-popover.tsx +0 -117
  452. package/src/components/hover-menu.tsx +0 -343
  453. package/src/components/iban-input-labels.ts +0 -14
  454. package/src/components/iban-input.tsx +0 -236
  455. package/src/components/icon-button.tsx +0 -959
  456. package/src/components/icon-picker-labels.ts +0 -13
  457. package/src/components/icon-picker.tsx +0 -232
  458. package/src/components/image-grid-labels.ts +0 -11
  459. package/src/components/image-grid.tsx +0 -202
  460. package/src/components/inline-confirm.tsx +0 -116
  461. package/src/components/inline-edit-field-labels.ts +0 -11
  462. package/src/components/inline-edit-field.tsx +0 -552
  463. package/src/components/inline-entity-combobox.tsx +0 -849
  464. package/src/components/input.tsx +0 -394
  465. package/src/components/jump-calendar.tsx +0 -265
  466. package/src/components/language-select.tsx +0 -93
  467. package/src/components/legal-labels.ts +0 -105
  468. package/src/components/legal-page.tsx +0 -711
  469. package/src/components/legal.tsx +0 -668
  470. package/src/components/lightbox-labels.ts +0 -18
  471. package/src/components/lightbox.tsx +0 -399
  472. package/src/components/line-items-labels.ts +0 -14
  473. package/src/components/line-items.tsx +0 -602
  474. package/src/components/list-labels.ts +0 -12
  475. package/src/components/list.tsx +0 -870
  476. package/src/components/loading-state.tsx +0 -78
  477. package/src/components/measured-grid-labels.ts +0 -25
  478. package/src/components/measured-grid.tsx +0 -837
  479. package/src/components/menu-item.tsx +0 -241
  480. package/src/components/mini-calendar-labels.ts +0 -17
  481. package/src/components/mini-calendar.tsx +0 -792
  482. package/src/components/modal.tsx +0 -534
  483. package/src/components/money-draft.ts +0 -83
  484. package/src/components/money-field.tsx +0 -76
  485. package/src/components/month-grid.tsx +0 -229
  486. package/src/components/month-picker-labels.ts +0 -22
  487. package/src/components/month-picker.tsx +0 -1087
  488. package/src/components/multi-entity-combobox.tsx +0 -369
  489. package/src/components/multi-select.tsx +0 -376
  490. package/src/components/nav-pills.tsx +0 -198
  491. package/src/components/number-field.tsx +0 -327
  492. package/src/components/number-input.tsx +0 -559
  493. package/src/components/numeric-field.ts +0 -169
  494. package/src/components/numpad-sheet.tsx +0 -285
  495. package/src/components/one-time-code-input.tsx +0 -270
  496. package/src/components/page-contents-labels.ts +0 -9
  497. package/src/components/page-contents.tsx +0 -282
  498. package/src/components/page-header-context.ts +0 -8
  499. package/src/components/page-header.tsx +0 -298
  500. package/src/components/password-strength-labels.ts +0 -22
  501. package/src/components/password-strength.tsx +0 -241
  502. package/src/components/phone-input-labels.ts +0 -10
  503. package/src/components/phone-input.tsx +0 -282
  504. package/src/components/picker-sheet.tsx +0 -330
  505. package/src/components/pie-chart-labels.ts +0 -40
  506. package/src/components/pie-chart.tsx +0 -480
  507. package/src/components/popover-labels.ts +0 -14
  508. package/src/components/popover.tsx +0 -268
  509. package/src/components/progress-bar-labels.ts +0 -10
  510. package/src/components/progress-bar.tsx +0 -479
  511. package/src/components/qr-code.tsx +0 -76
  512. package/src/components/reauth-dialog-labels.ts +0 -13
  513. package/src/components/reauth-dialog.tsx +0 -265
  514. package/src/components/row-actions-labels.ts +0 -12
  515. package/src/components/row-actions.tsx +0 -795
  516. package/src/components/scroll-area.tsx +0 -123
  517. package/src/components/search-field.tsx +0 -192
  518. package/src/components/select.tsx +0 -294
  519. package/src/components/separator.tsx +0 -33
  520. package/src/components/series-chart-axes.ts +0 -342
  521. package/src/components/series-chart-budget.ts +0 -150
  522. package/src/components/series-chart-labels.ts +0 -33
  523. package/src/components/series-chart-thin.ts +0 -180
  524. package/src/components/series-chart-ticks.ts +0 -194
  525. package/src/components/series-chart.tsx +0 -1906
  526. package/src/components/server-wake-labels.ts +0 -11
  527. package/src/components/server-wake.tsx +0 -129
  528. package/src/components/settings-fields.tsx +0 -237
  529. package/src/components/share-card-labels.ts +0 -30
  530. package/src/components/share-card.tsx +0 -675
  531. package/src/components/signature-pad-labels.ts +0 -24
  532. package/src/components/signature-pad.tsx +0 -747
  533. package/src/components/signed-amount-labels.ts +0 -10
  534. package/src/components/signed-amount.tsx +0 -336
  535. package/src/components/skeleton.tsx +0 -80
  536. package/src/components/slider.tsx +0 -338
  537. package/src/components/sparkline-labels.ts +0 -14
  538. package/src/components/sparkline.tsx +0 -322
  539. package/src/components/spinner.tsx +0 -81
  540. package/src/components/stat-tile-labels.ts +0 -15
  541. package/src/components/stat-tile.tsx +0 -754
  542. package/src/components/status-dot.tsx +0 -170
  543. package/src/components/swatch-picker-labels.ts +0 -10
  544. package/src/components/swatch-picker.tsx +0 -283
  545. package/src/components/swipeable-row.tsx +0 -386
  546. package/src/components/switch.tsx +0 -273
  547. package/src/components/table-skeleton.tsx +0 -136
  548. package/src/components/table.tsx +0 -647
  549. package/src/components/tabs-labels.ts +0 -13
  550. package/src/components/tabs.tsx +0 -693
  551. package/src/components/text-link.tsx +0 -359
  552. package/src/components/text.tsx +0 -150
  553. package/src/components/textarea.tsx +0 -330
  554. package/src/components/tile-radio.tsx +0 -332
  555. package/src/components/time-input.tsx +0 -121
  556. package/src/components/toast-labels.ts +0 -12
  557. package/src/components/toast.tsx +0 -519
  558. package/src/components/toggle-group.tsx +0 -752
  559. package/src/components/toggle-legend.tsx +0 -394
  560. package/src/components/tooltip.tsx +0 -1149
  561. package/src/components/translation-review-editor.tsx +0 -290
  562. package/src/components/translation-review-labels.ts +0 -181
  563. package/src/components/translation-review-parts.tsx +0 -272
  564. package/src/components/translation-review.tsx +0 -1240
  565. package/src/components/tree-view.tsx +0 -589
  566. package/src/components/treemap-labels.ts +0 -47
  567. package/src/components/treemap-layout.ts +0 -210
  568. package/src/components/treemap.tsx +0 -1184
  569. package/src/components/trigger-aria.ts +0 -44
  570. package/src/components/use-anchor-dir.ts +0 -20
  571. package/src/components/use-mobile-reveal.ts +0 -62
  572. package/src/components/use-table-state.ts +0 -753
  573. package/src/components/user-avatar.tsx +0 -116
  574. package/src/components/wizard-stepper.tsx +0 -42
  575. package/src/components/write-lock-labels.ts +0 -10
  576. package/src/components/write-lock.tsx +0 -253
  577. package/src/currencies.ts +0 -90
  578. package/src/data-table.ts +0 -42
  579. package/src/demo/demo-banner.tsx +0 -153
  580. package/src/demo/demo-countdown.ts +0 -136
  581. package/src/demo/demo-ended.tsx +0 -90
  582. package/src/demo/demo-labels.ts +0 -78
  583. package/src/demo/demo-session.ts +0 -32
  584. package/src/demo/demo-start.tsx +0 -156
  585. package/src/eslint/index.ts +0 -161
  586. package/src/eslint/role-utilities.generated.ts +0 -146
  587. package/src/eslint/role-utilities.ts +0 -233
  588. package/src/feedback/feedback-attachment-download.tsx +0 -77
  589. package/src/feedback/feedback-attachment-labels.ts +0 -37
  590. package/src/feedback/feedback-attachment.tsx +0 -1239
  591. package/src/feedback/feedback-capture.ts +0 -287
  592. package/src/feedback/feedback-comment-thread.tsx +0 -353
  593. package/src/feedback/feedback-context-labels.ts +0 -11
  594. package/src/feedback/feedback-context.tsx +0 -245
  595. package/src/feedback/feedback-crash.ts +0 -547
  596. package/src/feedback/feedback-diagnostics.tsx +0 -41
  597. package/src/feedback/feedback-dialog-labels.ts +0 -29
  598. package/src/feedback/feedback-dialog.tsx +0 -452
  599. package/src/feedback/feedback-entry-file.tsx +0 -150
  600. package/src/feedback/feedback-inbox.tsx +0 -552
  601. package/src/feedback/feedback-labels.ts +0 -144
  602. package/src/feedback/feedback-menu-labels.ts +0 -12
  603. package/src/feedback/feedback-menu.tsx +0 -177
  604. package/src/feedback/feedback-mutation.ts +0 -20
  605. package/src/feedback/feedback-page-link.ts +0 -90
  606. package/src/feedback/feedback-public-labels.ts +0 -81
  607. package/src/feedback/feedback-public-list.tsx +0 -524
  608. package/src/feedback/feedback-public-switch.tsx +0 -102
  609. package/src/feedback/feedback-public-types.ts +0 -101
  610. package/src/feedback/feedback-record.ts +0 -428
  611. package/src/feedback/feedback-replies-context.ts +0 -9
  612. package/src/feedback/feedback-replies.tsx +0 -343
  613. package/src/feedback/feedback-rework-entry-chip.tsx +0 -18
  614. package/src/feedback/feedback-row-detail-labels.ts +0 -31
  615. package/src/feedback/feedback-row-detail.tsx +0 -871
  616. package/src/feedback/feedback-status-undo.ts +0 -109
  617. package/src/feedback/feedback-submit-dialog.tsx +0 -131
  618. package/src/feedback/feedback-submit.tsx +0 -270
  619. package/src/feedback/feedback-swipe.tsx +0 -192
  620. package/src/feedback/feedback-table-labels.ts +0 -27
  621. package/src/feedback/feedback-table.tsx +0 -912
  622. package/src/feedback/feedback-thread-labels.ts +0 -22
  623. package/src/feedback/feedback-thread-parts-labels.ts +0 -41
  624. package/src/feedback/feedback-thread.tsx +0 -905
  625. package/src/feedback/feedback-vocabulary.ts +0 -263
  626. package/src/feedback/feedback-vote-button.tsx +0 -124
  627. package/src/feedback.ts +0 -116
  628. package/src/hooks/use-anchored-panel.ts +0 -396
  629. package/src/hooks/use-anchored-rect.ts +0 -77
  630. package/src/hooks/use-announce.ts +0 -85
  631. package/src/hooks/use-authed-src.ts +0 -167
  632. package/src/hooks/use-body-scroll-lock.ts +0 -119
  633. package/src/hooks/use-breakpoint.ts +0 -133
  634. package/src/hooks/use-close-transition.ts +0 -107
  635. package/src/hooks/use-copy-to-clipboard.ts +0 -122
  636. package/src/hooks/use-debounce.ts +0 -114
  637. package/src/hooks/use-dismiss.ts +0 -371
  638. package/src/hooks/use-document-title.ts +0 -33
  639. package/src/hooks/use-file-drop.ts +0 -121
  640. package/src/hooks/use-file-picker-labels.ts +0 -28
  641. package/src/hooks/use-file-picker.tsx +0 -359
  642. package/src/hooks/use-focus-trap.ts +0 -199
  643. package/src/hooks/use-hotkey.ts +0 -212
  644. package/src/hooks/use-large-text.ts +0 -41
  645. package/src/hooks/use-long-press.ts +0 -140
  646. package/src/hooks/use-media-query.ts +0 -39
  647. package/src/hooks/use-mounted.ts +0 -39
  648. package/src/hooks/use-noindex.ts +0 -37
  649. package/src/hooks/use-overlay-history.ts +0 -714
  650. package/src/hooks/use-row-swipe.ts +0 -207
  651. package/src/hooks/use-search-param-state.ts +0 -434
  652. package/src/hooks/use-windowed-rows.ts +0 -225
  653. package/src/i18n/defaults.ts +0 -267
  654. package/src/i18n/german.ts +0 -2059
  655. package/src/i18n/kit-labels.tsx +0 -1003
  656. package/src/i18n/languages.ts +0 -349
  657. package/src/i18n/locales/de-CH.ts +0 -24
  658. package/src/i18n/locales/en.ts +0 -267
  659. package/src/i18n/locales/es.ts +0 -1999
  660. package/src/i18n/locales/fr.ts +0 -2037
  661. package/src/i18n/locales/hu.ts +0 -2004
  662. package/src/i18n/locales/it.ts +0 -2014
  663. package/src/i18n/locales/zh.ts +0 -1912
  664. package/src/i18n/plurals.ts +0 -86
  665. package/src/i18n/review.ts +0 -475
  666. package/src/i18n/shared-labels.ts +0 -138
  667. package/src/i18n/swiss.ts +0 -29
  668. package/src/index.ts +0 -1191
  669. package/src/landing/access.ts +0 -93
  670. package/src/landing/landing-actions.tsx +0 -97
  671. package/src/landing/landing-labels.ts +0 -59
  672. package/src/landing/landing-sections.tsx +0 -341
  673. package/src/landing/page-seo.ts +0 -261
  674. package/src/landing/public-header.tsx +0 -121
  675. package/src/landing/public-page.tsx +0 -106
  676. package/src/landing/routing.tsx +0 -225
  677. package/src/lib/broadcast.ts +0 -52
  678. package/src/lib/calc.ts +0 -323
  679. package/src/lib/chip-edge-snap.ts +0 -212
  680. package/src/lib/clipboard-minus.ts +0 -107
  681. package/src/lib/clipping.ts +0 -34
  682. package/src/lib/cn.ts +0 -28
  683. package/src/lib/column-mapping.ts +0 -234
  684. package/src/lib/countries.ts +0 -91
  685. package/src/lib/data-attributes.ts +0 -14
  686. package/src/lib/date-format.ts +0 -227
  687. package/src/lib/dates.ts +0 -288
  688. package/src/lib/day-format.ts +0 -96
  689. package/src/lib/decimal-marks.ts +0 -108
  690. package/src/lib/describe-failure.ts +0 -18
  691. package/src/lib/device-settings.ts +0 -147
  692. package/src/lib/direction.ts +0 -74
  693. package/src/lib/document-navigation.ts +0 -17
  694. package/src/lib/document-title.ts +0 -41
  695. package/src/lib/download.ts +0 -172
  696. package/src/lib/float-clearance.ts +0 -70
  697. package/src/lib/floating-stack.ts +0 -70
  698. package/src/lib/focus-left.ts +0 -156
  699. package/src/lib/format.ts +0 -585
  700. package/src/lib/http-status.ts +0 -38
  701. package/src/lib/iban.ts +0 -251
  702. package/src/lib/ime.ts +0 -24
  703. package/src/lib/intl-cache.ts +0 -27
  704. package/src/lib/iso-day.ts +0 -13
  705. package/src/lib/lazy-with-fallback.ts +0 -82
  706. package/src/lib/logger.ts +0 -69
  707. package/src/lib/minus.ts +0 -50
  708. package/src/lib/path-pattern.ts +0 -55
  709. package/src/lib/person-name.ts +0 -155
  710. package/src/lib/phone.ts +0 -208
  711. package/src/lib/plain-text.ts +0 -22
  712. package/src/lib/presence.ts +0 -18
  713. package/src/lib/qr-encode.ts +0 -444
  714. package/src/lib/quiet-focus.ts +0 -32
  715. package/src/lib/reduced-motion.ts +0 -22
  716. package/src/lib/refs.ts +0 -22
  717. package/src/lib/round.ts +0 -43
  718. package/src/lib/safe-storage.ts +0 -119
  719. package/src/lib/server-wake.ts +0 -315
  720. package/src/lib/session-refresh.ts +0 -472
  721. package/src/lib/share-format.ts +0 -18
  722. package/src/lib/status-tone.ts +0 -43
  723. package/src/lib/strip-fade.ts +0 -287
  724. package/src/lib/table-text.ts +0 -554
  725. package/src/lib/text-case.ts +0 -19
  726. package/src/lib/text-match.ts +0 -169
  727. package/src/lib/thenable.ts +0 -19
  728. package/src/lib/translation-review.ts +0 -769
  729. package/src/notifications/device-zone-report.ts +0 -67
  730. package/src/notifications/feedback-notification-types-labels.ts +0 -33
  731. package/src/notifications/feedback-notification-types.ts +0 -71
  732. package/src/notifications/notification-bell.tsx +0 -119
  733. package/src/notifications/notification-inbox.tsx +0 -309
  734. package/src/notifications/notification-parts.tsx +0 -81
  735. package/src/notifications/notification-types-setting.tsx +0 -406
  736. package/src/notifications/notification-types.ts +0 -89
  737. package/src/notifications/notifications-labels.ts +0 -287
  738. package/src/notifications/push-device-setting.tsx +0 -230
  739. package/src/notifications/push-devices-setting.tsx +0 -374
  740. package/src/notifications/push-worker.ts +0 -168
  741. package/src/notifications/quiet-hours-setting.tsx +0 -238
  742. package/src/notifications/unsubscribe-panel.tsx +0 -200
  743. package/src/notifications/use-push-device.ts +0 -448
  744. package/src/offline/local-first.ts +0 -100
  745. package/src/offline/pwa-update-prompt-labels.ts +0 -9
  746. package/src/offline/pwa-update-prompt.tsx +0 -78
  747. package/src/offline/query-persistence.ts +0 -391
  748. package/src/offline/service-worker-updates.ts +0 -81
  749. package/src/offline/shared-computer-labels.ts +0 -8
  750. package/src/offline/shared-computer.tsx +0 -87
  751. package/src/offline/sign-out-pending-dialog-labels.ts +0 -21
  752. package/src/offline/sign-out-pending-dialog.tsx +0 -135
  753. package/src/offline.ts +0 -37
  754. package/src/push-worker.ts +0 -25
  755. package/src/rhf/fields.tsx +0 -2187
  756. package/src/rhf/form-core.tsx +0 -324
  757. package/src/rhf/form.tsx +0 -12
  758. package/src/rhf/line-items.tsx +0 -135
  759. package/src/rhf/use-rhf-wizard-step.ts +0 -104
  760. package/src/rhf.ts +0 -21
  761. package/src/search/command-palette-labels.ts +0 -16
  762. package/src/search/command-palette.tsx +0 -624
  763. package/src/search/global-search-labels.ts +0 -13
  764. package/src/search/global-search.tsx +0 -660
  765. package/src/search/search-index.ts +0 -283
  766. package/src/search/use-command-key.ts +0 -23
  767. package/src/search.ts +0 -14
  768. package/src/settings/device-reset-setting-labels.ts +0 -20
  769. package/src/settings/device-reset-setting.tsx +0 -116
  770. package/src/settings/settings-card-classes.ts +0 -11
  771. package/src/settings/settings-catalogue.ts +0 -150
  772. package/src/settings/settings-context.ts +0 -33
  773. package/src/settings/settings-heading.tsx +0 -67
  774. package/src/settings/settings-labels.ts +0 -87
  775. package/src/settings/settings-layout.tsx +0 -471
  776. package/src/settings/settings-section.tsx +0 -108
  777. package/src/settings/use-account-appearance.ts +0 -148
  778. package/src/settings/use-account-language.ts +0 -146
  779. package/src/settings/use-settings-focus.ts +0 -128
  780. package/src/settings/use-settings-route.ts +0 -258
  781. package/src/shell/account-menu-language.tsx +0 -130
  782. package/src/shell/account-menu-text-size.tsx +0 -80
  783. package/src/shell/admin-menu.tsx +0 -183
  784. package/src/shell/app-shell-labels.ts +0 -11
  785. package/src/shell/app-shell.tsx +0 -1560
  786. package/src/shell/auth-layout.tsx +0 -186
  787. package/src/shell/company-switcher-labels.ts +0 -13
  788. package/src/shell/company-switcher.tsx +0 -136
  789. package/src/shell/dev-menu.tsx +0 -257
  790. package/src/shell/fill-height.tsx +0 -45
  791. package/src/shell/language-code.ts +0 -5
  792. package/src/shell/option-switcher-menu.tsx +0 -84
  793. package/src/shell/sync-status-indicator.tsx +0 -303
  794. package/src/shell/top-bar-actions.tsx +0 -87
  795. package/src/shell/top-bar-brand.tsx +0 -78
  796. package/src/shell/top-bar-parts-labels.ts +0 -148
  797. package/src/shell/top-bar.tsx +0 -40
  798. package/src/shell/topbar-action-menu.tsx +0 -567
  799. package/src/shell/topbar-controls.tsx +0 -269
  800. package/src/shell/tour-menu.tsx +0 -187
  801. package/src/shell.ts +0 -108
  802. package/src/sync/db.ts +0 -129
  803. package/src/sync/engine.ts +0 -1011
  804. package/src/sync/hooks.ts +0 -98
  805. package/src/sync/testing.ts +0 -73
  806. package/src/sync/wire.ts +0 -61
  807. package/src/sync.ts +0 -37
  808. package/src/table-text.ts +0 -8
  809. package/src/testing/css-variables.generated.ts +0 -112
  810. package/src/testing/css-variables.ts +0 -189
  811. package/src/testing/index.ts +0 -29
  812. package/src/testing/react.ts +0 -58
  813. package/src/testing/tour.ts +0 -41
  814. package/src/testing/viewport.ts +0 -357
  815. package/src/theme/chart-palette.ts +0 -232
  816. package/src/theme/color.ts +0 -272
  817. package/src/theme/contrast-tokens.ts +0 -268
  818. package/src/theme/contrast.ts +0 -250
  819. package/src/theme/palette-derive.ts +0 -825
  820. package/src/theme/palette-presets.ts +0 -544
  821. package/src/theme/palette-store.ts +0 -108
  822. package/src/theme/text-size.ts +0 -289
  823. package/src/theme/theme-store.ts +0 -157
  824. package/src/tour/tour-catalogue-labels.ts +0 -75
  825. package/src/tour/tour-catalogue.tsx +0 -335
  826. package/src/tour/tour-dock.ts +0 -134
  827. package/src/tour/tour-engine.ts +0 -37
  828. package/src/tour/tour-gates.ts +0 -218
  829. package/src/tour/tour-guide-button.tsx +0 -64
  830. package/src/tour/tour-guide-labels.ts +0 -15
  831. package/src/tour/tour-info.ts +0 -144
  832. package/src/tour/tour-labels.ts +0 -23
  833. package/src/tour/tour-overlay.tsx +0 -1170
  834. package/src/tour/tour.tsx +0 -750
  835. package/src/tour/use-tour-progress.ts +0 -671
  836. package/src/tour/use-tour-url-sync.ts +0 -266
  837. package/src/tour.ts +0 -21
  838. package/src/vite-env.d.ts +0 -23
  839. package/src/wizard/stepper-nav.tsx +0 -360
  840. package/src/wizard/types.ts +0 -275
  841. package/src/wizard/use-wizard.ts +0 -560
  842. package/src/wizard/validation.ts +0 -39
  843. package/src/wizard/wizard-context.tsx +0 -74
  844. package/src/wizard/wizard-labels.ts +0 -29
  845. package/src/wizard/wizard-step.tsx +0 -76
  846. package/src/wizard/wizard-summary.tsx +0 -84
  847. package/src/wizard.ts +0 -15
@@ -1,1240 +0,0 @@
1
- import { useEffect, useId, useMemo, useRef, useState } from "react";
2
- import type { ReactNode } from "react";
3
- import { Check, ListChecks, PencilLine, RotateCcw } from "lucide-react";
4
-
5
- import { usePhoneLayout } from "../hooks/use-breakpoint";
6
- import { useKitLabels, useKitLocale, type LabelOverride } from "../i18n/kit-labels";
7
- import { cn } from "../lib/cn";
8
- import {
9
- DEFAULT_TRANSLATION_REVIEW_FILTER,
10
- DEFAULT_TRANSLATION_REVIEW_SWIPE,
11
- REVIEW_STATUSES,
12
- filterTranslationRows,
13
- groupTranslationRows,
14
- keyInAreas,
15
- reviewUndo,
16
- reviewWrite,
17
- summariseRows,
18
- translationReviewSwipePlan,
19
- unreviewedRows,
20
- } from "../lib/translation-review";
21
- import type {
22
- ReviewStatus,
23
- TranslationReviewFilter,
24
- TranslationReviewKey,
25
- TranslationReviewSwipeAction,
26
- TranslationReviewSwipeBinding,
27
- TranslationReviewWrite,
28
- TranslationRow,
29
- TranslationRowGroup,
30
- } from "../lib/translation-review";
31
- import { AlertBanner } from "./alert-banner";
32
- import { BulkActionBar } from "./bulk-action-bar";
33
- import { Checkbox } from "./checkbox";
34
- import { Chip } from "./chip";
35
- import { DataTable } from "./data-table";
36
- import type { DataTableColumn, DataTableProps, MobileSwipeActions } from "./data-table";
37
- import { Disclosure } from "./disclosure";
38
- import { SearchField } from "./search-field";
39
- import { toast } from "./toast";
40
- import type { SwipeAction } from "./swipeable-row";
41
- import { ToggleGroup } from "./toggle-group";
42
- import { useSharedTableView } from "./use-table-state";
43
- import { TranslationReviewEditor } from "./translation-review-editor";
44
- import { DEFAULT_TRANSLATION_REVIEW_LABELS, reviewStatusLabel } from "./translation-review-labels";
45
- import type { TranslationReviewLabels } from "./translation-review-labels";
46
- import { ReviewStatusChip, TranslationProgress } from "./translation-review-parts";
47
- import { Button } from "./button";
48
- import { IconButton } from "./icon-button";
49
- import { DEFAULT_ROW_ACTIONS_LABELS, rowActionsColumn, type RowAction } from "./row-actions";
50
- import { Select } from "./select";
51
- import { COMMIT_EXCEPT_BILLING, useWriteLock } from "./write-lock";
52
-
53
- export {
54
- REVIEW_STATUS_TONES,
55
- ReviewStatusChip,
56
- TranslationExportButton,
57
- TranslationLocaleTabs,
58
- TranslationProgress,
59
- } from "./translation-review-parts";
60
- export type {
61
- ReviewStatusChipProps,
62
- TranslationExportButtonProps,
63
- TranslationLocaleTab,
64
- TranslationLocaleTabsProps,
65
- TranslationProgressProps,
66
- } from "./translation-review-parts";
67
- export { TranslationReviewEditor } from "./translation-review-editor";
68
- export type { TranslationReviewEditorProps } from "./translation-review-editor";
69
- export { DEFAULT_TRANSLATION_REVIEW_LABELS } from "./translation-review-labels";
70
- export type { TranslationReviewLabels } from "./translation-review-labels";
71
-
72
- /**
73
- * An app's translation review page, as kit parts (H1, docs/i18n-harmonization.md):
74
- * keksdose's /translations, which kastlan was about to build a second time. The app keeps
75
- * what is its own — where the strings come from, which locale is read against which, who
76
- * may review what, every request — and hands the kit rows (`translationRows`) and two
77
- * callbacks. Everything a reviewer touches is here.
78
- *
79
- * Presentational — no request, no route — and one LOCALE at a time. Its table is the
80
- * kit's DataTable, so like every DataTable it renders under the app's Router. The page
81
- * around it puts
82
- * {@link TranslationLocaleTabs} above it and {@link TranslationExportButton} in its header,
83
- * because both span every locale while this panel is the one open.
84
- *
85
- * Callbacks may return promises, and the panel waits for them: an editor stays open with
86
- * its text and the error when a save is refused (a 403 for a key outside the reviewer's
87
- * areas, a network failure), a bulk action keeps its selection. Chunking a long bulk
88
- * write to the server's batch limit is the app's; the panel sends one call per action.
89
- */
90
-
91
- type MaybePromise = void | Promise<unknown>;
92
-
93
- /** What asked for a write — see {@link TranslationReviewSaveInfo}. */
94
- export type TranslationReviewOrigin = "editor" | "row" | "swipe" | "group" | "selection" | "undo";
95
-
96
- /**
97
- * The second argument of `onSave` and `onClear` (0.25): where the write came from, and
98
- * whether the panel answers it with a toast of its own.
99
- *
100
- * WHY THE APP IS TOLD. keksdose and kastlan toast "Saved" from their mutation on every
101
- * save. With {@link TranslationReviewPanelProps.undo} on, an approval is answered by the
102
- * kit's Undo toast instead — and a reviewer swiping through a list then read two toasts
103
- * per card. The app skips its own where `toasted` is true:
104
- *
105
- * onSave={(writes, { toasted }) => save.mutateAsync({ items: writes.map(toApiWrite), quiet: toasted })}
106
- *
107
- * An app that ignores the argument keeps working; it only says "Saved" twice.
108
- */
109
- export interface TranslationReviewSaveInfo {
110
- /** The open editor, a row's approve button, a swipe on a phone card, a group's
111
- * "Approve unreviewed", the selection's bulk bar, or an Undo pressed on the toast. */
112
- origin: TranslationReviewOrigin;
113
- /** The panel shows its own toast for this write (an approval's Undo toast, or a
114
- * swiped `clear`'s). */
115
- toasted: boolean;
116
- }
117
-
118
- /** How {@link TranslationReviewPanelProps.groupBy} names a row's group. */
119
- export type TranslationReviewGroupBy = "namespace" | "source" | ((row: TranslationRow) => string);
120
-
121
- export interface TranslationReviewPanelProps {
122
- /** One locale's rows, from `translationRows`. */
123
- rows: readonly TranslationRow[];
124
- /** The reviewed language's own name — the text column's header ("Français"). */
125
- localeLabel: string;
126
- /** The reference language's own name — the reference column's header. */
127
- referenceLabel: string;
128
- /** Store verdicts: one for an editor or a row's approve button, many for a bulk
129
- * approve. Left out (or `readOnly`): nothing can be changed. The second argument
130
- * says where the write came from ({@link TranslationReviewSaveInfo}). */
131
- onSave?: (writes: TranslationReviewWrite[], info: TranslationReviewSaveInfo) => MaybePromise;
132
- /** Clear verdicts — back to unreviewed. Left out: no reset, and an approval of a row
133
- * that had no verdict cannot be undone. */
134
- onClear?: (keys: TranslationReviewKey[], info: TranslationReviewSaveInfo) => MaybePromise;
135
- /** A locale the viewer may read but not review: the rows and their verdicts, no
136
- * actions, and a line saying so. */
137
- readOnly?: boolean;
138
- /**
139
- * The areas the reviewer is limited to (keksdose's lawyer: `["legal"]`), as the server
140
- * answers them; `null` is every area. Rows outside are not shown or counted — build the
141
- * rows with the same `areas` and they never exist — and a notice names the areas.
142
- */
143
- areas?: readonly string[] | null;
144
- /** An area's name for the notice ("the legal pages (Imprint, …)"). Default: the key. */
145
- areaLabels?: Readonly<Record<string, string>>;
146
- /** Default `keyInArea`: the key is the area or sits under it (`legal.…`, `legal:…`,
147
- * and the kit's `kit.legal.…` since 0.28). */
148
- inArea?: (key: string, area: string) => boolean;
149
- /** A source's name for the source filter, which appears once the rows come from more
150
- * than one (kastlan: `{ screen: "Screen", documents: "PDFs and emails" }`). Default:
151
- * the source itself. */
152
- sourceLabels?: Readonly<Record<string, string>>;
153
- /**
154
- * Controlled filters — for an app that keeps them in the URL (keksdose's `?status=`,
155
- * `?ns=`, `?q=`). Whatever is left out is the panel's own. Pair with `onFilterChange`.
156
- *
157
- * A field passed here is the app's, and the control shows what the app says: one the app
158
- * passes but does not write back from `onFilterChange` is a control that does nothing.
159
- * A development build warns about that once per field (0.25.1, Kurvenschmiede); a field
160
- * left out entirely is fine — the panel keeps it.
161
- */
162
- filter?: Partial<TranslationReviewFilter>;
163
- /** Every filter, on every change — write them in ONE update: `useSearchParamsState`'s
164
- * setter takes this object as it is (`filter={filter} onFilterChange={setFilter}`).
165
- * Single `useSearchParamState` setters compose since 0.25; before, four in a row
166
- * clobbered each other (keksdose live #378). */
167
- onFilterChange?: (filter: TranslationReviewFilter) => void;
168
- /** The progress bar over the filters. Default true. */
169
- progress?: boolean;
170
- /** Rows per page. Default 50. */
171
- pageSize?: number;
172
- /** Persists the table's sort and page size (DataTable's `storageKey`), under the
173
- * prefix below — under {@link groupBy}, the one sort and page size every group
174
- * shares, in the same place (0.35). */
175
- storageKey?: string;
176
- /**
177
- * The `localStorage` prefix in front of `storageKey` (DataTable's `storageKeyPrefix`).
178
- * Left out, it is the provider's `tableStorageKeyPrefix`, then the kit's
179
- * `"hbui-table:"` — so an app that sets its prefix on `UiKitProvider` needs nothing
180
- * here. Before 0.34 the panel took no prefix and its view always landed under the
181
- * kit's, which an app listing what it stores had to list as its own (KS U4).
182
- */
183
- storageKeyPrefix?: string;
184
- /** How a verdict's date reads in the editor. Default: the editor's — the
185
- * `<UiKitProvider formatDate>`, else the kit's medium date (0.52). */
186
- formatDate?: (iso: string) => string;
187
- /** Turns a callback's rejection into words. Default {@link TranslationReviewLabels.failed}. */
188
- formatError?: (error: unknown) => ReactNode;
189
- /**
190
- * Swipe the phone cards (0.25, keksdose live #377 — Marcel reviewing on a phone): toward
191
- * the reading END to approve, toward the START for "Needs a change", which OPENS the
192
- * editor with the cursor in the wording rather than sending the string back blind — a
193
- * verdict without a note or a better wording tells whoever fixes it nothing. In a
194
- * right-to-left page the two sides mirror with the reading direction.
195
- *
196
- * DataTable's swipe sides (`mobileSwipeActions`) on {@link SwipeableRow}: the same
197
- * gesture, the same feedback, and the same keyboard path — every swipe is also a real
198
- * button, read out and reachable by Tab, so the gesture is a shortcut and never the only
199
- * way. A row has no swipe while its own write is out (a swipe's, a button's, or the open
200
- * editor's own), under a {@link WriteLockProvider} lock, in a `readOnly` locale, and
201
- * toward "Approve" where there is nothing to approve (a missing string, an approved one).
202
- * A reviewer limited to `areas` never sees a row outside them, so never swipes one.
203
- *
204
- * An OPEN row swipes too, on its top line (keksdose live #416, Marcel: *"The swipe shall
205
- * still be possible since it is faster than scrolling down to the buttons"*), as
206
- * DataTable's open card does since 0.40: the editor below is outside the gesture, and a
207
- * swipe neither toggles it shut nor resets it. A swipe acts on the STORED string, as the
208
- * row's buttons do — Approve approves what the locale says, not the editor's draft — and
209
- * the editor stays open with whatever was typed in it; "Needs a change" puts the cursor
210
- * back at the end of that draft.
211
- *
212
- * Off by default, so no existing page gains a gesture on the bump. The gesture is the
213
- * phone's; on a wide screen the binding is the row's actions column instead (0.46,
214
- * keksdose dev#598): Approve where the row can take it, bound or not, then the bound
215
- * `edit` and `clear`, each once — under a write lock they stay and say why.
216
- *
217
- * A BINDING instead of `true` (keksdose — Marcel's live #377 rework, 2026-10-03: *"Add
218
- * the swipe options to be shown also to the settings /settings#interaction area where
219
- * the other swipe options are defined"*): keksdose lets the user bind every list's
220
- * swipes in Settings → Interaction, per side a primary action at the first threshold and
221
- * a secondary one at a longer drag, and a fixed mapping is one its settings cannot
222
- * reach. So each logical side takes an ordered ladder of
223
- * {@link TranslationReviewSwipeAction}s — index 0 at the first threshold, index 1 at the
224
- * longer drag, as {@link SwipeableRow} stages them:
225
- *
226
- * swipe={{ end: ["approve", "clear"], start: ["edit"] }}
227
- *
228
- * `approve` (with Undo), `edit` (the editor, cursor in the wording — "Needs a change"),
229
- * `clear` (the verdict taken back, with Undo, which stores it again; only with
230
- * `onClear`). An action a row cannot offer drops out and the ladder closes up behind it
231
- * ({@link translationReviewSwipePlan}): `approve` on an approved or missing string,
232
- * `clear` on a row with no verdict. Every rule above still holds for every binding —
233
- * no side swipes in a `readOnly` locale, under a write lock, or while the row's write
234
- * (or its open editor's) is out.
235
- *
236
- * `true` is {@link DEFAULT_TRANSLATION_REVIEW_SWIPE} — `{ end: ["approve"], start:
237
- * ["edit"] }`, 0.25's mapping exactly — and `false` or nothing is no swipe. A binding
238
- * turns the Undo toast on by default, as `true` does.
239
- */
240
- swipe?: boolean | TranslationReviewSwipeBinding;
241
- /**
242
- * Answer every approval with the kit's Undo toast (`toast.undo`): a row's button, a
243
- * swipe, the editor's Approve, a group's and the selection's bulk approve. Undo stores
244
- * again what the rows said before ({@link reviewUndo}) — or clears them, for rows that
245
- * had no verdict, which needs `onClear` (without it the toast reports and offers
246
- * nothing). A batch has ONE Undo.
247
- *
248
- * Default: on with `swipe` or `groupBy` — the two ways to approve in one movement or many
249
- * rows at once bring their way back — and off otherwise, so an existing page does not
250
- * gain a toast on the bump. Needs the app's `<Toaster>`. Tell the app's own "Saved"
251
- * toast apart with the `toasted` flag ({@link TranslationReviewSaveInfo}).
252
- */
253
- undo?: boolean;
254
- /**
255
- * Group the strings under a header per area (0.25, keksdose live #377: *"grouping to the
256
- * translation review entries for faster reviewing"*): `"namespace"` — the row's
257
- * `namespace`, which `translationRows` derives from the key (`budget`, `kit.dataTable`
258
- * with keksdose's `namespaceOf`) — `"source"`, or a function of the row.
259
- *
260
- * Each group is a section with a heading ({@link groupHeadingAs}): its name, "n
261
- * unreviewed / total", and "Approve unreviewed (n)", which approves the group's rows
262
- * nobody has given a verdict ({@link unreviewedRows}) under the same rules as one
263
- * approval — write lock, `readOnly`, one write at a time — with one Undo for the batch.
264
- * A batch larger than `pageSize` asks first, inside the opened group: more than a page
265
- * is more than the reviewer can have had on screen.
266
- *
267
- * WHAT IT MEANS FOR THE FILTERS, THE SORT AND THE PAGES. The filters narrow the rows
268
- * first, then the rows are grouped: a header counts what the filters show in it (so
269
- * "Approve unreviewed" never takes a row the reviewer filtered away), and an area with
270
- * nothing left is not shown. Groups stand in the order their first string has in the
271
- * reference; a column sort orders the rows INSIDE every group alike. Each group pages
272
- * on its own, `pageSize` rows at a time — a header then always stands over its own
273
- * rows, and paging one area does not move another — rather than one pager cutting
274
- * through the headers. The page SIZE is one for all of them, as the sort is: a
275
- * group's pager changes it everywhere.
276
- *
277
- * `storageKey` persists that one view — the sort and the page size — in the place the
278
- * ungrouped table keeps its own (same key, same prefix, same blob), read on mount and
279
- * written on a change, so a reviewer switching the grouping on or off keeps both
280
- * (0.35, Kurvenschmiede; before, it persisted the ungrouped table only). What only
281
- * the ungrouped table has — column widths, hidden columns — stays in the blob for it.
282
- *
283
- * Groups fold (each on its own, remembered while the panel lives), and a folded group's
284
- * rows are not rendered at all — only its header. Which groups start open is
285
- * {@link defaultGroupsOpen}: by default, on a phone they start folded when there are
286
- * several — the headers are then the area list, and the reviewer opens the one to work
287
- * through — and on a wide screen they start open while that is cheap (see there).
288
- */
289
- groupBy?: TranslationReviewGroupBy;
290
- /**
291
- * Which groups start open under {@link groupBy} (0.25.1). A group the reviewer opens or
292
- * folds keeps that; this is what every other group follows.
293
- *
294
- * - `"auto"` (default): on a phone, open when there is one group and folded when there
295
- * are several, as in 0.25.0. On a wide screen, open while opening every group renders
296
- * at most 100 rows (each group shows up to `pageSize` of them; the budget is one page
297
- * where `pageSize` is larger) — and folded above that, the headers then being the area
298
- * list, as on a phone.
299
- * - `true`: every group starts open, on a phone too.
300
- * - `false`: every group starts folded, on a wide screen too.
301
- *
302
- * WHY A BUDGET OF ROWS (keksdose, measured on 0.25.0): a wide screen opened EVERY group.
303
- * keksdose's /translations has 122 areas and ~4700 strings — 122 tables and 2053 rows at
304
- * once, ~46 000 elements, 5.2 s before the panel appeared and 2.5 s for a filter click
305
- * to paint, against 50 rows and 0.34 s ungrouped; its page test went from 4 s to 146 s.
306
- * kastlan (~31 areas, ~1300 rows open) and Kurvenschmiede (28, ~700) had the same page.
307
- * What costs is the rows on screen, so the rule counts them rather than the groups:
308
- * 100 rows is two of the panel's default pages — any one or two areas still open as
309
- * before, however long, and so do a dozen small ones, or a search whose hits fall in
310
- * many areas — while a whole catalogue starts as its list of areas: keksdose's shape
311
- * in jsdom, 122 headers and ~2100 elements instead of ~44 000 (the ungrouped panel has
312
- * ~1200). The rule follows the filters: narrow them and the groups open.
313
- *
314
- * A boolean rather than a controlled open state: the panel already keeps each group's
315
- * own state, and an app that only wants "all open" or "all folded" should not have to
316
- * keep a record of 122 flags to say it.
317
- */
318
- defaultGroupsOpen?: boolean | "auto";
319
- /** A group's name. Default: `sourceLabels` for `groupBy="source"`, else the key. */
320
- groupLabel?: (group: string) => string;
321
- /** The level of the group headings. Default `h2`: the panel is a page's body, under
322
- * the page's `h1`. */
323
- groupHeadingAs?: "h2" | "h3" | "h4";
324
- className?: string;
325
- labels?: LabelOverride<TranslationReviewLabels>;
326
- }
327
-
328
- /**
329
- * How many rows the groups may render at once on a wide screen before they start folded
330
- * ({@link TranslationReviewPanelProps.defaultGroupsOpen}): two of the default 50-row pages.
331
- * A row costs about twenty elements in either layout, so this is the cost of two pages of
332
- * the ungrouped table — not of 2053 rows in 122 tables.
333
- */
334
- const GROUPS_OPEN_ROW_BUDGET = 100;
335
-
336
- /**
337
- * Would opening every group stay within the budget? Each group renders up to `pageSize`
338
- * rows (it pages on its own); the budget is never less than one page, so a single group —
339
- * which renders at most one — always starts open, as on a phone.
340
- */
341
- function groupsFitOpen(groups: readonly TranslationRowGroup[], pageSize: number): boolean {
342
- const budget = Math.max(GROUPS_OPEN_ROW_BUDGET, pageSize);
343
- let rendered = 0;
344
- for (const group of groups) {
345
- rendered += Math.min(group.rows.length, pageSize);
346
- if (rendered > budget) return false;
347
- }
348
- return true;
349
- }
350
-
351
- /**
352
- * The kit's dev switch, as NumberInput's money guard and `lib/logger.ts` read it: a
353
- * development build — the consumer's bundler makes `import.meta.env.DEV` false in a
354
- * production one, and the branch is dead code there — and never under vitest, where every
355
- * consumer suite would print it; a test opts in with `vi.stubEnv("VITEST", "")`. Read per
356
- * call, and optional-chained: `import.meta.env` is a Vite injection, absent under Node.
357
- */
358
- function devWarnings(): boolean {
359
- return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;
360
- }
361
-
362
- type FilterField = keyof TranslationReviewFilter;
363
-
364
- /** How long after the last filter change the app's `filter` is looked at again: well past
365
- * a router's transition, so a URL that is still being written is not mistaken for one
366
- * that never will be. */
367
- const FILTER_ECHO_MS = 1500;
368
-
369
- /**
370
- * The development warning for a controlled filter that does not follow (Kurvenschmiede,
371
- * 0.25.1). A field the app passes in `filter` is the app's: the panel shows the app's
372
- * value, whatever the reviewer picked. An app that passes a field but does not write the
373
- * reported value back — its `onFilterChange` hands on only some fields, `filter` is built
374
- * as `{ ...DEFAULT_TRANSLATION_REVIEW_FILTER, ...fromTheUrl }`, a URL parser rejects what
375
- * its serialiser wrote, or there is no `onFilterChange` at all — has a control that
376
- * silently does nothing: the box unticks itself, the select jumps back.
377
- *
378
- * So, after the panel reports a change of a field the app controls, it looks at the
379
- * app's `filter` again once the change has had time to land ({@link FILTER_ECHO_MS}, read
380
- * from the last COMMITTED props, so a router transition still rendering does not count),
381
- * and warns — once per field — when that field still holds the value it had before.
382
- *
383
- * Quiet by construction for an app that passes no `filter`, and for a field the app LEAVES
384
- * OUT: that field is the panel's own and works (keksdose keeps the placeholder switch so,
385
- * on purpose). Quiet too when the app moved the field somewhere else itself. Development
386
- * builds only (see {@link devWarnings}): in production and under vitest nothing is
387
- * recorded and no timer is set.
388
- */
389
- function useFilterEchoCheck(filterProp: Partial<TranslationReviewFilter> | undefined) {
390
- const committed = useRef(filterProp);
391
- useEffect(() => {
392
- committed.current = filterProp;
393
- });
394
- // Per field: what the app's filter said when the reviewer first changed it, and what
395
- // the panel reported last.
396
- const pending = useRef(new Map<FilterField, { from: unknown; to: unknown }>());
397
- const warned = useRef(new Set<FilterField>());
398
- const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
399
- useEffect(() => () => clearTimeout(timer.current), []);
400
-
401
- return (next: TranslationReviewFilter) => {
402
- if (filterProp === undefined || !devWarnings()) return;
403
- for (const field of Object.keys(next) as FilterField[]) {
404
- const from = filterProp[field];
405
- if (from === undefined || warned.current.has(field)) continue;
406
- const seen = pending.current.get(field);
407
- if (seen) seen.to = next[field];
408
- else if (next[field] !== from) pending.current.set(field, { from, to: next[field] });
409
- }
410
- if (pending.current.size === 0) return;
411
- clearTimeout(timer.current);
412
- timer.current = setTimeout(() => {
413
- const now = committed.current;
414
- for (const [field, { from, to }] of pending.current) {
415
- if (to === from || now?.[field] !== from || warned.current.has(field)) continue;
416
- warned.current.add(field);
417
- console.warn(
418
- `[ui-kit] TranslationReviewPanel: the "${field}" filter was changed to ${JSON.stringify(to)}, but the ` +
419
- `\`filter\` prop still says ${JSON.stringify(from)}, so the control does nothing. A field passed in ` +
420
- `\`filter\` is the app's: write back every field \`onFilterChange\` reports, in one update ` +
421
- `(useSearchParamsState's setter takes the object as it is), or leave the field out of \`filter\` and ` +
422
- `the panel keeps it. (Development builds only; once per field.)`,
423
- );
424
- }
425
- pending.current.clear();
426
- }, FILTER_ECHO_MS);
427
- };
428
- }
429
-
430
- /** The row's text — or, for a missing string, the line that says so. */
431
- function TextCell({ row, labels, clamp }: { row: TranslationRow; labels: TranslationReviewLabels; clamp?: boolean }) {
432
- return row.text === "" ? (
433
- <span className="italic text-muted">{labels.missingText}</span>
434
- ) : (
435
- <span
436
- lang={row.locale}
437
- className={cn("whitespace-pre-line break-words", clamp && "line-clamp-3 large:line-clamp-none")}
438
- >
439
- {row.text}
440
- </span>
441
- );
442
- }
443
-
444
- /**
445
- * The page body for one locale: progress, filters (status, source, namespace, search,
446
- * placeholder problems), the strings with an editor that unfolds under each, and the
447
- * bulk actions over a selection.
448
- *
449
- * The status filter is the work queue in order — what is not there, what nobody has
450
- * read, what changed after it was read, what was sent back, what is done — with each
451
- * count on it. "Missing" is offered only where something is.
452
- *
453
- * Bulk approve takes the selected rows that are not approved yet and have a text:
454
- * approving an absent string says nothing, and re-approving one says it again. On a
455
- * phone the table has no checkboxes, so "Select all shown" selects what the filters
456
- * show — filter to `Unreviewed` in one area, read, select, approve. With `groupBy`, a
457
- * group's own "Approve unreviewed" is the shorter way, and with `swipe` a card is approved
458
- * by a swipe.
459
- *
460
- * The panel's writes go out ONE AT A TIME, in the order they were asked for: two writes on
461
- * one locale's verdicts can answer out of order, and an Undo must not overtake the
462
- * approval it takes back. A swipe on the next card while one is out is queued rather than
463
- * refused — a reviewer swiping down a list is faster than the server.
464
- */
465
- export function TranslationReviewPanel({
466
- rows: rowsProp,
467
- localeLabel,
468
- referenceLabel,
469
- onSave,
470
- onClear,
471
- readOnly = false,
472
- areas,
473
- areaLabels,
474
- inArea,
475
- sourceLabels,
476
- filter: filterProp,
477
- onFilterChange,
478
- progress = true,
479
- pageSize = 50,
480
- storageKey,
481
- storageKeyPrefix,
482
- formatDate,
483
- formatError,
484
- swipe = false,
485
- undo: undoProp,
486
- groupBy,
487
- groupLabel,
488
- groupHeadingAs = "h2",
489
- defaultGroupsOpen = "auto",
490
- className,
491
- labels: labelsProp,
492
- }: TranslationReviewPanelProps) {
493
- const labels = useKitLabels("translationReview", DEFAULT_TRANSLATION_REVIEW_LABELS, labelsProp);
494
- const locale = useKitLocale();
495
- // A verdict is an admin route, which a lapsed plan never locks
496
- // (docs/billing-harmonization.md §12.13, §12.36): the buttons take
497
- // COMMIT_EXCEPT_BILLING, and the swipes read the lock as they see it.
498
- const lock = useWriteLock(COMMIT_EXCEPT_BILLING);
499
- // The desktop actions column's plain-text name (dev#598), the provider's "Actions".
500
- const rowActionLabels = useKitLabels("rowActions", DEFAULT_ROW_ACTIONS_LABELS);
501
- // The phone layout DataTable switches its layout on, so "a phone" means the same here
502
- // as in the table, at every text size: `usePhoneLayout()` itself (§10.4; 0.32.1,
503
- // keksdose's 0.32 report — `!useBreakpoint("md")` could disagree with it at 767.5 px).
504
- const phone = usePhoneLayout();
505
- const baseId = useId();
506
- const rootRef = useRef<HTMLDivElement>(null);
507
- const editable = !readOnly && onSave !== undefined;
508
- const binding: TranslationReviewSwipeBinding | null =
509
- swipe === true ? DEFAULT_TRANSLATION_REVIEW_SWIPE : swipe || null;
510
- const undoOn = undoProp ?? (binding !== null || groupBy !== undefined);
511
-
512
- const [ownFilter, setOwnFilter] = useState<TranslationReviewFilter>(DEFAULT_TRANSLATION_REVIEW_FILTER);
513
- // The app's fields over the panel's own; a field the app leaves `undefined` is the
514
- // panel's, not a blank.
515
- const filter: TranslationReviewFilter = { ...ownFilter };
516
- for (const [name, value] of Object.entries(filterProp ?? {})) {
517
- if (value !== undefined) Object.assign(filter, { [name]: value });
518
- }
519
- const echoFilter = useFilterEchoCheck(filterProp);
520
- const [openId, setOpenId] = useState<string | null>(null);
521
- // The row whose editor a swipe opened, to be written in: its wording field takes focus.
522
- const [focusId, setFocusId] = useState<string | null>(null);
523
- const [selected, setSelected] = useState<ReadonlySet<string>>(() => new Set());
524
- // The writes asked for and not yet answered, in order: the first is the one out. The
525
- // editor keeps its own, for its own buttons.
526
- const [queued, setQueued] = useState<readonly string[]>([]);
527
- const claimed = useRef(new Set<string>());
528
- const tail = useRef<Promise<unknown>>(Promise.resolve());
529
- const undoCount = useRef(0);
530
- const [failure, setFailure] = useState<ReactNode>(null);
531
- const busy = queued.length > 0;
532
- const isQueued = (key: string) => queued.includes(key);
533
- // The rows whose open editor has a write of its own out: their swipes stand down until
534
- // it is answered, as for a write in the queue (0.46, keksdose live #416).
535
- const [editorWrites, setEditorWrites] = useState<ReadonlySet<string>>(() => new Set());
536
- const whileEditorWrites = async (rowId: string, write: () => MaybePromise) => {
537
- setEditorWrites((prev) => new Set(prev).add(rowId));
538
- try {
539
- await write();
540
- } finally {
541
- setEditorWrites((prev) => {
542
- const next = new Set(prev);
543
- next.delete(rowId);
544
- return next;
545
- });
546
- }
547
- };
548
-
549
- const rows = useMemo(
550
- () => (areas ? rowsProp.filter((row) => keyInAreas(row.key, areas, inArea)) : [...rowsProp]),
551
- [rowsProp, areas, inArea],
552
- );
553
- const summary = useMemo(() => summariseRows(rows), [rows]);
554
- const sources = useMemo(() => [...new Set(rows.map((r) => r.source))], [rows]);
555
- const source = sources.includes(filter.source) ? filter.source : "all";
556
- const namespaces = useMemo(
557
- () => [...new Set(rows.filter((r) => source === "all" || r.source === source).map((r) => r.namespace))],
558
- [rows, source],
559
- );
560
- const namespace = namespaces.includes(filter.namespace) ? filter.namespace : "all";
561
- const mismatches = useMemo(() => rows.filter((r) => r.placeholderMismatch).length, [rows]);
562
- // `filter` is rebuilt every render; its fields are what the list depends on.
563
- const { status, query, placeholdersOnly } = filter;
564
- const visible = useMemo(
565
- () => filterTranslationRows(rows, { status, source, namespace, query, placeholdersOnly }, locale),
566
- [rows, status, source, namespace, query, placeholdersOnly, locale],
567
- );
568
-
569
- // ── Groups ──
570
- const groups = useMemo(() => {
571
- if (groupBy === undefined) return null;
572
- const of =
573
- groupBy === "namespace"
574
- ? (r: TranslationRow) => r.namespace
575
- : groupBy === "source"
576
- ? (r: TranslationRow) => r.source
577
- : groupBy;
578
- return groupTranslationRows(visible, of);
579
- }, [visible, groupBy]);
580
- const groupName = (key: string) =>
581
- groupLabel?.(key) ?? (groupBy === "source" ? sourceLabels?.[key] : undefined) ?? (key || "—");
582
- // Only what the reviewer chose; a group nobody has touched follows the default, which
583
- // follows the filters (see `defaultGroupsOpen`).
584
- const [groupOpen, setGroupOpen] = useState<Readonly<Record<string, boolean>>>({});
585
- const groupOpenByDefault =
586
- defaultGroupsOpen !== "auto"
587
- ? defaultGroupsOpen
588
- : groups !== null && (phone ? groups.length <= 1 : groupsFitOpen(groups, pageSize));
589
- const isGroupOpen = (key: string) => groupOpen[key] ?? groupOpenByDefault;
590
- const setGroupOpenFor = (key: string, open: boolean) => setGroupOpen((prev) => ({ ...prev, [key]: open }));
591
- /** A group the reviewer works in — an editor opened in it — stays open when the default
592
- * turns (a search cleared, the rows grown past the budget): folding it would drop the
593
- * editor and whatever was typed in it. */
594
- const keepGroupOpen = (key: string | undefined) => {
595
- if (key !== undefined) setGroupOpen((prev) => (key in prev ? prev : { ...prev, [key]: true }));
596
- };
597
- // The group whose large batch is waiting for a yes — and, until its confirm button
598
- // has mounted and taken focus, the group that asked.
599
- const [confirming, setConfirming] = useState<string | null>(null);
600
- const confirmFocus = useRef<string | null>(null);
601
- // One view for every group's table — one sort, so a header click orders all of them
602
- // alike, and one page size — kept under `storageKey` in the single table's own blob, so
603
- // switching the grouping keeps both (0.35, Kurvenschmiede).
604
- const groupView = useSharedTableView({
605
- active: groupBy !== undefined,
606
- storageKey,
607
- storageKeyPrefix,
608
- defaultPageSize: pageSize,
609
- });
610
-
611
- const setFilter = (patch: Partial<TranslationReviewFilter>) => {
612
- const next = { ...filter, ...patch };
613
- setOwnFilter(next);
614
- onFilterChange?.(next);
615
- echoFilter(next);
616
- // A selection the new filter hides would be acted on unseen.
617
- setSelected(new Set());
618
- };
619
-
620
- /** Queue a write under `key` (one per key at a time); resolves whether it worked. */
621
- const run = (key: string, action: () => MaybePromise): Promise<boolean> => {
622
- if (claimed.current.has(key)) return Promise.resolve(false);
623
- claimed.current.add(key);
624
- setQueued((q) => [...q, key]);
625
- const step = tail.current.then(async () => {
626
- setFailure(null);
627
- try {
628
- await action();
629
- return true;
630
- } catch (caught) {
631
- setFailure(formatError ? formatError(caught) : labels.failed);
632
- return false;
633
- } finally {
634
- claimed.current.delete(key);
635
- setQueued((q) => q.filter((k) => k !== key));
636
- }
637
- });
638
- tail.current = step;
639
- return step;
640
- };
641
-
642
- /** "String approved — Undo", once `targets` (as they were BEFORE the write) are
643
- * approved — or, after a swiped `clear`, "String marked unreviewed — Undo". Either way
644
- * Undo puts back what the rows said before ({@link reviewUndo}), through the same queue
645
- * as every write. */
646
- const sayDone = (targets: readonly TranslationRow[], description?: ReactNode, cleared = false) => {
647
- const back = reviewUndo(targets);
648
- const message = (cleared ? labels.clearedToast : labels.approvedToast)(targets.length);
649
- const save = onSave;
650
- const clear = onClear;
651
- // Taking back the approval of a row that had no verdict is a clear: without
652
- // `onClear` there is no way to say it, so the toast reports and offers nothing.
653
- if (!save || (back.clears.length > 0 && !clear)) {
654
- toast.success(message, { description });
655
- return;
656
- }
657
- const key = `undo:${++undoCount.current}`;
658
- toast.undo(message, {
659
- description,
660
- onUndo: () =>
661
- void run(key, async () => {
662
- if (back.writes.length > 0) await save(back.writes, { origin: "undo", toasted: false });
663
- if (back.clears.length > 0) await clear?.(back.clears, { origin: "undo", toasted: false });
664
- }),
665
- });
666
- };
667
-
668
- const approve = async (
669
- key: string,
670
- targets: readonly TranslationRow[],
671
- origin: TranslationReviewOrigin,
672
- description?: ReactNode,
673
- ): Promise<boolean> => {
674
- if (!onSave || targets.length === 0) return false;
675
- const save = onSave;
676
- const ok = await run(key, () =>
677
- save(
678
- targets.map((r) => reviewWrite(r, "APPROVED")),
679
- { origin, toasted: undoOn },
680
- ),
681
- );
682
- if (ok && undoOn) sayDone(targets, description);
683
- return ok;
684
- };
685
-
686
- /** A swipe bound to `clear`: the row's verdict taken back, answered like an approval —
687
- * the Undo toast stores the verdict again. Under the row's own key, so it queues behind
688
- * (and never overlaps) an approval of the same row. */
689
- const clearRow = async (row: TranslationRow, origin: TranslationReviewOrigin = "swipe") => {
690
- if (!onClear || row.review === null) return;
691
- const clear = onClear;
692
- const keys = [{ locale: row.locale, key: row.key }];
693
- const ok = await run(`row:${row.id}`, () => clear(keys, { origin, toasted: undoOn }));
694
- if (ok && undoOn) sayDone([row], row.key, true);
695
- };
696
-
697
- /** The editor's own save: it waits, closes and shows its errors itself, so it does
698
- * not go through the queue — but an approval there is answered like any other. */
699
- const saveFromEditor = async (row: TranslationRow, write: TranslationReviewWrite) => {
700
- const toasted = undoOn && write.verdict === "APPROVED";
701
- await whileEditorWrites(row.id, () => onSave?.([write], { origin: "editor", toasted }));
702
- if (toasted) sayDone([row], row.key);
703
- };
704
-
705
- /**
706
- * Focus back into the panel once a group's batch has settled, if it fell out: the
707
- * button that was pressed goes with the rows it approved (and under the `Unreviewed`
708
- * filter the whole group can go). Two frames, so the app's new rows have been drawn.
709
- */
710
- const keepFocusAt = (groupKey: string) => {
711
- const check = () => {
712
- const root = rootRef.current;
713
- const active = document.activeElement;
714
- if (!root || (active && active !== document.body && active.isConnected)) return;
715
- const sections = [...root.querySelectorAll<HTMLElement>("[data-review-group]")];
716
- const section = sections.find((el) => el.dataset.reviewGroup === groupKey) ?? sections[0];
717
- section?.querySelector<HTMLElement>("[data-disclosure-trigger]")?.focus();
718
- };
719
- requestAnimationFrame(() => requestAnimationFrame(check));
720
- };
721
-
722
- const approveGroup = async (group: TranslationRowGroup, confirmed: boolean) => {
723
- const targets = unreviewedRows(group.rows);
724
- if (targets.length === 0) return;
725
- if (!confirmed && targets.length > pageSize) {
726
- // Asked inside the opened group, over the rows it is about.
727
- setGroupOpenFor(group.key, true);
728
- setConfirming(group.key);
729
- confirmFocus.current = group.key;
730
- return;
731
- }
732
- const ok = await approve(`group:${group.key}`, targets, "group", groupName(group.key));
733
- if (ok) setConfirming((key) => (key === group.key ? null : key));
734
- keepFocusAt(group.key);
735
- };
736
-
737
- const visibleIds = new Set(visible.map((r) => r.id));
738
- const selectedRows = visible.filter((r) => selected.has(r.id));
739
- // What "Select all shown" takes: grouped, the rows of the open groups — a folded
740
- // group's rows are not shown.
741
- const shownRows = groups ? groups.filter((g) => isGroupOpen(g.key)).flatMap((g) => g.rows) : visible;
742
- const allShownSelected = shownRows.length > 0 && shownRows.every((r) => selected.has(r.id));
743
- const toApprove = selectedRows.filter((r) => r.text !== "" && r.status !== "approved");
744
- const toReset = selectedRows.filter((r) => r.review !== null);
745
-
746
- const approveSelected = async () => {
747
- if (await approve("bulk-approve", toApprove, "selection")) setSelected(new Set());
748
- };
749
- const resetSelected = async () => {
750
- if (!onClear || toReset.length === 0) return;
751
- const clear = onClear;
752
- const keys = toReset.map((r) => ({ locale: r.locale, key: r.key }));
753
- if (await run("bulk-reset", () => clear(keys, { origin: "selection", toasted: false }))) {
754
- setSelected(new Set());
755
- }
756
- };
757
-
758
- // The status options: "missing" only where something is (or it is the filter).
759
- const statuses = REVIEW_STATUSES.filter((s) => s !== "missing" || summary.missing > 0 || filter.status === s);
760
- const scopeNames = (areas ?? []).map((area) => areaLabels?.[area] ?? area);
761
- const scopeText =
762
- scopeNames.length > 0 ? new Intl.ListFormat(locale, { type: "conjunction", style: "long" }).format(scopeNames) : "";
763
-
764
- const columns: DataTableColumn<TranslationRow>[] = [
765
- {
766
- key: "key",
767
- header: labels.key,
768
- cell: (r) => <code className="break-all text-xs text-secondary">{r.key}</code>,
769
- sortBy: (r) => r.key,
770
- mobileHidden: true,
771
- },
772
- {
773
- key: "reference",
774
- header: referenceLabel,
775
- cell: (r) => <span className="line-clamp-3 whitespace-pre-line break-words text-secondary">{r.reference}</span>,
776
- mobileHidden: true,
777
- },
778
- {
779
- key: "text",
780
- header: localeLabel,
781
- cell: (r) => <TextCell row={r} labels={labels} clamp />,
782
- mobilePrimary: true,
783
- },
784
- {
785
- key: "status",
786
- header: labels.statusColumn,
787
- cell: (r) => (
788
- <span className="inline-flex flex-wrap gap-1">
789
- <ReviewStatusChip status={r.status} labels={labelsProp} />
790
- {r.placeholderMismatch && (
791
- <Chip size="sm" tone="danger" variant="outline">
792
- {labels.placeholderChip}
793
- </Chip>
794
- )}
795
- </span>
796
- ),
797
- sortBy: (r) => REVIEW_STATUSES.indexOf(r.status),
798
- },
799
- ];
800
- if (editable && binding) {
801
- // keksdose dev#598 item 2: with a binding, the desktop row offers what the card's
802
- // swipes offer — `clear` was reachable only by a swipe. Approve first on every row
803
- // that can take it, bound or not (the desktop always had it), then the binding's
804
- // actions, end side first, each once. The swipe's words, glyphs and handlers; unlike
805
- // the swipe, a locked row keeps its buttons and says why (`commit`), and a row whose
806
- // write is out shows it in flight. Hidden on the phone, where the swipes are.
807
- columns.push(
808
- rowActionsColumn<TranslationRow>({
809
- key: "actions",
810
- headerText: rowActionLabels.actions,
811
- name: (r) => r.key,
812
- tooltipSide: "start",
813
- actions: (r) =>
814
- translationReviewSwipePlan({ end: ["approve", ...(binding.end ?? []), ...(binding.start ?? [])] }, r, {
815
- canClear: onClear !== undefined,
816
- }).end.map((id): RowAction => {
817
- const swipe = swipeAction(r, id);
818
- // `edit` only opens the editor, whose own Save is the commit.
819
- const writes = id !== "edit";
820
- return {
821
- key: id,
822
- // `edit` opens the editor: its own name, apart from the editor's submit
823
- // ("Needs a change"), which saves (keksdose B2/S3).
824
- label: id === "edit" ? (r.text === "" ? labels.suggestRow : labels.flagRow) : swipe.label,
825
- icon: <>{swipe.icon}</>,
826
- tone: "muted",
827
- commit: writes ? COMMIT_EXCEPT_BILLING : undefined,
828
- pending: writes && isQueued(`row:${r.id}`),
829
- disabled: busy,
830
- onSelect:
831
- id === "approve"
832
- ? () => void approve(`row:${r.id}`, [r], "row", r.key)
833
- : id === "clear"
834
- ? () => void clearRow(r, "row")
835
- : // The row's group stays open while its editor is (`keepGroupOpen`).
836
- () => swipeAction(r, id, groups?.find((g) => g.rows.includes(r))?.key).onCommit(),
837
- };
838
- }),
839
- column: { mobileHidden: true },
840
- }),
841
- );
842
- } else if (editable) {
843
- columns.push({
844
- key: "approve",
845
- // On screen, as every actions column's heading is (0.46.1, keksdose dev#598 rework).
846
- header: rowActionLabels.actions,
847
- headerText: rowActionLabels.actions,
848
- cell: (r) =>
849
- r.status === "approved" || r.text === "" ? null : (
850
- <IconButton
851
- type="button"
852
- size="xs"
853
- tone="muted"
854
- commit={COMMIT_EXCEPT_BILLING}
855
- label={labels.approve}
856
- // The icon alone at every text size (docs/text-size-harmonization.md §10.8):
857
- // the row's one action, in a hugging column of a dense table beside the
858
- // string under review, which "Approve" as text at Large would squeeze. A
859
- // single action stays inline in `RowActions` too, so the menu would not help;
860
- // the swipe and the editor say it in words.
861
- labelVisible={false}
862
- pending={isQueued(`row:${r.id}`)}
863
- disabled={busy}
864
- onClick={(event) => {
865
- // The row's own click opens the editor; this one only approves.
866
- event.stopPropagation();
867
- void approve(`row:${r.id}`, [r], "row", r.key);
868
- }}
869
- >
870
- <Check />
871
- </IconButton>
872
- ),
873
- });
874
- }
875
-
876
- /** One bound swipe action on one row, as {@link SwipeableRow} draws and commits it. */
877
- const swipeAction = (r: TranslationRow, id: TranslationReviewSwipeAction, groupKey?: string): SwipeAction => {
878
- const missing = r.text === "";
879
- switch (id) {
880
- case "approve":
881
- return {
882
- id,
883
- label: labels.approve,
884
- icon: <Check className="size-4" aria-hidden />,
885
- onCommit: () => void approve(`row:${r.id}`, [r], "swipe", r.key),
886
- // SwipeableRow paints the tone: its soft wash idle, its fill and that fill's
887
- // foreground armed (0.33).
888
- tone: "success",
889
- };
890
- case "edit":
891
- return {
892
- id,
893
- label: missing ? labels.suggest : labels.flag,
894
- icon: <PencilLine className="size-4" aria-hidden />,
895
- // Opens the editor rather than sending the string back: "needs a change"
896
- // with neither a note nor a better wording tells nobody anything.
897
- onCommit: () => {
898
- keepGroupOpen(groupKey);
899
- setOpenId(r.id);
900
- setFocusId(r.id);
901
- // On a row already open (#416) the editor is mounted and keeps its draft:
902
- // the cursor goes to the end of its wording now.
903
- if (openId === r.id) {
904
- const field = rootRef.current?.querySelector<HTMLTextAreaElement>("[data-review-wording]");
905
- field?.focus();
906
- field?.setSelectionRange(field.value.length, field.value.length);
907
- }
908
- },
909
- tone: missing ? "brand" : "danger",
910
- };
911
- case "clear":
912
- return {
913
- id,
914
- label: labels.reset,
915
- icon: <RotateCcw className="size-4" aria-hidden />,
916
- onCommit: () => void clearRow(r),
917
- // A step back, not a verdict: neither the approval's green nor the send-back's
918
- // red. `neutral` is the pair this swipe improvised and 0.33 made a token: the
919
- // muted ink as a fill under the surface colour.
920
- tone: "neutral",
921
- };
922
- }
923
- };
924
-
925
- /**
926
- * A phone card's swipes: the binding ({@link TranslationReviewPanelProps.swipe}) resolved
927
- * against the row, each side's ladder closed up over what the row cannot offer. None
928
- * where the row must not be acted on — the same conditions under which its buttons are
929
- * locked or absent.
930
- */
931
- const swipeActions = (r: TranslationRow, groupKey?: string): MobileSwipeActions | null => {
932
- if (!binding || !editable || lock.locked || isQueued(`row:${r.id}`) || editorWrites.has(r.id)) return null;
933
- const plan = translationReviewSwipePlan(binding, r, { canClear: onClear !== undefined });
934
- if (plan.start.length === 0 && plan.end.length === 0) return null;
935
- return {
936
- start: plan.start.map((id) => swipeAction(r, id, groupKey)),
937
- end: plan.end.map((id) => swipeAction(r, id, groupKey)),
938
- };
939
- };
940
-
941
- const toggleIn = (prev: ReadonlySet<string>, many: readonly TranslationRow[], checked: boolean) => {
942
- const next = new Set([...prev].filter((id) => visibleIds.has(id)));
943
- for (const r of many) {
944
- if (checked) next.add(r.id);
945
- else next.delete(r.id);
946
- }
947
- return next;
948
- };
949
- /** The checkbox column of a table over `list` — the whole list, or one group. */
950
- const selectionFor = (list: readonly TranslationRow[]): DataTableProps<TranslationRow>["selection"] => {
951
- if (!editable) return undefined;
952
- const picked = list.filter((r) => selected.has(r.id)).length;
953
- return {
954
- isSelected: (r) => selected.has(r.id),
955
- onToggle: (r, checked) => setSelected((prev) => toggleIn(prev, [r], checked)),
956
- onToggleMany: (many, checked) => setSelected((prev) => toggleIn(prev, many, checked)),
957
- allSelected: list.length > 0 && picked === list.length,
958
- someSelected: picked > 0 && picked < list.length,
959
- onToggleAll: (checked) => setSelected((prev) => toggleIn(prev, list, checked)),
960
- };
961
- };
962
-
963
- const table = (list: TranslationRow[], group?: { key: string; name: string }) => (
964
- <DataTable<TranslationRow>
965
- rows={list}
966
- rowKey={(r) => r.id}
967
- columns={columns}
968
- empty={labels.empty}
969
- defaultPageSize={pageSize}
970
- {...(group
971
- ? {
972
- // Inside the group's own card: no frame in a frame, no column rail per group,
973
- // no scroller per group, and the group's name as the table's.
974
- frame: false,
975
- chrome: "minimal" as const,
976
- maxBodyHeight: "none",
977
- sorts: groupView.sorts,
978
- onSortsChange: groupView.setSorts,
979
- pageSize: groupView.pageSize,
980
- onPageSizeChange: groupView.setPageSize,
981
- labels: { table: group.name },
982
- }
983
- : { storageKey, storageKeyPrefix })}
984
- isExpanded={(r) => r.id === openId}
985
- onRowClick={(r) => {
986
- if (openId !== r.id) keepGroupOpen(group?.key);
987
- setFocusId(null);
988
- setOpenId((open) => (open === r.id ? null : r.id));
989
- }}
990
- expandedRow={(r) => (
991
- <TranslationReviewEditor
992
- key={r.id}
993
- row={r}
994
- referenceLabel={referenceLabel}
995
- localeLabel={localeLabel}
996
- readOnly={!editable}
997
- focusWording={focusId === r.id}
998
- onSave={onSave ? (write) => saveFromEditor(r, write) : undefined}
999
- onClear={
1000
- onClear
1001
- ? (key) => whileEditorWrites(r.id, () => onClear([key], { origin: "editor", toasted: false }))
1002
- : undefined
1003
- }
1004
- onClose={() => {
1005
- setOpenId(null);
1006
- setFocusId(null);
1007
- }}
1008
- formatDate={formatDate}
1009
- formatError={formatError}
1010
- labels={labelsProp}
1011
- />
1012
- )}
1013
- mobileCard={(r) => (
1014
- <div className="min-w-0 space-y-1">
1015
- <code className="block break-all text-xs text-muted">{r.key}</code>
1016
- <div className="text-sm text-primary">
1017
- <TextCell row={r} labels={labels} clamp />
1018
- </div>
1019
- <div className="flex flex-wrap gap-1">
1020
- <ReviewStatusChip status={r.status} labels={labelsProp} />
1021
- {r.placeholderMismatch && (
1022
- <Chip size="sm" tone="danger" variant="outline">
1023
- {labels.placeholderChip}
1024
- </Chip>
1025
- )}
1026
- </div>
1027
- </div>
1028
- )}
1029
- mobileSwipeActions={binding ? (r) => swipeActions(r, group?.key) : undefined}
1030
- // The desktop's buttons for the binding are the panel's own "actions" column.
1031
- desktopSwipeActions={false}
1032
- selection={selectionFor(list)}
1033
- />
1034
- );
1035
-
1036
- const groupSection = (g: TranslationRowGroup, index: number) => {
1037
- const name = groupName(g.key);
1038
- const titleId = `${baseId}-group-${index}`;
1039
- const questionId = `${baseId}-confirm-${index}`;
1040
- const targets = editable ? unreviewedRows(g.rows) : [];
1041
- const key = `group:${g.key}`;
1042
- const open = isGroupOpen(g.key);
1043
- // A folded group renders its header and nothing else. Its body is handed over only
1044
- // while it is open, or when the REVIEWER folded it — so the fold animates over the
1045
- // rows it hides. A group folded by the default gets none: when the default turns (a
1046
- // search cleared over 122 areas), the groups that close would otherwise render their
1047
- // grown tables for the length of the fold.
1048
- const body = open || groupOpen[g.key] === false;
1049
- return (
1050
- <Disclosure
1051
- key={g.key}
1052
- data-review-group={g.key}
1053
- headingAs={groupHeadingAs}
1054
- title={<span id={titleId}>{name}</span>}
1055
- hint={labels.groupCount(g.summary.unreviewed, g.summary.total)}
1056
- open={open}
1057
- onOpenChange={(next) => {
1058
- setGroupOpenFor(g.key, next);
1059
- if (!next) setConfirming((c) => (c === g.key ? null : c));
1060
- }}
1061
- trailing={
1062
- targets.length > 0 ? (
1063
- <Button
1064
- variant="secondary"
1065
- size="sm"
1066
- commit={COMMIT_EXCEPT_BILLING}
1067
- // The visible words say what; the group's name, read after them, says where.
1068
- aria-describedby={titleId}
1069
- pending={isQueued(key) && confirming !== g.key}
1070
- disabled={busy}
1071
- onClick={() => void approveGroup(g, false)}
1072
- >
1073
- <Check className="size-4" aria-hidden />
1074
- {labels.approveGroup(targets.length)}
1075
- </Button>
1076
- ) : undefined
1077
- }
1078
- bodyClassName="space-y-0 px-0 pb-0 border-t border-subtle"
1079
- >
1080
- {body && confirming === g.key && targets.length > 0 && (
1081
- <div className="border-b border-subtle p-3">
1082
- <AlertBanner tone="warning" size="sm">
1083
- <div className="min-w-0 space-y-2">
1084
- <p id={questionId}>{labels.confirmGroup(targets.length, name)}</p>
1085
- <div className="flex flex-wrap gap-2">
1086
- <Button
1087
- // Focus lands here when the question appears: the expected answer to a
1088
- // question the reviewer just asked for, and Undo stays behind it.
1089
- ref={(el) => {
1090
- if (el && confirmFocus.current === g.key) {
1091
- confirmFocus.current = null;
1092
- el.focus();
1093
- }
1094
- }}
1095
- variant="default"
1096
- size="sm"
1097
- commit={COMMIT_EXCEPT_BILLING}
1098
- aria-describedby={questionId}
1099
- pending={isQueued(key)}
1100
- disabled={busy}
1101
- onClick={() => void approveGroup(g, true)}
1102
- >
1103
- {labels.approveGroup(targets.length)}
1104
- </Button>
1105
- <Button
1106
- variant="ghost"
1107
- size="sm"
1108
- disabled={isQueued(key)}
1109
- onClick={() => {
1110
- setConfirming(null);
1111
- keepFocusAt(g.key);
1112
- }}
1113
- >
1114
- {labels.cancel}
1115
- </Button>
1116
- </div>
1117
- </div>
1118
- </AlertBanner>
1119
- </div>
1120
- )}
1121
- {body && table(g.rows, { key: g.key, name })}
1122
- </Disclosure>
1123
- );
1124
- };
1125
- return (
1126
- <div ref={rootRef} className={cn("min-w-0 space-y-4", className)}>
1127
- {scopeText && (
1128
- <AlertBanner tone="info" variant="inline" size="sm" block live={false}>
1129
- {labels.scope(scopeText)}
1130
- </AlertBanner>
1131
- )}
1132
- {readOnly && (
1133
- <AlertBanner tone="info" variant="inline" size="sm" block live={false}>
1134
- {labels.readOnly}
1135
- </AlertBanner>
1136
- )}
1137
-
1138
- {progress && <TranslationProgress summary={summary} labels={labelsProp} />}
1139
-
1140
- <ToggleGroup<ReviewStatus | "all">
1141
- aria-label={labels.statusFilter}
1142
- overflow="wrap"
1143
- size="sm"
1144
- value={filter.status}
1145
- onChange={(next) => setFilter({ status: next })}
1146
- options={[
1147
- { value: "all", label: labels.filterCount(labels.all, summary.total) },
1148
- ...statuses.map((s) => ({ value: s, label: labels.filterCount(reviewStatusLabel(labels, s), summary[s]) })),
1149
- ]}
1150
- />
1151
-
1152
- <div className="flex flex-col gap-3 md:flex-row md:flex-wrap md:items-end">
1153
- {sources.length > 1 && (
1154
- <div className="md:w-48">
1155
- <Select label={labels.source} value={source} onChange={(e) => setFilter({ source: e.target.value })}>
1156
- <option value="all">{labels.allSources}</option>
1157
- {sources.map((s) => (
1158
- <option key={s} value={s}>
1159
- {sourceLabels?.[s] ?? s}
1160
- </option>
1161
- ))}
1162
- </Select>
1163
- </div>
1164
- )}
1165
- <div className="md:w-56">
1166
- <Select label={labels.namespace} value={namespace} onChange={(e) => setFilter({ namespace: e.target.value })}>
1167
- <option value="all">{labels.allNamespaces}</option>
1168
- {namespaces.map((ns) => (
1169
- <option key={ns} value={ns}>
1170
- {ns}
1171
- </option>
1172
- ))}
1173
- </Select>
1174
- </div>
1175
- <SearchField
1176
- className="min-w-0 md:flex-1"
1177
- aria-label={labels.search}
1178
- value={filter.query}
1179
- onChange={(next) => setFilter({ query: next })}
1180
- />
1181
- <Checkbox
1182
- label={labels.placeholdersOnly(mismatches)}
1183
- checked={filter.placeholdersOnly}
1184
- onCheckedChange={(next) => setFilter({ placeholdersOnly: next })}
1185
- />
1186
- </div>
1187
-
1188
- {editable && shownRows.length > 0 && (
1189
- // The phone's table has no checkboxes; this is its way into the bulk actions.
1190
- <Button
1191
- variant="ghost"
1192
- size="sm"
1193
- className="md:hidden"
1194
- disabled={allShownSelected}
1195
- onClick={() => setSelected((prev) => toggleIn(prev, shownRows, true))}
1196
- >
1197
- <ListChecks className="size-4" aria-hidden />
1198
- {labels.selectShown}
1199
- </Button>
1200
- )}
1201
-
1202
- {failure && (
1203
- <AlertBanner tone="danger" size="sm" role="alert">
1204
- {failure}
1205
- </AlertBanner>
1206
- )}
1207
-
1208
- {editable && (
1209
- <BulkActionBar count={selectedRows.length} onClear={() => setSelected(new Set())}>
1210
- <Button
1211
- variant="default"
1212
- size="sm"
1213
- commit={COMMIT_EXCEPT_BILLING}
1214
- pending={isQueued("bulk-approve")}
1215
- disabled={busy || toApprove.length === 0}
1216
- onClick={() => void approveSelected()}
1217
- >
1218
- <Check className="size-4" aria-hidden />
1219
- {labels.approveSelected}
1220
- </Button>
1221
- {onClear && (
1222
- <Button
1223
- variant="ghost"
1224
- size="sm"
1225
- commit={COMMIT_EXCEPT_BILLING}
1226
- pending={isQueued("bulk-reset")}
1227
- disabled={busy || toReset.length === 0}
1228
- onClick={() => void resetSelected()}
1229
- >
1230
- <RotateCcw className="size-4" aria-hidden />
1231
- {labels.resetSelected}
1232
- </Button>
1233
- )}
1234
- </BulkActionBar>
1235
- )}
1236
-
1237
- {groups && groups.length > 0 ? <div className="space-y-3">{groups.map(groupSection)}</div> : table(visible)}
1238
- </div>
1239
- );
1240
- }