@eifi1/ui-kit 0.31.0 → 0.32.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 (565) hide show
  1. package/README.md +53 -30
  2. package/dist/account/delete-account-setting.d.ts +8 -3
  3. package/dist/admin/admin-action-confirm.d.ts +8 -3
  4. package/dist/admin/admin-parts.d.ts +8 -1
  5. package/dist/admin/admin-parts.js +1 -1
  6. package/dist/admin/admin-parts.js.map +1 -1
  7. package/dist/admin/invitations-panel.d.ts +8 -3
  8. package/dist/admin/invitations-panel.js +29 -26
  9. package/dist/admin/invitations-panel.js.map +1 -1
  10. package/dist/admin/reviewer-scope-editor.d.ts +8 -3
  11. package/dist/admin/role-select.d.ts +10 -5
  12. package/dist/admin/transfer-ownership-dialog.d.ts +8 -3
  13. package/dist/admin/user-roster.d.ts +10 -5
  14. package/dist/admin/user-roster.js +8 -47
  15. package/dist/admin/user-roster.js.map +1 -1
  16. package/dist/auth/auth-errors.d.ts +43 -8
  17. package/dist/auth/auth-errors.js +24 -1
  18. package/dist/auth/auth-errors.js.map +1 -1
  19. package/dist/auth/register-form.d.ts +8 -3
  20. package/dist/auth/reset-password-form.d.ts +1 -1
  21. package/dist/auth/reset-password-form.js +2 -1
  22. package/dist/auth/reset-password-form.js.map +1 -1
  23. package/dist/auth/verify-email.d.ts +4 -0
  24. package/dist/auth/verify-email.js +3 -2
  25. package/dist/auth/verify-email.js.map +1 -1
  26. package/dist/billing/billing-banner.d.ts +184 -0
  27. package/dist/billing/billing-banner.js +98 -0
  28. package/dist/billing/billing-banner.js.map +1 -0
  29. package/dist/billing/billing-labels.d.ts +100 -0
  30. package/dist/billing/billing-labels.js +74 -0
  31. package/dist/billing/billing-labels.js.map +1 -0
  32. package/dist/billing/billing-lock.d.ts +158 -0
  33. package/dist/billing/billing-lock.js +17 -0
  34. package/dist/billing/billing-lock.js.map +1 -0
  35. package/dist/billing/billing-standing.d.ts +149 -0
  36. package/dist/billing/billing-standing.js +28 -0
  37. package/dist/billing/billing-standing.js.map +1 -0
  38. package/dist/billing/plan-card.d.ts +248 -0
  39. package/dist/billing/plan-card.js +248 -0
  40. package/dist/billing/plan-card.js.map +1 -0
  41. package/dist/billing/plan-limit-notice.d.ts +156 -0
  42. package/dist/billing/plan-limit-notice.js +76 -0
  43. package/dist/billing/plan-limit-notice.js.map +1 -0
  44. package/dist/billing/plan-limit.d.ts +34 -0
  45. package/dist/billing/plan-limit.js +20 -0
  46. package/dist/billing/plan-limit.js.map +1 -0
  47. package/dist/billing/plan-price.d.ts +61 -0
  48. package/dist/billing/plan-price.js +45 -0
  49. package/dist/billing/plan-price.js.map +1 -0
  50. package/dist/billing/subscription-actions.d.ts +133 -0
  51. package/dist/billing/subscription-actions.js +28 -0
  52. package/dist/billing/subscription-actions.js.map +1 -0
  53. package/dist/billing/subscription-status-chip.d.ts +149 -0
  54. package/dist/billing/subscription-status-chip.js +48 -0
  55. package/dist/billing/subscription-status-chip.js.map +1 -0
  56. package/dist/chart.d.ts +9 -3
  57. package/dist/components/account-chips.d.ts +16 -4
  58. package/dist/components/account-chips.js +21 -3
  59. package/dist/components/account-chips.js.map +1 -1
  60. package/dist/components/alert-banner.d.ts +6 -1
  61. package/dist/components/alert-banner.js +4 -3
  62. package/dist/components/alert-banner.js.map +1 -1
  63. package/dist/components/amount-input.d.ts +8 -3
  64. package/dist/components/amount-input.js +5 -5
  65. package/dist/components/amount-input.js.map +1 -1
  66. package/dist/components/appearance-labels.d.ts +32 -0
  67. package/dist/components/appearance-labels.js +18 -0
  68. package/dist/components/appearance-labels.js.map +1 -0
  69. package/dist/components/autocomplete.js +2 -2
  70. package/dist/components/autocomplete.js.map +1 -1
  71. package/dist/components/breadcrumbs.js +3 -2
  72. package/dist/components/breadcrumbs.js.map +1 -1
  73. package/dist/components/bulk-action-bar.d.ts +2 -2
  74. package/dist/components/bulk-action-bar.js +7 -12
  75. package/dist/components/bulk-action-bar.js.map +1 -1
  76. package/dist/components/button-group.d.ts +8 -3
  77. package/dist/components/calculator.d.ts +8 -3
  78. package/dist/components/calculator.js +4 -3
  79. package/dist/components/calculator.js.map +1 -1
  80. package/dist/components/calendar-heatmap.js +5 -5
  81. package/dist/components/calendar-heatmap.js.map +1 -1
  82. package/dist/components/chart-height.d.ts +23 -0
  83. package/dist/components/chart-height.js +15 -0
  84. package/dist/components/chart-height.js.map +1 -0
  85. package/dist/components/chart-zoom.js +6 -1
  86. package/dist/components/chart-zoom.js.map +1 -1
  87. package/dist/components/checkbox-group.js +2 -2
  88. package/dist/components/checkbox-group.js.map +1 -1
  89. package/dist/components/checkbox.d.ts +10 -0
  90. package/dist/components/checkbox.js +25 -10
  91. package/dist/components/checkbox.js.map +1 -1
  92. package/dist/components/chip.d.ts +19 -3
  93. package/dist/components/chip.js +47 -21
  94. package/dist/components/chip.js.map +1 -1
  95. package/dist/components/choice-card.d.ts +1 -1
  96. package/dist/components/choice-card.js +8 -7
  97. package/dist/components/choice-card.js.map +1 -1
  98. package/dist/components/column-mapper.js +1 -1
  99. package/dist/components/column-mapper.js.map +1 -1
  100. package/dist/components/combobox-core.js +8 -7
  101. package/dist/components/combobox-core.js.map +1 -1
  102. package/dist/components/combobox.js +8 -8
  103. package/dist/components/combobox.js.map +1 -1
  104. package/dist/components/confirm-dialog.d.ts +8 -3
  105. package/dist/components/confirm-dialog.js +7 -1
  106. package/dist/components/confirm-dialog.js.map +1 -1
  107. package/dist/components/copy-button.d.ts +10 -5
  108. package/dist/components/country-select.js +1 -1
  109. package/dist/components/country-select.js.map +1 -1
  110. package/dist/components/currency-select.js +3 -3
  111. package/dist/components/currency-select.js.map +1 -1
  112. package/dist/components/danger-confirm.d.ts +8 -3
  113. package/dist/components/data-table-cells.d.ts +1 -1
  114. package/dist/components/data-table-filter-popover.d.ts +1 -1
  115. package/dist/components/data-table-filter-popover.js +4 -1
  116. package/dist/components/data-table-filter-popover.js.map +1 -1
  117. package/dist/components/data-table-filters.d.ts +1 -1
  118. package/dist/components/data-table-labels.d.ts +1 -1
  119. package/dist/components/data-table-pagination.d.ts +1 -1
  120. package/dist/components/data-table.d.ts +1 -1
  121. package/dist/components/data-table.js +123 -52
  122. package/dist/components/data-table.js.map +1 -1
  123. package/dist/components/date-picker.d.ts +1 -1
  124. package/dist/components/date-picker.js +7 -5
  125. package/dist/components/date-picker.js.map +1 -1
  126. package/dist/components/description-list.js +2 -2
  127. package/dist/components/description-list.js.map +1 -1
  128. package/dist/components/dialog-frame.js +2 -1
  129. package/dist/components/dialog-frame.js.map +1 -1
  130. package/dist/components/disclosure.js +3 -2
  131. package/dist/components/disclosure.js.map +1 -1
  132. package/dist/components/dropdown.d.ts +1 -1
  133. package/dist/components/dropdown.js +7 -1
  134. package/dist/components/dropdown.js.map +1 -1
  135. package/dist/components/facing-pair.d.ts +19 -6
  136. package/dist/components/facing-pair.js +3 -2
  137. package/dist/components/facing-pair.js.map +1 -1
  138. package/dist/components/field-parts.d.ts +102 -6
  139. package/dist/components/field-parts.js +81 -6
  140. package/dist/components/field-parts.js.map +1 -1
  141. package/dist/components/field-sync.d.ts +4 -1
  142. package/dist/components/field-sync.js +12 -2
  143. package/dist/components/field-sync.js.map +1 -1
  144. package/dist/components/field.js +2 -2
  145. package/dist/components/field.js.map +1 -1
  146. package/dist/components/file-button.d.ts +8 -3
  147. package/dist/components/file-dropzone.d.ts +8 -3
  148. package/dist/components/floating-panel.js +7 -4
  149. package/dist/components/floating-panel.js.map +1 -1
  150. package/dist/components/focus-ring.d.ts +12 -0
  151. package/dist/components/focus-ring.js +8 -0
  152. package/dist/components/focus-ring.js.map +1 -0
  153. package/dist/components/form-actions.d.ts +8 -3
  154. package/dist/components/form-actions.js +33 -19
  155. package/dist/components/form-actions.js.map +1 -1
  156. package/dist/components/full-bleed-dialog.d.ts +11 -1
  157. package/dist/components/full-bleed-dialog.js +3 -1
  158. package/dist/components/full-bleed-dialog.js.map +1 -1
  159. package/dist/components/iban-input.d.ts +8 -3
  160. package/dist/components/image-grid.js +1 -1
  161. package/dist/components/image-grid.js.map +1 -1
  162. package/dist/components/inline-edit-field.js +6 -3
  163. package/dist/components/inline-edit-field.js.map +1 -1
  164. package/dist/components/language-select.d.ts +8 -3
  165. package/dist/components/legal-page.d.ts +26 -5
  166. package/dist/components/legal-page.js +3 -1
  167. package/dist/components/legal-page.js.map +1 -1
  168. package/dist/components/legal.d.ts +8 -3
  169. package/dist/components/legal.js +2 -1
  170. package/dist/components/legal.js.map +1 -1
  171. package/dist/components/lightbox.js +24 -2
  172. package/dist/components/lightbox.js.map +1 -1
  173. package/dist/components/line-items.js +2 -1
  174. package/dist/components/line-items.js.map +1 -1
  175. package/dist/components/list.d.ts +5 -0
  176. package/dist/components/list.js +13 -9
  177. package/dist/components/list.js.map +1 -1
  178. package/dist/components/measured-grid.d.ts +3 -1
  179. package/dist/components/measured-grid.js +38 -20
  180. package/dist/components/measured-grid.js.map +1 -1
  181. package/dist/components/menu-item.js +2 -1
  182. package/dist/components/menu-item.js.map +1 -1
  183. package/dist/components/mini-calendar.js +8 -4
  184. package/dist/components/mini-calendar.js.map +1 -1
  185. package/dist/components/money-field.d.ts +8 -3
  186. package/dist/components/month-picker.js +9 -5
  187. package/dist/components/month-picker.js.map +1 -1
  188. package/dist/components/nav-pills.js +1 -1
  189. package/dist/components/nav-pills.js.map +1 -1
  190. package/dist/components/number-field.d.ts +8 -3
  191. package/dist/components/number-input.d.ts +11 -4
  192. package/dist/components/number-input.js +8 -6
  193. package/dist/components/number-input.js.map +1 -1
  194. package/dist/components/numpad-sheet.d.ts +8 -3
  195. package/dist/components/numpad-sheet.js +2 -1
  196. package/dist/components/numpad-sheet.js.map +1 -1
  197. package/dist/components/one-time-code-input.js +2 -2
  198. package/dist/components/one-time-code-input.js.map +1 -1
  199. package/dist/components/page-contents.js +2 -2
  200. package/dist/components/page-contents.js.map +1 -1
  201. package/dist/components/phone-input.d.ts +8 -3
  202. package/dist/components/phone-input.js +2 -2
  203. package/dist/components/phone-input.js.map +1 -1
  204. package/dist/components/pie-chart.d.ts +8 -2
  205. package/dist/components/pie-chart.js +11 -7
  206. package/dist/components/pie-chart.js.map +1 -1
  207. package/dist/components/popover.d.ts +9 -1
  208. package/dist/components/popover.js +8 -4
  209. package/dist/components/popover.js.map +1 -1
  210. package/dist/components/row-actions.d.ts +100 -0
  211. package/dist/components/row-actions.js +165 -0
  212. package/dist/components/row-actions.js.map +1 -0
  213. package/dist/components/scroll-area.js +1 -1
  214. package/dist/components/scroll-area.js.map +1 -1
  215. package/dist/components/search-field.js +3 -2
  216. package/dist/components/search-field.js.map +1 -1
  217. package/dist/components/series-chart-budget.d.ts +30 -7
  218. package/dist/components/series-chart-budget.js +12 -4
  219. package/dist/components/series-chart-budget.js.map +1 -1
  220. package/dist/components/series-chart-ticks.d.ts +7 -1
  221. package/dist/components/series-chart-ticks.js +6 -0
  222. package/dist/components/series-chart-ticks.js.map +1 -1
  223. package/dist/components/series-chart.d.ts +58 -28
  224. package/dist/components/series-chart.js +42 -32
  225. package/dist/components/series-chart.js.map +1 -1
  226. package/dist/components/settings-fields.d.ts +51 -6
  227. package/dist/components/settings-fields.js +37 -0
  228. package/dist/components/settings-fields.js.map +1 -1
  229. package/dist/components/share-card.d.ts +11 -6
  230. package/dist/components/share-card.js +2 -0
  231. package/dist/components/share-card.js.map +1 -1
  232. package/dist/components/sign-chip.d.ts +4 -0
  233. package/dist/components/sign-chip.js +17 -4
  234. package/dist/components/sign-chip.js.map +1 -1
  235. package/dist/components/signature-pad.d.ts +3 -1
  236. package/dist/components/signature-pad.js +8 -5
  237. package/dist/components/signature-pad.js.map +1 -1
  238. package/dist/components/slider.js +7 -6
  239. package/dist/components/slider.js.map +1 -1
  240. package/dist/components/sparkline.d.ts +5 -1
  241. package/dist/components/sparkline.js +4 -2
  242. package/dist/components/sparkline.js.map +1 -1
  243. package/dist/components/stat-tile.d.ts +17 -1
  244. package/dist/components/stat-tile.js +48 -20
  245. package/dist/components/stat-tile.js.map +1 -1
  246. package/dist/components/status-dot.d.ts +4 -0
  247. package/dist/components/swipeable-row.js +2 -1
  248. package/dist/components/swipeable-row.js.map +1 -1
  249. package/dist/components/switch.d.ts +21 -7
  250. package/dist/components/switch.js +24 -11
  251. package/dist/components/switch.js.map +1 -1
  252. package/dist/components/table.js +2 -2
  253. package/dist/components/table.js.map +1 -1
  254. package/dist/components/text-link.d.ts +8 -3
  255. package/dist/components/text-link.js +1 -1
  256. package/dist/components/text-link.js.map +1 -1
  257. package/dist/components/text.d.ts +7 -2
  258. package/dist/components/text.js +5 -5
  259. package/dist/components/text.js.map +1 -1
  260. package/dist/components/tile-radio.js +2 -1
  261. package/dist/components/tile-radio.js.map +1 -1
  262. package/dist/components/time-input.d.ts +8 -3
  263. package/dist/components/toast.d.ts +18 -2
  264. package/dist/components/toast.js +8 -5
  265. package/dist/components/toast.js.map +1 -1
  266. package/dist/components/toggle-group.d.ts +16 -1
  267. package/dist/components/toggle-group.js +40 -11
  268. package/dist/components/toggle-group.js.map +1 -1
  269. package/dist/components/toggle-legend.d.ts +10 -1
  270. package/dist/components/toggle-legend.js +21 -7
  271. package/dist/components/toggle-legend.js.map +1 -1
  272. package/dist/components/translation-review-parts.d.ts +4 -0
  273. package/dist/components/translation-review.d.ts +4 -0
  274. package/dist/components/translation-review.js +3 -3
  275. package/dist/components/translation-review.js.map +1 -1
  276. package/dist/components/tree-view.js +3 -2
  277. package/dist/components/tree-view.js.map +1 -1
  278. package/dist/components/treemap.d.ts +5 -2
  279. package/dist/components/treemap.js +33 -16
  280. package/dist/components/treemap.js.map +1 -1
  281. package/dist/components/ui.d.ts +8 -3
  282. package/dist/components/ui.js +202 -114
  283. package/dist/components/ui.js.map +1 -1
  284. package/dist/components/use-table-state.d.ts +1 -1
  285. package/dist/components/user-avatar.d.ts +5 -1
  286. package/dist/components/user-avatar.js +1 -1
  287. package/dist/components/user-avatar.js.map +1 -1
  288. package/dist/{data-table-labels-DUELeXpa.d.ts → data-table-labels-kpAAVi6g.d.ts} +26 -2
  289. package/dist/data-table.d.ts +1 -1
  290. package/dist/{delete-account-setting-DCFS5Vof.d.ts → delete-account-setting-BvdQdbv3.d.ts} +613 -58
  291. package/dist/demo/demo-banner.js +1 -0
  292. package/dist/demo/demo-banner.js.map +1 -1
  293. package/dist/feedback/feedback-attachment.d.ts +8 -3
  294. package/dist/feedback/feedback-context.d.ts +8 -3
  295. package/dist/feedback/feedback-crash.d.ts +8 -3
  296. package/dist/feedback/feedback-dialog.d.ts +8 -3
  297. package/dist/feedback/feedback-dialog.js +3 -3
  298. package/dist/feedback/feedback-dialog.js.map +1 -1
  299. package/dist/feedback/feedback-inbox.d.ts +8 -3
  300. package/dist/feedback/feedback-inbox.js +2 -2
  301. package/dist/feedback/feedback-inbox.js.map +1 -1
  302. package/dist/feedback/feedback-labels.d.ts +8 -3
  303. package/dist/feedback/feedback-menu.d.ts +9 -4
  304. package/dist/feedback/feedback-record.d.ts +8 -3
  305. package/dist/feedback/feedback-row-detail.d.ts +8 -3
  306. package/dist/feedback/feedback-status-undo.d.ts +8 -3
  307. package/dist/feedback/feedback-submit.d.ts +8 -3
  308. package/dist/feedback/feedback-swipe.d.ts +8 -3
  309. package/dist/feedback/feedback-table.d.ts +8 -3
  310. package/dist/feedback/feedback-thread.d.ts +8 -3
  311. package/dist/feedback/feedback-thread.js +3 -3
  312. package/dist/feedback/feedback-thread.js.map +1 -1
  313. package/dist/feedback.d.ts +8 -3
  314. package/dist/hooks/use-anchored-panel.d.ts +32 -3
  315. package/dist/hooks/use-anchored-panel.js +21 -3
  316. package/dist/hooks/use-anchored-panel.js.map +1 -1
  317. package/dist/hooks/use-breakpoint.d.ts +77 -0
  318. package/dist/hooks/use-breakpoint.js +50 -0
  319. package/dist/hooks/use-breakpoint.js.map +1 -0
  320. package/dist/hooks/use-file-drop.d.ts +8 -3
  321. package/dist/hooks/use-large-text.d.ts +29 -0
  322. package/dist/hooks/use-large-text.js +20 -0
  323. package/dist/hooks/use-large-text.js.map +1 -0
  324. package/dist/hooks/use-windowed-rows.d.ts +52 -8
  325. package/dist/hooks/use-windowed-rows.js +94 -5
  326. package/dist/hooks/use-windowed-rows.js.map +1 -1
  327. package/dist/i18n/defaults.d.ts +8 -3
  328. package/dist/i18n/defaults.js +8 -0
  329. package/dist/i18n/defaults.js.map +1 -1
  330. package/dist/i18n/german.d.ts +8 -3
  331. package/dist/i18n/german.js +91 -3
  332. package/dist/i18n/german.js.map +1 -1
  333. package/dist/i18n/kit-labels.d.ts +8 -3
  334. package/dist/i18n/kit-labels.js.map +1 -1
  335. package/dist/i18n/languages.d.ts +10 -5
  336. package/dist/i18n/locales/de-CH.d.ts +8 -3
  337. package/dist/i18n/locales/en.d.ts +8 -3
  338. package/dist/i18n/locales/es.d.ts +8 -3
  339. package/dist/i18n/locales/es.js +86 -3
  340. package/dist/i18n/locales/es.js.map +1 -1
  341. package/dist/i18n/locales/fr.d.ts +8 -3
  342. package/dist/i18n/locales/fr.js +88 -3
  343. package/dist/i18n/locales/fr.js.map +1 -1
  344. package/dist/i18n/locales/hu.d.ts +8 -3
  345. package/dist/i18n/locales/hu.js +88 -3
  346. package/dist/i18n/locales/hu.js.map +1 -1
  347. package/dist/i18n/locales/it.d.ts +8 -3
  348. package/dist/i18n/locales/it.js +89 -3
  349. package/dist/i18n/locales/it.js.map +1 -1
  350. package/dist/i18n/locales/zh.d.ts +8 -3
  351. package/dist/i18n/locales/zh.js +86 -3
  352. package/dist/i18n/locales/zh.js.map +1 -1
  353. package/dist/i18n/review.d.ts +8 -3
  354. package/dist/i18n/review.js +16 -1
  355. package/dist/i18n/review.js.map +1 -1
  356. package/dist/index.d.ts +29 -13
  357. package/dist/index.js +77 -5
  358. package/dist/index.js.map +1 -1
  359. package/dist/landing/landing-actions.d.ts +12 -5
  360. package/dist/landing/landing-actions.js +1 -1
  361. package/dist/landing/landing-actions.js.map +1 -1
  362. package/dist/landing/landing-sections.d.ts +8 -3
  363. package/dist/landing/public-header.d.ts +9 -4
  364. package/dist/landing/public-header.js +3 -3
  365. package/dist/landing/public-header.js.map +1 -1
  366. package/dist/landing/routing.d.ts +27 -9
  367. package/dist/landing/routing.js +5 -2
  368. package/dist/landing/routing.js.map +1 -1
  369. package/dist/lib/cn.js +3 -2
  370. package/dist/lib/cn.js.map +1 -1
  371. package/dist/rhf/fields.d.ts +8 -3
  372. package/dist/rhf/fields.js +1 -1
  373. package/dist/rhf/fields.js.map +1 -1
  374. package/dist/rhf/form.d.ts +8 -3
  375. package/dist/rhf/form.js +2 -2
  376. package/dist/rhf/form.js.map +1 -1
  377. package/dist/rhf.d.ts +8 -3
  378. package/dist/search/command-palette.d.ts +1 -1
  379. package/dist/search/command-palette.js +9 -8
  380. package/dist/search/command-palette.js.map +1 -1
  381. package/dist/settings/settings-labels.d.ts +8 -1
  382. package/dist/settings/settings-labels.js +7 -2
  383. package/dist/settings/settings-labels.js.map +1 -1
  384. package/dist/settings/settings-layout.js +2 -1
  385. package/dist/settings/settings-layout.js.map +1 -1
  386. package/dist/settings/settings-section.d.ts +8 -3
  387. package/dist/settings/use-account-appearance.d.ts +92 -0
  388. package/dist/settings/use-account-appearance.js +63 -0
  389. package/dist/settings/use-account-appearance.js.map +1 -0
  390. package/dist/settings/use-settings-route.d.ts +1 -1
  391. package/dist/settings/use-settings-route.js +2 -3
  392. package/dist/settings/use-settings-route.js.map +1 -1
  393. package/dist/shell/app-shell.d.ts +12 -171
  394. package/dist/shell/app-shell.js +144 -28
  395. package/dist/shell/app-shell.js.map +1 -1
  396. package/dist/shell/auth-layout.js +1 -1
  397. package/dist/shell/auth-layout.js.map +1 -1
  398. package/dist/shell/option-switcher-menu.js +1 -1
  399. package/dist/shell/option-switcher-menu.js.map +1 -1
  400. package/dist/shell/top-bar-brand.d.ts +8 -3
  401. package/dist/shell/top-bar-brand.js +3 -1
  402. package/dist/shell/top-bar-brand.js.map +1 -1
  403. package/dist/shell/topbar-action-menu.d.ts +4 -0
  404. package/dist/shell/topbar-action-menu.js +3 -3
  405. package/dist/shell/topbar-action-menu.js.map +1 -1
  406. package/dist/shell/topbar-controls.js +4 -4
  407. package/dist/shell/topbar-controls.js.map +1 -1
  408. package/dist/shell.d.ts +10 -6
  409. package/dist/shell.js.map +1 -1
  410. package/dist/theme/contrast-tokens.d.ts +41 -0
  411. package/dist/theme/contrast-tokens.js +59 -0
  412. package/dist/theme/contrast-tokens.js.map +1 -0
  413. package/dist/theme/contrast.d.ts +109 -0
  414. package/dist/theme/contrast.js +128 -0
  415. package/dist/theme/contrast.js.map +1 -0
  416. package/dist/theme/palette-presets.d.ts +12 -1
  417. package/dist/theme/palette-presets.js +9 -4
  418. package/dist/theme/palette-presets.js.map +1 -1
  419. package/dist/theme/palette-store.js.map +1 -1
  420. package/dist/theme/text-size.d.ts +157 -0
  421. package/dist/theme/text-size.js +121 -0
  422. package/dist/theme/text-size.js.map +1 -0
  423. package/dist/wizard/stepper-nav.d.ts +8 -3
  424. package/dist/wizard.d.ts +8 -3
  425. package/package.json +1 -1
  426. package/src/admin/admin-parts.ts +9 -2
  427. package/src/admin/invitations-panel.tsx +36 -29
  428. package/src/admin/user-roster.tsx +17 -72
  429. package/src/auth/auth-errors.ts +68 -7
  430. package/src/auth/reset-password-form.tsx +6 -2
  431. package/src/auth/verify-email.tsx +14 -2
  432. package/src/billing/billing-banner.tsx +205 -0
  433. package/src/billing/billing-labels.ts +224 -0
  434. package/src/billing/billing-lock.ts +75 -0
  435. package/src/billing/billing-standing.ts +72 -0
  436. package/src/billing/plan-card.tsx +458 -0
  437. package/src/billing/plan-limit-notice.tsx +145 -0
  438. package/src/billing/plan-limit.ts +53 -0
  439. package/src/billing/plan-price.ts +101 -0
  440. package/src/billing/subscription-actions.tsx +76 -0
  441. package/src/billing/subscription-status-chip.tsx +95 -0
  442. package/src/components/account-chips.tsx +36 -10
  443. package/src/components/alert-banner.tsx +11 -6
  444. package/src/components/amount-input.tsx +5 -5
  445. package/src/components/appearance-labels.ts +40 -0
  446. package/src/components/autocomplete.tsx +2 -2
  447. package/src/components/breadcrumbs.tsx +3 -3
  448. package/src/components/bulk-action-bar.tsx +11 -14
  449. package/src/components/calculator.tsx +4 -3
  450. package/src/components/calendar-heatmap.tsx +5 -5
  451. package/src/components/chart-height.ts +30 -0
  452. package/src/components/chart-zoom.tsx +6 -1
  453. package/src/components/checkbox-group.tsx +2 -2
  454. package/src/components/checkbox.tsx +62 -20
  455. package/src/components/chip.tsx +105 -24
  456. package/src/components/choice-card.tsx +15 -10
  457. package/src/components/column-mapper.tsx +1 -1
  458. package/src/components/combobox-core.tsx +8 -7
  459. package/src/components/combobox.tsx +8 -8
  460. package/src/components/confirm-dialog.tsx +20 -1
  461. package/src/components/country-select.tsx +1 -1
  462. package/src/components/currency-select.tsx +3 -3
  463. package/src/components/data-table-filter-popover.tsx +7 -1
  464. package/src/components/data-table.tsx +160 -43
  465. package/src/components/date-picker.tsx +12 -9
  466. package/src/components/description-list.tsx +2 -2
  467. package/src/components/dialog-frame.tsx +2 -1
  468. package/src/components/disclosure.tsx +3 -2
  469. package/src/components/dropdown.tsx +10 -2
  470. package/src/components/facing-pair.ts +12 -5
  471. package/src/components/field-parts.tsx +190 -7
  472. package/src/components/field-sync.tsx +17 -3
  473. package/src/components/field.tsx +2 -2
  474. package/src/components/floating-panel.tsx +7 -4
  475. package/src/components/focus-ring.ts +12 -0
  476. package/src/components/form-actions.tsx +55 -18
  477. package/src/components/full-bleed-dialog.tsx +18 -4
  478. package/src/components/image-grid.tsx +1 -1
  479. package/src/components/inline-edit-field.tsx +6 -3
  480. package/src/components/legal-page.tsx +28 -2
  481. package/src/components/legal.tsx +19 -1
  482. package/src/components/lightbox.tsx +24 -2
  483. package/src/components/line-items.tsx +7 -1
  484. package/src/components/list.tsx +36 -10
  485. package/src/components/measured-grid.tsx +49 -24
  486. package/src/components/menu-item.tsx +2 -2
  487. package/src/components/mini-calendar.tsx +8 -4
  488. package/src/components/month-picker.tsx +9 -5
  489. package/src/components/nav-pills.tsx +1 -1
  490. package/src/components/number-input.tsx +14 -7
  491. package/src/components/numpad-sheet.tsx +2 -2
  492. package/src/components/one-time-code-input.tsx +2 -2
  493. package/src/components/page-contents.tsx +2 -2
  494. package/src/components/phone-input.tsx +2 -2
  495. package/src/components/pie-chart.tsx +19 -9
  496. package/src/components/popover.tsx +22 -5
  497. package/src/components/row-actions.tsx +344 -0
  498. package/src/components/scroll-area.tsx +1 -1
  499. package/src/components/search-field.tsx +4 -2
  500. package/src/components/series-chart-budget.ts +40 -8
  501. package/src/components/series-chart-ticks.ts +10 -0
  502. package/src/components/series-chart.tsx +123 -72
  503. package/src/components/settings-fields.tsx +81 -2
  504. package/src/components/share-card.tsx +10 -0
  505. package/src/components/sign-chip.tsx +28 -4
  506. package/src/components/signature-pad.tsx +13 -7
  507. package/src/components/slider.tsx +22 -9
  508. package/src/components/sparkline.tsx +11 -3
  509. package/src/components/stat-tile.tsx +70 -14
  510. package/src/components/swipeable-row.tsx +2 -1
  511. package/src/components/switch.tsx +57 -23
  512. package/src/components/table.tsx +2 -2
  513. package/src/components/text-link.tsx +1 -1
  514. package/src/components/text.tsx +11 -6
  515. package/src/components/tile-radio.tsx +4 -1
  516. package/src/components/toast.tsx +27 -7
  517. package/src/components/toggle-group.tsx +99 -12
  518. package/src/components/toggle-legend.tsx +53 -8
  519. package/src/components/translation-review.tsx +10 -5
  520. package/src/components/tree-view.tsx +3 -2
  521. package/src/components/treemap.tsx +27 -10
  522. package/src/components/ui.tsx +331 -145
  523. package/src/components/user-avatar.tsx +1 -1
  524. package/src/demo/demo-banner.tsx +5 -0
  525. package/src/feedback/feedback-dialog.tsx +3 -3
  526. package/src/feedback/feedback-inbox.tsx +2 -2
  527. package/src/feedback/feedback-thread.tsx +3 -3
  528. package/src/hooks/use-anchored-panel.ts +67 -5
  529. package/src/hooks/use-breakpoint.ts +121 -0
  530. package/src/hooks/use-large-text.ts +41 -0
  531. package/src/hooks/use-windowed-rows.ts +168 -12
  532. package/src/i18n/defaults.ts +8 -0
  533. package/src/i18n/german.ts +105 -1
  534. package/src/i18n/kit-labels.tsx +12 -0
  535. package/src/i18n/locales/es.ts +96 -1
  536. package/src/i18n/locales/fr.ts +100 -1
  537. package/src/i18n/locales/hu.ts +98 -1
  538. package/src/i18n/locales/it.ts +101 -1
  539. package/src/i18n/locales/zh.ts +92 -1
  540. package/src/i18n/review.ts +15 -0
  541. package/src/index.ts +82 -8
  542. package/src/landing/landing-actions.tsx +9 -5
  543. package/src/landing/public-header.tsx +4 -4
  544. package/src/landing/routing.tsx +22 -5
  545. package/src/lib/cn.ts +13 -2
  546. package/src/rhf/fields.tsx +1 -1
  547. package/src/rhf/form.tsx +2 -2
  548. package/src/search/command-palette.tsx +10 -9
  549. package/src/settings/settings-labels.ts +13 -1
  550. package/src/settings/settings-layout.tsx +2 -1
  551. package/src/settings/use-account-appearance.ts +148 -0
  552. package/src/settings/use-settings-route.ts +3 -4
  553. package/src/shell/app-shell.tsx +278 -54
  554. package/src/shell/auth-layout.tsx +1 -1
  555. package/src/shell/option-switcher-menu.tsx +1 -1
  556. package/src/shell/top-bar-brand.tsx +3 -1
  557. package/src/shell/topbar-action-menu.tsx +3 -3
  558. package/src/shell/topbar-controls.tsx +4 -4
  559. package/src/shell.ts +7 -1
  560. package/src/theme/contrast-tokens.ts +112 -0
  561. package/src/theme/contrast.ts +224 -0
  562. package/src/theme/palette-presets.ts +24 -4
  563. package/src/theme/palette-store.ts +6 -1
  564. package/src/theme/text-size.ts +267 -0
  565. package/tokens.css +143 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/translation-review.tsx"],"sourcesContent":["import { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Check, ListChecks, PencilLine, RotateCcw } from \"lucide-react\";\n\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport {\n DEFAULT_TRANSLATION_REVIEW_FILTER,\n DEFAULT_TRANSLATION_REVIEW_SWIPE,\n REVIEW_STATUSES,\n filterTranslationRows,\n groupTranslationRows,\n keyInAreas,\n reviewUndo,\n reviewWrite,\n summariseRows,\n translationReviewSwipePlan,\n unreviewedRows,\n} from \"../lib/translation-review\";\nimport type {\n ReviewStatus,\n TranslationReviewFilter,\n TranslationReviewKey,\n TranslationReviewSwipeAction,\n TranslationReviewSwipeBinding,\n TranslationReviewWrite,\n TranslationRow,\n TranslationRowGroup,\n} from \"../lib/translation-review\";\nimport { AlertBanner } from \"./alert-banner\";\nimport { BulkActionBar } from \"./bulk-action-bar\";\nimport { Checkbox } from \"./checkbox\";\nimport { Chip } from \"./chip\";\nimport { DataTable } from \"./data-table\";\nimport type { DataTableColumn, DataTableProps, MobileSwipeActions, SortState } from \"./data-table\";\nimport { Disclosure } from \"./disclosure\";\nimport { SearchField } from \"./search-field\";\nimport { toast } from \"./toast\";\nimport type { SwipeAction } from \"./swipeable-row\";\nimport { ToggleGroup } from \"./toggle-group\";\nimport { TranslationReviewEditor } from \"./translation-review-editor\";\nimport { DEFAULT_TRANSLATION_REVIEW_LABELS, reviewStatusLabel } from \"./translation-review-labels\";\nimport type { TranslationReviewLabels } from \"./translation-review-labels\";\nimport { ReviewStatusChip, TranslationProgress } from \"./translation-review-parts\";\nimport { Button, IconButton, Select } from \"./ui\";\nimport { useWriteLock } from \"./write-lock\";\n\nexport {\n REVIEW_STATUS_TONES,\n ReviewStatusChip,\n TranslationExportButton,\n TranslationLocaleTabs,\n TranslationProgress,\n} from \"./translation-review-parts\";\nexport type {\n ReviewStatusChipProps,\n TranslationExportButtonProps,\n TranslationLocaleTab,\n TranslationLocaleTabsProps,\n TranslationProgressProps,\n} from \"./translation-review-parts\";\nexport { TranslationReviewEditor } from \"./translation-review-editor\";\nexport type { TranslationReviewEditorProps } from \"./translation-review-editor\";\nexport { DEFAULT_TRANSLATION_REVIEW_LABELS } from \"./translation-review-labels\";\nexport type { TranslationReviewLabels } from \"./translation-review-labels\";\n\n/**\n * An app's translation review page, as kit parts (H1, docs/i18n-harmonization.md):\n * keksdose's /translations, which kastlan was about to build a second time. The app keeps\n * what is its own — where the strings come from, which locale is read against which, who\n * may review what, every request — and hands the kit rows (`translationRows`) and two\n * callbacks. Everything a reviewer touches is here.\n *\n * Presentational — no request, no route — and one LOCALE at a time. Its table is the\n * kit's DataTable, so like every DataTable it renders under the app's Router. The page\n * around it puts\n * {@link TranslationLocaleTabs} above it and {@link TranslationExportButton} in its header,\n * because both span every locale while this panel is the one open.\n *\n * Callbacks may return promises, and the panel waits for them: an editor stays open with\n * its text and the error when a save is refused (a 403 for a key outside the reviewer's\n * areas, a network failure), a bulk action keeps its selection. Chunking a long bulk\n * write to the server's batch limit is the app's; the panel sends one call per action.\n */\n\ntype MaybePromise = void | Promise<unknown>;\n\n/** What asked for a write — see {@link TranslationReviewSaveInfo}. */\nexport type TranslationReviewOrigin = \"editor\" | \"row\" | \"swipe\" | \"group\" | \"selection\" | \"undo\";\n\n/**\n * The second argument of `onSave` and `onClear` (0.25): where the write came from, and\n * whether the panel answers it with a toast of its own.\n *\n * WHY THE APP IS TOLD. keksdose and kastlan toast \"Saved\" from their mutation on every\n * save. With {@link TranslationReviewPanelProps.undo} on, an approval is answered by the\n * kit's Undo toast instead — and a reviewer swiping through a list then read two toasts\n * per card. The app skips its own where `toasted` is true:\n *\n * onSave={(writes, { toasted }) => save.mutateAsync({ items: writes.map(toApiWrite), quiet: toasted })}\n *\n * An app that ignores the argument keeps working; it only says \"Saved\" twice.\n */\nexport interface TranslationReviewSaveInfo {\n /** The open editor, a row's approve button, a swipe on a phone card, a group's\n * \"Approve unreviewed\", the selection's bulk bar, or an Undo pressed on the toast. */\n origin: TranslationReviewOrigin;\n /** The panel shows its own toast for this write (an approval's Undo toast, or a\n * swiped `clear`'s). */\n toasted: boolean;\n}\n\n/** How {@link TranslationReviewPanelProps.groupBy} names a row's group. */\nexport type TranslationReviewGroupBy = \"namespace\" | \"source\" | ((row: TranslationRow) => string);\n\nexport interface TranslationReviewPanelProps {\n /** One locale's rows, from `translationRows`. */\n rows: readonly TranslationRow[];\n /** The reviewed language's own name — the text column's header (\"Français\"). */\n localeLabel: string;\n /** The reference language's own name — the reference column's header. */\n referenceLabel: string;\n /** Store verdicts: one for an editor or a row's approve button, many for a bulk\n * approve. Left out (or `readOnly`): nothing can be changed. The second argument\n * says where the write came from ({@link TranslationReviewSaveInfo}). */\n onSave?: (writes: TranslationReviewWrite[], info: TranslationReviewSaveInfo) => MaybePromise;\n /** Clear verdicts — back to unreviewed. Left out: no reset, and an approval of a row\n * that had no verdict cannot be undone. */\n onClear?: (keys: TranslationReviewKey[], info: TranslationReviewSaveInfo) => MaybePromise;\n /** A locale the viewer may read but not review: the rows and their verdicts, no\n * actions, and a line saying so. */\n readOnly?: boolean;\n /**\n * The areas the reviewer is limited to (keksdose's lawyer: `[\"legal\"]`), as the server\n * answers them; `null` is every area. Rows outside are not shown or counted — build the\n * rows with the same `areas` and they never exist — and a notice names the areas.\n */\n areas?: readonly string[] | null;\n /** An area's name for the notice (\"the legal pages (Imprint, …)\"). Default: the key. */\n areaLabels?: Readonly<Record<string, string>>;\n /** Default `keyInArea`: the key is the area or sits under it (`legal.…`, `legal:…`,\n * and the kit's `kit.legal.…` since 0.28). */\n inArea?: (key: string, area: string) => boolean;\n /** A source's name for the source filter, which appears once the rows come from more\n * than one (kastlan: `{ screen: \"Screen\", documents: \"PDFs and emails\" }`). Default:\n * the source itself. */\n sourceLabels?: Readonly<Record<string, string>>;\n /**\n * Controlled filters — for an app that keeps them in the URL (keksdose's `?status=`,\n * `?ns=`, `?q=`). Whatever is left out is the panel's own. Pair with `onFilterChange`.\n *\n * A field passed here is the app's, and the control shows what the app says: one the app\n * passes but does not write back from `onFilterChange` is a control that does nothing.\n * A development build warns about that once per field (0.25.1, Kurvenschmiede); a field\n * left out entirely is fine — the panel keeps it.\n */\n filter?: Partial<TranslationReviewFilter>;\n /** Every filter, on every change — write them in ONE update: `useSearchParamsState`'s\n * setter takes this object as it is (`filter={filter} onFilterChange={setFilter}`).\n * Single `useSearchParamState` setters compose since 0.25; before, four in a row\n * clobbered each other (keksdose live #378). */\n onFilterChange?: (filter: TranslationReviewFilter) => void;\n /** The progress bar over the filters. Default true. */\n progress?: boolean;\n /** Rows per page. Default 50. */\n pageSize?: number;\n /** Persists the table's sort and page size (DataTable's `storageKey`). */\n storageKey?: string;\n /** How a verdict's date reads in the editor. */\n formatDate?: (iso: string) => string;\n /** Turns a callback's rejection into words. Default {@link TranslationReviewLabels.failed}. */\n formatError?: (error: unknown) => ReactNode;\n /**\n * Swipe the phone cards (0.25, keksdose live #377 — Marcel reviewing on a phone): toward\n * the reading END to approve, toward the START for \"Needs a change\", which OPENS the\n * editor with the cursor in the wording rather than sending the string back blind — a\n * verdict without a note or a better wording tells whoever fixes it nothing. In a\n * right-to-left page the two sides mirror with the reading direction.\n *\n * DataTable's swipe sides (`mobileSwipeActions`) on {@link SwipeableRow}: the same\n * gesture, the same feedback, and the same keyboard path — every swipe is also a real\n * button, read out and reachable by Tab, so the gesture is a shortcut and never the only\n * way. A row has no swipe while the editor under it is open, while its own write is out,\n * under a {@link WriteLockProvider} lock, in a `readOnly` locale, and toward \"Approve\"\n * where there is nothing to approve (a missing string, an approved one). A reviewer\n * limited to `areas` never sees a row outside them, so never swipes one.\n *\n * Off by default, so no existing page gains a gesture on the bump. Phones only; the\n * desktop table keeps its approve button.\n *\n * A BINDING instead of `true` (keksdose — Marcel's live #377 rework, 2026-10-03: *\"Add\n * the swipe options to be shown also to the settings /settings#interaction area where\n * the other swipe options are defined\"*): keksdose lets the user bind every list's\n * swipes in Settings → Interaction, per side a primary action at the first threshold and\n * a secondary one at a longer drag, and a fixed mapping is one its settings cannot\n * reach. So each logical side takes an ordered ladder of\n * {@link TranslationReviewSwipeAction}s — index 0 at the first threshold, index 1 at the\n * longer drag, as {@link SwipeableRow} stages them:\n *\n * swipe={{ end: [\"approve\", \"clear\"], start: [\"edit\"] }}\n *\n * `approve` (with Undo), `edit` (the editor, cursor in the wording — \"Needs a change\"),\n * `clear` (the verdict taken back, with Undo, which stores it again; only with\n * `onClear`). An action a row cannot offer drops out and the ladder closes up behind it\n * ({@link translationReviewSwipePlan}): `approve` on an approved or missing string,\n * `clear` on a row with no verdict. Every rule above still holds for every binding —\n * no side swipes in a `readOnly` locale, under a write lock, with the editor open or\n * the row's write out.\n *\n * `true` is {@link DEFAULT_TRANSLATION_REVIEW_SWIPE} — `{ end: [\"approve\"], start:\n * [\"edit\"] }`, 0.25's mapping exactly — and `false` or nothing is no swipe. A binding\n * turns the Undo toast on by default, as `true` does.\n */\n swipe?: boolean | TranslationReviewSwipeBinding;\n /**\n * Answer every approval with the kit's Undo toast (`toast.undo`): a row's button, a\n * swipe, the editor's Approve, a group's and the selection's bulk approve. Undo stores\n * again what the rows said before ({@link reviewUndo}) — or clears them, for rows that\n * had no verdict, which needs `onClear` (without it the toast reports and offers\n * nothing). A batch has ONE Undo.\n *\n * Default: on with `swipe` or `groupBy` — the two ways to approve in one movement or many\n * rows at once bring their way back — and off otherwise, so an existing page does not\n * gain a toast on the bump. Needs the app's `<Toaster>`. Tell the app's own \"Saved\"\n * toast apart with the `toasted` flag ({@link TranslationReviewSaveInfo}).\n */\n undo?: boolean;\n /**\n * Group the strings under a header per area (0.25, keksdose live #377: *\"grouping to the\n * translation review entries for faster reviewing\"*): `\"namespace\"` — the row's\n * `namespace`, which `translationRows` derives from the key (`budget`, `kit.dataTable`\n * with keksdose's `namespaceOf`) — `\"source\"`, or a function of the row.\n *\n * Each group is a section with a heading ({@link groupHeadingAs}): its name, \"n\n * unreviewed / total\", and \"Approve unreviewed (n)\", which approves the group's rows\n * nobody has given a verdict ({@link unreviewedRows}) under the same rules as one\n * approval — write lock, `readOnly`, one write at a time — with one Undo for the batch.\n * A batch larger than `pageSize` asks first, inside the opened group: more than a page\n * is more than the reviewer can have had on screen.\n *\n * WHAT IT MEANS FOR THE FILTERS, THE SORT AND THE PAGES. The filters narrow the rows\n * first, then the rows are grouped: a header counts what the filters show in it (so\n * \"Approve unreviewed\" never takes a row the reviewer filtered away), and an area with\n * nothing left is not shown. Groups stand in the order their first string has in the\n * reference; a column sort orders the rows INSIDE every group alike. Each group pages\n * on its own, `pageSize` rows at a time — a header then always stands over its own\n * rows, and paging one area does not move another — rather than one pager cutting\n * through the headers. `storageKey` keeps persisting the ungrouped table only.\n *\n * Groups fold (each on its own, remembered while the panel lives), and a folded group's\n * rows are not rendered at all — only its header. Which groups start open is\n * {@link defaultGroupsOpen}: by default, on a phone they start folded when there are\n * several — the headers are then the area list, and the reviewer opens the one to work\n * through — and on a wide screen they start open while that is cheap (see there).\n */\n groupBy?: TranslationReviewGroupBy;\n /**\n * Which groups start open under {@link groupBy} (0.25.1). A group the reviewer opens or\n * folds keeps that; this is what every other group follows.\n *\n * - `\"auto\"` (default): on a phone, open when there is one group and folded when there\n * are several, as in 0.25.0. On a wide screen, open while opening every group renders\n * at most 100 rows (each group shows up to `pageSize` of them; the budget is one page\n * where `pageSize` is larger) — and folded above that, the headers then being the area\n * list, as on a phone.\n * - `true`: every group starts open, on a phone too.\n * - `false`: every group starts folded, on a wide screen too.\n *\n * WHY A BUDGET OF ROWS (keksdose, measured on 0.25.0): a wide screen opened EVERY group.\n * keksdose's /translations has 122 areas and ~4700 strings — 122 tables and 2053 rows at\n * once, ~46 000 elements, 5.2 s before the panel appeared and 2.5 s for a filter click\n * to paint, against 50 rows and 0.34 s ungrouped; its page test went from 4 s to 146 s.\n * kastlan (~31 areas, ~1300 rows open) and Kurvenschmiede (28, ~700) had the same page.\n * What costs is the rows on screen, so the rule counts them rather than the groups:\n * 100 rows is two of the panel's default pages — any one or two areas still open as\n * before, however long, and so do a dozen small ones, or a search whose hits fall in\n * many areas — while a whole catalogue starts as its list of areas: keksdose's shape\n * in jsdom, 122 headers and ~2100 elements instead of ~44 000 (the ungrouped panel has\n * ~1200). The rule follows the filters: narrow them and the groups open.\n *\n * A boolean rather than a controlled open state: the panel already keeps each group's\n * own state, and an app that only wants \"all open\" or \"all folded\" should not have to\n * keep a record of 122 flags to say it.\n */\n defaultGroupsOpen?: boolean | \"auto\";\n /** A group's name. Default: `sourceLabels` for `groupBy=\"source\"`, else the key. */\n groupLabel?: (group: string) => string;\n /** The level of the group headings. Default `h2`: the panel is a page's body, under\n * the page's `h1`. */\n groupHeadingAs?: \"h2\" | \"h3\" | \"h4\";\n className?: string;\n labels?: Partial<TranslationReviewLabels>;\n}\n\n/** The wide screen's query — the same one DataTable switches its layout on, so \"a phone\"\n * means the same here as in the table. */\nconst MD_UP = \"(min-width: 768px)\";\n\n/**\n * How many rows the groups may render at once on a wide screen before they start folded\n * ({@link TranslationReviewPanelProps.defaultGroupsOpen}): two of the default 50-row pages.\n * A row costs about twenty elements in either layout, so this is the cost of two pages of\n * the ungrouped table — not of 2053 rows in 122 tables.\n */\nconst GROUPS_OPEN_ROW_BUDGET = 100;\n\n/**\n * Would opening every group stay within the budget? Each group renders up to `pageSize`\n * rows (it pages on its own); the budget is never less than one page, so a single group —\n * which renders at most one — always starts open, as on a phone.\n */\nfunction groupsFitOpen(groups: readonly TranslationRowGroup[], pageSize: number): boolean {\n const budget = Math.max(GROUPS_OPEN_ROW_BUDGET, pageSize);\n let rendered = 0;\n for (const group of groups) {\n rendered += Math.min(group.rows.length, pageSize);\n if (rendered > budget) return false;\n }\n return true;\n}\n\n/**\n * The kit's dev switch, as NumberInput's money guard and `lib/logger.ts` read it: a\n * development build — the consumer's bundler makes `import.meta.env.DEV` false in a\n * production one, and the branch is dead code there — and never under vitest, where every\n * consumer suite would print it; a test opts in with `vi.stubEnv(\"VITEST\", \"\")`. Read per\n * call, and optional-chained: `import.meta.env` is a Vite injection, absent under Node.\n */\nfunction devWarnings(): boolean {\n return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;\n}\n\ntype FilterField = keyof TranslationReviewFilter;\n\n/** How long after the last filter change the app's `filter` is looked at again: well past\n * a router's transition, so a URL that is still being written is not mistaken for one\n * that never will be. */\nconst FILTER_ECHO_MS = 1500;\n\n/**\n * The development warning for a controlled filter that does not follow (Kurvenschmiede,\n * 0.25.1). A field the app passes in `filter` is the app's: the panel shows the app's\n * value, whatever the reviewer picked. An app that passes a field but does not write the\n * reported value back — its `onFilterChange` hands on only some fields, `filter` is built\n * as `{ ...DEFAULT_TRANSLATION_REVIEW_FILTER, ...fromTheUrl }`, a URL parser rejects what\n * its serialiser wrote, or there is no `onFilterChange` at all — has a control that\n * silently does nothing: the box unticks itself, the select jumps back.\n *\n * So, after the panel reports a change of a field the app controls, it looks at the\n * app's `filter` again once the change has had time to land ({@link FILTER_ECHO_MS}, read\n * from the last COMMITTED props, so a router transition still rendering does not count),\n * and warns — once per field — when that field still holds the value it had before.\n *\n * Quiet by construction for an app that passes no `filter`, and for a field the app LEAVES\n * OUT: that field is the panel's own and works (keksdose keeps the placeholder switch so,\n * on purpose). Quiet too when the app moved the field somewhere else itself. Development\n * builds only (see {@link devWarnings}): in production and under vitest nothing is\n * recorded and no timer is set.\n */\nfunction useFilterEchoCheck(filterProp: Partial<TranslationReviewFilter> | undefined) {\n const committed = useRef(filterProp);\n useEffect(() => {\n committed.current = filterProp;\n });\n // Per field: what the app's filter said when the reviewer first changed it, and what\n // the panel reported last.\n const pending = useRef(new Map<FilterField, { from: unknown; to: unknown }>());\n const warned = useRef(new Set<FilterField>());\n const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n useEffect(() => () => clearTimeout(timer.current), []);\n\n return (next: TranslationReviewFilter) => {\n if (filterProp === undefined || !devWarnings()) return;\n for (const field of Object.keys(next) as FilterField[]) {\n const from = filterProp[field];\n if (from === undefined || warned.current.has(field)) continue;\n const seen = pending.current.get(field);\n if (seen) seen.to = next[field];\n else if (next[field] !== from) pending.current.set(field, { from, to: next[field] });\n }\n if (pending.current.size === 0) return;\n clearTimeout(timer.current);\n timer.current = setTimeout(() => {\n const now = committed.current;\n for (const [field, { from, to }] of pending.current) {\n if (to === from || now?.[field] !== from || warned.current.has(field)) continue;\n warned.current.add(field);\n console.warn(\n `[ui-kit] TranslationReviewPanel: the \"${field}\" filter was changed to ${JSON.stringify(to)}, but the ` +\n `\\`filter\\` prop still says ${JSON.stringify(from)}, so the control does nothing. A field passed in ` +\n `\\`filter\\` is the app's: write back every field \\`onFilterChange\\` reports, in one update ` +\n `(useSearchParamsState's setter takes the object as it is), or leave the field out of \\`filter\\` and ` +\n `the panel keeps it. (Development builds only; once per field.)`,\n );\n }\n pending.current.clear();\n }, FILTER_ECHO_MS);\n };\n}\n\n/** The row's text — or, for a missing string, the line that says so. */\nfunction TextCell({ row, labels, clamp }: { row: TranslationRow; labels: TranslationReviewLabels; clamp?: boolean }) {\n return row.text === \"\" ? (\n <span className=\"italic text-[var(--text-muted)]\">{labels.missingText}</span>\n ) : (\n <span lang={row.locale} className={cn(\"whitespace-pre-line break-words\", clamp && \"line-clamp-3\")}>\n {row.text}\n </span>\n );\n}\n\n/**\n * The page body for one locale: progress, filters (status, source, namespace, search,\n * placeholder problems), the strings with an editor that unfolds under each, and the\n * bulk actions over a selection.\n *\n * The status filter is the work queue in order — what is not there, what nobody has\n * read, what changed after it was read, what was sent back, what is done — with each\n * count on it. \"Missing\" is offered only where something is.\n *\n * Bulk approve takes the selected rows that are not approved yet and have a text:\n * approving an absent string says nothing, and re-approving one says it again. On a\n * phone the table has no checkboxes, so \"Select all shown\" selects what the filters\n * show — filter to `Unreviewed` in one area, read, select, approve. With `groupBy`, a\n * group's own \"Approve unreviewed\" is the shorter way, and with `swipe` a card is approved\n * by a swipe.\n *\n * The panel's writes go out ONE AT A TIME, in the order they were asked for: two writes on\n * one locale's verdicts can answer out of order, and an Undo must not overtake the\n * approval it takes back. A swipe on the next card while one is out is queued rather than\n * refused — a reviewer swiping down a list is faster than the server.\n */\nexport function TranslationReviewPanel({\n rows: rowsProp,\n localeLabel,\n referenceLabel,\n onSave,\n onClear,\n readOnly = false,\n areas,\n areaLabels,\n inArea,\n sourceLabels,\n filter: filterProp,\n onFilterChange,\n progress = true,\n pageSize = 50,\n storageKey,\n formatDate,\n formatError,\n swipe = false,\n undo: undoProp,\n groupBy,\n groupLabel,\n groupHeadingAs = \"h2\",\n defaultGroupsOpen = \"auto\",\n className,\n labels: labelsProp,\n}: TranslationReviewPanelProps) {\n const labels = useKitLabels(\"translationReview\", DEFAULT_TRANSLATION_REVIEW_LABELS, labelsProp);\n const locale = useKitLocale();\n const lock = useWriteLock();\n const phone = !useMediaQuery(MD_UP, true);\n const baseId = useId();\n const rootRef = useRef<HTMLDivElement>(null);\n const editable = !readOnly && onSave !== undefined;\n const binding: TranslationReviewSwipeBinding | null =\n swipe === true ? DEFAULT_TRANSLATION_REVIEW_SWIPE : swipe || null;\n const undoOn = undoProp ?? (binding !== null || groupBy !== undefined);\n\n const [ownFilter, setOwnFilter] = useState<TranslationReviewFilter>(DEFAULT_TRANSLATION_REVIEW_FILTER);\n // The app's fields over the panel's own; a field the app leaves `undefined` is the\n // panel's, not a blank.\n const filter: TranslationReviewFilter = { ...ownFilter };\n for (const [name, value] of Object.entries(filterProp ?? {})) {\n if (value !== undefined) Object.assign(filter, { [name]: value });\n }\n const echoFilter = useFilterEchoCheck(filterProp);\n const [openId, setOpenId] = useState<string | null>(null);\n // The row whose editor a swipe opened, to be written in: its wording field takes focus.\n const [focusId, setFocusId] = useState<string | null>(null);\n const [selected, setSelected] = useState<ReadonlySet<string>>(() => new Set());\n // The writes asked for and not yet answered, in order: the first is the one out. The\n // editor keeps its own, for its own buttons.\n const [queued, setQueued] = useState<readonly string[]>([]);\n const claimed = useRef(new Set<string>());\n const tail = useRef<Promise<unknown>>(Promise.resolve());\n const undoCount = useRef(0);\n const [failure, setFailure] = useState<ReactNode>(null);\n const busy = queued.length > 0;\n const isQueued = (key: string) => queued.includes(key);\n\n const rows = useMemo(\n () => (areas ? rowsProp.filter((row) => keyInAreas(row.key, areas, inArea)) : [...rowsProp]),\n [rowsProp, areas, inArea],\n );\n const summary = useMemo(() => summariseRows(rows), [rows]);\n const sources = useMemo(() => [...new Set(rows.map((r) => r.source))], [rows]);\n const source = sources.includes(filter.source) ? filter.source : \"all\";\n const namespaces = useMemo(\n () => [...new Set(rows.filter((r) => source === \"all\" || r.source === source).map((r) => r.namespace))],\n [rows, source],\n );\n const namespace = namespaces.includes(filter.namespace) ? filter.namespace : \"all\";\n const mismatches = useMemo(() => rows.filter((r) => r.placeholderMismatch).length, [rows]);\n // `filter` is rebuilt every render; its fields are what the list depends on.\n const { status, query, placeholdersOnly } = filter;\n const visible = useMemo(\n () => filterTranslationRows(rows, { status, source, namespace, query, placeholdersOnly }),\n [rows, status, source, namespace, query, placeholdersOnly],\n );\n\n // ── Groups ──\n const groups = useMemo(() => {\n if (groupBy === undefined) return null;\n const of =\n groupBy === \"namespace\"\n ? (r: TranslationRow) => r.namespace\n : groupBy === \"source\"\n ? (r: TranslationRow) => r.source\n : groupBy;\n return groupTranslationRows(visible, of);\n }, [visible, groupBy]);\n const groupName = (key: string) =>\n groupLabel?.(key) ?? (groupBy === \"source\" ? sourceLabels?.[key] : undefined) ?? (key || \"—\");\n // Only what the reviewer chose; a group nobody has touched follows the default, which\n // follows the filters (see `defaultGroupsOpen`).\n const [groupOpen, setGroupOpen] = useState<Readonly<Record<string, boolean>>>({});\n const groupOpenByDefault =\n defaultGroupsOpen !== \"auto\"\n ? defaultGroupsOpen\n : groups !== null && (phone ? groups.length <= 1 : groupsFitOpen(groups, pageSize));\n const isGroupOpen = (key: string) => groupOpen[key] ?? groupOpenByDefault;\n const setGroupOpenFor = (key: string, open: boolean) => setGroupOpen((prev) => ({ ...prev, [key]: open }));\n /** A group the reviewer works in — an editor opened in it — stays open when the default\n * turns (a search cleared, the rows grown past the budget): folding it would drop the\n * editor and whatever was typed in it. */\n const keepGroupOpen = (key: string | undefined) => {\n if (key !== undefined) setGroupOpen((prev) => (key in prev ? prev : { ...prev, [key]: true }));\n };\n // The group whose large batch is waiting for a yes — and, until its confirm button\n // has mounted and taken focus, the group that asked.\n const [confirming, setConfirming] = useState<string | null>(null);\n const confirmFocus = useRef<string | null>(null);\n // One sort for every group's table, so a header click orders all of them alike.\n const [groupSorts, setGroupSorts] = useState<SortState[]>([]);\n\n const setFilter = (patch: Partial<TranslationReviewFilter>) => {\n const next = { ...filter, ...patch };\n setOwnFilter(next);\n onFilterChange?.(next);\n echoFilter(next);\n // A selection the new filter hides would be acted on unseen.\n setSelected(new Set());\n };\n\n /** Queue a write under `key` (one per key at a time); resolves whether it worked. */\n const run = (key: string, action: () => MaybePromise): Promise<boolean> => {\n if (claimed.current.has(key)) return Promise.resolve(false);\n claimed.current.add(key);\n setQueued((q) => [...q, key]);\n const step = tail.current.then(async () => {\n setFailure(null);\n try {\n await action();\n return true;\n } catch (caught) {\n setFailure(formatError ? formatError(caught) : labels.failed);\n return false;\n } finally {\n claimed.current.delete(key);\n setQueued((q) => q.filter((k) => k !== key));\n }\n });\n tail.current = step;\n return step;\n };\n\n /** \"String approved — Undo\", once `targets` (as they were BEFORE the write) are\n * approved — or, after a swiped `clear`, \"String marked unreviewed — Undo\". Either way\n * Undo puts back what the rows said before ({@link reviewUndo}), through the same queue\n * as every write. */\n const sayDone = (targets: readonly TranslationRow[], description?: ReactNode, cleared = false) => {\n const back = reviewUndo(targets);\n const message = (cleared ? labels.clearedToast : labels.approvedToast)(targets.length);\n const save = onSave;\n const clear = onClear;\n // Taking back the approval of a row that had no verdict is a clear: without\n // `onClear` there is no way to say it, so the toast reports and offers nothing.\n if (!save || (back.clears.length > 0 && !clear)) {\n toast.success(message, { description });\n return;\n }\n const key = `undo:${++undoCount.current}`;\n toast.undo(message, {\n description,\n onUndo: () =>\n void run(key, async () => {\n if (back.writes.length > 0) await save(back.writes, { origin: \"undo\", toasted: false });\n if (back.clears.length > 0) await clear?.(back.clears, { origin: \"undo\", toasted: false });\n }),\n });\n };\n\n const approve = async (\n key: string,\n targets: readonly TranslationRow[],\n origin: TranslationReviewOrigin,\n description?: ReactNode,\n ): Promise<boolean> => {\n if (!onSave || targets.length === 0) return false;\n const save = onSave;\n const ok = await run(key, () => save(targets.map((r) => reviewWrite(r, \"APPROVED\")), { origin, toasted: undoOn }));\n if (ok && undoOn) sayDone(targets, description);\n return ok;\n };\n\n /** A swipe bound to `clear`: the row's verdict taken back, answered like an approval —\n * the Undo toast stores the verdict again. Under the row's own key, so it queues behind\n * (and never overlaps) an approval of the same row. */\n const clearRow = async (row: TranslationRow) => {\n if (!onClear || row.review === null) return;\n const clear = onClear;\n const keys = [{ locale: row.locale, key: row.key }];\n const ok = await run(`row:${row.id}`, () => clear(keys, { origin: \"swipe\", toasted: undoOn }));\n if (ok && undoOn) sayDone([row], row.key, true);\n };\n\n /** The editor's own save: it waits, closes and shows its errors itself, so it does\n * not go through the queue — but an approval there is answered like any other. */\n const saveFromEditor = async (row: TranslationRow, write: TranslationReviewWrite) => {\n const toasted = undoOn && write.verdict === \"APPROVED\";\n await onSave?.([write], { origin: \"editor\", toasted });\n if (toasted) sayDone([row], row.key);\n };\n\n /**\n * Focus back into the panel once a group's batch has settled, if it fell out: the\n * button that was pressed goes with the rows it approved (and under the `Unreviewed`\n * filter the whole group can go). Two frames, so the app's new rows have been drawn.\n */\n const keepFocusAt = (groupKey: string) => {\n const check = () => {\n const root = rootRef.current;\n const active = document.activeElement;\n if (!root || (active && active !== document.body && active.isConnected)) return;\n const sections = [...root.querySelectorAll<HTMLElement>(\"[data-review-group]\")];\n const section = sections.find((el) => el.dataset.reviewGroup === groupKey) ?? sections[0];\n section?.querySelector<HTMLElement>(\"[data-disclosure-trigger]\")?.focus();\n };\n requestAnimationFrame(() => requestAnimationFrame(check));\n };\n\n const approveGroup = async (group: TranslationRowGroup, confirmed: boolean) => {\n const targets = unreviewedRows(group.rows);\n if (targets.length === 0) return;\n if (!confirmed && targets.length > pageSize) {\n // Asked inside the opened group, over the rows it is about.\n setGroupOpenFor(group.key, true);\n setConfirming(group.key);\n confirmFocus.current = group.key;\n return;\n }\n const ok = await approve(`group:${group.key}`, targets, \"group\", groupName(group.key));\n if (ok) setConfirming((key) => (key === group.key ? null : key));\n keepFocusAt(group.key);\n };\n\n const visibleIds = new Set(visible.map((r) => r.id));\n const selectedRows = visible.filter((r) => selected.has(r.id));\n // What \"Select all shown\" takes: grouped, the rows of the open groups — a folded\n // group's rows are not shown.\n const shownRows = groups ? groups.filter((g) => isGroupOpen(g.key)).flatMap((g) => g.rows) : visible;\n const allShownSelected = shownRows.length > 0 && shownRows.every((r) => selected.has(r.id));\n const toApprove = selectedRows.filter((r) => r.text !== \"\" && r.status !== \"approved\");\n const toReset = selectedRows.filter((r) => r.review !== null);\n\n const approveSelected = async () => {\n if (await approve(\"bulk-approve\", toApprove, \"selection\")) setSelected(new Set());\n };\n const resetSelected = async () => {\n if (!onClear || toReset.length === 0) return;\n const clear = onClear;\n const keys = toReset.map((r) => ({ locale: r.locale, key: r.key }));\n if (await run(\"bulk-reset\", () => clear(keys, { origin: \"selection\", toasted: false }))) {\n setSelected(new Set());\n }\n };\n\n // The status options: \"missing\" only where something is (or it is the filter).\n const statuses = REVIEW_STATUSES.filter((s) => s !== \"missing\" || summary.missing > 0 || filter.status === s);\n const scopeNames = (areas ?? []).map((area) => areaLabels?.[area] ?? area);\n const scopeText =\n scopeNames.length > 0 ? new Intl.ListFormat(locale, { type: \"conjunction\", style: \"long\" }).format(scopeNames) : \"\";\n\n const columns: DataTableColumn<TranslationRow>[] = [\n {\n key: \"key\",\n header: labels.key,\n cell: (r) => <code className=\"break-all text-xs text-[var(--text-secondary)]\">{r.key}</code>,\n sortBy: (r) => r.key,\n mobileHidden: true,\n },\n {\n key: \"reference\",\n header: referenceLabel,\n cell: (r) => (\n <span className=\"line-clamp-3 whitespace-pre-line break-words text-[var(--text-secondary)]\">{r.reference}</span>\n ),\n mobileHidden: true,\n },\n {\n key: \"text\",\n header: localeLabel,\n cell: (r) => <TextCell row={r} labels={labels} clamp />,\n mobilePrimary: true,\n },\n {\n key: \"status\",\n header: labels.statusColumn,\n cell: (r) => (\n <span className=\"inline-flex flex-wrap gap-1\">\n <ReviewStatusChip status={r.status} labels={labelsProp} />\n {r.placeholderMismatch && (\n <Chip size=\"sm\" tone=\"danger\" variant=\"outline\">\n {labels.placeholderChip}\n </Chip>\n )}\n </span>\n ),\n sortBy: (r) => REVIEW_STATUSES.indexOf(r.status),\n },\n ];\n if (editable) {\n columns.push({\n key: \"approve\",\n header: <span className=\"sr-only\">{labels.approve}</span>,\n cell: (r) =>\n r.status === \"approved\" || r.text === \"\" ? null : (\n <IconButton\n type=\"button\"\n size=\"xs\"\n tone=\"muted\"\n commit\n label={labels.approve}\n pending={isQueued(`row:${r.id}`)}\n disabled={busy}\n onClick={(event) => {\n // The row's own click opens the editor; this one only approves.\n event.stopPropagation();\n void approve(`row:${r.id}`, [r], \"row\", r.key);\n }}\n >\n <Check />\n </IconButton>\n ),\n });\n }\n\n /** One bound swipe action on one row, as {@link SwipeableRow} draws and commits it. */\n const swipeAction = (r: TranslationRow, id: TranslationReviewSwipeAction, groupKey?: string): SwipeAction => {\n const missing = r.text === \"\";\n switch (id) {\n case \"approve\":\n return {\n label: labels.approve,\n icon: <Check className=\"size-4\" aria-hidden />,\n onCommit: () => void approve(`row:${r.id}`, [r], \"swipe\", r.key),\n // Text on the fill in its own contrast colour: the fills are pastels in dark\n // mode, where SwipeableRow's default white text was unreadable (0.26).\n className: \"bg-[var(--success)] text-[var(--success-contrast)]\",\n armedClassName: \"bg-[var(--success)] text-[var(--success-contrast)]\",\n };\n case \"edit\":\n return {\n label: missing ? labels.suggest : labels.flag,\n icon: <PencilLine className=\"size-4\" aria-hidden />,\n // Opens the editor rather than sending the string back: \"needs a change\"\n // with neither a note nor a better wording tells nobody anything.\n onCommit: () => {\n keepGroupOpen(groupKey);\n setOpenId(r.id);\n setFocusId(r.id);\n },\n className: missing\n ? \"bg-[var(--brand)] text-[var(--brand-contrast)]\"\n : \"bg-[var(--danger)] text-[var(--danger-contrast)]\",\n armedClassName: missing\n ? \"bg-[var(--brand-hover)] text-[var(--brand-contrast)]\"\n : \"bg-[var(--danger-hover)] text-[var(--danger-contrast)]\",\n };\n case \"clear\":\n return {\n label: labels.reset,\n icon: <RotateCcw className=\"size-4\" aria-hidden />,\n onCommit: () => void clearRow(r),\n // A step back, not a verdict: neither the approval's green nor the send-back's red.\n // The surface colour as text: white on the grey in light mode, the dark page on\n // the light grey in dark mode.\n className: \"bg-[var(--text-muted)] text-[var(--bg-surface)]\",\n armedClassName: \"bg-[var(--text-secondary)] text-[var(--bg-surface)]\",\n };\n }\n };\n\n /**\n * A phone card's swipes: the binding ({@link TranslationReviewPanelProps.swipe}) resolved\n * against the row, each side's ladder closed up over what the row cannot offer. None\n * where the row must not be acted on — the same conditions under which its buttons are\n * locked or absent.\n */\n const swipeActions = (r: TranslationRow, groupKey?: string): MobileSwipeActions | null => {\n if (!binding || !editable || lock.locked || isQueued(`row:${r.id}`)) return null;\n const plan = translationReviewSwipePlan(binding, r, { canClear: onClear !== undefined });\n if (plan.start.length === 0 && plan.end.length === 0) return null;\n return {\n start: plan.start.map((id) => swipeAction(r, id, groupKey)),\n end: plan.end.map((id) => swipeAction(r, id, groupKey)),\n };\n };\n\n const toggleIn = (prev: ReadonlySet<string>, many: readonly TranslationRow[], checked: boolean) => {\n const next = new Set([...prev].filter((id) => visibleIds.has(id)));\n for (const r of many) {\n if (checked) next.add(r.id);\n else next.delete(r.id);\n }\n return next;\n };\n /** The checkbox column of a table over `list` — the whole list, or one group. */\n const selectionFor = (list: readonly TranslationRow[]): DataTableProps<TranslationRow>[\"selection\"] => {\n if (!editable) return undefined;\n const picked = list.filter((r) => selected.has(r.id)).length;\n return {\n isSelected: (r) => selected.has(r.id),\n onToggle: (r, checked) => setSelected((prev) => toggleIn(prev, [r], checked)),\n onToggleMany: (many, checked) => setSelected((prev) => toggleIn(prev, many, checked)),\n allSelected: list.length > 0 && picked === list.length,\n someSelected: picked > 0 && picked < list.length,\n onToggleAll: (checked) => setSelected((prev) => toggleIn(prev, list, checked)),\n };\n };\n\n const table = (list: TranslationRow[], group?: { key: string; name: string }) => (\n <DataTable<TranslationRow>\n rows={list}\n rowKey={(r) => r.id}\n columns={columns}\n empty={labels.empty}\n defaultPageSize={pageSize}\n {...(group\n ? {\n // Inside the group's own card: no frame in a frame, no column rail per group,\n // no scroller per group, and the group's name as the table's.\n frame: false,\n chrome: \"minimal\" as const,\n maxBodyHeight: \"none\",\n sorts: groupSorts,\n onSortsChange: setGroupSorts,\n labels: { table: group.name },\n }\n : { storageKey })}\n isExpanded={(r) => r.id === openId}\n onRowClick={(r) => {\n if (openId !== r.id) keepGroupOpen(group?.key);\n setFocusId(null);\n setOpenId((open) => (open === r.id ? null : r.id));\n }}\n expandedRow={(r) => (\n <TranslationReviewEditor\n key={r.id}\n row={r}\n referenceLabel={referenceLabel}\n localeLabel={localeLabel}\n readOnly={!editable}\n focusWording={focusId === r.id}\n onSave={onSave ? (write) => saveFromEditor(r, write) : undefined}\n onClear={onClear ? (key) => onClear([key], { origin: \"editor\", toasted: false }) : undefined}\n onClose={() => {\n setOpenId(null);\n setFocusId(null);\n }}\n formatDate={formatDate}\n formatError={formatError}\n labels={labelsProp}\n />\n )}\n mobileCard={(r) => (\n <div className=\"min-w-0 space-y-1\">\n <code className=\"block break-all text-xs text-[var(--text-muted)]\">{r.key}</code>\n <div className=\"text-sm text-[var(--text-primary)]\">\n <TextCell row={r} labels={labels} clamp />\n </div>\n <div className=\"flex flex-wrap gap-1\">\n <ReviewStatusChip status={r.status} labels={labelsProp} />\n {r.placeholderMismatch && (\n <Chip size=\"sm\" tone=\"danger\" variant=\"outline\">\n {labels.placeholderChip}\n </Chip>\n )}\n </div>\n </div>\n )}\n mobileSwipeActions={binding ? (r) => swipeActions(r, group?.key) : undefined}\n selection={selectionFor(list)}\n />\n );\n\n const groupSection = (g: TranslationRowGroup, index: number) => {\n const name = groupName(g.key);\n const titleId = `${baseId}-group-${index}`;\n const questionId = `${baseId}-confirm-${index}`;\n const targets = editable ? unreviewedRows(g.rows) : [];\n const key = `group:${g.key}`;\n const open = isGroupOpen(g.key);\n // A folded group renders its header and nothing else. Its body is handed over only\n // while it is open, or when the REVIEWER folded it — so the fold animates over the\n // rows it hides. A group folded by the default gets none: when the default turns (a\n // search cleared over 122 areas), the groups that close would otherwise render their\n // grown tables for the length of the fold.\n const body = open || groupOpen[g.key] === false;\n return (\n <Disclosure\n key={g.key}\n data-review-group={g.key}\n headingAs={groupHeadingAs}\n title={<span id={titleId}>{name}</span>}\n hint={labels.groupCount(g.summary.unreviewed, g.summary.total)}\n open={open}\n onOpenChange={(next) => {\n setGroupOpenFor(g.key, next);\n if (!next) setConfirming((c) => (c === g.key ? null : c));\n }}\n trailing={\n targets.length > 0 ? (\n <Button\n variant=\"secondary\"\n size=\"sm\"\n commit\n // The visible words say what; the group's name, read after them, says where.\n aria-describedby={titleId}\n pending={isQueued(key) && confirming !== g.key}\n disabled={busy}\n onClick={() => void approveGroup(g, false)}\n >\n <Check className=\"size-4\" aria-hidden />\n {labels.approveGroup(targets.length)}\n </Button>\n ) : undefined\n }\n bodyClassName=\"space-y-0 px-0 pb-0 border-t border-[var(--border)]\"\n >\n {body && confirming === g.key && targets.length > 0 && (\n <div className=\"border-b border-[var(--border)] p-3\">\n <AlertBanner tone=\"warning\" size=\"sm\">\n <div className=\"min-w-0 space-y-2\">\n <p id={questionId}>{labels.confirmGroup(targets.length, name)}</p>\n <div className=\"flex flex-wrap gap-2\">\n <Button\n // Focus lands here when the question appears: the expected answer to a\n // question the reviewer just asked for, and Undo stays behind it.\n ref={(el) => {\n if (el && confirmFocus.current === g.key) {\n confirmFocus.current = null;\n el.focus();\n }\n }}\n variant=\"primary\"\n size=\"sm\"\n commit\n aria-describedby={questionId}\n pending={isQueued(key)}\n disabled={busy}\n onClick={() => void approveGroup(g, true)}\n >\n {labels.approveGroup(targets.length)}\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n disabled={isQueued(key)}\n onClick={() => {\n setConfirming(null);\n keepFocusAt(g.key);\n }}\n >\n {labels.cancel}\n </Button>\n </div>\n </div>\n </AlertBanner>\n </div>\n )}\n {body && table(g.rows, { key: g.key, name })}\n </Disclosure>\n );\n };\n return (\n <div ref={rootRef} className={cn(\"min-w-0 space-y-4\", className)}>\n {scopeText && (\n <AlertBanner tone=\"info\" variant=\"inline\" size=\"sm\" block live={false}>\n {labels.scope(scopeText)}\n </AlertBanner>\n )}\n {readOnly && (\n <AlertBanner tone=\"info\" variant=\"inline\" size=\"sm\" block live={false}>\n {labels.readOnly}\n </AlertBanner>\n )}\n\n {progress && <TranslationProgress summary={summary} labels={labelsProp} />}\n\n <ToggleGroup<ReviewStatus | \"all\">\n aria-label={labels.statusFilter}\n overflow=\"wrap\"\n size=\"sm\"\n value={filter.status}\n onChange={(next) => setFilter({ status: next })}\n options={[\n { value: \"all\", label: labels.filterCount(labels.all, summary.total) },\n ...statuses.map((s) => ({ value: s, label: labels.filterCount(reviewStatusLabel(labels, s), summary[s]) })),\n ]}\n />\n\n <div className=\"flex flex-col gap-3 md:flex-row md:flex-wrap md:items-end\">\n {sources.length > 1 && (\n <div className=\"md:w-48\">\n <Select label={labels.source} value={source} onChange={(e) => setFilter({ source: e.target.value })}>\n <option value=\"all\">{labels.allSources}</option>\n {sources.map((s) => (\n <option key={s} value={s}>\n {sourceLabels?.[s] ?? s}\n </option>\n ))}\n </Select>\n </div>\n )}\n <div className=\"md:w-56\">\n <Select label={labels.namespace} value={namespace} onChange={(e) => setFilter({ namespace: e.target.value })}>\n <option value=\"all\">{labels.allNamespaces}</option>\n {namespaces.map((ns) => (\n <option key={ns} value={ns}>\n {ns}\n </option>\n ))}\n </Select>\n </div>\n <SearchField\n className=\"min-w-0 md:flex-1\"\n label={labels.search}\n value={filter.query}\n onChange={(next) => setFilter({ query: next })}\n />\n <Checkbox\n label={labels.placeholdersOnly(mismatches)}\n checked={filter.placeholdersOnly}\n onCheckedChange={(next) => setFilter({ placeholdersOnly: next })}\n />\n </div>\n\n {editable && shownRows.length > 0 && (\n // The phone's table has no checkboxes; this is its way into the bulk actions.\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"md:hidden\"\n disabled={allShownSelected}\n onClick={() => setSelected((prev) => toggleIn(prev, shownRows, true))}\n >\n <ListChecks className=\"size-4\" aria-hidden />\n {labels.selectShown}\n </Button>\n )}\n\n {failure && (\n <AlertBanner tone=\"danger\" size=\"sm\" role=\"alert\">\n {failure}\n </AlertBanner>\n )}\n\n {editable && (\n <BulkActionBar count={selectedRows.length} onClear={() => setSelected(new Set())}>\n <Button\n variant=\"primary\"\n size=\"sm\"\n commit\n pending={isQueued(\"bulk-approve\")}\n disabled={busy || toApprove.length === 0}\n onClick={() => void approveSelected()}\n >\n <Check className=\"size-4\" aria-hidden />\n {labels.approveSelected}\n </Button>\n {onClear && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n commit\n pending={isQueued(\"bulk-reset\")}\n disabled={busy || toReset.length === 0}\n onClick={() => void resetSelected()}\n >\n <RotateCcw className=\"size-4\" aria-hidden />\n {labels.resetSelected}\n </Button>\n )}\n </BulkActionBar>\n )}\n\n {groups && groups.length > 0 ? (\n <div className=\"space-y-3\">{groups.map(groupSection)}</div>\n ) : (\n table(visible)\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["ReviewStatusChip","TranslationProgress","TranslationReviewEditor","DEFAULT_TRANSLATION_REVIEW_LABELS"]}
1
+ {"version":3,"sources":["../../src/components/translation-review.tsx"],"sourcesContent":["import { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Check, ListChecks, PencilLine, RotateCcw } from \"lucide-react\";\n\nimport { useBreakpoint } from \"../hooks/use-breakpoint\";\nimport { useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport {\n DEFAULT_TRANSLATION_REVIEW_FILTER,\n DEFAULT_TRANSLATION_REVIEW_SWIPE,\n REVIEW_STATUSES,\n filterTranslationRows,\n groupTranslationRows,\n keyInAreas,\n reviewUndo,\n reviewWrite,\n summariseRows,\n translationReviewSwipePlan,\n unreviewedRows,\n} from \"../lib/translation-review\";\nimport type {\n ReviewStatus,\n TranslationReviewFilter,\n TranslationReviewKey,\n TranslationReviewSwipeAction,\n TranslationReviewSwipeBinding,\n TranslationReviewWrite,\n TranslationRow,\n TranslationRowGroup,\n} from \"../lib/translation-review\";\nimport { AlertBanner } from \"./alert-banner\";\nimport { BulkActionBar } from \"./bulk-action-bar\";\nimport { Checkbox } from \"./checkbox\";\nimport { Chip } from \"./chip\";\nimport { DataTable } from \"./data-table\";\nimport type { DataTableColumn, DataTableProps, MobileSwipeActions, SortState } from \"./data-table\";\nimport { Disclosure } from \"./disclosure\";\nimport { SearchField } from \"./search-field\";\nimport { toast } from \"./toast\";\nimport type { SwipeAction } from \"./swipeable-row\";\nimport { ToggleGroup } from \"./toggle-group\";\nimport { TranslationReviewEditor } from \"./translation-review-editor\";\nimport { DEFAULT_TRANSLATION_REVIEW_LABELS, reviewStatusLabel } from \"./translation-review-labels\";\nimport type { TranslationReviewLabels } from \"./translation-review-labels\";\nimport { ReviewStatusChip, TranslationProgress } from \"./translation-review-parts\";\nimport { Button, IconButton, Select } from \"./ui\";\nimport { useWriteLock } from \"./write-lock\";\n\nexport {\n REVIEW_STATUS_TONES,\n ReviewStatusChip,\n TranslationExportButton,\n TranslationLocaleTabs,\n TranslationProgress,\n} from \"./translation-review-parts\";\nexport type {\n ReviewStatusChipProps,\n TranslationExportButtonProps,\n TranslationLocaleTab,\n TranslationLocaleTabsProps,\n TranslationProgressProps,\n} from \"./translation-review-parts\";\nexport { TranslationReviewEditor } from \"./translation-review-editor\";\nexport type { TranslationReviewEditorProps } from \"./translation-review-editor\";\nexport { DEFAULT_TRANSLATION_REVIEW_LABELS } from \"./translation-review-labels\";\nexport type { TranslationReviewLabels } from \"./translation-review-labels\";\n\n/**\n * An app's translation review page, as kit parts (H1, docs/i18n-harmonization.md):\n * keksdose's /translations, which kastlan was about to build a second time. The app keeps\n * what is its own — where the strings come from, which locale is read against which, who\n * may review what, every request — and hands the kit rows (`translationRows`) and two\n * callbacks. Everything a reviewer touches is here.\n *\n * Presentational — no request, no route — and one LOCALE at a time. Its table is the\n * kit's DataTable, so like every DataTable it renders under the app's Router. The page\n * around it puts\n * {@link TranslationLocaleTabs} above it and {@link TranslationExportButton} in its header,\n * because both span every locale while this panel is the one open.\n *\n * Callbacks may return promises, and the panel waits for them: an editor stays open with\n * its text and the error when a save is refused (a 403 for a key outside the reviewer's\n * areas, a network failure), a bulk action keeps its selection. Chunking a long bulk\n * write to the server's batch limit is the app's; the panel sends one call per action.\n */\n\ntype MaybePromise = void | Promise<unknown>;\n\n/** What asked for a write — see {@link TranslationReviewSaveInfo}. */\nexport type TranslationReviewOrigin = \"editor\" | \"row\" | \"swipe\" | \"group\" | \"selection\" | \"undo\";\n\n/**\n * The second argument of `onSave` and `onClear` (0.25): where the write came from, and\n * whether the panel answers it with a toast of its own.\n *\n * WHY THE APP IS TOLD. keksdose and kastlan toast \"Saved\" from their mutation on every\n * save. With {@link TranslationReviewPanelProps.undo} on, an approval is answered by the\n * kit's Undo toast instead — and a reviewer swiping through a list then read two toasts\n * per card. The app skips its own where `toasted` is true:\n *\n * onSave={(writes, { toasted }) => save.mutateAsync({ items: writes.map(toApiWrite), quiet: toasted })}\n *\n * An app that ignores the argument keeps working; it only says \"Saved\" twice.\n */\nexport interface TranslationReviewSaveInfo {\n /** The open editor, a row's approve button, a swipe on a phone card, a group's\n * \"Approve unreviewed\", the selection's bulk bar, or an Undo pressed on the toast. */\n origin: TranslationReviewOrigin;\n /** The panel shows its own toast for this write (an approval's Undo toast, or a\n * swiped `clear`'s). */\n toasted: boolean;\n}\n\n/** How {@link TranslationReviewPanelProps.groupBy} names a row's group. */\nexport type TranslationReviewGroupBy = \"namespace\" | \"source\" | ((row: TranslationRow) => string);\n\nexport interface TranslationReviewPanelProps {\n /** One locale's rows, from `translationRows`. */\n rows: readonly TranslationRow[];\n /** The reviewed language's own name — the text column's header (\"Français\"). */\n localeLabel: string;\n /** The reference language's own name — the reference column's header. */\n referenceLabel: string;\n /** Store verdicts: one for an editor or a row's approve button, many for a bulk\n * approve. Left out (or `readOnly`): nothing can be changed. The second argument\n * says where the write came from ({@link TranslationReviewSaveInfo}). */\n onSave?: (writes: TranslationReviewWrite[], info: TranslationReviewSaveInfo) => MaybePromise;\n /** Clear verdicts — back to unreviewed. Left out: no reset, and an approval of a row\n * that had no verdict cannot be undone. */\n onClear?: (keys: TranslationReviewKey[], info: TranslationReviewSaveInfo) => MaybePromise;\n /** A locale the viewer may read but not review: the rows and their verdicts, no\n * actions, and a line saying so. */\n readOnly?: boolean;\n /**\n * The areas the reviewer is limited to (keksdose's lawyer: `[\"legal\"]`), as the server\n * answers them; `null` is every area. Rows outside are not shown or counted — build the\n * rows with the same `areas` and they never exist — and a notice names the areas.\n */\n areas?: readonly string[] | null;\n /** An area's name for the notice (\"the legal pages (Imprint, …)\"). Default: the key. */\n areaLabels?: Readonly<Record<string, string>>;\n /** Default `keyInArea`: the key is the area or sits under it (`legal.…`, `legal:…`,\n * and the kit's `kit.legal.…` since 0.28). */\n inArea?: (key: string, area: string) => boolean;\n /** A source's name for the source filter, which appears once the rows come from more\n * than one (kastlan: `{ screen: \"Screen\", documents: \"PDFs and emails\" }`). Default:\n * the source itself. */\n sourceLabels?: Readonly<Record<string, string>>;\n /**\n * Controlled filters — for an app that keeps them in the URL (keksdose's `?status=`,\n * `?ns=`, `?q=`). Whatever is left out is the panel's own. Pair with `onFilterChange`.\n *\n * A field passed here is the app's, and the control shows what the app says: one the app\n * passes but does not write back from `onFilterChange` is a control that does nothing.\n * A development build warns about that once per field (0.25.1, Kurvenschmiede); a field\n * left out entirely is fine — the panel keeps it.\n */\n filter?: Partial<TranslationReviewFilter>;\n /** Every filter, on every change — write them in ONE update: `useSearchParamsState`'s\n * setter takes this object as it is (`filter={filter} onFilterChange={setFilter}`).\n * Single `useSearchParamState` setters compose since 0.25; before, four in a row\n * clobbered each other (keksdose live #378). */\n onFilterChange?: (filter: TranslationReviewFilter) => void;\n /** The progress bar over the filters. Default true. */\n progress?: boolean;\n /** Rows per page. Default 50. */\n pageSize?: number;\n /** Persists the table's sort and page size (DataTable's `storageKey`). */\n storageKey?: string;\n /** How a verdict's date reads in the editor. */\n formatDate?: (iso: string) => string;\n /** Turns a callback's rejection into words. Default {@link TranslationReviewLabels.failed}. */\n formatError?: (error: unknown) => ReactNode;\n /**\n * Swipe the phone cards (0.25, keksdose live #377 — Marcel reviewing on a phone): toward\n * the reading END to approve, toward the START for \"Needs a change\", which OPENS the\n * editor with the cursor in the wording rather than sending the string back blind — a\n * verdict without a note or a better wording tells whoever fixes it nothing. In a\n * right-to-left page the two sides mirror with the reading direction.\n *\n * DataTable's swipe sides (`mobileSwipeActions`) on {@link SwipeableRow}: the same\n * gesture, the same feedback, and the same keyboard path — every swipe is also a real\n * button, read out and reachable by Tab, so the gesture is a shortcut and never the only\n * way. A row has no swipe while the editor under it is open, while its own write is out,\n * under a {@link WriteLockProvider} lock, in a `readOnly` locale, and toward \"Approve\"\n * where there is nothing to approve (a missing string, an approved one). A reviewer\n * limited to `areas` never sees a row outside them, so never swipes one.\n *\n * Off by default, so no existing page gains a gesture on the bump. Phones only; the\n * desktop table keeps its approve button.\n *\n * A BINDING instead of `true` (keksdose — Marcel's live #377 rework, 2026-10-03: *\"Add\n * the swipe options to be shown also to the settings /settings#interaction area where\n * the other swipe options are defined\"*): keksdose lets the user bind every list's\n * swipes in Settings → Interaction, per side a primary action at the first threshold and\n * a secondary one at a longer drag, and a fixed mapping is one its settings cannot\n * reach. So each logical side takes an ordered ladder of\n * {@link TranslationReviewSwipeAction}s — index 0 at the first threshold, index 1 at the\n * longer drag, as {@link SwipeableRow} stages them:\n *\n * swipe={{ end: [\"approve\", \"clear\"], start: [\"edit\"] }}\n *\n * `approve` (with Undo), `edit` (the editor, cursor in the wording — \"Needs a change\"),\n * `clear` (the verdict taken back, with Undo, which stores it again; only with\n * `onClear`). An action a row cannot offer drops out and the ladder closes up behind it\n * ({@link translationReviewSwipePlan}): `approve` on an approved or missing string,\n * `clear` on a row with no verdict. Every rule above still holds for every binding —\n * no side swipes in a `readOnly` locale, under a write lock, with the editor open or\n * the row's write out.\n *\n * `true` is {@link DEFAULT_TRANSLATION_REVIEW_SWIPE} — `{ end: [\"approve\"], start:\n * [\"edit\"] }`, 0.25's mapping exactly — and `false` or nothing is no swipe. A binding\n * turns the Undo toast on by default, as `true` does.\n */\n swipe?: boolean | TranslationReviewSwipeBinding;\n /**\n * Answer every approval with the kit's Undo toast (`toast.undo`): a row's button, a\n * swipe, the editor's Approve, a group's and the selection's bulk approve. Undo stores\n * again what the rows said before ({@link reviewUndo}) — or clears them, for rows that\n * had no verdict, which needs `onClear` (without it the toast reports and offers\n * nothing). A batch has ONE Undo.\n *\n * Default: on with `swipe` or `groupBy` — the two ways to approve in one movement or many\n * rows at once bring their way back — and off otherwise, so an existing page does not\n * gain a toast on the bump. Needs the app's `<Toaster>`. Tell the app's own \"Saved\"\n * toast apart with the `toasted` flag ({@link TranslationReviewSaveInfo}).\n */\n undo?: boolean;\n /**\n * Group the strings under a header per area (0.25, keksdose live #377: *\"grouping to the\n * translation review entries for faster reviewing\"*): `\"namespace\"` — the row's\n * `namespace`, which `translationRows` derives from the key (`budget`, `kit.dataTable`\n * with keksdose's `namespaceOf`) — `\"source\"`, or a function of the row.\n *\n * Each group is a section with a heading ({@link groupHeadingAs}): its name, \"n\n * unreviewed / total\", and \"Approve unreviewed (n)\", which approves the group's rows\n * nobody has given a verdict ({@link unreviewedRows}) under the same rules as one\n * approval — write lock, `readOnly`, one write at a time — with one Undo for the batch.\n * A batch larger than `pageSize` asks first, inside the opened group: more than a page\n * is more than the reviewer can have had on screen.\n *\n * WHAT IT MEANS FOR THE FILTERS, THE SORT AND THE PAGES. The filters narrow the rows\n * first, then the rows are grouped: a header counts what the filters show in it (so\n * \"Approve unreviewed\" never takes a row the reviewer filtered away), and an area with\n * nothing left is not shown. Groups stand in the order their first string has in the\n * reference; a column sort orders the rows INSIDE every group alike. Each group pages\n * on its own, `pageSize` rows at a time — a header then always stands over its own\n * rows, and paging one area does not move another — rather than one pager cutting\n * through the headers. `storageKey` keeps persisting the ungrouped table only.\n *\n * Groups fold (each on its own, remembered while the panel lives), and a folded group's\n * rows are not rendered at all — only its header. Which groups start open is\n * {@link defaultGroupsOpen}: by default, on a phone they start folded when there are\n * several — the headers are then the area list, and the reviewer opens the one to work\n * through — and on a wide screen they start open while that is cheap (see there).\n */\n groupBy?: TranslationReviewGroupBy;\n /**\n * Which groups start open under {@link groupBy} (0.25.1). A group the reviewer opens or\n * folds keeps that; this is what every other group follows.\n *\n * - `\"auto\"` (default): on a phone, open when there is one group and folded when there\n * are several, as in 0.25.0. On a wide screen, open while opening every group renders\n * at most 100 rows (each group shows up to `pageSize` of them; the budget is one page\n * where `pageSize` is larger) — and folded above that, the headers then being the area\n * list, as on a phone.\n * - `true`: every group starts open, on a phone too.\n * - `false`: every group starts folded, on a wide screen too.\n *\n * WHY A BUDGET OF ROWS (keksdose, measured on 0.25.0): a wide screen opened EVERY group.\n * keksdose's /translations has 122 areas and ~4700 strings — 122 tables and 2053 rows at\n * once, ~46 000 elements, 5.2 s before the panel appeared and 2.5 s for a filter click\n * to paint, against 50 rows and 0.34 s ungrouped; its page test went from 4 s to 146 s.\n * kastlan (~31 areas, ~1300 rows open) and Kurvenschmiede (28, ~700) had the same page.\n * What costs is the rows on screen, so the rule counts them rather than the groups:\n * 100 rows is two of the panel's default pages — any one or two areas still open as\n * before, however long, and so do a dozen small ones, or a search whose hits fall in\n * many areas — while a whole catalogue starts as its list of areas: keksdose's shape\n * in jsdom, 122 headers and ~2100 elements instead of ~44 000 (the ungrouped panel has\n * ~1200). The rule follows the filters: narrow them and the groups open.\n *\n * A boolean rather than a controlled open state: the panel already keeps each group's\n * own state, and an app that only wants \"all open\" or \"all folded\" should not have to\n * keep a record of 122 flags to say it.\n */\n defaultGroupsOpen?: boolean | \"auto\";\n /** A group's name. Default: `sourceLabels` for `groupBy=\"source\"`, else the key. */\n groupLabel?: (group: string) => string;\n /** The level of the group headings. Default `h2`: the panel is a page's body, under\n * the page's `h1`. */\n groupHeadingAs?: \"h2\" | \"h3\" | \"h4\";\n className?: string;\n labels?: Partial<TranslationReviewLabels>;\n}\n\n\n/**\n * How many rows the groups may render at once on a wide screen before they start folded\n * ({@link TranslationReviewPanelProps.defaultGroupsOpen}): two of the default 50-row pages.\n * A row costs about twenty elements in either layout, so this is the cost of two pages of\n * the ungrouped table — not of 2053 rows in 122 tables.\n */\nconst GROUPS_OPEN_ROW_BUDGET = 100;\n\n/**\n * Would opening every group stay within the budget? Each group renders up to `pageSize`\n * rows (it pages on its own); the budget is never less than one page, so a single group —\n * which renders at most one — always starts open, as on a phone.\n */\nfunction groupsFitOpen(groups: readonly TranslationRowGroup[], pageSize: number): boolean {\n const budget = Math.max(GROUPS_OPEN_ROW_BUDGET, pageSize);\n let rendered = 0;\n for (const group of groups) {\n rendered += Math.min(group.rows.length, pageSize);\n if (rendered > budget) return false;\n }\n return true;\n}\n\n/**\n * The kit's dev switch, as NumberInput's money guard and `lib/logger.ts` read it: a\n * development build — the consumer's bundler makes `import.meta.env.DEV` false in a\n * production one, and the branch is dead code there — and never under vitest, where every\n * consumer suite would print it; a test opts in with `vi.stubEnv(\"VITEST\", \"\")`. Read per\n * call, and optional-chained: `import.meta.env` is a Vite injection, absent under Node.\n */\nfunction devWarnings(): boolean {\n return Boolean(import.meta.env?.DEV) && !import.meta.env?.VITEST;\n}\n\ntype FilterField = keyof TranslationReviewFilter;\n\n/** How long after the last filter change the app's `filter` is looked at again: well past\n * a router's transition, so a URL that is still being written is not mistaken for one\n * that never will be. */\nconst FILTER_ECHO_MS = 1500;\n\n/**\n * The development warning for a controlled filter that does not follow (Kurvenschmiede,\n * 0.25.1). A field the app passes in `filter` is the app's: the panel shows the app's\n * value, whatever the reviewer picked. An app that passes a field but does not write the\n * reported value back — its `onFilterChange` hands on only some fields, `filter` is built\n * as `{ ...DEFAULT_TRANSLATION_REVIEW_FILTER, ...fromTheUrl }`, a URL parser rejects what\n * its serialiser wrote, or there is no `onFilterChange` at all — has a control that\n * silently does nothing: the box unticks itself, the select jumps back.\n *\n * So, after the panel reports a change of a field the app controls, it looks at the\n * app's `filter` again once the change has had time to land ({@link FILTER_ECHO_MS}, read\n * from the last COMMITTED props, so a router transition still rendering does not count),\n * and warns — once per field — when that field still holds the value it had before.\n *\n * Quiet by construction for an app that passes no `filter`, and for a field the app LEAVES\n * OUT: that field is the panel's own and works (keksdose keeps the placeholder switch so,\n * on purpose). Quiet too when the app moved the field somewhere else itself. Development\n * builds only (see {@link devWarnings}): in production and under vitest nothing is\n * recorded and no timer is set.\n */\nfunction useFilterEchoCheck(filterProp: Partial<TranslationReviewFilter> | undefined) {\n const committed = useRef(filterProp);\n useEffect(() => {\n committed.current = filterProp;\n });\n // Per field: what the app's filter said when the reviewer first changed it, and what\n // the panel reported last.\n const pending = useRef(new Map<FilterField, { from: unknown; to: unknown }>());\n const warned = useRef(new Set<FilterField>());\n const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n useEffect(() => () => clearTimeout(timer.current), []);\n\n return (next: TranslationReviewFilter) => {\n if (filterProp === undefined || !devWarnings()) return;\n for (const field of Object.keys(next) as FilterField[]) {\n const from = filterProp[field];\n if (from === undefined || warned.current.has(field)) continue;\n const seen = pending.current.get(field);\n if (seen) seen.to = next[field];\n else if (next[field] !== from) pending.current.set(field, { from, to: next[field] });\n }\n if (pending.current.size === 0) return;\n clearTimeout(timer.current);\n timer.current = setTimeout(() => {\n const now = committed.current;\n for (const [field, { from, to }] of pending.current) {\n if (to === from || now?.[field] !== from || warned.current.has(field)) continue;\n warned.current.add(field);\n console.warn(\n `[ui-kit] TranslationReviewPanel: the \"${field}\" filter was changed to ${JSON.stringify(to)}, but the ` +\n `\\`filter\\` prop still says ${JSON.stringify(from)}, so the control does nothing. A field passed in ` +\n `\\`filter\\` is the app's: write back every field \\`onFilterChange\\` reports, in one update ` +\n `(useSearchParamsState's setter takes the object as it is), or leave the field out of \\`filter\\` and ` +\n `the panel keeps it. (Development builds only; once per field.)`,\n );\n }\n pending.current.clear();\n }, FILTER_ECHO_MS);\n };\n}\n\n/** The row's text — or, for a missing string, the line that says so. */\nfunction TextCell({ row, labels, clamp }: { row: TranslationRow; labels: TranslationReviewLabels; clamp?: boolean }) {\n return row.text === \"\" ? (\n <span className=\"italic text-[var(--text-muted)]\">{labels.missingText}</span>\n ) : (\n <span lang={row.locale} className={cn(\"whitespace-pre-line break-words\", clamp && \"line-clamp-3\")}>\n {row.text}\n </span>\n );\n}\n\n/**\n * The page body for one locale: progress, filters (status, source, namespace, search,\n * placeholder problems), the strings with an editor that unfolds under each, and the\n * bulk actions over a selection.\n *\n * The status filter is the work queue in order — what is not there, what nobody has\n * read, what changed after it was read, what was sent back, what is done — with each\n * count on it. \"Missing\" is offered only where something is.\n *\n * Bulk approve takes the selected rows that are not approved yet and have a text:\n * approving an absent string says nothing, and re-approving one says it again. On a\n * phone the table has no checkboxes, so \"Select all shown\" selects what the filters\n * show — filter to `Unreviewed` in one area, read, select, approve. With `groupBy`, a\n * group's own \"Approve unreviewed\" is the shorter way, and with `swipe` a card is approved\n * by a swipe.\n *\n * The panel's writes go out ONE AT A TIME, in the order they were asked for: two writes on\n * one locale's verdicts can answer out of order, and an Undo must not overtake the\n * approval it takes back. A swipe on the next card while one is out is queued rather than\n * refused — a reviewer swiping down a list is faster than the server.\n */\nexport function TranslationReviewPanel({\n rows: rowsProp,\n localeLabel,\n referenceLabel,\n onSave,\n onClear,\n readOnly = false,\n areas,\n areaLabels,\n inArea,\n sourceLabels,\n filter: filterProp,\n onFilterChange,\n progress = true,\n pageSize = 50,\n storageKey,\n formatDate,\n formatError,\n swipe = false,\n undo: undoProp,\n groupBy,\n groupLabel,\n groupHeadingAs = \"h2\",\n defaultGroupsOpen = \"auto\",\n className,\n labels: labelsProp,\n}: TranslationReviewPanelProps) {\n const labels = useKitLabels(\"translationReview\", DEFAULT_TRANSLATION_REVIEW_LABELS, labelsProp);\n const locale = useKitLocale();\n const lock = useWriteLock();\n // The wide screen's breakpoint — the one DataTable switches its layout on, so \"a phone\"\n // means the same here as in the table, at every text size.\n const phone = !useBreakpoint(\"md\", true);\n const baseId = useId();\n const rootRef = useRef<HTMLDivElement>(null);\n const editable = !readOnly && onSave !== undefined;\n const binding: TranslationReviewSwipeBinding | null =\n swipe === true ? DEFAULT_TRANSLATION_REVIEW_SWIPE : swipe || null;\n const undoOn = undoProp ?? (binding !== null || groupBy !== undefined);\n\n const [ownFilter, setOwnFilter] = useState<TranslationReviewFilter>(DEFAULT_TRANSLATION_REVIEW_FILTER);\n // The app's fields over the panel's own; a field the app leaves `undefined` is the\n // panel's, not a blank.\n const filter: TranslationReviewFilter = { ...ownFilter };\n for (const [name, value] of Object.entries(filterProp ?? {})) {\n if (value !== undefined) Object.assign(filter, { [name]: value });\n }\n const echoFilter = useFilterEchoCheck(filterProp);\n const [openId, setOpenId] = useState<string | null>(null);\n // The row whose editor a swipe opened, to be written in: its wording field takes focus.\n const [focusId, setFocusId] = useState<string | null>(null);\n const [selected, setSelected] = useState<ReadonlySet<string>>(() => new Set());\n // The writes asked for and not yet answered, in order: the first is the one out. The\n // editor keeps its own, for its own buttons.\n const [queued, setQueued] = useState<readonly string[]>([]);\n const claimed = useRef(new Set<string>());\n const tail = useRef<Promise<unknown>>(Promise.resolve());\n const undoCount = useRef(0);\n const [failure, setFailure] = useState<ReactNode>(null);\n const busy = queued.length > 0;\n const isQueued = (key: string) => queued.includes(key);\n\n const rows = useMemo(\n () => (areas ? rowsProp.filter((row) => keyInAreas(row.key, areas, inArea)) : [...rowsProp]),\n [rowsProp, areas, inArea],\n );\n const summary = useMemo(() => summariseRows(rows), [rows]);\n const sources = useMemo(() => [...new Set(rows.map((r) => r.source))], [rows]);\n const source = sources.includes(filter.source) ? filter.source : \"all\";\n const namespaces = useMemo(\n () => [...new Set(rows.filter((r) => source === \"all\" || r.source === source).map((r) => r.namespace))],\n [rows, source],\n );\n const namespace = namespaces.includes(filter.namespace) ? filter.namespace : \"all\";\n const mismatches = useMemo(() => rows.filter((r) => r.placeholderMismatch).length, [rows]);\n // `filter` is rebuilt every render; its fields are what the list depends on.\n const { status, query, placeholdersOnly } = filter;\n const visible = useMemo(\n () => filterTranslationRows(rows, { status, source, namespace, query, placeholdersOnly }),\n [rows, status, source, namespace, query, placeholdersOnly],\n );\n\n // ── Groups ──\n const groups = useMemo(() => {\n if (groupBy === undefined) return null;\n const of =\n groupBy === \"namespace\"\n ? (r: TranslationRow) => r.namespace\n : groupBy === \"source\"\n ? (r: TranslationRow) => r.source\n : groupBy;\n return groupTranslationRows(visible, of);\n }, [visible, groupBy]);\n const groupName = (key: string) =>\n groupLabel?.(key) ?? (groupBy === \"source\" ? sourceLabels?.[key] : undefined) ?? (key || \"—\");\n // Only what the reviewer chose; a group nobody has touched follows the default, which\n // follows the filters (see `defaultGroupsOpen`).\n const [groupOpen, setGroupOpen] = useState<Readonly<Record<string, boolean>>>({});\n const groupOpenByDefault =\n defaultGroupsOpen !== \"auto\"\n ? defaultGroupsOpen\n : groups !== null && (phone ? groups.length <= 1 : groupsFitOpen(groups, pageSize));\n const isGroupOpen = (key: string) => groupOpen[key] ?? groupOpenByDefault;\n const setGroupOpenFor = (key: string, open: boolean) => setGroupOpen((prev) => ({ ...prev, [key]: open }));\n /** A group the reviewer works in — an editor opened in it — stays open when the default\n * turns (a search cleared, the rows grown past the budget): folding it would drop the\n * editor and whatever was typed in it. */\n const keepGroupOpen = (key: string | undefined) => {\n if (key !== undefined) setGroupOpen((prev) => (key in prev ? prev : { ...prev, [key]: true }));\n };\n // The group whose large batch is waiting for a yes — and, until its confirm button\n // has mounted and taken focus, the group that asked.\n const [confirming, setConfirming] = useState<string | null>(null);\n const confirmFocus = useRef<string | null>(null);\n // One sort for every group's table, so a header click orders all of them alike.\n const [groupSorts, setGroupSorts] = useState<SortState[]>([]);\n\n const setFilter = (patch: Partial<TranslationReviewFilter>) => {\n const next = { ...filter, ...patch };\n setOwnFilter(next);\n onFilterChange?.(next);\n echoFilter(next);\n // A selection the new filter hides would be acted on unseen.\n setSelected(new Set());\n };\n\n /** Queue a write under `key` (one per key at a time); resolves whether it worked. */\n const run = (key: string, action: () => MaybePromise): Promise<boolean> => {\n if (claimed.current.has(key)) return Promise.resolve(false);\n claimed.current.add(key);\n setQueued((q) => [...q, key]);\n const step = tail.current.then(async () => {\n setFailure(null);\n try {\n await action();\n return true;\n } catch (caught) {\n setFailure(formatError ? formatError(caught) : labels.failed);\n return false;\n } finally {\n claimed.current.delete(key);\n setQueued((q) => q.filter((k) => k !== key));\n }\n });\n tail.current = step;\n return step;\n };\n\n /** \"String approved — Undo\", once `targets` (as they were BEFORE the write) are\n * approved — or, after a swiped `clear`, \"String marked unreviewed — Undo\". Either way\n * Undo puts back what the rows said before ({@link reviewUndo}), through the same queue\n * as every write. */\n const sayDone = (targets: readonly TranslationRow[], description?: ReactNode, cleared = false) => {\n const back = reviewUndo(targets);\n const message = (cleared ? labels.clearedToast : labels.approvedToast)(targets.length);\n const save = onSave;\n const clear = onClear;\n // Taking back the approval of a row that had no verdict is a clear: without\n // `onClear` there is no way to say it, so the toast reports and offers nothing.\n if (!save || (back.clears.length > 0 && !clear)) {\n toast.success(message, { description });\n return;\n }\n const key = `undo:${++undoCount.current}`;\n toast.undo(message, {\n description,\n onUndo: () =>\n void run(key, async () => {\n if (back.writes.length > 0) await save(back.writes, { origin: \"undo\", toasted: false });\n if (back.clears.length > 0) await clear?.(back.clears, { origin: \"undo\", toasted: false });\n }),\n });\n };\n\n const approve = async (\n key: string,\n targets: readonly TranslationRow[],\n origin: TranslationReviewOrigin,\n description?: ReactNode,\n ): Promise<boolean> => {\n if (!onSave || targets.length === 0) return false;\n const save = onSave;\n const ok = await run(key, () => save(targets.map((r) => reviewWrite(r, \"APPROVED\")), { origin, toasted: undoOn }));\n if (ok && undoOn) sayDone(targets, description);\n return ok;\n };\n\n /** A swipe bound to `clear`: the row's verdict taken back, answered like an approval —\n * the Undo toast stores the verdict again. Under the row's own key, so it queues behind\n * (and never overlaps) an approval of the same row. */\n const clearRow = async (row: TranslationRow) => {\n if (!onClear || row.review === null) return;\n const clear = onClear;\n const keys = [{ locale: row.locale, key: row.key }];\n const ok = await run(`row:${row.id}`, () => clear(keys, { origin: \"swipe\", toasted: undoOn }));\n if (ok && undoOn) sayDone([row], row.key, true);\n };\n\n /** The editor's own save: it waits, closes and shows its errors itself, so it does\n * not go through the queue — but an approval there is answered like any other. */\n const saveFromEditor = async (row: TranslationRow, write: TranslationReviewWrite) => {\n const toasted = undoOn && write.verdict === \"APPROVED\";\n await onSave?.([write], { origin: \"editor\", toasted });\n if (toasted) sayDone([row], row.key);\n };\n\n /**\n * Focus back into the panel once a group's batch has settled, if it fell out: the\n * button that was pressed goes with the rows it approved (and under the `Unreviewed`\n * filter the whole group can go). Two frames, so the app's new rows have been drawn.\n */\n const keepFocusAt = (groupKey: string) => {\n const check = () => {\n const root = rootRef.current;\n const active = document.activeElement;\n if (!root || (active && active !== document.body && active.isConnected)) return;\n const sections = [...root.querySelectorAll<HTMLElement>(\"[data-review-group]\")];\n const section = sections.find((el) => el.dataset.reviewGroup === groupKey) ?? sections[0];\n section?.querySelector<HTMLElement>(\"[data-disclosure-trigger]\")?.focus();\n };\n requestAnimationFrame(() => requestAnimationFrame(check));\n };\n\n const approveGroup = async (group: TranslationRowGroup, confirmed: boolean) => {\n const targets = unreviewedRows(group.rows);\n if (targets.length === 0) return;\n if (!confirmed && targets.length > pageSize) {\n // Asked inside the opened group, over the rows it is about.\n setGroupOpenFor(group.key, true);\n setConfirming(group.key);\n confirmFocus.current = group.key;\n return;\n }\n const ok = await approve(`group:${group.key}`, targets, \"group\", groupName(group.key));\n if (ok) setConfirming((key) => (key === group.key ? null : key));\n keepFocusAt(group.key);\n };\n\n const visibleIds = new Set(visible.map((r) => r.id));\n const selectedRows = visible.filter((r) => selected.has(r.id));\n // What \"Select all shown\" takes: grouped, the rows of the open groups — a folded\n // group's rows are not shown.\n const shownRows = groups ? groups.filter((g) => isGroupOpen(g.key)).flatMap((g) => g.rows) : visible;\n const allShownSelected = shownRows.length > 0 && shownRows.every((r) => selected.has(r.id));\n const toApprove = selectedRows.filter((r) => r.text !== \"\" && r.status !== \"approved\");\n const toReset = selectedRows.filter((r) => r.review !== null);\n\n const approveSelected = async () => {\n if (await approve(\"bulk-approve\", toApprove, \"selection\")) setSelected(new Set());\n };\n const resetSelected = async () => {\n if (!onClear || toReset.length === 0) return;\n const clear = onClear;\n const keys = toReset.map((r) => ({ locale: r.locale, key: r.key }));\n if (await run(\"bulk-reset\", () => clear(keys, { origin: \"selection\", toasted: false }))) {\n setSelected(new Set());\n }\n };\n\n // The status options: \"missing\" only where something is (or it is the filter).\n const statuses = REVIEW_STATUSES.filter((s) => s !== \"missing\" || summary.missing > 0 || filter.status === s);\n const scopeNames = (areas ?? []).map((area) => areaLabels?.[area] ?? area);\n const scopeText =\n scopeNames.length > 0 ? new Intl.ListFormat(locale, { type: \"conjunction\", style: \"long\" }).format(scopeNames) : \"\";\n\n const columns: DataTableColumn<TranslationRow>[] = [\n {\n key: \"key\",\n header: labels.key,\n cell: (r) => <code className=\"break-all text-xs text-[var(--text-secondary)]\">{r.key}</code>,\n sortBy: (r) => r.key,\n mobileHidden: true,\n },\n {\n key: \"reference\",\n header: referenceLabel,\n cell: (r) => (\n <span className=\"line-clamp-3 whitespace-pre-line break-words text-[var(--text-secondary)]\">{r.reference}</span>\n ),\n mobileHidden: true,\n },\n {\n key: \"text\",\n header: localeLabel,\n cell: (r) => <TextCell row={r} labels={labels} clamp />,\n mobilePrimary: true,\n },\n {\n key: \"status\",\n header: labels.statusColumn,\n cell: (r) => (\n <span className=\"inline-flex flex-wrap gap-1\">\n <ReviewStatusChip status={r.status} labels={labelsProp} />\n {r.placeholderMismatch && (\n <Chip size=\"sm\" tone=\"danger\" variant=\"outline\">\n {labels.placeholderChip}\n </Chip>\n )}\n </span>\n ),\n sortBy: (r) => REVIEW_STATUSES.indexOf(r.status),\n },\n ];\n if (editable) {\n columns.push({\n key: \"approve\",\n header: <span className=\"sr-only\">{labels.approve}</span>,\n cell: (r) =>\n r.status === \"approved\" || r.text === \"\" ? null : (\n <IconButton\n type=\"button\"\n size=\"xs\"\n tone=\"muted\"\n commit\n label={labels.approve}\n // The icon alone at every text size (docs/text-size-harmonization.md §10.8):\n // the row's one action, in a hugging column of a dense table beside the\n // string under review, which \"Approve\" as text at Large would squeeze. A\n // single action stays inline in `RowActions` too, so the menu would not help;\n // the swipe and the editor say it in words.\n labelVisible={false}\n pending={isQueued(`row:${r.id}`)}\n disabled={busy}\n onClick={(event) => {\n // The row's own click opens the editor; this one only approves.\n event.stopPropagation();\n void approve(`row:${r.id}`, [r], \"row\", r.key);\n }}\n >\n <Check />\n </IconButton>\n ),\n });\n }\n\n /** One bound swipe action on one row, as {@link SwipeableRow} draws and commits it. */\n const swipeAction = (r: TranslationRow, id: TranslationReviewSwipeAction, groupKey?: string): SwipeAction => {\n const missing = r.text === \"\";\n switch (id) {\n case \"approve\":\n return {\n label: labels.approve,\n icon: <Check className=\"size-4\" aria-hidden />,\n onCommit: () => void approve(`row:${r.id}`, [r], \"swipe\", r.key),\n // Text on the fill in its own contrast colour: the fills are pastels in dark\n // mode, where SwipeableRow's default white text was unreadable (0.26).\n className: \"bg-[var(--success)] text-[var(--success-contrast)]\",\n armedClassName: \"bg-[var(--success)] text-[var(--success-contrast)]\",\n };\n case \"edit\":\n return {\n label: missing ? labels.suggest : labels.flag,\n icon: <PencilLine className=\"size-4\" aria-hidden />,\n // Opens the editor rather than sending the string back: \"needs a change\"\n // with neither a note nor a better wording tells nobody anything.\n onCommit: () => {\n keepGroupOpen(groupKey);\n setOpenId(r.id);\n setFocusId(r.id);\n },\n className: missing\n ? \"bg-[var(--brand)] text-[var(--brand-contrast)]\"\n : \"bg-[var(--danger)] text-[var(--danger-contrast)]\",\n armedClassName: missing\n ? \"bg-[var(--brand-hover)] text-[var(--brand-contrast)]\"\n : \"bg-[var(--danger-hover)] text-[var(--danger-contrast)]\",\n };\n case \"clear\":\n return {\n label: labels.reset,\n icon: <RotateCcw className=\"size-4\" aria-hidden />,\n onCommit: () => void clearRow(r),\n // A step back, not a verdict: neither the approval's green nor the send-back's red.\n // The surface colour as text: white on the grey in light mode, the dark page on\n // the light grey in dark mode.\n className: \"bg-[var(--text-muted)] text-[var(--bg-surface)]\",\n armedClassName: \"bg-[var(--text-secondary)] text-[var(--bg-surface)]\",\n };\n }\n };\n\n /**\n * A phone card's swipes: the binding ({@link TranslationReviewPanelProps.swipe}) resolved\n * against the row, each side's ladder closed up over what the row cannot offer. None\n * where the row must not be acted on — the same conditions under which its buttons are\n * locked or absent.\n */\n const swipeActions = (r: TranslationRow, groupKey?: string): MobileSwipeActions | null => {\n if (!binding || !editable || lock.locked || isQueued(`row:${r.id}`)) return null;\n const plan = translationReviewSwipePlan(binding, r, { canClear: onClear !== undefined });\n if (plan.start.length === 0 && plan.end.length === 0) return null;\n return {\n start: plan.start.map((id) => swipeAction(r, id, groupKey)),\n end: plan.end.map((id) => swipeAction(r, id, groupKey)),\n };\n };\n\n const toggleIn = (prev: ReadonlySet<string>, many: readonly TranslationRow[], checked: boolean) => {\n const next = new Set([...prev].filter((id) => visibleIds.has(id)));\n for (const r of many) {\n if (checked) next.add(r.id);\n else next.delete(r.id);\n }\n return next;\n };\n /** The checkbox column of a table over `list` — the whole list, or one group. */\n const selectionFor = (list: readonly TranslationRow[]): DataTableProps<TranslationRow>[\"selection\"] => {\n if (!editable) return undefined;\n const picked = list.filter((r) => selected.has(r.id)).length;\n return {\n isSelected: (r) => selected.has(r.id),\n onToggle: (r, checked) => setSelected((prev) => toggleIn(prev, [r], checked)),\n onToggleMany: (many, checked) => setSelected((prev) => toggleIn(prev, many, checked)),\n allSelected: list.length > 0 && picked === list.length,\n someSelected: picked > 0 && picked < list.length,\n onToggleAll: (checked) => setSelected((prev) => toggleIn(prev, list, checked)),\n };\n };\n\n const table = (list: TranslationRow[], group?: { key: string; name: string }) => (\n <DataTable<TranslationRow>\n rows={list}\n rowKey={(r) => r.id}\n columns={columns}\n empty={labels.empty}\n defaultPageSize={pageSize}\n {...(group\n ? {\n // Inside the group's own card: no frame in a frame, no column rail per group,\n // no scroller per group, and the group's name as the table's.\n frame: false,\n chrome: \"minimal\" as const,\n maxBodyHeight: \"none\",\n sorts: groupSorts,\n onSortsChange: setGroupSorts,\n labels: { table: group.name },\n }\n : { storageKey })}\n isExpanded={(r) => r.id === openId}\n onRowClick={(r) => {\n if (openId !== r.id) keepGroupOpen(group?.key);\n setFocusId(null);\n setOpenId((open) => (open === r.id ? null : r.id));\n }}\n expandedRow={(r) => (\n <TranslationReviewEditor\n key={r.id}\n row={r}\n referenceLabel={referenceLabel}\n localeLabel={localeLabel}\n readOnly={!editable}\n focusWording={focusId === r.id}\n onSave={onSave ? (write) => saveFromEditor(r, write) : undefined}\n onClear={onClear ? (key) => onClear([key], { origin: \"editor\", toasted: false }) : undefined}\n onClose={() => {\n setOpenId(null);\n setFocusId(null);\n }}\n formatDate={formatDate}\n formatError={formatError}\n labels={labelsProp}\n />\n )}\n mobileCard={(r) => (\n <div className=\"min-w-0 space-y-1\">\n <code className=\"block break-all text-xs text-[var(--text-muted)]\">{r.key}</code>\n <div className=\"text-sm text-[var(--text-primary)]\">\n <TextCell row={r} labels={labels} clamp />\n </div>\n <div className=\"flex flex-wrap gap-1\">\n <ReviewStatusChip status={r.status} labels={labelsProp} />\n {r.placeholderMismatch && (\n <Chip size=\"sm\" tone=\"danger\" variant=\"outline\">\n {labels.placeholderChip}\n </Chip>\n )}\n </div>\n </div>\n )}\n mobileSwipeActions={binding ? (r) => swipeActions(r, group?.key) : undefined}\n selection={selectionFor(list)}\n />\n );\n\n const groupSection = (g: TranslationRowGroup, index: number) => {\n const name = groupName(g.key);\n const titleId = `${baseId}-group-${index}`;\n const questionId = `${baseId}-confirm-${index}`;\n const targets = editable ? unreviewedRows(g.rows) : [];\n const key = `group:${g.key}`;\n const open = isGroupOpen(g.key);\n // A folded group renders its header and nothing else. Its body is handed over only\n // while it is open, or when the REVIEWER folded it — so the fold animates over the\n // rows it hides. A group folded by the default gets none: when the default turns (a\n // search cleared over 122 areas), the groups that close would otherwise render their\n // grown tables for the length of the fold.\n const body = open || groupOpen[g.key] === false;\n return (\n <Disclosure\n key={g.key}\n data-review-group={g.key}\n headingAs={groupHeadingAs}\n title={<span id={titleId}>{name}</span>}\n hint={labels.groupCount(g.summary.unreviewed, g.summary.total)}\n open={open}\n onOpenChange={(next) => {\n setGroupOpenFor(g.key, next);\n if (!next) setConfirming((c) => (c === g.key ? null : c));\n }}\n trailing={\n targets.length > 0 ? (\n <Button\n variant=\"secondary\"\n size=\"sm\"\n commit\n // The visible words say what; the group's name, read after them, says where.\n aria-describedby={titleId}\n pending={isQueued(key) && confirming !== g.key}\n disabled={busy}\n onClick={() => void approveGroup(g, false)}\n >\n <Check className=\"size-4\" aria-hidden />\n {labels.approveGroup(targets.length)}\n </Button>\n ) : undefined\n }\n bodyClassName=\"space-y-0 px-0 pb-0 border-t border-[var(--border)]\"\n >\n {body && confirming === g.key && targets.length > 0 && (\n <div className=\"border-b border-[var(--border)] p-3\">\n <AlertBanner tone=\"warning\" size=\"sm\">\n <div className=\"min-w-0 space-y-2\">\n <p id={questionId}>{labels.confirmGroup(targets.length, name)}</p>\n <div className=\"flex flex-wrap gap-2\">\n <Button\n // Focus lands here when the question appears: the expected answer to a\n // question the reviewer just asked for, and Undo stays behind it.\n ref={(el) => {\n if (el && confirmFocus.current === g.key) {\n confirmFocus.current = null;\n el.focus();\n }\n }}\n variant=\"primary\"\n size=\"sm\"\n commit\n aria-describedby={questionId}\n pending={isQueued(key)}\n disabled={busy}\n onClick={() => void approveGroup(g, true)}\n >\n {labels.approveGroup(targets.length)}\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n disabled={isQueued(key)}\n onClick={() => {\n setConfirming(null);\n keepFocusAt(g.key);\n }}\n >\n {labels.cancel}\n </Button>\n </div>\n </div>\n </AlertBanner>\n </div>\n )}\n {body && table(g.rows, { key: g.key, name })}\n </Disclosure>\n );\n };\n return (\n <div ref={rootRef} className={cn(\"min-w-0 space-y-4\", className)}>\n {scopeText && (\n <AlertBanner tone=\"info\" variant=\"inline\" size=\"sm\" block live={false}>\n {labels.scope(scopeText)}\n </AlertBanner>\n )}\n {readOnly && (\n <AlertBanner tone=\"info\" variant=\"inline\" size=\"sm\" block live={false}>\n {labels.readOnly}\n </AlertBanner>\n )}\n\n {progress && <TranslationProgress summary={summary} labels={labelsProp} />}\n\n <ToggleGroup<ReviewStatus | \"all\">\n aria-label={labels.statusFilter}\n overflow=\"wrap\"\n size=\"sm\"\n value={filter.status}\n onChange={(next) => setFilter({ status: next })}\n options={[\n { value: \"all\", label: labels.filterCount(labels.all, summary.total) },\n ...statuses.map((s) => ({ value: s, label: labels.filterCount(reviewStatusLabel(labels, s), summary[s]) })),\n ]}\n />\n\n <div className=\"flex flex-col gap-3 md:flex-row md:flex-wrap md:items-end\">\n {sources.length > 1 && (\n <div className=\"md:w-48\">\n <Select label={labels.source} value={source} onChange={(e) => setFilter({ source: e.target.value })}>\n <option value=\"all\">{labels.allSources}</option>\n {sources.map((s) => (\n <option key={s} value={s}>\n {sourceLabels?.[s] ?? s}\n </option>\n ))}\n </Select>\n </div>\n )}\n <div className=\"md:w-56\">\n <Select label={labels.namespace} value={namespace} onChange={(e) => setFilter({ namespace: e.target.value })}>\n <option value=\"all\">{labels.allNamespaces}</option>\n {namespaces.map((ns) => (\n <option key={ns} value={ns}>\n {ns}\n </option>\n ))}\n </Select>\n </div>\n <SearchField\n className=\"min-w-0 md:flex-1\"\n label={labels.search}\n value={filter.query}\n onChange={(next) => setFilter({ query: next })}\n />\n <Checkbox\n label={labels.placeholdersOnly(mismatches)}\n checked={filter.placeholdersOnly}\n onCheckedChange={(next) => setFilter({ placeholdersOnly: next })}\n />\n </div>\n\n {editable && shownRows.length > 0 && (\n // The phone's table has no checkboxes; this is its way into the bulk actions.\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"md:hidden\"\n disabled={allShownSelected}\n onClick={() => setSelected((prev) => toggleIn(prev, shownRows, true))}\n >\n <ListChecks className=\"size-4\" aria-hidden />\n {labels.selectShown}\n </Button>\n )}\n\n {failure && (\n <AlertBanner tone=\"danger\" size=\"sm\" role=\"alert\">\n {failure}\n </AlertBanner>\n )}\n\n {editable && (\n <BulkActionBar count={selectedRows.length} onClear={() => setSelected(new Set())}>\n <Button\n variant=\"primary\"\n size=\"sm\"\n commit\n pending={isQueued(\"bulk-approve\")}\n disabled={busy || toApprove.length === 0}\n onClick={() => void approveSelected()}\n >\n <Check className=\"size-4\" aria-hidden />\n {labels.approveSelected}\n </Button>\n {onClear && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n commit\n pending={isQueued(\"bulk-reset\")}\n disabled={busy || toReset.length === 0}\n onClick={() => void resetSelected()}\n >\n <RotateCcw className=\"size-4\" aria-hidden />\n {labels.resetSelected}\n </Button>\n )}\n </BulkActionBar>\n )}\n\n {groups && groups.length > 0 ? (\n <div className=\"space-y-3\">{groups.map(groupSection)}</div>\n ) : (\n table(visible)\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["ReviewStatusChip","TranslationProgress","TranslationReviewEditor","DEFAULT_TRANSLATION_REVIEW_LABELS"]}
@@ -5,6 +5,7 @@ import { ChevronRight } from "lucide-react";
5
5
  import { cn } from "../lib/cn.js";
6
6
  import { horizontalStep } from "../lib/direction.js";
7
7
  import { Spinner } from "./ui.js";
8
+ import { FOCUS_RING } from "./focus-ring.js";
8
9
  const DEFAULT_INDENT = 16;
9
10
  const CHEVRON = 16;
10
11
  const ROW_PAD = 4;
@@ -72,7 +73,7 @@ function TreeRow({
72
73
  e.stopPropagation();
73
74
  onToggle();
74
75
  },
75
- className: "flex size-4 shrink-0 items-center justify-center rounded text-[var(--text-muted)] outline-none hover:text-[var(--text-primary)] focus-visible:ring-2 focus-visible:ring-[var(--brand)]",
76
+ className: cn("flex size-4 shrink-0 items-center justify-center rounded text-[var(--text-muted)] outline-none hover:text-[var(--text-primary)]", FOCUS_RING),
76
77
  children: chevron
77
78
  }
78
79
  ) : expandable ? /* @__PURE__ */ jsx("span", { "data-tree-toggle": "", className: "flex size-4 shrink-0 cursor-pointer items-center justify-center text-[var(--text-muted)]", children: chevron }) : /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "size-4 shrink-0" }),
@@ -329,7 +330,7 @@ function TreeView({
329
330
  onFocus: (e) => {
330
331
  if (e.target === e.currentTarget) setFocusedId(node.id);
331
332
  },
332
- className: "outline-none [&:focus-visible>[data-tree-row]]:ring-2 [&:focus-visible>[data-tree-row]]:ring-[var(--brand)]",
333
+ className: "outline-none [&:focus-visible>[data-tree-row]]:ring-[length:var(--focus-ring-width)] [&:focus-visible>[data-tree-row]]:ring-[var(--brand)]",
333
334
  children: [
334
335
  /* @__PURE__ */ jsx(
335
336
  TreeRow,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/tree-view.tsx"],"sourcesContent":["import { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, MouseEvent, ReactNode } from \"react\";\nimport { ChevronRight } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { Spinner } from \"./ui\";\n\n/**\n * A hierarchy you walk with the keyboard: kastlan's chart of accounts (Assets › Current\n * assets › Bank › 1020 Operating account), and — through {@link TreeRow} alone — the\n * row labels of its gantt, which indent and fold like a tree but sit beside a timeline\n * and so cannot be one.\n *\n * Both apps had written it as nested `<div>`s with a chevron button per row: every\n * row a Tab stop (a 300-account chart was 300 presses to get past), no way to say\n * \"level 3 of 5, collapsed\" to a screen reader, and no ←/→ at all. This is the WAI-ARIA\n * tree pattern instead:\n *\n * - `role=\"tree\"` › `treeitem` (with `aria-level`, `aria-expanded` on a parent,\n * `aria-selected` when selectable) › `role=\"group\"` for its children — so the\n * reader announces the position and the state it needs.\n * - ONE Tab stop (roving tabindex). ↑/↓ walk the visible rows, → opens a closed\n * parent and then steps into it, ← closes an open one and then steps out to the\n * parent, Home/End jump to the ends, `*` opens every sibling, and typing a few\n * letters jumps to the next row that starts with them. ←/→ follow the reading\n * direction (`horizontalStep`): in an RTL tree the children hang off to the left,\n * and ← is what opens them.\n * - Enter / Space, or a click, select. The chevron alone toggles on click.\n *\n * `expanded` and `selected` each work controlled or uncontrolled.\n */\n\n/* ── Nodes ───────────────────────────────────────────────────────────────── */\n\nexport interface TreeNode<T = unknown> {\n /** Unique across the whole tree — it is what `expanded` and `selected` hold. */\n id: string;\n label: ReactNode;\n /** The text type-ahead matches. Defaults to the row's rendered text. */\n textValue?: string;\n /** Before the label: an account-type icon, a colour dot. */\n icon?: ReactNode;\n /** After the label, pushed to the end of the row: a balance, a count, an action. */\n trailing?: ReactNode;\n children?: readonly TreeNode<T>[];\n /**\n * The node has children that are not loaded yet — it gets a chevron, and opening it\n * calls `loadChildren`. Pass the children in `items` once they arrive.\n */\n hasChildren?: boolean;\n /** Focusable and announced as unavailable (`aria-disabled`), but not selectable. */\n disabled?: boolean;\n /** Anything of the host's own, handed back to `renderItem` and `onSelectedChange`. */\n data?: T;\n}\n\n/** What {@link TreeViewProps.renderItem} is told about a row. */\nexport interface TreeItemState {\n level: number;\n expanded: boolean;\n selected: boolean;\n /** It has children, loaded or not — it shows a chevron. */\n expandable: boolean;\n loading: boolean;\n disabled: boolean;\n}\n\n/* ── TreeRow ─────────────────────────────────────────────────────────────── */\n\nexport interface TreeRowProps extends ComponentPropsWithoutRef<\"div\"> {\n /** 1 for a top-level row. Sets the indent and the number of guides. */\n level: number;\n /** `true`/`false` for a row that can fold; leave it `undefined` for a leaf, which\n * gets a chevron-sized gap instead so labels at one level still line up. */\n expanded?: boolean;\n /**\n * Make the chevron a real button (with `aria-expanded`, named by the label) that\n * calls this. For a row used ON ITS OWN — kastlan's gantt labels, where each row is\n * a line of a timeline and not an item of a tree widget. Inside {@link TreeView} it\n * is omitted: the tree owns the keys, and a button per row would be a Tab stop per\n * row, which is what the tree exists to remove.\n */\n onToggle?: () => void;\n /** The toggle button's name. By default it is named by the row's label (the state\n * is `aria-expanded`'s to say); required when `children` replaces the label. */\n toggleLabel?: string;\n label: ReactNode;\n icon?: ReactNode;\n trailing?: ReactNode;\n /** Pixels per level. Default 16. */\n indent?: number;\n /** Draw a vertical hairline per ancestor level. Default `true`. */\n guides?: boolean;\n selected?: boolean;\n /** A spinner in place of the chevron, while lazy children load. */\n loading?: boolean;\n /** Replace the label + trailing part of the row (the indent and chevron stay). */\n children?: ReactNode;\n}\n\nconst DEFAULT_INDENT = 16;\n/** The chevron's box; the guide for a level runs through its centre. */\nconst CHEVRON = 16;\n/** The row's start padding, before any indent. */\nconst ROW_PAD = 4;\n\n/**\n * One row: indent, guides, chevron, icon, label, trailing — no tree semantics of its\n * own. {@link TreeView} is built from it, and a host that needs the look without the\n * widget (a gantt's label column) uses it directly.\n *\n * Laid out with logical properties throughout (`padding-inline-start`,\n * `inset-inline-start`), so the indent, the guides and the chevron all hang from the\n * reading start in an RTL layout without a second code path.\n */\nexport function TreeRow({\n level,\n expanded,\n onToggle,\n toggleLabel,\n label,\n icon,\n trailing,\n indent = DEFAULT_INDENT,\n guides = true,\n selected,\n loading,\n children,\n className,\n style,\n ...rest\n}: TreeRowProps) {\n const labelId = useId();\n const depth = Math.max(0, level - 1);\n const expandable = expanded !== undefined;\n // A right-pointing chevron that turns DOWN when open. In RTL it is mirrored to point\n // left, and turning that one clockwise would point it UP — so it turns the other way.\n const chevron = (\n <ChevronRight\n aria-hidden\n className={cn(\n \"size-4 transition-transform motion-reduce:transition-none rtl:-scale-x-100\",\n expanded && \"rotate-90 rtl:-rotate-90\",\n )}\n />\n );\n return (\n <div\n {...rest}\n data-tree-row=\"\"\n data-selected={selected || undefined}\n style={{ paddingInlineStart: ROW_PAD + depth * indent, ...style }}\n className={cn(\n \"relative flex min-h-8 items-center gap-1.5 rounded pe-2 text-sm\",\n selected\n ? \"bg-[var(--bg-active)] font-medium text-[var(--text-primary)]\"\n : \"text-[var(--text-secondary)]\",\n className,\n )}\n >\n {guides &&\n Array.from({ length: depth }, (_, i) => (\n <span\n key={i}\n aria-hidden\n data-tree-guide=\"\"\n className=\"pointer-events-none absolute inset-y-0 w-px bg-[var(--border)]\"\n style={{ insetInlineStart: ROW_PAD + i * indent + CHEVRON / 2 }}\n />\n ))}\n {loading ? (\n <span className=\"flex size-4 shrink-0 items-center justify-center\">\n <Spinner label={null} className=\"size-3 border\" />\n </span>\n ) : expandable && onToggle ? (\n <button\n type=\"button\"\n aria-expanded={expanded}\n aria-label={toggleLabel}\n aria-labelledby={toggleLabel ? undefined : labelId}\n onClick={(e) => {\n e.stopPropagation();\n onToggle();\n }}\n className=\"flex size-4 shrink-0 items-center justify-center rounded text-[var(--text-muted)] outline-none hover:text-[var(--text-primary)] focus-visible:ring-2 focus-visible:ring-[var(--brand)]\"\n >\n {chevron}\n </button>\n ) : expandable ? (\n <span data-tree-toggle=\"\" className=\"flex size-4 shrink-0 cursor-pointer items-center justify-center text-[var(--text-muted)]\">\n {chevron}\n </span>\n ) : (\n <span aria-hidden className=\"size-4 shrink-0\" />\n )}\n {children ?? (\n <>\n {icon && <span className=\"flex shrink-0 items-center\">{icon}</span>}\n <span id={labelId} data-tree-label=\"\" className=\"min-w-0 flex-1 truncate\">\n {label}\n </span>\n {trailing !== undefined && trailing !== null && (\n <span className=\"ms-auto flex shrink-0 items-center gap-1 text-xs tabular-nums text-[var(--text-muted)]\">\n {trailing}\n </span>\n )}\n </>\n )}\n </div>\n );\n}\n\n/* ── TreeView ────────────────────────────────────────────────────────────── */\n\nexport interface TreeViewProps<T = unknown>\n extends Omit<ComponentPropsWithoutRef<\"ul\">, \"onSelect\" | \"children\" | \"defaultValue\"> {\n items: readonly TreeNode<T>[];\n /** Open nodes, controlled. */\n expanded?: readonly string[];\n /** Open nodes on first render, uncontrolled. */\n defaultExpanded?: readonly string[];\n onExpandedChange?: (expanded: string[]) => void;\n /** The selected node's id, controlled; `null` for none. */\n selected?: string | null;\n defaultSelected?: string | null;\n onSelectedChange?: (id: string, node: TreeNode<T>) => void;\n /** `\"none\"` makes it a navigation-only tree: no `aria-selected`, Enter toggles. */\n selectionMode?: \"single\" | \"none\";\n /** The whole row content after the chevron, per node. Wins over `label`/`trailing`. */\n renderItem?: (node: TreeNode<T>, state: TreeItemState) => ReactNode;\n /**\n * Called when a node with `hasChildren` and no `children` is opened. While the\n * promise is pending the row shows a spinner and is `aria-busy`; put the children\n * into `items` before it resolves (or after — the row opens either way).\n */\n loadChildren?: (node: TreeNode<T>) => Promise<void> | void;\n /** Pixels per level. Default 16. */\n indent?: number;\n /** Vertical hairlines per level. Default `true`. */\n indentGuides?: boolean;\n /** Extra classes for each row (the `TreeRow`), e.g. a denser height. */\n rowClassName?: string;\n}\n\ninterface FlatNode<T> {\n node: TreeNode<T>;\n level: number;\n parentId: string | null;\n}\n\nconst isExpandable = (n: TreeNode<unknown>) => Boolean(n.children?.length || n.hasChildren);\n\n/** Controlled when `value` is given, otherwise state seeded from `initial`. */\nfunction useControllable<V>(value: V | undefined, initial: V): [V, (next: V) => void] {\n const [own, setOwn] = useState<V>(initial);\n const controlled = value !== undefined;\n const set = (next: V) => {\n if (!controlled) setOwn(next);\n };\n return [controlled ? value : own, set];\n}\n\n/** How long a type-ahead run stays open between keys — the APG's suggested half second. */\nconst TYPEAHEAD_MS = 500;\n\nexport function TreeView<T = unknown>({\n items,\n expanded: expandedProp,\n defaultExpanded,\n onExpandedChange,\n selected: selectedProp,\n defaultSelected,\n onSelectedChange,\n selectionMode = \"single\",\n renderItem,\n loadChildren,\n indent = DEFAULT_INDENT,\n indentGuides = true,\n rowClassName,\n className,\n onKeyDown,\n onClick,\n ...rest\n}: TreeViewProps<T>) {\n const [expandedList, setExpandedOwn] = useControllable<readonly string[]>(\n expandedProp,\n defaultExpanded ?? [],\n );\n const [selected, setSelectedOwn] = useControllable<string | null>(\n selectedProp,\n defaultSelected ?? null,\n );\n const expandedSet = useMemo(() => new Set(expandedList), [expandedList]);\n const [loading, setLoading] = useState<ReadonlySet<string>>(() => new Set());\n const [focusedId, setFocusedId] = useState<string | null>(null);\n const itemRefs = useRef(new Map<string, HTMLLIElement>());\n const pendingFocus = useRef<string | null>(null);\n const typeahead = useRef({ text: \"\", at: 0 });\n const baseId = useId();\n\n // Every node by id (for parents and lookups), and the rows currently on screen in\n // order — the list the arrow keys walk.\n const { byId, visible } = useMemo(() => {\n const map = new Map<string, FlatNode<T>>();\n const rows: FlatNode<T>[] = [];\n const walk = (nodes: readonly TreeNode<T>[], level: number, parentId: string | null, shown: boolean) => {\n for (const node of nodes) {\n const flat = { node, level, parentId };\n map.set(node.id, flat);\n if (shown) rows.push(flat);\n if (node.children) walk(node.children, level + 1, node.id, shown && expandedSet.has(node.id));\n }\n };\n walk(items, 1, null, true);\n return { byId: map, visible: rows };\n }, [items, expandedSet]);\n\n // The one tabbable row. Focus that was inside a branch that has since closed (a\n // click on its parent's chevron) moves up to the nearest ancestor still on screen,\n // so the tab stop never points at a row that is not rendered.\n const visibleIds = useMemo(() => new Set(visible.map((f) => f.node.id)), [visible]);\n const nearestVisible = (id: string | null): string | null => {\n let cur = id;\n while (cur && !visibleIds.has(cur)) cur = byId.get(cur)?.parentId ?? null;\n return cur;\n };\n const tabbable =\n nearestVisible(focusedId) ?? nearestVisible(selected) ?? visible[0]?.node.id ?? null;\n\n useEffect(() => {\n const id = pendingFocus.current;\n if (!id) return;\n pendingFocus.current = null;\n itemRefs.current.get(id)?.focus();\n });\n\n const focusRow = (id: string) => {\n setFocusedId(id);\n // After the commit: moving into a branch that just opened targets a row that is\n // not in the DOM yet.\n pendingFocus.current = id;\n itemRefs.current.get(id)?.focus();\n };\n\n const setExpanded = (next: Set<string>) => {\n const list = [...next];\n setExpandedOwn(list);\n onExpandedChange?.(list);\n };\n\n const open = (node: TreeNode<T>) => {\n if (expandedSet.has(node.id) || !isExpandable(node)) return;\n const next = new Set(expandedSet);\n next.add(node.id);\n setExpanded(next);\n if (loadChildren && node.hasChildren && !node.children?.length) {\n const pending = loadChildren(node);\n if (pending && typeof pending.then === \"function\") {\n setLoading((s) => new Set(s).add(node.id));\n const done = () =>\n setLoading((s) => {\n const n = new Set(s);\n n.delete(node.id);\n return n;\n });\n pending.then(done, done);\n }\n }\n };\n\n const close = (node: TreeNode<T>) => {\n if (!expandedSet.has(node.id)) return;\n const next = new Set(expandedSet);\n next.delete(node.id);\n setExpanded(next);\n };\n\n const toggle = (node: TreeNode<T>) => (expandedSet.has(node.id) ? close(node) : open(node));\n\n const select = (node: TreeNode<T>) => {\n if (node.disabled) return;\n if (selectionMode === \"none\") {\n toggle(node);\n return;\n }\n setSelectedOwn(node.id);\n onSelectedChange?.(node.id, node);\n };\n\n const idOf = (target: EventTarget | null): string | null =>\n (target as Element | null)?.closest?.<HTMLElement>('[role=\"treeitem\"]')?.dataset.treeId ?? null;\n\n const textOf = (flat: FlatNode<T>): string => {\n if (flat.node.textValue !== undefined) return flat.node.textValue;\n if (typeof flat.node.label === \"string\") return flat.node.label;\n const row = itemRefs.current.get(flat.node.id)?.querySelector(\"[data-tree-row]\");\n return row?.textContent ?? \"\";\n };\n\n const findByText = (fromIndex: number, text: string): string | null => {\n const needle = text.toLocaleLowerCase();\n for (let i = 1; i <= visible.length; i++) {\n const flat = visible[(fromIndex + i) % visible.length];\n if (textOf(flat).trim().toLocaleLowerCase().startsWith(needle)) return flat.node.id;\n }\n return null;\n };\n\n const handleKeyDown = (e: KeyboardEvent<HTMLUListElement>) => {\n onKeyDown?.(e);\n if (e.defaultPrevented) return;\n const id = idOf(e.target);\n const flat = id ? byId.get(id) : undefined;\n if (!flat) return;\n const { node } = flat;\n const index = visible.findIndex((f) => f.node.id === node.id);\n const expandedNow = expandedSet.has(node.id);\n // Forward / backward along the reading direction, from the row itself.\n const step = horizontalStep(e.key, e.target as Element);\n // A space inside a type-ahead run is part of the name being typed (\"current a…\"),\n // not a request to select.\n const inRun = Date.now() - typeahead.current.at < TYPEAHEAD_MS && typeahead.current.text !== \"\";\n const key = e.key === \" \" && inRun ? \"typeahead\" : e.key;\n\n let handled = true;\n if (step === 1) {\n if (!isExpandable(node)) handled = false;\n else if (!expandedNow) open(node);\n else if (node.children?.length) focusRow(node.children[0].id);\n } else if (step === -1) {\n if (expandedNow) close(node);\n else if (flat.parentId) focusRow(flat.parentId);\n else handled = false;\n } else {\n switch (key) {\n case \"ArrowDown\":\n if (index < visible.length - 1) focusRow(visible[index + 1].node.id);\n break;\n case \"ArrowUp\":\n if (index > 0) focusRow(visible[index - 1].node.id);\n break;\n case \"Home\":\n if (visible.length) focusRow(visible[0].node.id);\n break;\n case \"End\":\n if (visible.length) focusRow(visible[visible.length - 1].node.id);\n break;\n case \"Enter\":\n case \" \":\n select(node);\n break;\n case \"*\": {\n // Every sibling at this level, as the pattern suggests.\n const siblings = flat.parentId ? (byId.get(flat.parentId)?.node.children ?? []) : items;\n const next = new Set(expandedSet);\n for (const s of siblings) if (isExpandable(s)) next.add(s.id);\n setExpanded(next);\n break;\n }\n default: {\n const printable = e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey;\n if (!printable) {\n handled = false;\n break;\n }\n const now = Date.now();\n const run = now - typeahead.current.at < TYPEAHEAD_MS ? typeahead.current.text : \"\";\n typeahead.current = { text: run + e.key, at: now };\n const typed = typeahead.current.text;\n // \"aaa\" cycles through the rows starting with \"a\", as a file list does;\n // anything else extends the prefix, searched from the row after this one\n // (or from this one, when it still matches the longer prefix).\n const repeated = typed.length > 1 && [...typed].every((c) => c === typed[0]);\n const target = repeated\n ? findByText(index, typed[0])\n : typed.length > 1 && textOf(flat).trim().toLocaleLowerCase().startsWith(typed.toLocaleLowerCase())\n ? node.id\n : findByText(index, typed);\n if (target) focusRow(target);\n }\n }\n }\n if (handled) {\n e.preventDefault();\n e.stopPropagation();\n }\n };\n\n const handleClick = (e: MouseEvent<HTMLUListElement>) => {\n onClick?.(e);\n const id = idOf(e.target);\n const flat = id ? byId.get(id) : undefined;\n if (!flat) return;\n // Controls a host rendered into a row (an action in `trailing`) are their own.\n const control = (e.target as Element).closest(\"button, a[href], input, select, textarea\");\n if (control && itemRefs.current.get(flat.node.id)?.contains(control)) return;\n setFocusedId(flat.node.id);\n if ((e.target as Element).closest(\"[data-tree-toggle]\")) {\n toggle(flat.node);\n return;\n }\n select(flat.node);\n };\n\n const renderNodes = (nodes: readonly TreeNode<T>[], level: number, path: string): ReactNode =>\n nodes.map((node, i) => {\n // Named by its own row, not by its content: a treeitem's content is the whole\n // open branch below it, and \"Assets Current assets 1020 Bank …\" is not a name.\n // A path of indices, because a host's id may hold spaces an idref cannot.\n const rowId = `${baseId}-${path}${i}`;\n const expandable = isExpandable(node);\n const isOpen = expandable && expandedSet.has(node.id);\n const isSelected = selectionMode !== \"none\" && selected === node.id;\n const isLoading = loading.has(node.id);\n const state: TreeItemState = {\n level,\n expanded: isOpen,\n selected: isSelected,\n expandable,\n loading: isLoading,\n disabled: Boolean(node.disabled),\n };\n return (\n <li\n key={node.id}\n ref={(el) => {\n if (el) itemRefs.current.set(node.id, el);\n else itemRefs.current.delete(node.id);\n }}\n role=\"treeitem\"\n data-tree-id={node.id}\n aria-labelledby={rowId}\n aria-level={level}\n aria-expanded={expandable ? isOpen : undefined}\n aria-selected={selectionMode === \"none\" ? undefined : isSelected}\n aria-disabled={node.disabled || undefined}\n aria-busy={isLoading || undefined}\n tabIndex={node.id === tabbable ? 0 : -1}\n onFocus={(e) => {\n // Only the row itself: a focus event bubbling up from a nested row, or from\n // a control inside this one, is not this row being focused.\n if (e.target === e.currentTarget) setFocusedId(node.id);\n }}\n // The ring goes on this row's own box, not the <li>: the <li> contains the\n // whole open branch below it, and ringing that would outline a subtree.\n className=\"outline-none [&:focus-visible>[data-tree-row]]:ring-2 [&:focus-visible>[data-tree-row]]:ring-[var(--brand)]\"\n >\n <TreeRow\n id={rowId}\n level={level}\n expanded={expandable ? isOpen : undefined}\n label={node.label}\n icon={node.icon}\n trailing={node.trailing}\n indent={indent}\n guides={indentGuides}\n selected={isSelected}\n loading={isLoading}\n className={cn(\n node.disabled ? \"cursor-not-allowed opacity-50\" : \"cursor-pointer hover:bg-[var(--bg-hover)]\",\n rowClassName,\n )}\n >\n {renderItem ? renderItem(node, state) : undefined}\n </TreeRow>\n {isOpen && (\n <ul role=\"group\">\n {/* Empty while lazy children load: a \"Loading…\" line in here would be\n a child that is not a treeitem, and the parent row already says\n `aria-busy` and shows the spinner. */}\n {node.children?.length ? renderNodes(node.children, level + 1, `${path}${i}-`) : null}\n </ul>\n )}\n </li>\n );\n });\n\n return (\n <ul\n {...rest}\n role=\"tree\"\n onKeyDown={handleKeyDown}\n onClick={handleClick}\n className={cn(\"flex flex-col text-sm\", className)}\n >\n {renderNodes(items, 1, \"\")}\n </ul>\n );\n}\n"],"mappings":";AA0II,SA0DI,UA1DJ,KA0DI,YA1DJ;AA1IJ,SAAS,WAAW,OAAO,SAAS,QAAQ,gBAAgB;AAE5D,SAAS,oBAAoB;AAC7B,SAAS,UAAU;AACnB,SAAS,sBAAsB;AAC/B,SAAS,eAAe;AA+FxB,MAAM,iBAAiB;AAEvB,MAAM,UAAU;AAEhB,MAAM,UAAU;AAWT,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAiB;AACf,QAAM,UAAU,MAAM;AACtB,QAAM,QAAQ,KAAK,IAAI,GAAG,QAAQ,CAAC;AACnC,QAAM,aAAa,aAAa;AAGhC,QAAM,UACJ;AAAA,IAAC;AAAA;AAAA,MACC,eAAW;AAAA,MACX,WAAW;AAAA,QACT;AAAA,QACA,YAAY;AAAA,MACd;AAAA;AAAA,EACF;AAEF,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,iBAAc;AAAA,MACd,iBAAe,YAAY;AAAA,MAC3B,OAAO,EAAE,oBAAoB,UAAU,QAAQ,QAAQ,GAAG,MAAM;AAAA,MAChE,WAAW;AAAA,QACT;AAAA,QACA,WACI,iEACA;AAAA,QACJ;AAAA,MACF;AAAA,MAEC;AAAA,kBACC,MAAM,KAAK,EAAE,QAAQ,MAAM,GAAG,CAAC,GAAG,MAChC;AAAA,UAAC;AAAA;AAAA,YAEC,eAAW;AAAA,YACX,mBAAgB;AAAA,YAChB,WAAU;AAAA,YACV,OAAO,EAAE,kBAAkB,UAAU,IAAI,SAAS,UAAU,EAAE;AAAA;AAAA,UAJzD;AAAA,QAKP,CACD;AAAA,QACF,UACC,oBAAC,UAAK,WAAU,oDACd,8BAAC,WAAQ,OAAO,MAAM,WAAU,iBAAgB,GAClD,IACE,cAAc,WAChB;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,iBAAe;AAAA,YACf,cAAY;AAAA,YACZ,mBAAiB,cAAc,SAAY;AAAA,YAC3C,SAAS,CAAC,MAAM;AACd,gBAAE,gBAAgB;AAClB,uBAAS;AAAA,YACX;AAAA,YACA,WAAU;AAAA,YAET;AAAA;AAAA,QACH,IACE,aACF,oBAAC,UAAK,oBAAiB,IAAG,WAAU,4FACjC,mBACH,IAEA,oBAAC,UAAK,eAAW,MAAC,WAAU,mBAAkB;AAAA,QAE/C,YACC,iCACG;AAAA,kBAAQ,oBAAC,UAAK,WAAU,8BAA8B,gBAAK;AAAA,UAC5D,oBAAC,UAAK,IAAI,SAAS,mBAAgB,IAAG,WAAU,2BAC7C,iBACH;AAAA,UACC,aAAa,UAAa,aAAa,QACtC,oBAAC,UAAK,WAAU,0FACb,oBACH;AAAA,WAEJ;AAAA;AAAA;AAAA,EAEJ;AAEJ;AAwCA,MAAM,eAAe,CAAC,MAAyB,QAAQ,EAAE,UAAU,UAAU,EAAE,WAAW;AAG1F,SAAS,gBAAmB,OAAsB,SAAoC;AACpF,QAAM,CAAC,KAAK,MAAM,IAAI,SAAY,OAAO;AACzC,QAAM,aAAa,UAAU;AAC7B,QAAM,MAAM,CAAC,SAAY;AACvB,QAAI,CAAC,WAAY,QAAO,IAAI;AAAA,EAC9B;AACA,SAAO,CAAC,aAAa,QAAQ,KAAK,GAAG;AACvC;AAGA,MAAM,eAAe;AAEd,SAAS,SAAsB;AAAA,EACpC;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT,eAAe;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAqB;AACnB,QAAM,CAAC,cAAc,cAAc,IAAI;AAAA,IACrC;AAAA,IACA,mBAAmB,CAAC;AAAA,EACtB;AACA,QAAM,CAAC,UAAU,cAAc,IAAI;AAAA,IACjC;AAAA,IACA,mBAAmB;AAAA,EACrB;AACA,QAAM,cAAc,QAAQ,MAAM,IAAI,IAAI,YAAY,GAAG,CAAC,YAAY,CAAC;AACvE,QAAM,CAAC,SAAS,UAAU,IAAI,SAA8B,MAAM,oBAAI,IAAI,CAAC;AAC3E,QAAM,CAAC,WAAW,YAAY,IAAI,SAAwB,IAAI;AAC9D,QAAM,WAAW,OAAO,oBAAI,IAA2B,CAAC;AACxD,QAAM,eAAe,OAAsB,IAAI;AAC/C,QAAM,YAAY,OAAO,EAAE,MAAM,IAAI,IAAI,EAAE,CAAC;AAC5C,QAAM,SAAS,MAAM;AAIrB,QAAM,EAAE,MAAM,QAAQ,IAAI,QAAQ,MAAM;AACtC,UAAM,MAAM,oBAAI,IAAyB;AACzC,UAAM,OAAsB,CAAC;AAC7B,UAAM,OAAO,CAAC,OAA+B,OAAe,UAAyB,UAAmB;AACtG,iBAAW,QAAQ,OAAO;AACxB,cAAM,OAAO,EAAE,MAAM,OAAO,SAAS;AACrC,YAAI,IAAI,KAAK,IAAI,IAAI;AACrB,YAAI,MAAO,MAAK,KAAK,IAAI;AACzB,YAAI,KAAK,SAAU,MAAK,KAAK,UAAU,QAAQ,GAAG,KAAK,IAAI,SAAS,YAAY,IAAI,KAAK,EAAE,CAAC;AAAA,MAC9F;AAAA,IACF;AACA,SAAK,OAAO,GAAG,MAAM,IAAI;AACzB,WAAO,EAAE,MAAM,KAAK,SAAS,KAAK;AAAA,EACpC,GAAG,CAAC,OAAO,WAAW,CAAC;AAKvB,QAAM,aAAa,QAAQ,MAAM,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC;AAClF,QAAM,iBAAiB,CAAC,OAAqC;AAC3D,QAAI,MAAM;AACV,WAAO,OAAO,CAAC,WAAW,IAAI,GAAG,EAAG,OAAM,KAAK,IAAI,GAAG,GAAG,YAAY;AACrE,WAAO;AAAA,EACT;AACA,QAAM,WACJ,eAAe,SAAS,KAAK,eAAe,QAAQ,KAAK,QAAQ,CAAC,GAAG,KAAK,MAAM;AAElF,YAAU,MAAM;AACd,UAAM,KAAK,aAAa;AACxB,QAAI,CAAC,GAAI;AACT,iBAAa,UAAU;AACvB,aAAS,QAAQ,IAAI,EAAE,GAAG,MAAM;AAAA,EAClC,CAAC;AAED,QAAM,WAAW,CAAC,OAAe;AAC/B,iBAAa,EAAE;AAGf,iBAAa,UAAU;AACvB,aAAS,QAAQ,IAAI,EAAE,GAAG,MAAM;AAAA,EAClC;AAEA,QAAM,cAAc,CAAC,SAAsB;AACzC,UAAM,OAAO,CAAC,GAAG,IAAI;AACrB,mBAAe,IAAI;AACnB,uBAAmB,IAAI;AAAA,EACzB;AAEA,QAAM,OAAO,CAAC,SAAsB;AAClC,QAAI,YAAY,IAAI,KAAK,EAAE,KAAK,CAAC,aAAa,IAAI,EAAG;AACrD,UAAM,OAAO,IAAI,IAAI,WAAW;AAChC,SAAK,IAAI,KAAK,EAAE;AAChB,gBAAY,IAAI;AAChB,QAAI,gBAAgB,KAAK,eAAe,CAAC,KAAK,UAAU,QAAQ;AAC9D,YAAM,UAAU,aAAa,IAAI;AACjC,UAAI,WAAW,OAAO,QAAQ,SAAS,YAAY;AACjD,mBAAW,CAAC,MAAM,IAAI,IAAI,CAAC,EAAE,IAAI,KAAK,EAAE,CAAC;AACzC,cAAM,OAAO,MACX,WAAW,CAAC,MAAM;AAChB,gBAAM,IAAI,IAAI,IAAI,CAAC;AACnB,YAAE,OAAO,KAAK,EAAE;AAChB,iBAAO;AAAA,QACT,CAAC;AACH,gBAAQ,KAAK,MAAM,IAAI;AAAA,MACzB;AAAA,IACF;AAAA,EACF;AAEA,QAAM,QAAQ,CAAC,SAAsB;AACnC,QAAI,CAAC,YAAY,IAAI,KAAK,EAAE,EAAG;AAC/B,UAAM,OAAO,IAAI,IAAI,WAAW;AAChC,SAAK,OAAO,KAAK,EAAE;AACnB,gBAAY,IAAI;AAAA,EAClB;AAEA,QAAM,SAAS,CAAC,SAAuB,YAAY,IAAI,KAAK,EAAE,IAAI,MAAM,IAAI,IAAI,KAAK,IAAI;AAEzF,QAAM,SAAS,CAAC,SAAsB;AACpC,QAAI,KAAK,SAAU;AACnB,QAAI,kBAAkB,QAAQ;AAC5B,aAAO,IAAI;AACX;AAAA,IACF;AACA,mBAAe,KAAK,EAAE;AACtB,uBAAmB,KAAK,IAAI,IAAI;AAAA,EAClC;AAEA,QAAM,OAAO,CAAC,WACX,QAA2B,UAAuB,mBAAmB,GAAG,QAAQ,UAAU;AAE7F,QAAM,SAAS,CAAC,SAA8B;AAC5C,QAAI,KAAK,KAAK,cAAc,OAAW,QAAO,KAAK,KAAK;AACxD,QAAI,OAAO,KAAK,KAAK,UAAU,SAAU,QAAO,KAAK,KAAK;AAC1D,UAAM,MAAM,SAAS,QAAQ,IAAI,KAAK,KAAK,EAAE,GAAG,cAAc,iBAAiB;AAC/E,WAAO,KAAK,eAAe;AAAA,EAC7B;AAEA,QAAM,aAAa,CAAC,WAAmB,SAAgC;AACrE,UAAM,SAAS,KAAK,kBAAkB;AACtC,aAAS,IAAI,GAAG,KAAK,QAAQ,QAAQ,KAAK;AACxC,YAAM,OAAO,SAAS,YAAY,KAAK,QAAQ,MAAM;AACrD,UAAI,OAAO,IAAI,EAAE,KAAK,EAAE,kBAAkB,EAAE,WAAW,MAAM,EAAG,QAAO,KAAK,KAAK;AAAA,IACnF;AACA,WAAO;AAAA,EACT;AAEA,QAAM,gBAAgB,CAAC,MAAuC;AAC5D,gBAAY,CAAC;AACb,QAAI,EAAE,iBAAkB;AACxB,UAAM,KAAK,KAAK,EAAE,MAAM;AACxB,UAAM,OAAO,KAAK,KAAK,IAAI,EAAE,IAAI;AACjC,QAAI,CAAC,KAAM;AACX,UAAM,EAAE,KAAK,IAAI;AACjB,UAAM,QAAQ,QAAQ,UAAU,CAAC,MAAM,EAAE,KAAK,OAAO,KAAK,EAAE;AAC5D,UAAM,cAAc,YAAY,IAAI,KAAK,EAAE;AAE3C,UAAM,OAAO,eAAe,EAAE,KAAK,EAAE,MAAiB;AAGtD,UAAM,QAAQ,KAAK,IAAI,IAAI,UAAU,QAAQ,KAAK,gBAAgB,UAAU,QAAQ,SAAS;AAC7F,UAAM,MAAM,EAAE,QAAQ,OAAO,QAAQ,cAAc,EAAE;AAErD,QAAI,UAAU;AACd,QAAI,SAAS,GAAG;AACd,UAAI,CAAC,aAAa,IAAI,EAAG,WAAU;AAAA,eAC1B,CAAC,YAAa,MAAK,IAAI;AAAA,eACvB,KAAK,UAAU,OAAQ,UAAS,KAAK,SAAS,CAAC,EAAE,EAAE;AAAA,IAC9D,WAAW,SAAS,IAAI;AACtB,UAAI,YAAa,OAAM,IAAI;AAAA,eAClB,KAAK,SAAU,UAAS,KAAK,QAAQ;AAAA,UACzC,WAAU;AAAA,IACjB,OAAO;AACL,cAAQ,KAAK;AAAA,QACX,KAAK;AACH,cAAI,QAAQ,QAAQ,SAAS,EAAG,UAAS,QAAQ,QAAQ,CAAC,EAAE,KAAK,EAAE;AACnE;AAAA,QACF,KAAK;AACH,cAAI,QAAQ,EAAG,UAAS,QAAQ,QAAQ,CAAC,EAAE,KAAK,EAAE;AAClD;AAAA,QACF,KAAK;AACH,cAAI,QAAQ,OAAQ,UAAS,QAAQ,CAAC,EAAE,KAAK,EAAE;AAC/C;AAAA,QACF,KAAK;AACH,cAAI,QAAQ,OAAQ,UAAS,QAAQ,QAAQ,SAAS,CAAC,EAAE,KAAK,EAAE;AAChE;AAAA,QACF,KAAK;AAAA,QACL,KAAK;AACH,iBAAO,IAAI;AACX;AAAA,QACF,KAAK,KAAK;AAER,gBAAM,WAAW,KAAK,WAAY,KAAK,IAAI,KAAK,QAAQ,GAAG,KAAK,YAAY,CAAC,IAAK;AAClF,gBAAM,OAAO,IAAI,IAAI,WAAW;AAChC,qBAAW,KAAK,SAAU,KAAI,aAAa,CAAC,EAAG,MAAK,IAAI,EAAE,EAAE;AAC5D,sBAAY,IAAI;AAChB;AAAA,QACF;AAAA,QACA,SAAS;AACP,gBAAM,YAAY,EAAE,IAAI,WAAW,KAAK,CAAC,EAAE,WAAW,CAAC,EAAE,WAAW,CAAC,EAAE;AACvE,cAAI,CAAC,WAAW;AACd,sBAAU;AACV;AAAA,UACF;AACA,gBAAM,MAAM,KAAK,IAAI;AACrB,gBAAM,MAAM,MAAM,UAAU,QAAQ,KAAK,eAAe,UAAU,QAAQ,OAAO;AACjF,oBAAU,UAAU,EAAE,MAAM,MAAM,EAAE,KAAK,IAAI,IAAI;AACjD,gBAAM,QAAQ,UAAU,QAAQ;AAIhC,gBAAM,WAAW,MAAM,SAAS,KAAK,CAAC,GAAG,KAAK,EAAE,MAAM,CAAC,MAAM,MAAM,MAAM,CAAC,CAAC;AAC3E,gBAAM,SAAS,WACX,WAAW,OAAO,MAAM,CAAC,CAAC,IAC1B,MAAM,SAAS,KAAK,OAAO,IAAI,EAAE,KAAK,EAAE,kBAAkB,EAAE,WAAW,MAAM,kBAAkB,CAAC,IAC9F,KAAK,KACL,WAAW,OAAO,KAAK;AAC7B,cAAI,OAAQ,UAAS,MAAM;AAAA,QAC7B;AAAA,MACF;AAAA,IACF;AACA,QAAI,SAAS;AACX,QAAE,eAAe;AACjB,QAAE,gBAAgB;AAAA,IACpB;AAAA,EACF;AAEA,QAAM,cAAc,CAAC,MAAoC;AACvD,cAAU,CAAC;AACX,UAAM,KAAK,KAAK,EAAE,MAAM;AACxB,UAAM,OAAO,KAAK,KAAK,IAAI,EAAE,IAAI;AACjC,QAAI,CAAC,KAAM;AAEX,UAAM,UAAW,EAAE,OAAmB,QAAQ,0CAA0C;AACxF,QAAI,WAAW,SAAS,QAAQ,IAAI,KAAK,KAAK,EAAE,GAAG,SAAS,OAAO,EAAG;AACtE,iBAAa,KAAK,KAAK,EAAE;AACzB,QAAK,EAAE,OAAmB,QAAQ,oBAAoB,GAAG;AACvD,aAAO,KAAK,IAAI;AAChB;AAAA,IACF;AACA,WAAO,KAAK,IAAI;AAAA,EAClB;AAEA,QAAM,cAAc,CAAC,OAA+B,OAAe,SACjE,MAAM,IAAI,CAAC,MAAM,MAAM;AAIrB,UAAM,QAAQ,GAAG,MAAM,IAAI,IAAI,GAAG,CAAC;AACnC,UAAM,aAAa,aAAa,IAAI;AACpC,UAAM,SAAS,cAAc,YAAY,IAAI,KAAK,EAAE;AACpD,UAAM,aAAa,kBAAkB,UAAU,aAAa,KAAK;AACjE,UAAM,YAAY,QAAQ,IAAI,KAAK,EAAE;AACrC,UAAM,QAAuB;AAAA,MAC3B;AAAA,MACA,UAAU;AAAA,MACV,UAAU;AAAA,MACV;AAAA,MACA,SAAS;AAAA,MACT,UAAU,QAAQ,KAAK,QAAQ;AAAA,IACjC;AACA,WACE;AAAA,MAAC;AAAA;AAAA,QAEC,KAAK,CAAC,OAAO;AACX,cAAI,GAAI,UAAS,QAAQ,IAAI,KAAK,IAAI,EAAE;AAAA,cACnC,UAAS,QAAQ,OAAO,KAAK,EAAE;AAAA,QACtC;AAAA,QACA,MAAK;AAAA,QACL,gBAAc,KAAK;AAAA,QACnB,mBAAiB;AAAA,QACjB,cAAY;AAAA,QACZ,iBAAe,aAAa,SAAS;AAAA,QACrC,iBAAe,kBAAkB,SAAS,SAAY;AAAA,QACtD,iBAAe,KAAK,YAAY;AAAA,QAChC,aAAW,aAAa;AAAA,QACxB,UAAU,KAAK,OAAO,WAAW,IAAI;AAAA,QACrC,SAAS,CAAC,MAAM;AAGd,cAAI,EAAE,WAAW,EAAE,cAAe,cAAa,KAAK,EAAE;AAAA,QACxD;AAAA,QAGA,WAAU;AAAA,QAEV;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,IAAI;AAAA,cACJ;AAAA,cACA,UAAU,aAAa,SAAS;AAAA,cAChC,OAAO,KAAK;AAAA,cACZ,MAAM,KAAK;AAAA,cACX,UAAU,KAAK;AAAA,cACf;AAAA,cACA,QAAQ;AAAA,cACR,UAAU;AAAA,cACV,SAAS;AAAA,cACT,WAAW;AAAA,gBACT,KAAK,WAAW,kCAAkC;AAAA,gBAClD;AAAA,cACF;AAAA,cAEC,uBAAa,WAAW,MAAM,KAAK,IAAI;AAAA;AAAA,UAC1C;AAAA,UACC,UACC,oBAAC,QAAG,MAAK,SAIN,eAAK,UAAU,SAAS,YAAY,KAAK,UAAU,QAAQ,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,IAAI,MACnF;AAAA;AAAA;AAAA,MA/CG,KAAK;AAAA,IAiDZ;AAAA,EAEJ,CAAC;AAEH,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,MAAK;AAAA,MACL,WAAW;AAAA,MACX,SAAS;AAAA,MACT,WAAW,GAAG,yBAAyB,SAAS;AAAA,MAE/C,sBAAY,OAAO,GAAG,EAAE;AAAA;AAAA,EAC3B;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/tree-view.tsx"],"sourcesContent":["import { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, MouseEvent, ReactNode } from \"react\";\nimport { ChevronRight } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { Spinner } from \"./ui\";\nimport { FOCUS_RING } from \"./focus-ring\";\n\n/**\n * A hierarchy you walk with the keyboard: kastlan's chart of accounts (Assets › Current\n * assets › Bank › 1020 Operating account), and — through {@link TreeRow} alone — the\n * row labels of its gantt, which indent and fold like a tree but sit beside a timeline\n * and so cannot be one.\n *\n * Both apps had written it as nested `<div>`s with a chevron button per row: every\n * row a Tab stop (a 300-account chart was 300 presses to get past), no way to say\n * \"level 3 of 5, collapsed\" to a screen reader, and no ←/→ at all. This is the WAI-ARIA\n * tree pattern instead:\n *\n * - `role=\"tree\"` › `treeitem` (with `aria-level`, `aria-expanded` on a parent,\n * `aria-selected` when selectable) › `role=\"group\"` for its children — so the\n * reader announces the position and the state it needs.\n * - ONE Tab stop (roving tabindex). ↑/↓ walk the visible rows, → opens a closed\n * parent and then steps into it, ← closes an open one and then steps out to the\n * parent, Home/End jump to the ends, `*` opens every sibling, and typing a few\n * letters jumps to the next row that starts with them. ←/→ follow the reading\n * direction (`horizontalStep`): in an RTL tree the children hang off to the left,\n * and ← is what opens them.\n * - Enter / Space, or a click, select. The chevron alone toggles on click.\n *\n * `expanded` and `selected` each work controlled or uncontrolled.\n */\n\n/* ── Nodes ───────────────────────────────────────────────────────────────── */\n\nexport interface TreeNode<T = unknown> {\n /** Unique across the whole tree — it is what `expanded` and `selected` hold. */\n id: string;\n label: ReactNode;\n /** The text type-ahead matches. Defaults to the row's rendered text. */\n textValue?: string;\n /** Before the label: an account-type icon, a colour dot. */\n icon?: ReactNode;\n /** After the label, pushed to the end of the row: a balance, a count, an action. */\n trailing?: ReactNode;\n children?: readonly TreeNode<T>[];\n /**\n * The node has children that are not loaded yet — it gets a chevron, and opening it\n * calls `loadChildren`. Pass the children in `items` once they arrive.\n */\n hasChildren?: boolean;\n /** Focusable and announced as unavailable (`aria-disabled`), but not selectable. */\n disabled?: boolean;\n /** Anything of the host's own, handed back to `renderItem` and `onSelectedChange`. */\n data?: T;\n}\n\n/** What {@link TreeViewProps.renderItem} is told about a row. */\nexport interface TreeItemState {\n level: number;\n expanded: boolean;\n selected: boolean;\n /** It has children, loaded or not — it shows a chevron. */\n expandable: boolean;\n loading: boolean;\n disabled: boolean;\n}\n\n/* ── TreeRow ─────────────────────────────────────────────────────────────── */\n\nexport interface TreeRowProps extends ComponentPropsWithoutRef<\"div\"> {\n /** 1 for a top-level row. Sets the indent and the number of guides. */\n level: number;\n /** `true`/`false` for a row that can fold; leave it `undefined` for a leaf, which\n * gets a chevron-sized gap instead so labels at one level still line up. */\n expanded?: boolean;\n /**\n * Make the chevron a real button (with `aria-expanded`, named by the label) that\n * calls this. For a row used ON ITS OWN — kastlan's gantt labels, where each row is\n * a line of a timeline and not an item of a tree widget. Inside {@link TreeView} it\n * is omitted: the tree owns the keys, and a button per row would be a Tab stop per\n * row, which is what the tree exists to remove.\n */\n onToggle?: () => void;\n /** The toggle button's name. By default it is named by the row's label (the state\n * is `aria-expanded`'s to say); required when `children` replaces the label. */\n toggleLabel?: string;\n label: ReactNode;\n icon?: ReactNode;\n trailing?: ReactNode;\n /** Pixels per level. Default 16. */\n indent?: number;\n /** Draw a vertical hairline per ancestor level. Default `true`. */\n guides?: boolean;\n selected?: boolean;\n /** A spinner in place of the chevron, while lazy children load. */\n loading?: boolean;\n /** Replace the label + trailing part of the row (the indent and chevron stay). */\n children?: ReactNode;\n}\n\nconst DEFAULT_INDENT = 16;\n/** The chevron's box; the guide for a level runs through its centre. */\nconst CHEVRON = 16;\n/** The row's start padding, before any indent. */\nconst ROW_PAD = 4;\n\n/**\n * One row: indent, guides, chevron, icon, label, trailing — no tree semantics of its\n * own. {@link TreeView} is built from it, and a host that needs the look without the\n * widget (a gantt's label column) uses it directly.\n *\n * Laid out with logical properties throughout (`padding-inline-start`,\n * `inset-inline-start`), so the indent, the guides and the chevron all hang from the\n * reading start in an RTL layout without a second code path.\n */\nexport function TreeRow({\n level,\n expanded,\n onToggle,\n toggleLabel,\n label,\n icon,\n trailing,\n indent = DEFAULT_INDENT,\n guides = true,\n selected,\n loading,\n children,\n className,\n style,\n ...rest\n}: TreeRowProps) {\n const labelId = useId();\n const depth = Math.max(0, level - 1);\n const expandable = expanded !== undefined;\n // A right-pointing chevron that turns DOWN when open. In RTL it is mirrored to point\n // left, and turning that one clockwise would point it UP — so it turns the other way.\n const chevron = (\n <ChevronRight\n aria-hidden\n className={cn(\n \"size-4 transition-transform motion-reduce:transition-none rtl:-scale-x-100\",\n expanded && \"rotate-90 rtl:-rotate-90\",\n )}\n />\n );\n return (\n <div\n {...rest}\n data-tree-row=\"\"\n data-selected={selected || undefined}\n style={{ paddingInlineStart: ROW_PAD + depth * indent, ...style }}\n className={cn(\n \"relative flex min-h-8 items-center gap-1.5 rounded pe-2 text-sm\",\n selected\n ? \"bg-[var(--bg-active)] font-medium text-[var(--text-primary)]\"\n : \"text-[var(--text-secondary)]\",\n className,\n )}\n >\n {guides &&\n Array.from({ length: depth }, (_, i) => (\n <span\n key={i}\n aria-hidden\n data-tree-guide=\"\"\n className=\"pointer-events-none absolute inset-y-0 w-px bg-[var(--border)]\"\n style={{ insetInlineStart: ROW_PAD + i * indent + CHEVRON / 2 }}\n />\n ))}\n {loading ? (\n <span className=\"flex size-4 shrink-0 items-center justify-center\">\n <Spinner label={null} className=\"size-3 border\" />\n </span>\n ) : expandable && onToggle ? (\n <button\n type=\"button\"\n aria-expanded={expanded}\n aria-label={toggleLabel}\n aria-labelledby={toggleLabel ? undefined : labelId}\n onClick={(e) => {\n e.stopPropagation();\n onToggle();\n }}\n className={cn(\"flex size-4 shrink-0 items-center justify-center rounded text-[var(--text-muted)] outline-none hover:text-[var(--text-primary)]\", FOCUS_RING)}\n >\n {chevron}\n </button>\n ) : expandable ? (\n <span data-tree-toggle=\"\" className=\"flex size-4 shrink-0 cursor-pointer items-center justify-center text-[var(--text-muted)]\">\n {chevron}\n </span>\n ) : (\n <span aria-hidden className=\"size-4 shrink-0\" />\n )}\n {children ?? (\n <>\n {icon && <span className=\"flex shrink-0 items-center\">{icon}</span>}\n <span id={labelId} data-tree-label=\"\" className=\"min-w-0 flex-1 truncate\">\n {label}\n </span>\n {trailing !== undefined && trailing !== null && (\n <span className=\"ms-auto flex shrink-0 items-center gap-1 text-xs tabular-nums text-[var(--text-muted)]\">\n {trailing}\n </span>\n )}\n </>\n )}\n </div>\n );\n}\n\n/* ── TreeView ────────────────────────────────────────────────────────────── */\n\nexport interface TreeViewProps<T = unknown>\n extends Omit<ComponentPropsWithoutRef<\"ul\">, \"onSelect\" | \"children\" | \"defaultValue\"> {\n items: readonly TreeNode<T>[];\n /** Open nodes, controlled. */\n expanded?: readonly string[];\n /** Open nodes on first render, uncontrolled. */\n defaultExpanded?: readonly string[];\n onExpandedChange?: (expanded: string[]) => void;\n /** The selected node's id, controlled; `null` for none. */\n selected?: string | null;\n defaultSelected?: string | null;\n onSelectedChange?: (id: string, node: TreeNode<T>) => void;\n /** `\"none\"` makes it a navigation-only tree: no `aria-selected`, Enter toggles. */\n selectionMode?: \"single\" | \"none\";\n /** The whole row content after the chevron, per node. Wins over `label`/`trailing`. */\n renderItem?: (node: TreeNode<T>, state: TreeItemState) => ReactNode;\n /**\n * Called when a node with `hasChildren` and no `children` is opened. While the\n * promise is pending the row shows a spinner and is `aria-busy`; put the children\n * into `items` before it resolves (or after — the row opens either way).\n */\n loadChildren?: (node: TreeNode<T>) => Promise<void> | void;\n /** Pixels per level. Default 16. */\n indent?: number;\n /** Vertical hairlines per level. Default `true`. */\n indentGuides?: boolean;\n /** Extra classes for each row (the `TreeRow`), e.g. a denser height. */\n rowClassName?: string;\n}\n\ninterface FlatNode<T> {\n node: TreeNode<T>;\n level: number;\n parentId: string | null;\n}\n\nconst isExpandable = (n: TreeNode<unknown>) => Boolean(n.children?.length || n.hasChildren);\n\n/** Controlled when `value` is given, otherwise state seeded from `initial`. */\nfunction useControllable<V>(value: V | undefined, initial: V): [V, (next: V) => void] {\n const [own, setOwn] = useState<V>(initial);\n const controlled = value !== undefined;\n const set = (next: V) => {\n if (!controlled) setOwn(next);\n };\n return [controlled ? value : own, set];\n}\n\n/** How long a type-ahead run stays open between keys — the APG's suggested half second. */\nconst TYPEAHEAD_MS = 500;\n\nexport function TreeView<T = unknown>({\n items,\n expanded: expandedProp,\n defaultExpanded,\n onExpandedChange,\n selected: selectedProp,\n defaultSelected,\n onSelectedChange,\n selectionMode = \"single\",\n renderItem,\n loadChildren,\n indent = DEFAULT_INDENT,\n indentGuides = true,\n rowClassName,\n className,\n onKeyDown,\n onClick,\n ...rest\n}: TreeViewProps<T>) {\n const [expandedList, setExpandedOwn] = useControllable<readonly string[]>(\n expandedProp,\n defaultExpanded ?? [],\n );\n const [selected, setSelectedOwn] = useControllable<string | null>(\n selectedProp,\n defaultSelected ?? null,\n );\n const expandedSet = useMemo(() => new Set(expandedList), [expandedList]);\n const [loading, setLoading] = useState<ReadonlySet<string>>(() => new Set());\n const [focusedId, setFocusedId] = useState<string | null>(null);\n const itemRefs = useRef(new Map<string, HTMLLIElement>());\n const pendingFocus = useRef<string | null>(null);\n const typeahead = useRef({ text: \"\", at: 0 });\n const baseId = useId();\n\n // Every node by id (for parents and lookups), and the rows currently on screen in\n // order — the list the arrow keys walk.\n const { byId, visible } = useMemo(() => {\n const map = new Map<string, FlatNode<T>>();\n const rows: FlatNode<T>[] = [];\n const walk = (nodes: readonly TreeNode<T>[], level: number, parentId: string | null, shown: boolean) => {\n for (const node of nodes) {\n const flat = { node, level, parentId };\n map.set(node.id, flat);\n if (shown) rows.push(flat);\n if (node.children) walk(node.children, level + 1, node.id, shown && expandedSet.has(node.id));\n }\n };\n walk(items, 1, null, true);\n return { byId: map, visible: rows };\n }, [items, expandedSet]);\n\n // The one tabbable row. Focus that was inside a branch that has since closed (a\n // click on its parent's chevron) moves up to the nearest ancestor still on screen,\n // so the tab stop never points at a row that is not rendered.\n const visibleIds = useMemo(() => new Set(visible.map((f) => f.node.id)), [visible]);\n const nearestVisible = (id: string | null): string | null => {\n let cur = id;\n while (cur && !visibleIds.has(cur)) cur = byId.get(cur)?.parentId ?? null;\n return cur;\n };\n const tabbable =\n nearestVisible(focusedId) ?? nearestVisible(selected) ?? visible[0]?.node.id ?? null;\n\n useEffect(() => {\n const id = pendingFocus.current;\n if (!id) return;\n pendingFocus.current = null;\n itemRefs.current.get(id)?.focus();\n });\n\n const focusRow = (id: string) => {\n setFocusedId(id);\n // After the commit: moving into a branch that just opened targets a row that is\n // not in the DOM yet.\n pendingFocus.current = id;\n itemRefs.current.get(id)?.focus();\n };\n\n const setExpanded = (next: Set<string>) => {\n const list = [...next];\n setExpandedOwn(list);\n onExpandedChange?.(list);\n };\n\n const open = (node: TreeNode<T>) => {\n if (expandedSet.has(node.id) || !isExpandable(node)) return;\n const next = new Set(expandedSet);\n next.add(node.id);\n setExpanded(next);\n if (loadChildren && node.hasChildren && !node.children?.length) {\n const pending = loadChildren(node);\n if (pending && typeof pending.then === \"function\") {\n setLoading((s) => new Set(s).add(node.id));\n const done = () =>\n setLoading((s) => {\n const n = new Set(s);\n n.delete(node.id);\n return n;\n });\n pending.then(done, done);\n }\n }\n };\n\n const close = (node: TreeNode<T>) => {\n if (!expandedSet.has(node.id)) return;\n const next = new Set(expandedSet);\n next.delete(node.id);\n setExpanded(next);\n };\n\n const toggle = (node: TreeNode<T>) => (expandedSet.has(node.id) ? close(node) : open(node));\n\n const select = (node: TreeNode<T>) => {\n if (node.disabled) return;\n if (selectionMode === \"none\") {\n toggle(node);\n return;\n }\n setSelectedOwn(node.id);\n onSelectedChange?.(node.id, node);\n };\n\n const idOf = (target: EventTarget | null): string | null =>\n (target as Element | null)?.closest?.<HTMLElement>('[role=\"treeitem\"]')?.dataset.treeId ?? null;\n\n const textOf = (flat: FlatNode<T>): string => {\n if (flat.node.textValue !== undefined) return flat.node.textValue;\n if (typeof flat.node.label === \"string\") return flat.node.label;\n const row = itemRefs.current.get(flat.node.id)?.querySelector(\"[data-tree-row]\");\n return row?.textContent ?? \"\";\n };\n\n const findByText = (fromIndex: number, text: string): string | null => {\n const needle = text.toLocaleLowerCase();\n for (let i = 1; i <= visible.length; i++) {\n const flat = visible[(fromIndex + i) % visible.length];\n if (textOf(flat).trim().toLocaleLowerCase().startsWith(needle)) return flat.node.id;\n }\n return null;\n };\n\n const handleKeyDown = (e: KeyboardEvent<HTMLUListElement>) => {\n onKeyDown?.(e);\n if (e.defaultPrevented) return;\n const id = idOf(e.target);\n const flat = id ? byId.get(id) : undefined;\n if (!flat) return;\n const { node } = flat;\n const index = visible.findIndex((f) => f.node.id === node.id);\n const expandedNow = expandedSet.has(node.id);\n // Forward / backward along the reading direction, from the row itself.\n const step = horizontalStep(e.key, e.target as Element);\n // A space inside a type-ahead run is part of the name being typed (\"current a…\"),\n // not a request to select.\n const inRun = Date.now() - typeahead.current.at < TYPEAHEAD_MS && typeahead.current.text !== \"\";\n const key = e.key === \" \" && inRun ? \"typeahead\" : e.key;\n\n let handled = true;\n if (step === 1) {\n if (!isExpandable(node)) handled = false;\n else if (!expandedNow) open(node);\n else if (node.children?.length) focusRow(node.children[0].id);\n } else if (step === -1) {\n if (expandedNow) close(node);\n else if (flat.parentId) focusRow(flat.parentId);\n else handled = false;\n } else {\n switch (key) {\n case \"ArrowDown\":\n if (index < visible.length - 1) focusRow(visible[index + 1].node.id);\n break;\n case \"ArrowUp\":\n if (index > 0) focusRow(visible[index - 1].node.id);\n break;\n case \"Home\":\n if (visible.length) focusRow(visible[0].node.id);\n break;\n case \"End\":\n if (visible.length) focusRow(visible[visible.length - 1].node.id);\n break;\n case \"Enter\":\n case \" \":\n select(node);\n break;\n case \"*\": {\n // Every sibling at this level, as the pattern suggests.\n const siblings = flat.parentId ? (byId.get(flat.parentId)?.node.children ?? []) : items;\n const next = new Set(expandedSet);\n for (const s of siblings) if (isExpandable(s)) next.add(s.id);\n setExpanded(next);\n break;\n }\n default: {\n const printable = e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey;\n if (!printable) {\n handled = false;\n break;\n }\n const now = Date.now();\n const run = now - typeahead.current.at < TYPEAHEAD_MS ? typeahead.current.text : \"\";\n typeahead.current = { text: run + e.key, at: now };\n const typed = typeahead.current.text;\n // \"aaa\" cycles through the rows starting with \"a\", as a file list does;\n // anything else extends the prefix, searched from the row after this one\n // (or from this one, when it still matches the longer prefix).\n const repeated = typed.length > 1 && [...typed].every((c) => c === typed[0]);\n const target = repeated\n ? findByText(index, typed[0])\n : typed.length > 1 && textOf(flat).trim().toLocaleLowerCase().startsWith(typed.toLocaleLowerCase())\n ? node.id\n : findByText(index, typed);\n if (target) focusRow(target);\n }\n }\n }\n if (handled) {\n e.preventDefault();\n e.stopPropagation();\n }\n };\n\n const handleClick = (e: MouseEvent<HTMLUListElement>) => {\n onClick?.(e);\n const id = idOf(e.target);\n const flat = id ? byId.get(id) : undefined;\n if (!flat) return;\n // Controls a host rendered into a row (an action in `trailing`) are their own.\n const control = (e.target as Element).closest(\"button, a[href], input, select, textarea\");\n if (control && itemRefs.current.get(flat.node.id)?.contains(control)) return;\n setFocusedId(flat.node.id);\n if ((e.target as Element).closest(\"[data-tree-toggle]\")) {\n toggle(flat.node);\n return;\n }\n select(flat.node);\n };\n\n const renderNodes = (nodes: readonly TreeNode<T>[], level: number, path: string): ReactNode =>\n nodes.map((node, i) => {\n // Named by its own row, not by its content: a treeitem's content is the whole\n // open branch below it, and \"Assets Current assets 1020 Bank …\" is not a name.\n // A path of indices, because a host's id may hold spaces an idref cannot.\n const rowId = `${baseId}-${path}${i}`;\n const expandable = isExpandable(node);\n const isOpen = expandable && expandedSet.has(node.id);\n const isSelected = selectionMode !== \"none\" && selected === node.id;\n const isLoading = loading.has(node.id);\n const state: TreeItemState = {\n level,\n expanded: isOpen,\n selected: isSelected,\n expandable,\n loading: isLoading,\n disabled: Boolean(node.disabled),\n };\n return (\n <li\n key={node.id}\n ref={(el) => {\n if (el) itemRefs.current.set(node.id, el);\n else itemRefs.current.delete(node.id);\n }}\n role=\"treeitem\"\n data-tree-id={node.id}\n aria-labelledby={rowId}\n aria-level={level}\n aria-expanded={expandable ? isOpen : undefined}\n aria-selected={selectionMode === \"none\" ? undefined : isSelected}\n aria-disabled={node.disabled || undefined}\n aria-busy={isLoading || undefined}\n tabIndex={node.id === tabbable ? 0 : -1}\n onFocus={(e) => {\n // Only the row itself: a focus event bubbling up from a nested row, or from\n // a control inside this one, is not this row being focused.\n if (e.target === e.currentTarget) setFocusedId(node.id);\n }}\n // The ring goes on this row's own box, not the <li>: the <li> contains the\n // whole open branch below it, and ringing that would outline a subtree.\n className=\"outline-none [&:focus-visible>[data-tree-row]]:ring-[length:var(--focus-ring-width)] [&:focus-visible>[data-tree-row]]:ring-[var(--brand)]\"\n >\n <TreeRow\n id={rowId}\n level={level}\n expanded={expandable ? isOpen : undefined}\n label={node.label}\n icon={node.icon}\n trailing={node.trailing}\n indent={indent}\n guides={indentGuides}\n selected={isSelected}\n loading={isLoading}\n className={cn(\n node.disabled ? \"cursor-not-allowed opacity-50\" : \"cursor-pointer hover:bg-[var(--bg-hover)]\",\n rowClassName,\n )}\n >\n {renderItem ? renderItem(node, state) : undefined}\n </TreeRow>\n {isOpen && (\n <ul role=\"group\">\n {/* Empty while lazy children load: a \"Loading…\" line in here would be\n a child that is not a treeitem, and the parent row already says\n `aria-busy` and shows the spinner. */}\n {node.children?.length ? renderNodes(node.children, level + 1, `${path}${i}-`) : null}\n </ul>\n )}\n </li>\n );\n });\n\n return (\n <ul\n {...rest}\n role=\"tree\"\n onKeyDown={handleKeyDown}\n onClick={handleClick}\n className={cn(\"flex flex-col text-sm\", className)}\n >\n {renderNodes(items, 1, \"\")}\n </ul>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ComponentPropsWithoutRef, ReactNode } from 'react';
3
3
  import { Direction } from '../lib/direction.js';
4
+ import { ChartHeight } from './chart-height.js';
4
5
 
5
6
  /**
6
7
  * One tile.
@@ -48,8 +49,10 @@ interface TreemapProps extends Omit<ComponentPropsWithoutRef<"div">, "children">
48
49
  * host that has a currency or a locale passes this — the kit knows neither.
49
50
  */
50
51
  valueFormatter?: (value: number, share: number | undefined) => ReactNode;
51
- /** Chart height in px. Default 320. */
52
- height?: number;
52
+ /** Chart height: a CSS length (`"20rem"`, `"min(20rem, 50dvh)"`), a Tailwind class,
53
+ * or px. Default `"20rem"` — 320 px at Normal, growing with the text size like the
54
+ * labels in the tiles (0.32, docs/text-size-harmonization.md §10.10). */
55
+ height?: ChartHeight;
53
56
  /** A tile was activated — by click, or by Enter/Space on a focused tile. Setting
54
57
  * this makes every tile a focusable `role="button"` named by its node name. */
55
58
  onNodeClick?: (id: string, name: string) => void;