@eifi1/ui-kit 0.11.0 → 0.13.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 (397) hide show
  1. package/README.md +59 -34
  2. package/dist/chart.d.ts +3 -1
  3. package/dist/chart.js +2 -0
  4. package/dist/chart.js.map +1 -1
  5. package/dist/components/account-settings-labels.d.ts +105 -0
  6. package/dist/components/account-settings-labels.js +81 -0
  7. package/dist/components/account-settings-labels.js.map +1 -0
  8. package/dist/components/account-settings.d.ts +36 -52
  9. package/dist/components/account-settings.js +44 -8
  10. package/dist/components/account-settings.js.map +1 -1
  11. package/dist/components/amount-input.d.ts +40 -4
  12. package/dist/components/amount-input.js +50 -12
  13. package/dist/components/amount-input.js.map +1 -1
  14. package/dist/components/authed-image.d.ts +68 -0
  15. package/dist/components/authed-image.js +117 -0
  16. package/dist/components/authed-image.js.map +1 -0
  17. package/dist/components/breadcrumbs.d.ts +2 -1
  18. package/dist/components/breadcrumbs.js +5 -2
  19. package/dist/components/breadcrumbs.js.map +1 -1
  20. package/dist/components/bulk-action-bar.d.ts +10 -2
  21. package/dist/components/bulk-action-bar.js.map +1 -1
  22. package/dist/components/button-group.d.ts +51 -2
  23. package/dist/components/button-group.js +5 -1
  24. package/dist/components/button-group.js.map +1 -1
  25. package/dist/components/calculator.d.ts +15 -4
  26. package/dist/components/calendar-heatmap.d.ts +41 -3
  27. package/dist/components/calendar-heatmap.js +48 -7
  28. package/dist/components/calendar-heatmap.js.map +1 -1
  29. package/dist/components/chart.js +14 -8
  30. package/dist/components/chart.js.map +1 -1
  31. package/dist/components/chip.d.ts +2 -1
  32. package/dist/components/chip.js +26 -18
  33. package/dist/components/chip.js.map +1 -1
  34. package/dist/components/choice-card.d.ts +35 -3
  35. package/dist/components/choice-card.js +83 -25
  36. package/dist/components/choice-card.js.map +1 -1
  37. package/dist/components/combobox-core.d.ts +3 -1
  38. package/dist/components/combobox-core.js +18 -5
  39. package/dist/components/combobox-core.js.map +1 -1
  40. package/dist/components/combobox.d.ts +2 -2
  41. package/dist/components/combobox.js +31 -9
  42. package/dist/components/combobox.js.map +1 -1
  43. package/dist/components/copy-button.d.ts +51 -70
  44. package/dist/components/copy-button.js +1 -1
  45. package/dist/components/copy-button.js.map +1 -1
  46. package/dist/components/data-table-cells.d.ts +63 -0
  47. package/dist/components/data-table-cells.js +64 -0
  48. package/dist/components/data-table-cells.js.map +1 -0
  49. package/dist/components/data-table-filter-popover.d.ts +1 -1
  50. package/dist/components/data-table-filter-popover.js +8 -1
  51. package/dist/components/data-table-filter-popover.js.map +1 -1
  52. package/dist/components/data-table-filters.d.ts +1 -1
  53. package/dist/components/data-table-filters.js +31 -1
  54. package/dist/components/data-table-filters.js.map +1 -1
  55. package/dist/components/data-table-labels.d.ts +13 -0
  56. package/dist/components/data-table-labels.js +8 -1
  57. package/dist/components/data-table-labels.js.map +1 -1
  58. package/dist/components/data-table.d.ts +1 -1
  59. package/dist/components/data-table.js +125 -27
  60. package/dist/components/data-table.js.map +1 -1
  61. package/dist/components/date-picker.d.ts +3 -1
  62. package/dist/components/date-picker.js +15 -2
  63. package/dist/components/date-picker.js.map +1 -1
  64. package/dist/components/description-list.d.ts +23 -3
  65. package/dist/components/description-list.js +23 -3
  66. package/dist/components/description-list.js.map +1 -1
  67. package/dist/components/dialog-frame.d.ts +1 -0
  68. package/dist/components/entity-combobox.d.ts +1 -1
  69. package/dist/components/entity-combobox.js +40 -20
  70. package/dist/components/entity-combobox.js.map +1 -1
  71. package/dist/components/error-boundary.d.ts +281 -0
  72. package/dist/components/error-boundary.js +302 -0
  73. package/dist/components/error-boundary.js.map +1 -0
  74. package/dist/components/field-sync.js +90 -83
  75. package/dist/components/field-sync.js.map +1 -1
  76. package/dist/components/field.d.ts +30 -4
  77. package/dist/components/field.js +40 -17
  78. package/dist/components/field.js.map +1 -1
  79. package/dist/components/file-button.d.ts +50 -208
  80. package/dist/components/file-dropzone.d.ts +49 -2
  81. package/dist/components/floating-panel.d.ts +13 -4
  82. package/dist/components/floating-panel.js +7 -3
  83. package/dist/components/floating-panel.js.map +1 -1
  84. package/dist/components/form-actions.d.ts +53 -0
  85. package/dist/components/form-actions.js +98 -0
  86. package/dist/components/form-actions.js.map +1 -0
  87. package/dist/components/full-bleed-dialog.js +4 -0
  88. package/dist/components/full-bleed-dialog.js.map +1 -1
  89. package/dist/components/hover-menu.js +64 -56
  90. package/dist/components/hover-menu.js.map +1 -1
  91. package/dist/components/image-grid.d.ts +67 -0
  92. package/dist/components/image-grid.js +112 -0
  93. package/dist/components/image-grid.js.map +1 -0
  94. package/dist/components/lightbox.d.ts +104 -0
  95. package/dist/components/lightbox.js +229 -0
  96. package/dist/components/lightbox.js.map +1 -0
  97. package/dist/components/line-items.d.ts +114 -0
  98. package/dist/components/line-items.js +238 -0
  99. package/dist/components/line-items.js.map +1 -0
  100. package/dist/components/list.d.ts +37 -4
  101. package/dist/components/list.js +35 -14
  102. package/dist/components/list.js.map +1 -1
  103. package/dist/components/loading-state.d.ts +24 -0
  104. package/dist/components/loading-state.js +32 -0
  105. package/dist/components/loading-state.js.map +1 -0
  106. package/dist/components/menu-item.d.ts +2 -1
  107. package/dist/components/menu-item.js +5 -1
  108. package/dist/components/menu-item.js.map +1 -1
  109. package/dist/components/mini-calendar.d.ts +13 -1
  110. package/dist/components/mini-calendar.js +46 -2
  111. package/dist/components/mini-calendar.js.map +1 -1
  112. package/dist/components/modal.d.ts +20 -1
  113. package/dist/components/modal.js +26 -2
  114. package/dist/components/modal.js.map +1 -1
  115. package/dist/components/month-picker.d.ts +21 -1
  116. package/dist/components/month-picker.js +139 -26
  117. package/dist/components/month-picker.js.map +1 -1
  118. package/dist/components/multi-entity-combobox.d.ts +1 -1
  119. package/dist/components/multi-entity-combobox.js +42 -20
  120. package/dist/components/multi-entity-combobox.js.map +1 -1
  121. package/dist/components/nav-pills.d.ts +2 -1
  122. package/dist/components/nav-pills.js +5 -1
  123. package/dist/components/nav-pills.js.map +1 -1
  124. package/dist/components/number-field.d.ts +24 -5
  125. package/dist/components/number-field.js +5 -7
  126. package/dist/components/number-field.js.map +1 -1
  127. package/dist/components/number-input.d.ts +25 -5
  128. package/dist/components/number-input.js +7 -1
  129. package/dist/components/number-input.js.map +1 -1
  130. package/dist/components/numpad-sheet.d.ts +20 -5
  131. package/dist/components/numpad-sheet.js +3 -2
  132. package/dist/components/numpad-sheet.js.map +1 -1
  133. package/dist/components/page-header.d.ts +35 -6
  134. package/dist/components/page-header.js +7 -1
  135. package/dist/components/page-header.js.map +1 -1
  136. package/dist/components/passkeys-setting.d.ts +67 -0
  137. package/dist/components/passkeys-setting.js +214 -0
  138. package/dist/components/passkeys-setting.js.map +1 -0
  139. package/dist/components/picker-sheet.js +5 -1
  140. package/dist/components/picker-sheet.js.map +1 -1
  141. package/dist/components/pie-chart-labels.d.ts +20 -0
  142. package/dist/components/pie-chart-labels.js +12 -0
  143. package/dist/components/pie-chart-labels.js.map +1 -0
  144. package/dist/components/pie-chart.d.ts +101 -0
  145. package/dist/components/pie-chart.js +283 -0
  146. package/dist/components/pie-chart.js.map +1 -0
  147. package/dist/components/popover.js +3 -0
  148. package/dist/components/popover.js.map +1 -1
  149. package/dist/components/progress-bar.d.ts +44 -3
  150. package/dist/components/progress-bar.js +58 -22
  151. package/dist/components/progress-bar.js.map +1 -1
  152. package/dist/components/qr-code.d.ts +34 -0
  153. package/dist/components/qr-code.js +57 -0
  154. package/dist/components/qr-code.js.map +1 -0
  155. package/dist/components/search-field.d.ts +15 -3
  156. package/dist/components/search-field.js +5 -1
  157. package/dist/components/search-field.js.map +1 -1
  158. package/dist/components/settings-fields.d.ts +49 -1
  159. package/dist/components/signed-amount.d.ts +122 -0
  160. package/dist/components/signed-amount.js +152 -0
  161. package/dist/components/signed-amount.js.map +1 -0
  162. package/dist/components/skeleton.d.ts +10 -3
  163. package/dist/components/skeleton.js +12 -2
  164. package/dist/components/skeleton.js.map +1 -1
  165. package/dist/components/stat-tile.d.ts +2 -1
  166. package/dist/components/stat-tile.js +6 -3
  167. package/dist/components/stat-tile.js.map +1 -1
  168. package/dist/components/swipeable-row.js +4 -3
  169. package/dist/components/swipeable-row.js.map +1 -1
  170. package/dist/components/table.d.ts +39 -5
  171. package/dist/components/table.js +45 -11
  172. package/dist/components/table.js.map +1 -1
  173. package/dist/components/text-link.d.ts +238 -0
  174. package/dist/components/text-link.js +144 -0
  175. package/dist/components/text-link.js.map +1 -0
  176. package/dist/components/text.d.ts +26 -2
  177. package/dist/components/text.js +16 -2
  178. package/dist/components/text.js.map +1 -1
  179. package/dist/components/time-input.d.ts +49 -1
  180. package/dist/components/toggle-group.d.ts +35 -0
  181. package/dist/components/toggle-group.js +43 -14
  182. package/dist/components/toggle-group.js.map +1 -1
  183. package/dist/components/toggle-legend.d.ts +53 -3
  184. package/dist/components/toggle-legend.js +54 -1
  185. package/dist/components/toggle-legend.js.map +1 -1
  186. package/dist/components/tooltip.d.ts +51 -1
  187. package/dist/components/tooltip.js +59 -35
  188. package/dist/components/tooltip.js.map +1 -1
  189. package/dist/components/ui.d.ts +51 -779
  190. package/dist/components/ui.js +364 -34
  191. package/dist/components/ui.js.map +1 -1
  192. package/dist/components/use-table-state.d.ts +3 -52
  193. package/dist/components/use-table-state.js +128 -30
  194. package/dist/components/use-table-state.js.map +1 -1
  195. package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
  196. package/dist/data-table.d.ts +2 -1
  197. package/dist/data-table.js +5 -1
  198. package/dist/data-table.js.map +1 -1
  199. package/dist/feedback/feedback-attachment.js +4 -6
  200. package/dist/feedback/feedback-attachment.js.map +1 -1
  201. package/dist/feedback/feedback-dialog.d.ts +26 -3
  202. package/dist/feedback/feedback-dialog.js +21 -5
  203. package/dist/feedback/feedback-dialog.js.map +1 -1
  204. package/dist/feedback/feedback-inbox.js +86 -75
  205. package/dist/feedback/feedback-inbox.js.map +1 -1
  206. package/dist/feedback/feedback-thread.d.ts +173 -0
  207. package/dist/feedback/feedback-thread.js +264 -0
  208. package/dist/feedback/feedback-thread.js.map +1 -0
  209. package/dist/feedback.d.ts +2 -1
  210. package/dist/feedback.js +1 -0
  211. package/dist/feedback.js.map +1 -1
  212. package/dist/hooks/use-authed-src.d.ts +71 -0
  213. package/dist/hooks/use-authed-src.js +63 -0
  214. package/dist/hooks/use-authed-src.js.map +1 -0
  215. package/dist/hooks/use-file-drop.d.ts +49 -2
  216. package/dist/hooks/use-hotkey.d.ts +59 -0
  217. package/dist/hooks/use-hotkey.js +105 -0
  218. package/dist/hooks/use-hotkey.js.map +1 -0
  219. package/dist/hooks/use-search-param-state.d.ts +83 -0
  220. package/dist/hooks/use-search-param-state.js +94 -0
  221. package/dist/hooks/use-search-param-state.js.map +1 -0
  222. package/dist/i18n/defaults.d.ts +15 -4
  223. package/dist/i18n/defaults.js +31 -1
  224. package/dist/i18n/defaults.js.map +1 -1
  225. package/dist/i18n/kit-labels.d.ts +46 -312
  226. package/dist/i18n/kit-labels.js +20 -5
  227. package/dist/i18n/kit-labels.js.map +1 -1
  228. package/dist/i18n/locales/de-CH-informal.d.ts +15 -4
  229. package/dist/i18n/locales/de-CH.d.ts +15 -4
  230. package/dist/i18n/locales/de-informal.d.ts +15 -4
  231. package/dist/i18n/locales/de.d.ts +15 -4
  232. package/dist/i18n/locales/de.js +164 -3
  233. package/dist/i18n/locales/de.js.map +1 -1
  234. package/dist/i18n/locales/es.d.ts +15 -4
  235. package/dist/i18n/locales/es.js +161 -3
  236. package/dist/i18n/locales/es.js.map +1 -1
  237. package/dist/i18n/locales/fr.d.ts +15 -4
  238. package/dist/i18n/locales/fr.js +161 -3
  239. package/dist/i18n/locales/fr.js.map +1 -1
  240. package/dist/i18n/locales/hu.d.ts +15 -4
  241. package/dist/i18n/locales/hu.js +161 -3
  242. package/dist/i18n/locales/hu.js.map +1 -1
  243. package/dist/i18n/locales/it.d.ts +15 -4
  244. package/dist/i18n/locales/it.js +161 -3
  245. package/dist/i18n/locales/it.js.map +1 -1
  246. package/dist/i18n/locales/zh.d.ts +15 -4
  247. package/dist/i18n/locales/zh.js +161 -3
  248. package/dist/i18n/locales/zh.js.map +1 -1
  249. package/dist/index.d.ts +39 -19
  250. package/dist/index.js +40 -1
  251. package/dist/index.js.map +1 -1
  252. package/dist/kit-labels-Mz8zZHIF.d.ts +1744 -0
  253. package/dist/lib/format.d.ts +162 -0
  254. package/dist/lib/format.js +151 -0
  255. package/dist/lib/format.js.map +1 -0
  256. package/dist/lib/qr-encode.d.ts +44 -0
  257. package/dist/lib/qr-encode.js +338 -0
  258. package/dist/lib/qr-encode.js.map +1 -0
  259. package/dist/rhf/fields.d.ts +283 -0
  260. package/dist/rhf/fields.js +631 -0
  261. package/dist/rhf/fields.js.map +1 -0
  262. package/dist/rhf/form.d.ts +49 -1
  263. package/dist/rhf/line-items.d.ts +31 -0
  264. package/dist/rhf/line-items.js +59 -0
  265. package/dist/rhf/line-items.js.map +1 -0
  266. package/dist/rhf.d.ts +61 -1
  267. package/dist/rhf.js +2 -0
  268. package/dist/rhf.js.map +1 -1
  269. package/dist/search/command-palette.js +20 -14
  270. package/dist/search/command-palette.js.map +1 -1
  271. package/dist/shell/app-shell.d.ts +99 -2
  272. package/dist/shell/app-shell.js +120 -57
  273. package/dist/shell/app-shell.js.map +1 -1
  274. package/dist/shell/auth-layout.d.ts +74 -0
  275. package/dist/shell/auth-layout.js +97 -0
  276. package/dist/shell/auth-layout.js.map +1 -0
  277. package/dist/shell/top-bar-brand.d.ts +81 -0
  278. package/dist/shell/top-bar-brand.js +47 -0
  279. package/dist/shell/top-bar-brand.js.map +1 -0
  280. package/dist/shell/topbar-action-menu.d.ts +21 -4
  281. package/dist/shell/topbar-action-menu.js +18 -2
  282. package/dist/shell/topbar-action-menu.js.map +1 -1
  283. package/dist/shell.d.ts +56 -1
  284. package/dist/shell.js +2 -0
  285. package/dist/shell.js.map +1 -1
  286. package/dist/tour/tour.js +10 -4
  287. package/dist/tour/tour.js.map +1 -1
  288. package/dist/wizard/stepper-nav.d.ts +48 -1
  289. package/dist/wizard/use-wizard.js +4 -2
  290. package/dist/wizard/use-wizard.js.map +1 -1
  291. package/dist/wizard/wizard-context.d.ts +24 -1
  292. package/dist/wizard/wizard-context.js +19 -2
  293. package/dist/wizard/wizard-context.js.map +1 -1
  294. package/dist/wizard/wizard-step.d.ts +30 -5
  295. package/dist/wizard/wizard-step.js +34 -3
  296. package/dist/wizard/wizard-step.js.map +1 -1
  297. package/dist/wizard.d.ts +50 -3
  298. package/package.json +6 -6
  299. package/src/chart.ts +4 -0
  300. package/src/components/account-settings-labels.ts +190 -0
  301. package/src/components/account-settings.tsx +110 -63
  302. package/src/components/amount-input.tsx +109 -10
  303. package/src/components/authed-image.tsx +190 -0
  304. package/src/components/breadcrumbs.tsx +8 -2
  305. package/src/components/bulk-action-bar.tsx +16 -3
  306. package/src/components/button-group.tsx +7 -2
  307. package/src/components/calendar-heatmap.tsx +105 -10
  308. package/src/components/chart.tsx +19 -8
  309. package/src/components/chip.tsx +14 -4
  310. package/src/components/choice-card.tsx +136 -25
  311. package/src/components/combobox-core.tsx +33 -9
  312. package/src/components/combobox.tsx +33 -9
  313. package/src/components/copy-button.tsx +3 -2
  314. package/src/components/data-table-cells.tsx +119 -0
  315. package/src/components/data-table-filter-popover.tsx +13 -5
  316. package/src/components/data-table-filters.ts +77 -0
  317. package/src/components/data-table-labels.ts +23 -0
  318. package/src/components/data-table.tsx +230 -13
  319. package/src/components/date-picker.tsx +19 -3
  320. package/src/components/description-list.tsx +52 -3
  321. package/src/components/entity-combobox.tsx +25 -7
  322. package/src/components/error-boundary.tsx +640 -0
  323. package/src/components/field-sync.tsx +48 -40
  324. package/src/components/field.tsx +56 -5
  325. package/src/components/floating-panel.tsx +20 -7
  326. package/src/components/form-actions.tsx +191 -0
  327. package/src/components/full-bleed-dialog.tsx +4 -0
  328. package/src/components/hover-menu.tsx +26 -17
  329. package/src/components/image-grid.tsx +181 -0
  330. package/src/components/lightbox.tsx +374 -0
  331. package/src/components/line-items.tsx +385 -0
  332. package/src/components/list.tsx +82 -19
  333. package/src/components/loading-state.tsx +47 -0
  334. package/src/components/menu-item.tsx +7 -2
  335. package/src/components/mini-calendar.tsx +72 -3
  336. package/src/components/modal.tsx +65 -3
  337. package/src/components/month-picker.tsx +181 -26
  338. package/src/components/multi-entity-combobox.tsx +27 -7
  339. package/src/components/nav-pills.tsx +8 -1
  340. package/src/components/number-field.tsx +15 -10
  341. package/src/components/number-input.tsx +17 -1
  342. package/src/components/numpad-sheet.tsx +6 -1
  343. package/src/components/page-header.tsx +42 -5
  344. package/src/components/passkeys-setting.tsx +331 -0
  345. package/src/components/picker-sheet.tsx +10 -1
  346. package/src/components/pie-chart-labels.ts +32 -0
  347. package/src/components/pie-chart.tsx +479 -0
  348. package/src/components/popover.tsx +3 -0
  349. package/src/components/progress-bar.tsx +129 -19
  350. package/src/components/qr-code.tsx +83 -0
  351. package/src/components/search-field.tsx +20 -4
  352. package/src/components/signed-amount.tsx +295 -0
  353. package/src/components/skeleton.tsx +21 -3
  354. package/src/components/stat-tile.tsx +11 -3
  355. package/src/components/swipeable-row.tsx +7 -7
  356. package/src/components/table.tsx +102 -13
  357. package/src/components/text-link.tsx +356 -0
  358. package/src/components/text.tsx +46 -2
  359. package/src/components/toggle-group.tsx +93 -20
  360. package/src/components/toggle-legend.tsx +108 -2
  361. package/src/components/tooltip.tsx +79 -8
  362. package/src/components/ui.tsx +707 -49
  363. package/src/components/use-table-state.ts +265 -33
  364. package/src/data-table.ts +10 -0
  365. package/src/feedback/feedback-attachment.tsx +10 -7
  366. package/src/feedback/feedback-dialog.tsx +49 -7
  367. package/src/feedback/feedback-inbox.tsx +7 -0
  368. package/src/feedback/feedback-thread.tsx +510 -0
  369. package/src/feedback.ts +1 -0
  370. package/src/hooks/use-authed-src.ts +161 -0
  371. package/src/hooks/use-hotkey.ts +172 -0
  372. package/src/hooks/use-search-param-state.ts +187 -0
  373. package/src/i18n/defaults.ts +30 -0
  374. package/src/i18n/kit-labels.tsx +118 -8
  375. package/src/i18n/locales/de.ts +161 -0
  376. package/src/i18n/locales/es.ts +158 -0
  377. package/src/i18n/locales/fr.ts +161 -0
  378. package/src/i18n/locales/hu.ts +159 -0
  379. package/src/i18n/locales/it.ts +159 -0
  380. package/src/i18n/locales/zh.ts +158 -0
  381. package/src/index.ts +59 -0
  382. package/src/lib/format.ts +348 -0
  383. package/src/lib/qr-encode.ts +420 -0
  384. package/src/rhf/fields.tsx +1135 -0
  385. package/src/rhf/line-items.tsx +130 -0
  386. package/src/rhf.ts +6 -0
  387. package/src/search/command-palette.tsx +29 -19
  388. package/src/shell/app-shell.tsx +153 -3
  389. package/src/shell/auth-layout.tsx +190 -0
  390. package/src/shell/top-bar-brand.tsx +76 -0
  391. package/src/shell/topbar-action-menu.tsx +32 -4
  392. package/src/shell.ts +2 -0
  393. package/src/tour/tour.tsx +16 -5
  394. package/src/wizard/use-wizard.ts +8 -4
  395. package/src/wizard/wizard-context.tsx +41 -1
  396. package/src/wizard/wizard-step.tsx +75 -6
  397. package/tokens.css +12 -0
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useMemo, useRef, useState } from "react";
2
- import { useSearchParams } from "react-router";
2
+ import { useLocation, useSearchParams } from "react-router";
3
3
  import { readStored, writeStored } from "../lib/safe-storage";
4
4
  import {
5
5
  decodeFilterValue,
@@ -8,7 +8,7 @@ import {
8
8
  resolveFilter,
9
9
  } from "./data-table-filters";
10
10
  import { decodeSorts, encodeSorts, normalizeSorts, type SortState } from "./data-table-sort";
11
- import type { DataTableColumn, FilterState } from "./data-table";
11
+ import type { DataTableColumn, FilterState, ServerPagination } from "./data-table";
12
12
 
13
13
  // ---------- Persisted + URL-encoded view state ----------
14
14
  //
@@ -51,16 +51,63 @@ const URL_FILTER_PREFIX = "f.";
51
51
  const URL_SORT_KEY = "sort";
52
52
  const URL_PAGE_KEY = "p";
53
53
  const URL_PAGE_SIZE_KEY = "ps";
54
+ /** The free-text search term — managed only by {@link useTableUrlState}, and only when
55
+ * it is asked to (`search: true`); a plain `urlSync` table leaves a `q` alone. */
56
+ const URL_SEARCH_KEY = "q";
54
57
 
55
- interface UrlState {
58
+ /**
59
+ * How a table is mirrored into the URL: `true`, or `{ prefix }` for a table that shares
60
+ * its page with another URL-synced one. The prefix is put verbatim in front of EVERY key
61
+ * the table writes and reads — `{ prefix: "inv." }` gives `inv.f.<column>`, `inv.sort`,
62
+ * `inv.p`, `inv.ps` (and `inv.q`) — so two tables on one page stop sharing a sort and a
63
+ * page. Without one the keys are unprefixed, as they always were.
64
+ */
65
+ export type TableUrlSync = boolean | { prefix?: string };
66
+
67
+ /** The query keys of one table, with its prefix applied. */
68
+ interface UrlKeys {
69
+ filter: string;
70
+ sort: string;
71
+ page: string;
72
+ pageSize: string;
73
+ search: string;
74
+ }
75
+
76
+ function urlKeys(prefix = ""): UrlKeys {
77
+ return {
78
+ filter: prefix + URL_FILTER_PREFIX,
79
+ sort: prefix + URL_SORT_KEY,
80
+ page: prefix + URL_PAGE_KEY,
81
+ pageSize: prefix + URL_PAGE_SIZE_KEY,
82
+ search: prefix + URL_SEARCH_KEY,
83
+ };
84
+ }
85
+
86
+ /** `urlSync` split into "is it on" and "under which prefix". */
87
+ function resolveTableUrlSync(urlSync: TableUrlSync | undefined): { enabled: boolean; prefix: string } {
88
+ if (!urlSync) return { enabled: false, prefix: "" };
89
+ return { enabled: true, prefix: urlSync === true ? "" : (urlSync.prefix ?? "") };
90
+ }
91
+
92
+ /** What {@link readTableUrlState} found in a query string. */
93
+ export interface TableUrlState {
56
94
  sorts?: SortState[];
57
95
  filters?: FilterState;
58
- pageSize?: number; // Infinity for "all"
59
- page?: number; // 0-based
96
+ /** `Infinity` for `ps=all`. */
97
+ pageSize?: number;
98
+ /** 0-based (the URL's `p` is 1-based). */
99
+ page?: number;
60
100
  }
61
101
 
62
102
  /**
63
- * The managed keys out of the ROUTER's query string — the one `setSearchParams` writes.
103
+ * What a table's URL says, read out of a query string: the column filters (`f.<key>`),
104
+ * the sort, the page size and the 0-based page. Absent keys are absent here too, so a
105
+ * caller can tell "not in the link" from "the default". The same reader `urlSync` uses
106
+ * on mount, exported for an owner that keeps the state itself — see
107
+ * {@link useTableUrlState}.
108
+ *
109
+ * Pass the ROUTER's query string — the one `setSearchParams` writes — and the table's
110
+ * `prefix` when it has one (see {@link TableUrlSync}).
64
111
  *
65
112
  * This read `window.location.search` until 0.7.0, which is the same string only under a
66
113
  * BrowserRouter. Under a HashRouter the router's query lives after the `#`
@@ -69,30 +116,35 @@ interface UrlState {
69
116
  * empty state. Reading and writing through `useSearchParams` is one source for both
70
117
  * directions under every router, MemoryRouter included.
71
118
  */
72
- function readUrlState<T>(sp: URLSearchParams, columns: DataTableColumn<T>[]): UrlState {
73
- const out: UrlState = {};
119
+ export function readTableUrlState<T>(
120
+ sp: URLSearchParams,
121
+ columns: DataTableColumn<T>[],
122
+ prefix?: string,
123
+ ): TableUrlState {
124
+ const keys = urlKeys(prefix);
125
+ const out: TableUrlState = {};
74
126
 
75
127
  const filters: FilterState = {};
76
128
  for (const col of columns) {
77
129
  const filter = resolveFilter(col);
78
130
  if (!filter) continue;
79
- const raw = sp.get(URL_FILTER_PREFIX + col.key);
131
+ const raw = sp.get(keys.filter + col.key);
80
132
  if (raw == null) continue;
81
133
  filters[col.key] = decodeFilterValue(filter, raw);
82
134
  }
83
135
  if (Object.keys(filters).length) out.filters = filters;
84
136
 
85
- const sortRaw = sp.get(URL_SORT_KEY);
137
+ const sortRaw = sp.get(keys.sort);
86
138
  if (sortRaw) {
87
139
  const decoded = decodeSorts(sortRaw, new Set(columns.map((c) => c.key)));
88
140
  if (decoded.length) out.sorts = decoded;
89
141
  }
90
142
 
91
- const psRaw = sp.get(URL_PAGE_SIZE_KEY);
143
+ const psRaw = sp.get(keys.pageSize);
92
144
  if (psRaw === "all") out.pageSize = Infinity;
93
145
  else if (psRaw && Number(psRaw) > 0) out.pageSize = Number(psRaw);
94
146
 
95
- const pRaw = sp.get(URL_PAGE_KEY);
147
+ const pRaw = sp.get(keys.page);
96
148
  if (pRaw && Number(pRaw) >= 1) out.page = Number(pRaw) - 1;
97
149
 
98
150
  return out;
@@ -114,21 +166,22 @@ function urlSignature<T>(
114
166
  pageSize: number,
115
167
  page: number,
116
168
  defaultPageSize: number,
169
+ keys: UrlKeys,
117
170
  ): string {
118
171
  const parts: string[] = [];
119
172
  for (const col of columns) {
120
173
  const state = filters[col.key];
121
174
  if (!state || !isFilterActive(state)) continue;
122
175
  const enc = encodeFilterValue(state);
123
- if (enc != null) parts.push(`${URL_FILTER_PREFIX}${col.key}=${enc}`);
176
+ if (enc != null) parts.push(`${keys.filter}${col.key}=${enc}`);
124
177
  }
125
- parts.push(`${URL_SORT_KEY}=${encodeSorts(sorts)}`);
178
+ parts.push(`${keys.sort}=${encodeSorts(sorts)}`);
126
179
  parts.push(
127
- `${URL_PAGE_SIZE_KEY}=${
180
+ `${keys.pageSize}=${
128
181
  pageSize === Infinity ? "all" : pageSize === defaultPageSize ? "" : String(pageSize)
129
182
  }`,
130
183
  );
131
- parts.push(`${URL_PAGE_KEY}=${page > 0 ? String(page + 1) : ""}`);
184
+ parts.push(`${keys.page}=${page > 0 ? String(page + 1) : ""}`);
132
185
  return parts.join("&");
133
186
  }
134
187
 
@@ -140,33 +193,44 @@ function writeUrlState<T>(
140
193
  pageSize: number,
141
194
  page: number,
142
195
  defaultPageSize: number,
196
+ // `undefined`: the search term is not ours to manage, and a `q` is left as it is.
197
+ search: string | undefined,
198
+ // The current entry's route state, carried over the replace. A query rewrite is not
199
+ // a navigation, and dropping the state would, for one, lose the mark a URL-bound
200
+ // dialog's opener leaves there (see useDialogParam).
201
+ state: unknown,
202
+ keys: UrlKeys,
143
203
  ): void {
144
204
  setSearchParams(
145
205
  (prev) => {
146
206
  const next = new URLSearchParams(prev);
147
207
  // clear managed keys
148
- for (const col of columns) next.delete(URL_FILTER_PREFIX + col.key);
149
- next.delete(URL_SORT_KEY);
150
- next.delete(URL_PAGE_KEY);
151
- next.delete(URL_PAGE_SIZE_KEY);
208
+ for (const col of columns) next.delete(keys.filter + col.key);
209
+ next.delete(keys.sort);
210
+ next.delete(keys.page);
211
+ next.delete(keys.pageSize);
212
+ if (search !== undefined) {
213
+ next.delete(keys.search);
214
+ if (search.trim()) next.set(keys.search, search);
215
+ }
152
216
  // write filters
153
217
  for (const col of columns) {
154
218
  const state = filters[col.key];
155
219
  if (!state || !isFilterActive(state)) continue;
156
220
  const enc = encodeFilterValue(state);
157
- if (enc != null) next.set(URL_FILTER_PREFIX + col.key, enc);
221
+ if (enc != null) next.set(keys.filter + col.key, enc);
158
222
  }
159
223
  // write sort
160
224
  const sortEnc = encodeSorts(sorts);
161
- if (sortEnc) next.set(URL_SORT_KEY, sortEnc);
225
+ if (sortEnc) next.set(keys.sort, sortEnc);
162
226
  // write page size if non-default
163
- if (pageSize === Infinity) next.set(URL_PAGE_SIZE_KEY, "all");
164
- else if (pageSize !== defaultPageSize) next.set(URL_PAGE_SIZE_KEY, String(pageSize));
227
+ if (pageSize === Infinity) next.set(keys.pageSize, "all");
228
+ else if (pageSize !== defaultPageSize) next.set(keys.pageSize, String(pageSize));
165
229
  // write page if not first
166
- if (page > 0) next.set(URL_PAGE_KEY, String(page + 1));
230
+ if (page > 0) next.set(keys.page, String(page + 1));
167
231
  return next;
168
232
  },
169
- { replace: true },
233
+ { replace: true, state },
170
234
  );
171
235
  }
172
236
 
@@ -194,7 +258,7 @@ export function useTableState<T>({
194
258
  columns,
195
259
  storageKey,
196
260
  storageKeyPrefix,
197
- urlSync,
261
+ urlSync: urlSyncOption,
198
262
  defaultPageSize,
199
263
  sortsProp,
200
264
  filtersProp,
@@ -202,18 +266,21 @@ export function useTableState<T>({
202
266
  columns: DataTableColumn<T>[];
203
267
  storageKey?: string;
204
268
  storageKeyPrefix: string;
205
- urlSync?: boolean;
269
+ urlSync?: TableUrlSync;
206
270
  defaultPageSize: number;
207
271
  sortsProp?: SortState[];
208
272
  filtersProp?: FilterState;
209
273
  }) {
274
+ const { enabled: urlSync, prefix: urlPrefix } = resolveTableUrlSync(urlSyncOption);
275
+ const keys = useMemo(() => urlKeys(urlPrefix), [urlPrefix]);
210
276
  // Called unconditionally (a Router is required either way — see src/data-table.ts),
211
- // and the SAME pair is used to read and to write; see `readUrlState`.
277
+ // and the SAME pair is used to read and to write; see `readTableUrlState`.
212
278
  const [searchParams, setSearchParams] = useSearchParams();
279
+ const location = useLocation();
213
280
  // Read URL once on mount so URL-encoded views (e.g. shared links, deep links from
214
281
  // other pages) populate initial state. After mount, state is local.
215
- const [urlInitial] = useState<UrlState>(() =>
216
- urlSync ? readUrlState(searchParams, columns) : {},
282
+ const [urlInitial] = useState<TableUrlState>(() =>
283
+ urlSync ? readTableUrlState(searchParams, columns, urlPrefix) : {},
217
284
  );
218
285
  const initial = useMemo(
219
286
  () => loadPersisted(storageKey, storageKeyPrefix),
@@ -299,7 +366,7 @@ export function useTableState<T>({
299
366
 
300
367
  const lastUrl = useRef<string | null>(null);
301
368
  const urlSig = urlSync
302
- ? urlSignature(columns, sorts, filters, pageSize, page, defaultPageSize)
369
+ ? urlSignature(columns, sorts, filters, pageSize, page, defaultPageSize, keys)
303
370
  : "";
304
371
  useEffect(() => {
305
372
  if (!urlSync) return;
@@ -308,7 +375,7 @@ export function useTableState<T>({
308
375
  // `columns` is deliberately not a dependency and does not need to be: the
309
376
  // signature above is computed from it, so a column set that would produce a
310
377
  // different address produces a different signature first.
311
- writeUrlState(setSearchParams, columns, sorts, filters, pageSize, page, defaultPageSize);
378
+ writeUrlState(setSearchParams, columns, sorts, filters, pageSize, page, defaultPageSize, undefined, location.state, keys);
312
379
  // eslint-disable-next-line react-hooks/exhaustive-deps
313
380
  }, [urlSync, urlSig]);
314
381
 
@@ -330,3 +397,168 @@ export function useTableState<T>({
330
397
  headRefs,
331
398
  };
332
399
  }
400
+
401
+ // ---------- Owner-held table state, mirrored to the URL ----------
402
+
403
+ export interface UseTableUrlStateOptions<T> {
404
+ /** The table's columns — read for their keys and filter types only. */
405
+ columns: DataTableColumn<T>[];
406
+ /** Default 25. A page size equal to it is left out of the URL. */
407
+ defaultPageSize?: number;
408
+ /** Mirror the state into the URL and seed it from there on mount. Default true.
409
+ * `{ prefix }` puts a prefix in front of every key, for a page with two URL-synced
410
+ * tables — see {@link TableUrlSync}. */
411
+ urlSync?: TableUrlSync;
412
+ /** Shorthand for `urlSync: { prefix }`; wins over a prefix given there. */
413
+ urlPrefix?: string;
414
+ /** Also hold a free-text search term, under `q`. Off by default, so a `q` that
415
+ * belongs to someone else on the page is never touched. */
416
+ search?: boolean;
417
+ }
418
+
419
+ /** The part of {@link DataTableProps} a server-driven table takes from its owner. */
420
+ export interface TableUrlStateProps {
421
+ serverPagination: ServerPagination;
422
+ filters: FilterState;
423
+ onFiltersChange: (next: FilterState) => void;
424
+ sorts: SortState[];
425
+ onSortsChange: (next: SortState[]) => void;
426
+ }
427
+
428
+ export interface UseTableUrlStateReturn {
429
+ /** 0-based. */
430
+ page: number;
431
+ setPage: (page: number) => void;
432
+ pageSize: number;
433
+ /** Also back to the first page. */
434
+ setPageSize: (pageSize: number) => void;
435
+ filters: FilterState;
436
+ /** Also back to the first page. */
437
+ setFilters: (next: FilterState) => void;
438
+ sorts: SortState[];
439
+ /** Also back to the first page. */
440
+ setSorts: (next: SortState[]) => void;
441
+ /** The search term (`""` unless `search: true`). */
442
+ search: string;
443
+ /** Also back to the first page. */
444
+ setSearch: (next: string) => void;
445
+ /**
446
+ * Everything the table needs, to spread onto it:
447
+ * `<DataTable {...table.tableProps(data?.total ?? 0, { isLoading })} rows={…} … />`.
448
+ */
449
+ tableProps: (total: number, options?: { isLoading?: boolean }) => TableUrlStateProps;
450
+ }
451
+
452
+ interface OwnedState {
453
+ sorts: SortState[];
454
+ filters: FilterState;
455
+ pageSize: number;
456
+ page: number;
457
+ search: string;
458
+ }
459
+
460
+ /**
461
+ * The controlled state of a `serverPagination` table — filters, sorts, page, page size
462
+ * and optionally a search term — kept by the OWNER and mirrored into the URL in the
463
+ * table's own scheme (`f.<key>`, `sort`, `p`, `ps`, and `q`).
464
+ *
465
+ * A server-driven table owns none of this: the owner has to hold it to turn it into
466
+ * the query that fetches `rows`, and until now it also had to restate the table's URL
467
+ * codec to keep the view bookmarkable (kastlan's use-server-table.ts copied
468
+ * `readUrlState` / `writeUrlState` to do it). Same rules as `urlSync`:
469
+ *
470
+ * * **The URL is read once, on mount** — a shared or deep link (see {@link filterHref})
471
+ * seeds the view; after that the state is local and written out with `replace`.
472
+ * * **Any change but the page itself goes back to page 1** — a narrower filter must not
473
+ * leave the user on page 7 of 2.
474
+ * * **Written by value**: an unchanged view writes nothing, however often the owner
475
+ * renders.
476
+ *
477
+ * Mapping the state onto the backend's query parameters stays the owner's: that is the
478
+ * part the kit cannot know.
479
+ */
480
+ export function useTableUrlState<T>({
481
+ columns,
482
+ defaultPageSize = 25,
483
+ urlSync: urlSyncOption = true,
484
+ urlPrefix: urlPrefixOption,
485
+ search: manageSearch = false,
486
+ }: UseTableUrlStateOptions<T>): UseTableUrlStateReturn {
487
+ const resolved = resolveTableUrlSync(urlSyncOption);
488
+ const urlSync = resolved.enabled;
489
+ const urlPrefix = urlPrefixOption ?? resolved.prefix;
490
+ const keys = useMemo(() => urlKeys(urlPrefix), [urlPrefix]);
491
+ const [searchParams, setSearchParams] = useSearchParams();
492
+ const location = useLocation();
493
+ const [state, setState] = useState<OwnedState>(() => {
494
+ const url = urlSync ? readTableUrlState(searchParams, columns, urlPrefix) : {};
495
+ return {
496
+ sorts: url.sorts ?? [],
497
+ filters: url.filters ?? {},
498
+ pageSize: url.pageSize ?? defaultPageSize,
499
+ page: url.page ?? 0,
500
+ search: urlSync && manageSearch ? (searchParams.get(keys.search) ?? "") : "",
501
+ };
502
+ });
503
+
504
+ const lastUrl = useRef<string | null>(null);
505
+ const urlSig = urlSync
506
+ ? urlSignature(columns, state.sorts, state.filters, state.pageSize, state.page, defaultPageSize, keys) +
507
+ (manageSearch ? `&${keys.search}=${state.search.trim() ? state.search : ""}` : "")
508
+ : "";
509
+ useEffect(() => {
510
+ if (!urlSync) return;
511
+ // First run included: a link that arrives with `p=1` or a default `ps` is
512
+ // normalised to the table's own spelling, as `urlSync` does.
513
+ if (lastUrl.current === urlSig) return;
514
+ lastUrl.current = urlSig;
515
+ writeUrlState(
516
+ setSearchParams,
517
+ columns,
518
+ state.sorts,
519
+ state.filters,
520
+ state.pageSize,
521
+ state.page,
522
+ defaultPageSize,
523
+ manageSearch ? state.search : undefined,
524
+ location.state,
525
+ keys,
526
+ );
527
+ // Keyed on the signature, which is computed from everything the write reads — see
528
+ // the same effect in useTableState.
529
+ // eslint-disable-next-line react-hooks/exhaustive-deps
530
+ }, [urlSync, urlSig]);
531
+
532
+ const setPage = (page: number) => setState((s) => ({ ...s, page }));
533
+ const setPageSize = (pageSize: number) => setState((s) => ({ ...s, pageSize, page: 0 }));
534
+ const setFilters = (filters: FilterState) => setState((s) => ({ ...s, filters, page: 0 }));
535
+ const setSorts = (sorts: SortState[]) => setState((s) => ({ ...s, sorts, page: 0 }));
536
+ const setSearch = (search: string) => setState((s) => ({ ...s, search, page: 0 }));
537
+
538
+ return {
539
+ page: state.page,
540
+ setPage,
541
+ pageSize: state.pageSize,
542
+ setPageSize,
543
+ filters: state.filters,
544
+ setFilters,
545
+ sorts: state.sorts,
546
+ setSorts,
547
+ search: state.search,
548
+ setSearch,
549
+ tableProps: (total, options) => ({
550
+ serverPagination: {
551
+ page: state.page,
552
+ pageSize: state.pageSize,
553
+ total,
554
+ onPageChange: setPage,
555
+ onPageSizeChange: setPageSize,
556
+ isLoading: options?.isLoading,
557
+ },
558
+ filters: state.filters,
559
+ onFiltersChange: setFilters,
560
+ sorts: state.sorts,
561
+ onSortsChange: setSorts,
562
+ }),
563
+ };
564
+ }
package/src/data-table.ts CHANGED
@@ -19,4 +19,14 @@ export type {
19
19
  MobileSwipeActions,
20
20
  DataTableDensity,
21
21
  DataTableChrome,
22
+ DataTableRowAction,
22
23
  } from "./components/data-table";
24
+ export * from "./components/data-table-cells";
25
+ export { useTableUrlState, readTableUrlState } from "./components/use-table-state";
26
+ export type {
27
+ TableUrlState,
28
+ TableUrlSync,
29
+ TableUrlStateProps,
30
+ UseTableUrlStateOptions,
31
+ UseTableUrlStateReturn,
32
+ } from "./components/use-table-state";
@@ -94,7 +94,10 @@ export function FeedbackAttachmentField({
94
94
  pasteFrom?: RefObject<HTMLElement | null>;
95
95
  className?: string;
96
96
  }) {
97
- const [preview, setPreview] = useState<string | null>(null);
97
+ // The object URL is keyed to the file it was made for, so a stale one (from the
98
+ // previous file, or after `value` is cleared) is never shown — no reset needed.
99
+ const [previewFor, setPreviewFor] = useState<{ file: File; url: string } | null>(null);
100
+ const preview = value && previewFor?.file === value ? previewFor.url : null;
98
101
  const [capturing, setCapturing] = useState(false);
99
102
  const fileInputRef = useRef<HTMLInputElement>(null);
100
103
  // `file.size` from `<UiKitProvider labels>`, formatted in its locale — see FileDropzone.
@@ -103,12 +106,13 @@ export function FeedbackAttachmentField({
103
106
 
104
107
  // Object-URL preview lifecycle (create on change, revoke on cleanup).
105
108
  useEffect(() => {
106
- if (!value) {
107
- setPreview(null);
108
- return;
109
- }
109
+ if (!value) return;
110
110
  const url = URL.createObjectURL(value);
111
- setPreview(url);
111
+ // The URL is an external resource that must be created and revoked in step
112
+ // with the effect (a render-time `createObjectURL` would leak, and StrictMode's
113
+ // remount would revoke a memoised one out from under the <img>).
114
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- publishing an external resource's handle
115
+ setPreviewFor({ file: value, url });
112
116
  return () => URL.revokeObjectURL(url);
113
117
  }, [value]);
114
118
 
@@ -161,7 +165,6 @@ export function FeedbackAttachmentField({
161
165
  };
162
166
  target.addEventListener("paste", onPaste);
163
167
  return () => target.removeEventListener("paste", onPaste);
164
- // eslint-disable-next-line react-hooks/exhaustive-deps -- takeImage reads `latest`, which is a ref
165
168
  }, [documentPaste, pasteFrom]);
166
169
  const listensElsewhere = documentPaste || pasteFrom !== undefined;
167
170
 
@@ -1,8 +1,9 @@
1
- import { useEffect, useState } from "react";
1
+ import { useState } from "react";
2
2
  import type { ReactNode } from "react";
3
3
  import { Button, Input, PHONE_QUERY, Select, Textarea } from "../components/ui";
4
4
  import { Modal } from "../components/modal";
5
5
  import { useMediaQuery } from "../hooks/use-media-query";
6
+ import { useKitLabels } from "../i18n/kit-labels";
6
7
  import {
7
8
  DEFAULT_ATTACHMENT_ACCEPT,
8
9
  DEFAULT_MAX_ATTACHMENT_BYTES,
@@ -39,6 +40,38 @@ export interface FeedbackDialogLabels extends FeedbackAttachmentLabels {
39
40
  save: string;
40
41
  }
41
42
 
43
+ /**
44
+ * The dialog's own strings — the `feedbackDialog` namespace of `<UiKitProvider
45
+ * labels>` (0.12.0). The attachment keys are not repeated here: they are the
46
+ * `feedbackAttachment` namespace's, which the field inside already reads.
47
+ *
48
+ * Before, every key was required at the call site, so each app restated ten strings
49
+ * wherever it mounted the dialog (kastlan feedback-button.tsx built the object inline).
50
+ */
51
+ export interface FeedbackDialogTextLabels {
52
+ title: string;
53
+ category: string;
54
+ subject: string;
55
+ body: string;
56
+ /** The heading over the attachment buttons. */
57
+ attachment: string;
58
+ /** The line beside the buttons naming the shortcut. */
59
+ submitHint: string;
60
+ cancel: string;
61
+ save: string;
62
+ }
63
+
64
+ export const DEFAULT_FEEDBACK_DIALOG_LABELS: FeedbackDialogTextLabels = {
65
+ title: "Send feedback",
66
+ category: "Category",
67
+ subject: "Subject",
68
+ body: "What happened?",
69
+ attachment: "Screenshot",
70
+ submitHint: "Ctrl/⌘ + Enter to send",
71
+ cancel: "Cancel",
72
+ save: "Send",
73
+ };
74
+
42
75
  export interface FeedbackSubmission {
43
76
  title: string;
44
77
  body: string;
@@ -67,7 +100,7 @@ export function FeedbackDialog({
67
100
  categories,
68
101
  category,
69
102
  onCategoryChange,
70
- labels,
103
+ labels: labelsProp,
71
104
  onSubmit,
72
105
  submitting = false,
73
106
  contextSlot,
@@ -81,7 +114,9 @@ export function FeedbackDialog({
81
114
  categories: FeedbackCategoryOption[];
82
115
  category: string;
83
116
  onCategoryChange: (value: string) => void;
84
- labels: FeedbackDialogLabels;
117
+ /** Prop > `<UiKitProvider labels={{ feedbackDialog, feedbackAttachment }}>` >
118
+ * English. Optional since 0.12.0; a whole `FeedbackDialogLabels` still fits. */
119
+ labels?: Partial<FeedbackDialogLabels>;
85
120
  onSubmit: (data: FeedbackSubmission) => void | Promise<void>;
86
121
  submitting?: boolean;
87
122
  contextSlot?: ReactNode;
@@ -99,15 +134,22 @@ export function FeedbackDialog({
99
134
  const [body, setBody] = useState("");
100
135
  const [attachment, setAttachment] = useState<File | null>(null);
101
136
  const isMobile = useMediaQuery(PHONE_QUERY, false);
137
+ const labels = useKitLabels("feedbackDialog", DEFAULT_FEEDBACK_DIALOG_LABELS, labelsProp);
138
+ // The attachment field resolves its own keys from `feedbackAttachment`; it gets the
139
+ // prop's (so a caller's `attachmentAdd` still wins) plus the resolved heading.
140
+ const attachmentLabels = { ...labelsProp, attachment: labels.attachment };
102
141
 
103
- // Reset the form whenever the dialog is (re)opened.
104
- useEffect(() => {
142
+ // Reset the form whenever the dialog is (re)opened — during render, on the
143
+ // closed-to-open transition, so the old draft never paints for a frame.
144
+ const [wasOpen, setWasOpen] = useState(open);
145
+ if (open !== wasOpen) {
146
+ setWasOpen(open);
105
147
  if (open) {
106
148
  setTitle("");
107
149
  setBody("");
108
150
  setAttachment(null);
109
151
  }
110
- }, [open]);
152
+ }
111
153
 
112
154
  const canSubmit = !!title && !!body && !submitting;
113
155
  const trySubmit = () => {
@@ -138,7 +180,7 @@ export function FeedbackDialog({
138
180
  <FeedbackAttachmentField
139
181
  value={attachment}
140
182
  onChange={setAttachment}
141
- labels={labels}
183
+ labels={attachmentLabels}
142
184
  accept={attachmentAccept}
143
185
  maxBytes={maxAttachmentBytes}
144
186
  onError={onAttachmentError}
@@ -360,6 +360,10 @@ export function FeedbackStatusTransitions({
360
360
  className?: string;
361
361
  }) {
362
362
  return (
363
+ // Not a control: it only stops clicks on the buttons inside from bubbling to
364
+ // the row. The buttons are the keyboard path, and Enter/Space on them fires
365
+ // their own click, which lands here the same way.
366
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- propagation guard around real buttons
363
367
  <div
364
368
  className={cn(variant === "icon" ? "flex items-center gap-0.5" : "flex flex-wrap gap-1.5", className)}
365
369
  // A status control inside a clickable row must not also open the row.
@@ -511,6 +515,9 @@ export function FeedbackNoteEditor({
511
515
  const root = useRef<HTMLDivElement>(null);
512
516
  const submit = () => onSave(draft, file);
513
517
  return (
518
+ // Not a control: a delegated Ctrl/Cmd+Enter shortcut for the textarea inside,
519
+ // which is what holds focus. The save button is the non-shortcut path.
520
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions -- delegated keyboard shortcut for the focused field
514
521
  <div
515
522
  ref={root}
516
523
  className="space-y-2"