@eifi1/ui-kit 0.4.0 → 0.5.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 (410) hide show
  1. package/LICENSE +32 -0
  2. package/README.md +498 -57
  3. package/dist/chart.d.ts +10 -0
  4. package/dist/chart.js +10 -0
  5. package/dist/chart.js.map +1 -0
  6. package/dist/components/account-settings.d.ts +11 -0
  7. package/dist/components/account-settings.js +19 -12
  8. package/dist/components/account-settings.js.map +1 -1
  9. package/dist/components/alert-banner.d.ts +10 -6
  10. package/dist/components/alert-banner.js +20 -16
  11. package/dist/components/alert-banner.js.map +1 -1
  12. package/dist/components/amount-input.d.ts +30 -4
  13. package/dist/components/amount-input.js +36 -14
  14. package/dist/components/amount-input.js.map +1 -1
  15. package/dist/components/calculator.d.ts +24 -7
  16. package/dist/components/calculator.js +37 -20
  17. package/dist/components/calculator.js.map +1 -1
  18. package/dist/components/chart-zoom.d.ts +156 -0
  19. package/dist/components/chart-zoom.js +262 -0
  20. package/dist/components/chart-zoom.js.map +1 -0
  21. package/dist/components/chart.d.ts +28 -3
  22. package/dist/components/chart.js +71 -32
  23. package/dist/components/chart.js.map +1 -1
  24. package/dist/components/checkbox.d.ts +51 -0
  25. package/dist/components/checkbox.js +123 -0
  26. package/dist/components/checkbox.js.map +1 -0
  27. package/dist/components/chip.d.ts +128 -0
  28. package/dist/components/chip.js +357 -0
  29. package/dist/components/chip.js.map +1 -0
  30. package/dist/components/choice-parts.d.ts +24 -0
  31. package/dist/components/choice-parts.js +18 -0
  32. package/dist/components/choice-parts.js.map +1 -0
  33. package/dist/components/combobox-core.d.ts +52 -9
  34. package/dist/components/combobox-core.js +91 -20
  35. package/dist/components/combobox-core.js.map +1 -1
  36. package/dist/components/combobox.d.ts +43 -33
  37. package/dist/components/combobox.js +409 -340
  38. package/dist/components/combobox.js.map +1 -1
  39. package/dist/components/currency-select.d.ts +48 -8
  40. package/dist/components/currency-select.js +107 -65
  41. package/dist/components/currency-select.js.map +1 -1
  42. package/dist/components/data-table-filter-popover.d.ts +15 -3
  43. package/dist/components/data-table-filter-popover.js +23 -17
  44. package/dist/components/data-table-filter-popover.js.map +1 -1
  45. package/dist/components/data-table-filters.d.ts +1 -1
  46. package/dist/components/data-table-filters.js +3 -2
  47. package/dist/components/data-table-filters.js.map +1 -1
  48. package/dist/components/data-table-labels.d.ts +53 -2
  49. package/dist/components/data-table-labels.js +31 -2
  50. package/dist/components/data-table-labels.js.map +1 -1
  51. package/dist/components/data-table-pagination.d.ts +3 -1
  52. package/dist/components/data-table-pagination.js +16 -10
  53. package/dist/components/data-table-pagination.js.map +1 -1
  54. package/dist/components/data-table-sort.js +1 -0
  55. package/dist/components/data-table-sort.js.map +1 -1
  56. package/dist/components/data-table.d.ts +1 -1
  57. package/dist/components/data-table.js +201 -85
  58. package/dist/components/data-table.js.map +1 -1
  59. package/dist/components/date-picker.d.ts +25 -9
  60. package/dist/components/date-picker.js +232 -94
  61. package/dist/components/date-picker.js.map +1 -1
  62. package/dist/components/dropdown.d.ts +96 -20
  63. package/dist/components/dropdown.js +77 -33
  64. package/dist/components/dropdown.js.map +1 -1
  65. package/dist/components/entity-combobox.d.ts +5 -4
  66. package/dist/components/entity-combobox.js +107 -76
  67. package/dist/components/entity-combobox.js.map +1 -1
  68. package/dist/components/facing-pair.d.ts +36 -0
  69. package/dist/components/facing-pair.js +40 -0
  70. package/dist/components/facing-pair.js.map +1 -0
  71. package/dist/components/field-sync.d.ts +164 -0
  72. package/dist/components/field-sync.js +264 -0
  73. package/dist/components/field-sync.js.map +1 -0
  74. package/dist/components/file-dropzone.d.ts +38 -6
  75. package/dist/components/file-dropzone.js +34 -9
  76. package/dist/components/file-dropzone.js.map +1 -1
  77. package/dist/components/full-bleed-dialog.d.ts +37 -26
  78. package/dist/components/full-bleed-dialog.js +20 -5
  79. package/dist/components/full-bleed-dialog.js.map +1 -1
  80. package/dist/components/grouped-picker.d.ts +21 -10
  81. package/dist/components/grouped-picker.js +66 -52
  82. package/dist/components/grouped-picker.js.map +1 -1
  83. package/dist/components/hover-menu.d.ts +20 -4
  84. package/dist/components/hover-menu.js +15 -4
  85. package/dist/components/hover-menu.js.map +1 -1
  86. package/dist/components/mini-calendar.d.ts +102 -14
  87. package/dist/components/mini-calendar.js +256 -70
  88. package/dist/components/mini-calendar.js.map +1 -1
  89. package/dist/components/modal.d.ts +9 -3
  90. package/dist/components/modal.js +34 -45
  91. package/dist/components/modal.js.map +1 -1
  92. package/dist/components/month-picker.d.ts +79 -0
  93. package/dist/components/month-picker.js +342 -0
  94. package/dist/components/month-picker.js.map +1 -0
  95. package/dist/components/multi-entity-combobox.d.ts +7 -6
  96. package/dist/components/multi-entity-combobox.js +112 -72
  97. package/dist/components/multi-entity-combobox.js.map +1 -1
  98. package/dist/components/multi-select.d.ts +13 -11
  99. package/dist/components/multi-select.js +161 -84
  100. package/dist/components/multi-select.js.map +1 -1
  101. package/dist/components/number-field.d.ts +92 -0
  102. package/dist/components/number-field.js +111 -0
  103. package/dist/components/number-field.js.map +1 -0
  104. package/dist/components/number-input.d.ts +28 -2
  105. package/dist/components/number-input.js +12 -2
  106. package/dist/components/number-input.js.map +1 -1
  107. package/dist/components/numpad-sheet.d.ts +32 -4
  108. package/dist/components/numpad-sheet.js +30 -13
  109. package/dist/components/numpad-sheet.js.map +1 -1
  110. package/dist/components/page-contents.d.ts +90 -0
  111. package/dist/components/page-contents.js +170 -0
  112. package/dist/components/page-contents.js.map +1 -0
  113. package/dist/components/password-strength.d.ts +106 -0
  114. package/dist/components/password-strength.js +132 -0
  115. package/dist/components/password-strength.js.map +1 -0
  116. package/dist/components/picker-sheet.d.ts +23 -15
  117. package/dist/components/picker-sheet.js +34 -14
  118. package/dist/components/picker-sheet.js.map +1 -1
  119. package/dist/components/popover.d.ts +62 -4
  120. package/dist/components/popover.js +52 -2
  121. package/dist/components/popover.js.map +1 -1
  122. package/dist/components/search-field.d.ts +25 -7
  123. package/dist/components/search-field.js +7 -5
  124. package/dist/components/search-field.js.map +1 -1
  125. package/dist/components/series-chart-labels.d.ts +14 -0
  126. package/dist/components/series-chart-labels.js +11 -0
  127. package/dist/components/series-chart-labels.js.map +1 -0
  128. package/dist/components/series-chart.d.ts +207 -0
  129. package/dist/components/series-chart.js +223 -0
  130. package/dist/components/series-chart.js.map +1 -0
  131. package/dist/components/settings-fields.d.ts +14 -4
  132. package/dist/components/settings-fields.js +19 -8
  133. package/dist/components/settings-fields.js.map +1 -1
  134. package/dist/components/signature-pad.d.ts +119 -0
  135. package/dist/components/signature-pad.js +405 -0
  136. package/dist/components/signature-pad.js.map +1 -0
  137. package/dist/components/slider.d.ts +80 -0
  138. package/dist/components/slider.js +155 -0
  139. package/dist/components/slider.js.map +1 -0
  140. package/dist/components/sparkline.d.ts +93 -0
  141. package/dist/components/sparkline.js +205 -0
  142. package/dist/components/sparkline.js.map +1 -0
  143. package/dist/components/stat-tile.d.ts +151 -0
  144. package/dist/components/stat-tile.js +236 -0
  145. package/dist/components/stat-tile.js.map +1 -0
  146. package/dist/components/swipeable-row.d.ts +25 -2
  147. package/dist/components/swipeable-row.js +31 -0
  148. package/dist/components/swipeable-row.js.map +1 -1
  149. package/dist/components/switch.d.ts +60 -0
  150. package/dist/components/switch.js +81 -0
  151. package/dist/components/switch.js.map +1 -0
  152. package/dist/components/time-input.d.ts +58 -0
  153. package/dist/components/time-input.js +50 -0
  154. package/dist/components/time-input.js.map +1 -0
  155. package/dist/components/toggle-group.d.ts +18 -3
  156. package/dist/components/toggle-group.js +10 -6
  157. package/dist/components/toggle-group.js.map +1 -1
  158. package/dist/components/toggle-legend.d.ts +115 -0
  159. package/dist/components/toggle-legend.js +114 -0
  160. package/dist/components/toggle-legend.js.map +1 -0
  161. package/dist/components/tooltip.d.ts +40 -11
  162. package/dist/components/tooltip.js +77 -20
  163. package/dist/components/tooltip.js.map +1 -1
  164. package/dist/components/treemap.d.ts +132 -0
  165. package/dist/components/treemap.js +179 -0
  166. package/dist/components/treemap.js.map +1 -0
  167. package/dist/components/ui.d.ts +159 -46
  168. package/dist/components/ui.js +220 -115
  169. package/dist/components/ui.js.map +1 -1
  170. package/dist/components/use-mobile-reveal.js +1 -0
  171. package/dist/components/use-mobile-reveal.js.map +1 -1
  172. package/dist/components/use-table-state.d.ts +1 -1
  173. package/dist/components/use-table-state.js +38 -17
  174. package/dist/components/use-table-state.js.map +1 -1
  175. package/dist/components/user-avatar.d.ts +15 -4
  176. package/dist/components/user-avatar.js +5 -4
  177. package/dist/components/user-avatar.js.map +1 -1
  178. package/dist/components/wizard-stepper.js +3 -2
  179. package/dist/components/wizard-stepper.js.map +1 -1
  180. package/dist/{data-table-filters-aXyf0Xub.d.ts → data-table-filters-DM1auUfH.d.ts} +27 -5
  181. package/dist/data-table.d.ts +7 -0
  182. package/dist/data-table.js +12 -0
  183. package/dist/data-table.js.map +1 -0
  184. package/dist/feedback/feedback-attachment.js +12 -11
  185. package/dist/feedback/feedback-attachment.js.map +1 -1
  186. package/dist/feedback/feedback-dialog.js +2 -1
  187. package/dist/feedback/feedback-dialog.js.map +1 -1
  188. package/dist/feedback/feedback-inbox.d.ts +33 -7
  189. package/dist/feedback/feedback-inbox.js +49 -37
  190. package/dist/feedback/feedback-inbox.js.map +1 -1
  191. package/dist/feedback.d.ts +5 -0
  192. package/dist/feedback.js +5 -0
  193. package/dist/feedback.js.map +1 -0
  194. package/dist/hooks/use-anchored-panel.js +1 -0
  195. package/dist/hooks/use-anchored-panel.js.map +1 -1
  196. package/dist/hooks/use-anchored-rect.js +1 -0
  197. package/dist/hooks/use-anchored-rect.js.map +1 -1
  198. package/dist/hooks/use-announce.d.ts +46 -0
  199. package/dist/hooks/use-announce.js +37 -0
  200. package/dist/hooks/use-announce.js.map +1 -0
  201. package/dist/hooks/use-body-scroll-lock.js +1 -0
  202. package/dist/hooks/use-body-scroll-lock.js.map +1 -1
  203. package/dist/hooks/use-close-transition.js +1 -0
  204. package/dist/hooks/use-close-transition.js.map +1 -1
  205. package/dist/hooks/use-dismiss.js +1 -0
  206. package/dist/hooks/use-dismiss.js.map +1 -1
  207. package/dist/hooks/use-focus-trap.d.ts +16 -0
  208. package/dist/hooks/use-focus-trap.js +67 -0
  209. package/dist/hooks/use-focus-trap.js.map +1 -0
  210. package/dist/hooks/use-media-query.js +1 -0
  211. package/dist/hooks/use-media-query.js.map +1 -1
  212. package/dist/hooks/use-overlay-history.js +27 -1
  213. package/dist/hooks/use-overlay-history.js.map +1 -1
  214. package/dist/hooks/use-row-swipe.js +1 -0
  215. package/dist/hooks/use-row-swipe.js.map +1 -1
  216. package/dist/i18n/defaults.d.ts +31 -0
  217. package/dist/i18n/defaults.js +63 -0
  218. package/dist/i18n/defaults.js.map +1 -0
  219. package/dist/i18n/kit-labels.d.ts +250 -0
  220. package/dist/i18n/kit-labels.js +184 -0
  221. package/dist/i18n/kit-labels.js.map +1 -0
  222. package/dist/index.d.ts +47 -22
  223. package/dist/index.js +28 -0
  224. package/dist/index.js.map +1 -1
  225. package/dist/lib/calc.js +1 -0
  226. package/dist/lib/calc.js.map +1 -1
  227. package/dist/lib/cn.js +1 -0
  228. package/dist/lib/cn.js.map +1 -1
  229. package/dist/lib/dates.js +1 -0
  230. package/dist/lib/dates.js.map +1 -1
  231. package/dist/lib/logger.js +1 -0
  232. package/dist/lib/logger.js.map +1 -1
  233. package/dist/lib/safe-storage.js +1 -0
  234. package/dist/lib/safe-storage.js.map +1 -1
  235. package/dist/search/command-palette.d.ts +11 -1
  236. package/dist/search/command-palette.js +21 -15
  237. package/dist/search/command-palette.js.map +1 -1
  238. package/dist/search.d.ts +2 -0
  239. package/dist/search.js +3 -0
  240. package/dist/search.js.map +1 -0
  241. package/dist/shell/app-shell.d.ts +33 -4
  242. package/dist/shell/app-shell.js +275 -78
  243. package/dist/shell/app-shell.js.map +1 -1
  244. package/dist/shell/option-switcher-menu.js +3 -2
  245. package/dist/shell/option-switcher-menu.js.map +1 -1
  246. package/dist/shell/role-switcher.d.ts +6 -0
  247. package/dist/shell/role-switcher.js +7 -4
  248. package/dist/shell/role-switcher.js.map +1 -1
  249. package/dist/shell/top-bar.d.ts +15 -7
  250. package/dist/shell/top-bar.js +3 -5
  251. package/dist/shell/top-bar.js.map +1 -1
  252. package/dist/shell/topbar-action-menu.js +3 -2
  253. package/dist/shell/topbar-action-menu.js.map +1 -1
  254. package/dist/shell/topbar-controls.d.ts +5 -2
  255. package/dist/shell/topbar-controls.js +20 -15
  256. package/dist/shell/topbar-controls.js.map +1 -1
  257. package/dist/shell.d.ts +13 -0
  258. package/dist/shell.js +8 -0
  259. package/dist/shell.js.map +1 -0
  260. package/dist/theme/chart-palette.d.ts +19 -12
  261. package/dist/theme/chart-palette.js +24 -30
  262. package/dist/theme/chart-palette.js.map +1 -1
  263. package/dist/theme/color.d.ts +78 -0
  264. package/dist/theme/color.js +148 -0
  265. package/dist/theme/color.js.map +1 -0
  266. package/dist/theme/palette-derive.d.ts +140 -0
  267. package/dist/theme/palette-derive.js +376 -0
  268. package/dist/theme/palette-derive.js.map +1 -0
  269. package/dist/theme/palette-presets.d.ts +27 -5
  270. package/dist/theme/palette-presets.js +231 -33
  271. package/dist/theme/palette-presets.js.map +1 -1
  272. package/dist/theme/palette-store.js +1 -0
  273. package/dist/theme/palette-store.js.map +1 -1
  274. package/dist/theme/theme-store.js +1 -0
  275. package/dist/theme/theme-store.js.map +1 -1
  276. package/dist/tour/tour.d.ts +2 -1
  277. package/dist/tour/tour.js +15 -9
  278. package/dist/tour/tour.js.map +1 -1
  279. package/dist/tour.d.ts +2 -0
  280. package/dist/tour.js +3 -0
  281. package/dist/tour.js.map +1 -0
  282. package/dist/wizard/stepper-nav.js +10 -8
  283. package/dist/wizard/stepper-nav.js.map +1 -1
  284. package/dist/wizard/types.d.ts +18 -2
  285. package/dist/wizard/types.js +4 -1
  286. package/dist/wizard/types.js.map +1 -1
  287. package/dist/wizard/use-wizard.js +12 -4
  288. package/dist/wizard/use-wizard.js.map +1 -1
  289. package/dist/wizard/validation.js +1 -0
  290. package/dist/wizard/validation.js.map +1 -1
  291. package/dist/wizard/wizard-context.js +1 -0
  292. package/dist/wizard/wizard-context.js.map +1 -1
  293. package/dist/wizard/wizard-step.js +1 -0
  294. package/dist/wizard/wizard-step.js.map +1 -1
  295. package/dist/wizard/wizard-summary.js +5 -3
  296. package/dist/wizard/wizard-summary.js.map +1 -1
  297. package/dist/wizard.d.ts +8 -0
  298. package/dist/wizard.js +9 -0
  299. package/dist/wizard.js.map +1 -0
  300. package/package.json +67 -14
  301. package/src/chart.ts +22 -0
  302. package/src/components/account-settings.tsx +275 -0
  303. package/src/components/alert-banner.tsx +75 -0
  304. package/src/components/amount-input.tsx +471 -0
  305. package/src/components/calculator.tsx +252 -0
  306. package/src/components/chart-zoom.tsx +499 -0
  307. package/src/components/chart.tsx +363 -0
  308. package/src/components/checkbox.tsx +222 -0
  309. package/src/components/chip.tsx +570 -0
  310. package/src/components/choice-parts.ts +33 -0
  311. package/src/components/combobox-core.tsx +540 -0
  312. package/src/components/combobox.tsx +1026 -0
  313. package/src/components/currency-select.tsx +269 -0
  314. package/src/components/data-table-filter-popover.tsx +252 -0
  315. package/src/components/data-table-filters.ts +176 -0
  316. package/src/components/data-table-labels.ts +190 -0
  317. package/src/components/data-table-pagination.tsx +126 -0
  318. package/src/components/data-table-sort.ts +70 -0
  319. package/src/components/data-table.tsx +1740 -0
  320. package/src/components/date-picker.tsx +631 -0
  321. package/src/components/dropdown.tsx +341 -0
  322. package/src/components/entity-combobox.tsx +215 -0
  323. package/src/components/facing-pair.ts +86 -0
  324. package/src/components/field-sync.tsx +472 -0
  325. package/src/components/file-dropzone.tsx +181 -0
  326. package/src/components/full-bleed-dialog.tsx +179 -0
  327. package/src/components/grouped-picker.tsx +157 -0
  328. package/src/components/hover-menu.tsx +183 -0
  329. package/src/components/mini-calendar.tsx +554 -0
  330. package/src/components/modal.tsx +287 -0
  331. package/src/components/month-picker.tsx +549 -0
  332. package/src/components/multi-entity-combobox.tsx +226 -0
  333. package/src/components/multi-select.tsx +303 -0
  334. package/src/components/number-field.tsx +225 -0
  335. package/src/components/number-input.tsx +211 -0
  336. package/src/components/numpad-sheet.tsx +264 -0
  337. package/src/components/page-contents.tsx +289 -0
  338. package/src/components/password-strength.tsx +258 -0
  339. package/src/components/picker-sheet.tsx +251 -0
  340. package/src/components/popover.tsx +195 -0
  341. package/src/components/search-field.tsx +146 -0
  342. package/src/components/series-chart-labels.ts +26 -0
  343. package/src/components/series-chart.tsx +481 -0
  344. package/src/components/settings-fields.tsx +68 -0
  345. package/src/components/signature-pad.tsx +630 -0
  346. package/src/components/slider.tsx +313 -0
  347. package/src/components/sparkline.tsx +329 -0
  348. package/src/components/stat-tile.tsx +448 -0
  349. package/src/components/swipeable-row.tsx +271 -0
  350. package/src/components/switch.tsx +170 -0
  351. package/src/components/time-input.tsx +107 -0
  352. package/src/components/toggle-group.tsx +153 -0
  353. package/src/components/toggle-legend.tsx +229 -0
  354. package/src/components/tooltip.tsx +477 -0
  355. package/src/components/treemap.tsx +390 -0
  356. package/src/components/ui.tsx +1284 -0
  357. package/src/components/use-mobile-reveal.ts +54 -0
  358. package/src/components/use-table-state.ts +295 -0
  359. package/src/components/user-avatar.tsx +61 -0
  360. package/src/components/wizard-stepper.tsx +37 -0
  361. package/src/data-table.ts +19 -0
  362. package/src/feedback/feedback-attachment.tsx +246 -0
  363. package/src/feedback/feedback-dialog.tsx +209 -0
  364. package/src/feedback/feedback-inbox.tsx +615 -0
  365. package/src/feedback.ts +8 -0
  366. package/src/hooks/use-anchored-panel.ts +169 -0
  367. package/src/hooks/use-anchored-rect.ts +78 -0
  368. package/src/hooks/use-announce.ts +85 -0
  369. package/src/hooks/use-body-scroll-lock.ts +94 -0
  370. package/src/hooks/use-close-transition.ts +99 -0
  371. package/src/hooks/use-dismiss.ts +77 -0
  372. package/src/hooks/use-focus-trap.ts +160 -0
  373. package/src/hooks/use-media-query.ts +23 -0
  374. package/src/hooks/use-overlay-history.ts +345 -0
  375. package/src/hooks/use-row-swipe.ts +212 -0
  376. package/src/i18n/defaults.ts +68 -0
  377. package/src/i18n/kit-labels.tsx +447 -0
  378. package/src/index.ts +169 -0
  379. package/src/lib/calc.ts +309 -0
  380. package/src/lib/cn.ts +5 -0
  381. package/src/lib/dates.ts +187 -0
  382. package/src/lib/logger.ts +73 -0
  383. package/src/lib/safe-storage.ts +37 -0
  384. package/src/search/command-palette.tsx +342 -0
  385. package/src/search.ts +3 -0
  386. package/src/shell/app-shell.tsx +772 -0
  387. package/src/shell/option-switcher-menu.tsx +72 -0
  388. package/src/shell/role-switcher.tsx +46 -0
  389. package/src/shell/top-bar.tsx +39 -0
  390. package/src/shell/topbar-action-menu.tsx +91 -0
  391. package/src/shell/topbar-controls.tsx +229 -0
  392. package/src/shell.ts +10 -0
  393. package/src/theme/chart-palette.ts +145 -0
  394. package/src/theme/color.ts +266 -0
  395. package/src/theme/palette-derive.ts +681 -0
  396. package/src/theme/palette-presets.ts +760 -0
  397. package/src/theme/palette-store.ts +94 -0
  398. package/src/theme/theme-store.ts +145 -0
  399. package/src/tour/tour.tsx +586 -0
  400. package/src/tour.ts +3 -0
  401. package/src/vite-env.d.ts +26 -0
  402. package/src/wizard/stepper-nav.tsx +221 -0
  403. package/src/wizard/types.ts +178 -0
  404. package/src/wizard/use-wizard.ts +345 -0
  405. package/src/wizard/validation.ts +37 -0
  406. package/src/wizard/wizard-context.tsx +29 -0
  407. package/src/wizard/wizard-step.tsx +14 -0
  408. package/src/wizard/wizard-summary.tsx +69 -0
  409. package/src/wizard.ts +14 -0
  410. package/tokens.css +222 -20
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx, jsxs } from "react/jsx-runtime";
2
3
  import { cn } from "../lib/cn.js";
3
4
  function WizardStepper({
@@ -7,14 +8,14 @@ function WizardStepper({
7
8
  }) {
8
9
  const currentIndex = steps.findIndex((s) => s.key === current);
9
10
  return /* @__PURE__ */ jsx("ol", { className: "flex items-center gap-1 text-xs", "aria-label": ariaLabel, children: steps.map((step, i) => /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-1", children: [
10
- i > 0 && /* @__PURE__ */ jsx("span", { className: "w-4 border-t border-slate-300 dark:border-slate-600" }),
11
+ i > 0 && /* @__PURE__ */ jsx("span", { className: "w-4 border-t border-[var(--border)]" }),
11
12
  /* @__PURE__ */ jsx(
12
13
  "span",
13
14
  {
14
15
  "aria-current": step.key === current ? "step" : void 0,
15
16
  className: cn(
16
17
  "rounded-full px-2 py-0.5",
17
- i === currentIndex ? "bg-slate-900 text-white dark:bg-slate-100 dark:text-slate-900 font-medium" : i < currentIndex ? "text-slate-700 dark:text-slate-200" : "text-slate-400 dark:text-slate-500"
18
+ i === currentIndex ? "bg-[var(--bg-inverse)] text-[var(--text-inverse)] font-medium" : i < currentIndex ? "text-[var(--text-secondary)]" : "text-[var(--text-placeholder)]"
18
19
  ),
19
20
  children: step.label
20
21
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/wizard-stepper.tsx"],"sourcesContent":["import { cn } from \"../lib/cn\";\n\n/** Compact step indicator for the import wizards. Past steps are plain, the\n * current one is highlighted, future ones muted. */\nexport function WizardStepper({\n steps,\n current,\n ariaLabel,\n}: {\n steps: { key: string; label: string }[];\n current: string;\n ariaLabel?: string;\n}) {\n const currentIndex = steps.findIndex((s) => s.key === current);\n return (\n <ol className=\"flex items-center gap-1 text-xs\" aria-label={ariaLabel}>\n {steps.map((step, i) => (\n <li key={step.key} className=\"flex items-center gap-1\">\n {i > 0 && <span className=\"w-4 border-t border-slate-300 dark:border-slate-600\" />}\n <span\n aria-current={step.key === current ? \"step\" : undefined}\n className={cn(\n \"rounded-full px-2 py-0.5\",\n i === currentIndex\n ? \"bg-slate-900 text-white dark:bg-slate-100 dark:text-slate-900 font-medium\"\n : i < currentIndex\n ? \"text-slate-700 dark:text-slate-200\"\n : \"text-slate-400 dark:text-slate-500\",\n )}\n >\n {step.label}\n </span>\n </li>\n ))}\n </ol>\n );\n}\n"],"mappings":"AAiBQ,SACY,KADZ;AAjBR,SAAS,UAAU;AAIZ,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,eAAe,MAAM,UAAU,CAAC,MAAM,EAAE,QAAQ,OAAO;AAC7D,SACE,oBAAC,QAAG,WAAU,mCAAkC,cAAY,WACzD,gBAAM,IAAI,CAAC,MAAM,MAChB,qBAAC,QAAkB,WAAU,2BAC1B;AAAA,QAAI,KAAK,oBAAC,UAAK,WAAU,uDAAsD;AAAA,IAChF;AAAA,MAAC;AAAA;AAAA,QACC,gBAAc,KAAK,QAAQ,UAAU,SAAS;AAAA,QAC9C,WAAW;AAAA,UACT;AAAA,UACA,MAAM,eACF,8EACA,IAAI,eACF,uCACA;AAAA,QACR;AAAA,QAEC,eAAK;AAAA;AAAA,IACR;AAAA,OAdO,KAAK,GAed,CACD,GACH;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/wizard-stepper.tsx"],"sourcesContent":["import { cn } from \"../lib/cn\";\n\n/** Compact step indicator for the import wizards. Past steps are plain, the\n * current one is highlighted, future ones muted. */\nexport function WizardStepper({\n steps,\n current,\n ariaLabel,\n}: {\n steps: { key: string; label: string }[];\n current: string;\n ariaLabel?: string;\n}) {\n const currentIndex = steps.findIndex((s) => s.key === current);\n return (\n <ol className=\"flex items-center gap-1 text-xs\" aria-label={ariaLabel}>\n {steps.map((step, i) => (\n <li key={step.key} className=\"flex items-center gap-1\">\n {i > 0 && <span className=\"w-4 border-t border-[var(--border)]\" />}\n <span\n aria-current={step.key === current ? \"step\" : undefined}\n className={cn(\n \"rounded-full px-2 py-0.5\",\n i === currentIndex\n ? \"bg-[var(--bg-inverse)] text-[var(--text-inverse)] font-medium\"\n : i < currentIndex\n ? \"text-[var(--text-secondary)]\"\n : \"text-[var(--text-placeholder)]\",\n )}\n >\n {step.label}\n </span>\n </li>\n ))}\n </ol>\n );\n}\n"],"mappings":";AAiBQ,SACY,KADZ;AAjBR,SAAS,UAAU;AAIZ,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,eAAe,MAAM,UAAU,CAAC,MAAM,EAAE,QAAQ,OAAO;AAC7D,SACE,oBAAC,QAAG,WAAU,mCAAkC,cAAY,WACzD,gBAAM,IAAI,CAAC,MAAM,MAChB,qBAAC,QAAkB,WAAU,2BAC1B;AAAA,QAAI,KAAK,oBAAC,UAAK,WAAU,uCAAsC;AAAA,IAChE;AAAA,MAAC;AAAA;AAAA,QACC,gBAAc,KAAK,QAAQ,UAAU,SAAS;AAAA,QAC9C,WAAW;AAAA,UACT;AAAA,UACA,MAAM,eACF,kEACA,IAAI,eACF,iCACA;AAAA,QACR;AAAA,QAEC,eAAK;AAAA;AAAA,IACR;AAAA,OAdO,KAAK,GAed,CACD,GACH;AAEJ;","names":[]}
@@ -145,13 +145,29 @@ interface DataTableProps<T> {
145
145
  isSelectable?: (row: T) => boolean;
146
146
  isSelected: (row: T) => boolean;
147
147
  onToggle: (row: T, checked: boolean) => void;
148
+ /**
149
+ * Commit a whole Shift+click range in ONE call (audit 2026-09-22).
150
+ *
151
+ * Optional and purely additive: without it the range is still walked row by
152
+ * row, exactly as before. With it, an owner holding a `Set` copies that Set
153
+ * once instead of once per row — a 400-row range was 400 `onToggle` calls,
154
+ * 400 Set copies and 400 renders of this table, all to reach a state the
155
+ * owner could have reached in one.
156
+ *
157
+ * `rows` is already filtered by `isSelectable` and is in the order the table
158
+ * is showing, so it can be applied as-is.
159
+ */
160
+ onToggleMany?: (rows: T[], checked: boolean) => void;
148
161
  allSelected: boolean;
149
162
  someSelected: boolean;
150
163
  onToggleAll: (checked: boolean) => void;
151
164
  };
152
- /** User-facing strings (English defaults); pass translated overrides. */
165
+ /** User-facing strings (English defaults); pass translated overrides. Merged over
166
+ * `dataTable` from `<UiKitProvider labels>`, so a call site only states what
167
+ * differs for THIS table — typically `table`, its accessible name. */
153
168
  labels?: Partial<DataTableLabels>;
154
- /** BCP-47 locale for the date-filter picker. Defaults to "en". */
169
+ /** BCP-47 locale for the date filter's calendar and the pager's numbers. Falls
170
+ * back to `<UiKitProvider locale>`, then to the runtime's default. */
155
171
  locale?: string;
156
172
  /** localStorage namespace prefix for `storageKey` persistence. Defaults to
157
173
  * "hbui-table:"; pass your app's own prefix to keep a stable namespace. */
@@ -204,7 +220,7 @@ interface DataTableProps<T> {
204
220
  }
205
221
  type FilterState = Record<string, FilterValue>;
206
222
 
207
- declare function DataTable<T>({ rows, columns, rowKey, defaultPageSize, onRowClick, rowHref, expandedRow, isExpanded, rowClassName, rowAttributes, paginated, empty, storageKey, urlSync, fillHeight, maxBodyHeight, serverPagination, filters: filtersProp, onFiltersChange, sorts: sortsProp, onSortsChange, selection, labels: labelsProp, locale, storageKeyPrefix, mobileExpandAsDialog, mobileGroupBy, mobileGroupLabel, mobileCard, mobileSwipeActions, leadingRow, }: DataTableProps<T>): react.JSX.Element;
223
+ declare function DataTable<T>({ rows, columns, rowKey, defaultPageSize, onRowClick, rowHref, expandedRow, isExpanded, rowClassName, rowAttributes, paginated, empty, storageKey, urlSync, fillHeight, maxBodyHeight, serverPagination, filters: filtersProp, onFiltersChange, sorts: sortsProp, onSortsChange, selection, labels: labelsProp, locale: localeProp, storageKeyPrefix, mobileExpandAsDialog, mobileGroupBy, mobileGroupLabel, mobileCard, mobileSwipeActions, leadingRow, }: DataTableProps<T>): react.JSX.Element;
208
224
 
209
225
  /** How a column is filtered. `text`/`select`/`date`/`number` each read a value
210
226
  * off the row and compare it against the user's {@link FilterValue}. */
@@ -246,8 +262,14 @@ type FilterValue = {
246
262
  declare function resolveFilter<T>(col: DataTableColumn<T>): ColumnFilter<T> | null;
247
263
  /** Whether a filter value is actually narrowing anything (vs its empty default). */
248
264
  declare function isFilterActive(v: FilterValue | undefined): boolean;
249
- /** Whether `row` passes column `col`'s filter given the user's `state`. */
250
- declare function rowMatches<T>(col: DataTableColumn<T>, row: T, state: FilterValue): boolean;
265
+ /**
266
+ * Whether `row` passes column `col`'s filter given the user's `state`.
267
+ *
268
+ * `locale` is for the text filter's case folding. Plain `toLowerCase` folds by the
269
+ * root rules, under which a Turkish user typing "istanbul" does not find "İSTANBUL";
270
+ * `toLocaleLowerCase(locale)` does. Optional, and `undefined` is the runtime's default.
271
+ */
272
+ declare function rowMatches<T>(col: DataTableColumn<T>, row: T, state: FilterValue, locale?: string): boolean;
251
273
  /** The empty filter state for a column's filter type. */
252
274
  declare function defaultFilterState<T>(filter: ColumnFilter<T>): FilterValue;
253
275
  /** URL-safe encoding of a filter value (`f.<column>` param), null when empty. */
@@ -0,0 +1,7 @@
1
+ export { DEFAULT_DATA_TABLE_LABELS, DataTableLabels, missingDataTableLabels, resolveDataTableLabels } from './components/data-table-labels.js';
2
+ export { SortDir, SortState, decodeSorts, encodeSorts, nextSorts, normalizeSorts } from './components/data-table-sort.js';
3
+ export { C as ColumnFilter, D as DataTable, a as DataTableColumn, b as DataTableProps, F as FilterState, c as FilterValue, S as ServerPagination, d as decodeFilterValue, e as decodeFilterValueOfType, f as defaultFilterState, g as encodeFilterValue, i as isFilterActive, r as resolveFilter, h as rowMatches } from './data-table-filters-DM1auUfH.js';
4
+ export { PAGE_SIZE_OPTIONS, Pagination } from './components/data-table-pagination.js';
5
+ export { FilterPopover } from './components/data-table-filter-popover.js';
6
+ import 'react';
7
+ import './components/swipeable-row.js';
@@ -0,0 +1,12 @@
1
+ "use client";
2
+ export * from "./components/data-table-labels.js";
3
+ export * from "./components/data-table-sort.js";
4
+ export * from "./components/data-table-filters.js";
5
+ export * from "./components/data-table-pagination.js";
6
+ import { FilterPopover } from "./components/data-table-filter-popover.js";
7
+ import { DataTable } from "./components/data-table.js";
8
+ export {
9
+ DataTable,
10
+ FilterPopover
11
+ };
12
+ //# sourceMappingURL=data-table.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/data-table.ts"],"sourcesContent":["// `@eifi1/ui-kit/data-table` — the table suite.\n//\n// A re-slicing of the main barrel, not a new API. This entry point requires\n// `react-router`: DataTable calls useSearchParams unconditionally, above any `urlSync`\n// branch, so a Router is needed even with URL sync switched off.\nexport * from \"./components/data-table-labels\";\nexport * from \"./components/data-table-sort\";\nexport * from \"./components/data-table-filters\";\nexport * from \"./components/data-table-pagination\";\nexport { FilterPopover } from \"./components/data-table-filter-popover\";\n// `SortState` comes from data-table-sort above; re-exporting the module wholesale\n// would collide with it, which is why the main barrel names these explicitly too.\nexport { DataTable } from \"./components/data-table\";\nexport type {\n DataTableColumn,\n DataTableProps,\n ServerPagination,\n FilterState,\n} from \"./components/data-table\";\n"],"mappings":";AAKA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;AACd,SAAS,qBAAqB;AAG9B,SAAS,iBAAiB;","names":[]}
@@ -1,7 +1,10 @@
1
+ "use client";
1
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { cn } from "../lib/cn.js";
2
4
  import { useEffect, useRef, useState } from "react";
3
5
  import { Camera, FileText, Paperclip, X } from "lucide-react";
4
6
  import { Button } from "../components/ui.js";
7
+ import { useKitFileLabels } from "../i18n/kit-labels.js";
5
8
  const DEFAULT_ATTACHMENT_ACCEPT = ["image/png", "image/jpeg", "image/webp", "image/gif"];
6
9
  const DEFAULT_MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
7
10
  function FeedbackAttachmentField({
@@ -19,6 +22,7 @@ function FeedbackAttachmentField({
19
22
  const [preview, setPreview] = useState(null);
20
23
  const [capturing, setCapturing] = useState(false);
21
24
  const fileInputRef = useRef(null);
25
+ const fileText = useKitFileLabels();
22
26
  useEffect(() => {
23
27
  if (!value) {
24
28
  setPreview(null);
@@ -66,29 +70,26 @@ function FeedbackAttachmentField({
66
70
  return /* @__PURE__ */ jsxs(
67
71
  "div",
68
72
  {
69
- className,
73
+ className: cn("relative", className),
70
74
  onPaste: listensElsewhere ? void 0 : (event) => takeImage(event.clipboardData, () => event.preventDefault()),
71
75
  children: [
72
- labels.attachment && /* @__PURE__ */ jsx("div", { className: "mb-1 text-xs font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400", children: labels.attachment }),
76
+ labels.attachment && /* @__PURE__ */ jsx("div", { className: "mb-1 text-xs font-medium uppercase tracking-wide text-[var(--text-muted)]", children: labels.attachment }),
73
77
  value ? /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
74
78
  value.type.startsWith("image/") && preview ? /* @__PURE__ */ jsx(
75
79
  "img",
76
80
  {
77
81
  src: preview,
78
82
  alt: value.name,
79
- className: "h-20 w-20 rounded border border-slate-200 object-cover dark:border-slate-700"
83
+ className: "h-20 w-20 rounded border border-[var(--border)] object-cover"
80
84
  }
81
85
  ) : (
82
86
  // Non-image attachments (PDF, text) can't preview as an <img>, so
83
87
  // show a neutral file tile with the name/size beside it instead.
84
- /* @__PURE__ */ jsx("div", { className: "flex h-20 w-20 items-center justify-center rounded border border-slate-200 text-slate-400 dark:border-slate-700 dark:text-slate-500", children: /* @__PURE__ */ jsx(FileText, { className: "size-8" }) })
88
+ /* @__PURE__ */ jsx("div", { className: "flex h-20 w-20 items-center justify-center rounded border border-[var(--border)] text-[var(--text-placeholder)]", children: /* @__PURE__ */ jsx(FileText, { className: "size-8" }) })
85
89
  ),
86
90
  /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
87
- /* @__PURE__ */ jsx("div", { className: "truncate text-sm text-slate-700 dark:text-slate-200", children: value.name }),
88
- /* @__PURE__ */ jsxs("div", { className: "text-xs text-slate-500 dark:text-slate-400", children: [
89
- Math.round(value.size / 1024),
90
- " KB"
91
- ] })
91
+ /* @__PURE__ */ jsx("div", { className: "truncate text-sm text-[var(--text-secondary)]", children: value.name }),
92
+ /* @__PURE__ */ jsx("div", { className: "text-xs text-[var(--text-muted)]", children: fileText.size(value.size) })
92
93
  ] }),
93
94
  /* @__PURE__ */ jsx(
94
95
  "button",
@@ -96,7 +97,7 @@ function FeedbackAttachmentField({
96
97
  type: "button",
97
98
  onClick: () => onChange(null),
98
99
  "aria-label": labels.attachmentRemove,
99
- className: "rounded p-1.5 text-slate-400 hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-slate-800 dark:hover:text-slate-200",
100
+ className: "rounded p-1.5 text-[var(--text-placeholder)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]",
100
101
  children: /* @__PURE__ */ jsx(X, { className: "size-4" })
101
102
  }
102
103
  )
@@ -128,7 +129,7 @@ function FeedbackAttachmentField({
128
129
  }
129
130
  )
130
131
  ] }),
131
- /* @__PURE__ */ jsx("p", { className: "text-xs text-slate-500 dark:text-slate-400", children: labels.attachmentPaste ?? "\u2026or paste a screenshot from the clipboard." })
132
+ /* @__PURE__ */ jsx("p", { className: "text-xs text-[var(--text-muted)]", children: labels.attachmentPaste ?? "\u2026or paste a screenshot from the clipboard." })
132
133
  ] }),
133
134
  /* @__PURE__ */ jsx(
134
135
  "input",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/feedback/feedback-attachment.tsx"],"sourcesContent":["import { useEffect, useRef, useState, type RefObject } from \"react\";\nimport { Camera, FileText, Paperclip, X } from \"lucide-react\";\nimport { Button } from \"../components/ui\";\nimport type { FeedbackAttachmentLabels } from \"./feedback-dialog\";\n\nexport const DEFAULT_ATTACHMENT_ACCEPT = [\"image/png\", \"image/jpeg\", \"image/webp\", \"image/gif\"];\nexport const DEFAULT_MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;\n\n/**\n * Picking one picture: the file dialog, a capture of the app view, or a paste.\n *\n * Lifted out of {@link FeedbackDialog} so the *reply* half of the feature can\n * have it too (Steering Design feedback #128). A report is a conversation — it\n * is filed, it is answered, and the reporter sends it back saying that is not\n * what they meant — and the screenshot showing what they mean is taken at\n * whichever of those points they looked. Only the first of them had a way to\n * attach one, so everything after it had to be described in words.\n *\n * **Three ways in, and they are three because no one of them covers the others.**\n * The file dialog needs a file on disk; `onCaptureScreenshot` snapshots the\n * *whole* app view; and the clipboard is where a region snip already is on every\n * platform (Steering Design feedback #39). Somebody who wanted to show one panel\n * had to save a crop and then find it again.\n *\n * `documentPaste` and `pasteFrom` decide where the paste is listened for, and\n * it is a real choice rather than a flag with a default. A modal traps focus,\n * so while it is up every paste in the page is meant for it — including one\n * made with nothing in particular focused, which never reaches a React\n * `onPaste` on a child: that is `documentPaste`. An editor **inline on a\n * page** is not that: the page around it has its own fields, so it listens\n * within its own subtree and a paste elsewhere stays where it was aimed. But\n * \"its own subtree\" has to include the text box the paste is actually made in,\n * and that box is this field's *sibling*, not its child — a paste in it bubbles\n * to their common parent and never through here. `pasteFrom` is that parent\n * (Steering Design feedback #140): the element whose subtree is listened to,\n * handed in by whoever renders both the box and this field side by side.\n */\nexport function FeedbackAttachmentField({\n value,\n onChange,\n labels,\n accept = DEFAULT_ATTACHMENT_ACCEPT,\n maxBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onError,\n onCaptureScreenshot,\n documentPaste = false,\n pasteFrom,\n className,\n}: {\n value: File | null;\n onChange: (file: File | null) => void;\n labels: FeedbackAttachmentLabels;\n accept?: string[];\n maxBytes?: number;\n onError?: (kind: \"type\" | \"size\") => void;\n /** Snapshot the app view behind this and return it as a File. A \"Capture\n * screenshot\" button appears only when it is given. */\n onCaptureScreenshot?: () => Promise<File | null>;\n /** Listen for the paste on `document` rather than on this field's own\n * subtree. For a modal, which owns the whole page while it is up. */\n documentPaste?: boolean;\n /** Listen for the paste within this element's subtree rather than this\n * field's own — for a field standing *beside* the text box a paste is made\n * in, whose common parent is where the event bubbles to. Ignored when\n * `documentPaste` is set. */\n pasteFrom?: RefObject<HTMLElement | null>;\n className?: string;\n}) {\n const [preview, setPreview] = useState<string | null>(null);\n const [capturing, setCapturing] = useState(false);\n const fileInputRef = useRef<HTMLInputElement>(null);\n\n // Object-URL preview lifecycle (create on change, revoke on cleanup).\n useEffect(() => {\n if (!value) {\n setPreview(null);\n return;\n }\n const url = URL.createObjectURL(value);\n setPreview(url);\n return () => URL.revokeObjectURL(url);\n }, [value]);\n\n const pick = (file: File | undefined | null) => {\n if (!file) return;\n if (!accept.includes(file.type)) {\n onError?.(\"type\");\n return;\n }\n if (file.size > maxBytes) {\n onError?.(\"size\");\n return;\n }\n onChange(file);\n };\n\n // The paste handler is registered once rather than per render, so it reads\n // `pick` — which closes over props that change identity on every render — out\n // of a ref rather than out of its own dependency list.\n const latest = useRef(pick);\n useEffect(() => {\n latest.current = pick;\n });\n\n const takeImage = (clipboard: DataTransfer | null, stop: () => void) => {\n const items = Array.from(clipboard?.items ?? []);\n const image = items.find((item) => item.kind === \"file\" && item.type.startsWith(\"image/\"));\n const file = image?.getAsFile();\n if (!file) return;\n // Only once there IS an image: a paste of text into a field must stay a\n // paste of text, and a clipboard holding both is a copy whose text half is\n // what the field was focused for.\n stop();\n // Clipboard images arrive named \"image.png\" at best and unnamed at worst,\n // and the name is what the inbox shows beside the thumbnail. A name that\n // says where it came from is more use than the browser's.\n latest.current(new File([file], pastedName(file.type), { type: file.type }));\n };\n\n // The element listened on, where it is not this field's own subtree: the\n // document for a modal, the given parent for an inline editor. Read inside\n // the effect rather than in render, because a ref's `current` is only set\n // once the parent has mounted — which it has by the time effects run.\n useEffect(() => {\n const target: EventTarget | null = documentPaste ? document : (pasteFrom?.current ?? null);\n if (!target) return;\n const onPaste = (event: Event) => {\n const clipboard = (event as ClipboardEvent).clipboardData;\n takeImage(clipboard, () => event.preventDefault());\n };\n target.addEventListener(\"paste\", onPaste);\n return () => target.removeEventListener(\"paste\", onPaste);\n // eslint-disable-next-line react-hooks/exhaustive-deps -- takeImage reads `latest`, which is a ref\n }, [documentPaste, pasteFrom]);\n const listensElsewhere = documentPaste || pasteFrom !== undefined;\n\n return (\n <div\n className={className}\n onPaste={\n listensElsewhere\n ? undefined\n : (event) => takeImage(event.clipboardData, () => event.preventDefault())\n }\n >\n {labels.attachment && (\n <div className=\"mb-1 text-xs font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400\">\n {labels.attachment}\n </div>\n )}\n {value ? (\n <div className=\"flex items-start gap-2\">\n {value.type.startsWith(\"image/\") && preview ? (\n <img\n src={preview}\n alt={value.name}\n className=\"h-20 w-20 rounded border border-slate-200 object-cover dark:border-slate-700\"\n />\n ) : (\n // Non-image attachments (PDF, text) can't preview as an <img>, so\n // show a neutral file tile with the name/size beside it instead.\n <div className=\"flex h-20 w-20 items-center justify-center rounded border border-slate-200 text-slate-400 dark:border-slate-700 dark:text-slate-500\">\n <FileText className=\"size-8\" />\n </div>\n )}\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate text-sm text-slate-700 dark:text-slate-200\">{value.name}</div>\n <div className=\"text-xs text-slate-500 dark:text-slate-400\">\n {Math.round(value.size / 1024)} KB\n </div>\n </div>\n <button\n type=\"button\"\n onClick={() => onChange(null)}\n aria-label={labels.attachmentRemove}\n className=\"rounded p-1.5 text-slate-400 hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-slate-800 dark:hover:text-slate-200\"\n >\n <X className=\"size-4\" />\n </button>\n </div>\n ) : (\n <div className=\"space-y-1.5\">\n <div className=\"flex flex-wrap gap-2\">\n <Button type=\"button\" variant=\"secondary\" onClick={() => fileInputRef.current?.click()}>\n <Paperclip className=\"size-4\" /> {labels.attachmentAdd}\n </Button>\n {onCaptureScreenshot && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n disabled={capturing}\n onClick={() => {\n if (capturing) return;\n setCapturing(true);\n void onCaptureScreenshot()\n .then((file) => {\n if (file) pick(file);\n })\n .finally(() => setCapturing(false));\n }}\n >\n <Camera className=\"size-4\" />{\" \"}\n {capturing ? \"…\" : (labels.attachmentCapture ?? \"Capture screenshot\")}\n </Button>\n )}\n </div>\n {/* Said out loud, because a gesture with no affordance is a gesture\n nobody finds. */}\n <p className=\"text-xs text-slate-500 dark:text-slate-400\">\n {labels.attachmentPaste ?? \"…or paste a screenshot from the clipboard.\"}\n </p>\n </div>\n )}\n <input\n ref={fileInputRef}\n type=\"file\"\n accept={accept.join(\",\")}\n className=\"sr-only\"\n onChange={(e) => {\n pick(e.target.files?.[0]);\n e.target.value = \"\";\n }}\n />\n </div>\n );\n}\n\n/** What a pasted image is called once it is an attachment.\n *\n * The extension is read off the mime type rather than assumed to be `.png`:\n * Safari puts TIFF on the clipboard and a file called `pasted.png` that is not\n * a PNG is one the receiving end opens wrong. */\nexport function pastedName(type: string): string {\n const subtype = type.split(\"/\")[1] ?? \"png\";\n // `image/svg+xml` and friends carry a suffix that is not part of the\n // extension, and none of them are in the accepted list anyway.\n return `pasted.${subtype.split(\"+\")[0]}`;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/feedback/feedback-attachment.tsx"],"sourcesContent":["import { cn } from \"../lib/cn\";\nimport { useEffect, useRef, useState, type RefObject } from \"react\";\nimport { Camera, FileText, Paperclip, X } from \"lucide-react\";\nimport { Button } from \"../components/ui\";\nimport { useKitFileLabels } from \"../i18n/kit-labels\";\nimport type { FeedbackAttachmentLabels } from \"./feedback-dialog\";\n\nexport const DEFAULT_ATTACHMENT_ACCEPT = [\"image/png\", \"image/jpeg\", \"image/webp\", \"image/gif\"];\nexport const DEFAULT_MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;\n\n/**\n * Picking one picture: the file dialog, a capture of the app view, or a paste.\n *\n * Lifted out of {@link FeedbackDialog} so the *reply* half of the feature can\n * have it too (Steering Design feedback #128). A report is a conversation — it\n * is filed, it is answered, and the reporter sends it back saying that is not\n * what they meant — and the screenshot showing what they mean is taken at\n * whichever of those points they looked. Only the first of them had a way to\n * attach one, so everything after it had to be described in words.\n *\n * **Three ways in, and they are three because no one of them covers the others.**\n * The file dialog needs a file on disk; `onCaptureScreenshot` snapshots the\n * *whole* app view; and the clipboard is where a region snip already is on every\n * platform (Steering Design feedback #39). Somebody who wanted to show one panel\n * had to save a crop and then find it again.\n *\n * `documentPaste` and `pasteFrom` decide where the paste is listened for, and\n * it is a real choice rather than a flag with a default. A modal traps focus,\n * so while it is up every paste in the page is meant for it — including one\n * made with nothing in particular focused, which never reaches a React\n * `onPaste` on a child: that is `documentPaste`. An editor **inline on a\n * page** is not that: the page around it has its own fields, so it listens\n * within its own subtree and a paste elsewhere stays where it was aimed. But\n * \"its own subtree\" has to include the text box the paste is actually made in,\n * and that box is this field's *sibling*, not its child — a paste in it bubbles\n * to their common parent and never through here. `pasteFrom` is that parent\n * (Steering Design feedback #140): the element whose subtree is listened to,\n * handed in by whoever renders both the box and this field side by side.\n */\nexport function FeedbackAttachmentField({\n value,\n onChange,\n labels,\n accept = DEFAULT_ATTACHMENT_ACCEPT,\n maxBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onError,\n onCaptureScreenshot,\n documentPaste = false,\n pasteFrom,\n className,\n}: {\n value: File | null;\n onChange: (file: File | null) => void;\n labels: FeedbackAttachmentLabels;\n accept?: string[];\n maxBytes?: number;\n onError?: (kind: \"type\" | \"size\") => void;\n /** Snapshot the app view behind this and return it as a File. A \"Capture\n * screenshot\" button appears only when it is given. */\n onCaptureScreenshot?: () => Promise<File | null>;\n /** Listen for the paste on `document` rather than on this field's own\n * subtree. For a modal, which owns the whole page while it is up. */\n documentPaste?: boolean;\n /** Listen for the paste within this element's subtree rather than this\n * field's own — for a field standing *beside* the text box a paste is made\n * in, whose common parent is where the event bubbles to. Ignored when\n * `documentPaste` is set. */\n pasteFrom?: RefObject<HTMLElement | null>;\n className?: string;\n}) {\n const [preview, setPreview] = useState<string | null>(null);\n const [capturing, setCapturing] = useState(false);\n const fileInputRef = useRef<HTMLInputElement>(null);\n // `file.size` from `<UiKitProvider labels>`, formatted in its locale — see FileDropzone.\n const fileText = useKitFileLabels();\n\n // Object-URL preview lifecycle (create on change, revoke on cleanup).\n useEffect(() => {\n if (!value) {\n setPreview(null);\n return;\n }\n const url = URL.createObjectURL(value);\n setPreview(url);\n return () => URL.revokeObjectURL(url);\n }, [value]);\n\n const pick = (file: File | undefined | null) => {\n if (!file) return;\n if (!accept.includes(file.type)) {\n onError?.(\"type\");\n return;\n }\n if (file.size > maxBytes) {\n onError?.(\"size\");\n return;\n }\n onChange(file);\n };\n\n // The paste handler is registered once rather than per render, so it reads\n // `pick` — which closes over props that change identity on every render — out\n // of a ref rather than out of its own dependency list.\n const latest = useRef(pick);\n useEffect(() => {\n latest.current = pick;\n });\n\n const takeImage = (clipboard: DataTransfer | null, stop: () => void) => {\n const items = Array.from(clipboard?.items ?? []);\n const image = items.find((item) => item.kind === \"file\" && item.type.startsWith(\"image/\"));\n const file = image?.getAsFile();\n if (!file) return;\n // Only once there IS an image: a paste of text into a field must stay a\n // paste of text, and a clipboard holding both is a copy whose text half is\n // what the field was focused for.\n stop();\n // Clipboard images arrive named \"image.png\" at best and unnamed at worst,\n // and the name is what the inbox shows beside the thumbnail. A name that\n // says where it came from is more use than the browser's.\n latest.current(new File([file], pastedName(file.type), { type: file.type }));\n };\n\n // The element listened on, where it is not this field's own subtree: the\n // document for a modal, the given parent for an inline editor. Read inside\n // the effect rather than in render, because a ref's `current` is only set\n // once the parent has mounted — which it has by the time effects run.\n useEffect(() => {\n const target: EventTarget | null = documentPaste ? document : (pasteFrom?.current ?? null);\n if (!target) return;\n const onPaste = (event: Event) => {\n const clipboard = (event as ClipboardEvent).clipboardData;\n takeImage(clipboard, () => event.preventDefault());\n };\n target.addEventListener(\"paste\", onPaste);\n return () => target.removeEventListener(\"paste\", onPaste);\n // eslint-disable-next-line react-hooks/exhaustive-deps -- takeImage reads `latest`, which is a ref\n }, [documentPaste, pasteFrom]);\n const listensElsewhere = documentPaste || pasteFrom !== undefined;\n\n return (\n <div\n // `relative` for the same reason as FileDropzone: the `sr-only` file input below\n // is `position: absolute`, and without a positioned ancestor it is laid out\n // against the initial containing block — extending the DOCUMENT height to its own\n // offset and producing a phantom second scrollbar on any long page.\n className={cn(\"relative\", className)}\n onPaste={\n listensElsewhere\n ? undefined\n : (event) => takeImage(event.clipboardData, () => event.preventDefault())\n }\n >\n {labels.attachment && (\n <div className=\"mb-1 text-xs font-medium uppercase tracking-wide text-[var(--text-muted)]\">\n {labels.attachment}\n </div>\n )}\n {value ? (\n <div className=\"flex items-start gap-2\">\n {value.type.startsWith(\"image/\") && preview ? (\n <img\n src={preview}\n alt={value.name}\n className=\"h-20 w-20 rounded border border-[var(--border)] object-cover\"\n />\n ) : (\n // Non-image attachments (PDF, text) can't preview as an <img>, so\n // show a neutral file tile with the name/size beside it instead.\n <div className=\"flex h-20 w-20 items-center justify-center rounded border border-[var(--border)] text-[var(--text-placeholder)]\">\n <FileText className=\"size-8\" />\n </div>\n )}\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate text-sm text-[var(--text-secondary)]\">{value.name}</div>\n <div className=\"text-xs text-[var(--text-muted)]\">\n {fileText.size(value.size)}\n </div>\n </div>\n <button\n type=\"button\"\n onClick={() => onChange(null)}\n aria-label={labels.attachmentRemove}\n className=\"rounded p-1.5 text-[var(--text-placeholder)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]\"\n >\n <X className=\"size-4\" />\n </button>\n </div>\n ) : (\n <div className=\"space-y-1.5\">\n <div className=\"flex flex-wrap gap-2\">\n <Button type=\"button\" variant=\"secondary\" onClick={() => fileInputRef.current?.click()}>\n <Paperclip className=\"size-4\" /> {labels.attachmentAdd}\n </Button>\n {onCaptureScreenshot && (\n <Button\n type=\"button\"\n variant=\"secondary\"\n disabled={capturing}\n onClick={() => {\n if (capturing) return;\n setCapturing(true);\n void onCaptureScreenshot()\n .then((file) => {\n if (file) pick(file);\n })\n .finally(() => setCapturing(false));\n }}\n >\n <Camera className=\"size-4\" />{\" \"}\n {capturing ? \"…\" : (labels.attachmentCapture ?? \"Capture screenshot\")}\n </Button>\n )}\n </div>\n {/* Said out loud, because a gesture with no affordance is a gesture\n nobody finds. */}\n <p className=\"text-xs text-[var(--text-muted)]\">\n {labels.attachmentPaste ?? \"…or paste a screenshot from the clipboard.\"}\n </p>\n </div>\n )}\n <input\n ref={fileInputRef}\n type=\"file\"\n accept={accept.join(\",\")}\n className=\"sr-only\"\n onChange={(e) => {\n pick(e.target.files?.[0]);\n e.target.value = \"\";\n }}\n />\n </div>\n );\n}\n\n/** What a pasted image is called once it is an attachment.\n *\n * The extension is read off the mime type rather than assumed to be `.png`:\n * Safari puts TIFF on the clipboard and a file called `pasted.png` that is not\n * a PNG is one the receiving end opens wrong. */\nexport function pastedName(type: string): string {\n const subtype = type.split(\"/\")[1] ?? \"png\";\n // `image/svg+xml` and friends carry a suffix that is not part of the\n // extension, and none of them are in the accepted list anyway.\n return `pasted.${subtype.split(\"+\")[0]}`;\n}\n"],"mappings":"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","names":[]}
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
3
  import { useEffect, useState } from "react";
3
4
  import { Button, Input, PHONE_QUERY, Select, Textarea } from "../components/ui.js";
@@ -92,7 +93,7 @@ function FeedbackDialog({
92
93
  contextSlot
93
94
  ] }),
94
95
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2", children: [
95
- /* @__PURE__ */ jsx("div", { className: "text-xs text-slate-400 dark:text-slate-500", children: labels.submitHint }),
96
+ /* @__PURE__ */ jsx("div", { className: "text-xs text-[var(--text-placeholder)]", children: labels.submitHint }),
96
97
  /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
97
98
  /* @__PURE__ */ jsx(Button, { variant: "ghost", onClick: onClose, children: labels.cancel }),
98
99
  /* @__PURE__ */ jsx(Button, { onClick: trySubmit, disabled: !canSubmit, children: labels.save })
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/feedback/feedback-dialog.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Button, Input, PHONE_QUERY, Select, Textarea } from \"../components/ui\";\nimport { Modal } from \"../components/modal\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport {\n DEFAULT_ATTACHMENT_ACCEPT,\n DEFAULT_MAX_ATTACHMENT_BYTES,\n FeedbackAttachmentField,\n} from \"./feedback-attachment\";\n\nexport interface FeedbackCategoryOption {\n value: string;\n label: string;\n}\n\nexport interface FeedbackAttachmentLabels {\n /** The heading over the field. Optional: a note editor puts the buttons\n * straight under its textarea, where a second heading is noise. */\n attachment?: string;\n attachmentAdd: string;\n /** Label for the \"capture screenshot\" button. Optional — falls back to an English default. */\n attachmentCapture?: string;\n /** The line under the attachment buttons saying a screenshot can be pasted\n * straight in. Optional — falls back to an English default. */\n attachmentPaste?: string;\n attachmentRemove: string;\n}\n\nexport interface FeedbackDialogLabels extends FeedbackAttachmentLabels {\n title: string;\n category: string;\n subject: string;\n body: string;\n attachment: string;\n submitHint: string;\n cancel: string;\n save: string;\n}\n\nexport interface FeedbackSubmission {\n title: string;\n body: string;\n category: string;\n attachment: File | null;\n}\n\n/**\n * The generic feedback form dialog: category + subject + body + an optional image\n * attachment, with Ctrl/Cmd+Enter to submit. Domain-free — the app supplies the\n * category options, labels and an `onSubmit` that talks to its own backend, plus\n * an optional `contextSlot` for app-specific context (user, current URL, …).\n *\n * **A screenshot can be pasted straight in.** Ctrl/Cmd+V anywhere in the dialog\n * takes an image off the clipboard and makes it the attachment, through the same\n * validation and the same preview as the file picker. It is the gesture the two\n * ways in did not cover: `onCaptureScreenshot` snapshots the *whole* app view,\n * and the file picker needs a file — so somebody who wanted to show one panel,\n * or one region of one, had to save a crop to disk first and then find it again\n * (Steering Design feedback #39). The clipboard is where a region snip already\n * is on every platform.\n */\nexport function FeedbackDialog({\n open,\n onClose,\n categories,\n category,\n onCategoryChange,\n labels,\n onSubmit,\n submitting = false,\n contextSlot,\n attachmentAccept = DEFAULT_ATTACHMENT_ACCEPT,\n maxAttachmentBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onAttachmentError,\n onCaptureScreenshot,\n}: {\n open: boolean;\n onClose: () => void;\n categories: FeedbackCategoryOption[];\n category: string;\n onCategoryChange: (value: string) => void;\n labels: FeedbackDialogLabels;\n onSubmit: (data: FeedbackSubmission) => void | Promise<void>;\n submitting?: boolean;\n contextSlot?: ReactNode;\n attachmentAccept?: string[];\n maxAttachmentBytes?: number;\n onAttachmentError?: (kind: \"type\" | \"size\") => void;\n /**\n * Optional: capture a screenshot of the underlying app view and return it as a\n * File. When provided, a \"Capture screenshot\" button is shown next to \"Add\n * attachment\"; the returned file is fed through the same validation + preview.\n */\n onCaptureScreenshot?: () => Promise<File | null>;\n}) {\n const [title, setTitle] = useState(\"\");\n const [body, setBody] = useState(\"\");\n const [attachment, setAttachment] = useState<File | null>(null);\n const isMobile = useMediaQuery(PHONE_QUERY, false);\n\n // Reset the form whenever the dialog is (re)opened.\n useEffect(() => {\n if (open) {\n setTitle(\"\");\n setBody(\"\");\n setAttachment(null);\n }\n }, [open]);\n\n const canSubmit = !!title && !!body && !submitting;\n const trySubmit = () => {\n if (canSubmit) void onSubmit({ title, body, category, attachment });\n };\n\n if (!open) return null;\n\n const categoryField = (\n <Select label={labels.category} value={category} onChange={(e) => onCategoryChange(e.target.value)}>\n {categories.map((c) => (\n <option key={c.value} value={c.value}>\n {c.label}\n </option>\n ))}\n </Select>\n );\n // On a phone the subject leads as a heading rather than as the second of two\n // identical boxes under a select (Keksdose feedback #179). `variant=\"display\"`\n // is a no-op above the phone breakpoint, so the desktop dialog is unchanged.\n const subjectField = (\n <Input label={labels.subject} value={title} onChange={(e) => setTitle(e.target.value)} variant=\"display\" />\n );\n const bodyField = (\n <Textarea rows={5} label={labels.body} value={body} onChange={(e) => setBody(e.target.value)} />\n );\n const attachmentField = (\n <FeedbackAttachmentField\n value={attachment}\n onChange={setAttachment}\n labels={labels}\n accept={attachmentAccept}\n maxBytes={maxAttachmentBytes}\n onError={onAttachmentError}\n onCaptureScreenshot={onCaptureScreenshot}\n // On `document`, not on the panel: the Modal focuses its own panel on open\n // and traps Tab inside it, so while this dialog is up every paste in the\n // page is meant for it — including the one made with nothing in\n // particular focused, which never reaches a React `onPaste` on a child.\n documentPaste={open}\n />\n );\n\n return (\n <Modal\n onClose={onClose}\n // The one dialog whose subject is the page behind it (Keksdose dev#460: \"Make\n // the feedback dialog draggable so I can see behind it if it blocks\n // something\") — you are describing what is under it while you type.\n draggable\n onKeyDown={(e) => {\n if ((e.ctrlKey || e.metaKey) && e.key === \"Enter\") {\n e.preventDefault();\n trySubmit();\n }\n }}\n >\n {/* Also the drag handle: pressing anywhere on the panel's own chrome moves it,\n and the heading is the strip a user reaches for. */}\n <h2 className=\"mb-3 text-lg font-semibold\">{labels.title}</h2>\n <div className=\"space-y-3\">\n {isMobile ? (\n // Phone shape, the same reasoning as the transaction editor's (#177):\n // the desktop order is the DATA MODEL's order — category, then subject,\n // then body — but nobody opens this dialog to pick a category. What they\n // came to do is say the thing, so that leads; the classification and the\n // evidence follow, a tier down.\n <>\n {subjectField}\n {bodyField}\n <div className=\"space-y-3 border-t border-[var(--border)] pt-3\">\n {categoryField}\n {attachmentField}\n {contextSlot}\n </div>\n </>\n ) : (\n <>\n {categoryField}\n {subjectField}\n {bodyField}\n {attachmentField}\n {contextSlot}\n </>\n )}\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"text-xs text-slate-400 dark:text-slate-500\">{labels.submitHint}</div>\n <div className=\"flex gap-2\">\n <Button variant=\"ghost\" onClick={onClose}>\n {labels.cancel}\n </Button>\n <Button onClick={trySubmit} disabled={!canSubmit}>\n {labels.save}\n </Button>\n </div>\n </div>\n </div>\n </Modal>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/feedback/feedback-dialog.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Button, Input, PHONE_QUERY, Select, Textarea } from \"../components/ui\";\nimport { Modal } from \"../components/modal\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport {\n DEFAULT_ATTACHMENT_ACCEPT,\n DEFAULT_MAX_ATTACHMENT_BYTES,\n FeedbackAttachmentField,\n} from \"./feedback-attachment\";\n\nexport interface FeedbackCategoryOption {\n value: string;\n label: string;\n}\n\nexport interface FeedbackAttachmentLabels {\n /** The heading over the field. Optional: a note editor puts the buttons\n * straight under its textarea, where a second heading is noise. */\n attachment?: string;\n attachmentAdd: string;\n /** Label for the \"capture screenshot\" button. Optional — falls back to an English default. */\n attachmentCapture?: string;\n /** The line under the attachment buttons saying a screenshot can be pasted\n * straight in. Optional — falls back to an English default. */\n attachmentPaste?: string;\n attachmentRemove: string;\n}\n\nexport interface FeedbackDialogLabels extends FeedbackAttachmentLabels {\n title: string;\n category: string;\n subject: string;\n body: string;\n attachment: string;\n submitHint: string;\n cancel: string;\n save: string;\n}\n\nexport interface FeedbackSubmission {\n title: string;\n body: string;\n category: string;\n attachment: File | null;\n}\n\n/**\n * The generic feedback form dialog: category + subject + body + an optional image\n * attachment, with Ctrl/Cmd+Enter to submit. Domain-free — the app supplies the\n * category options, labels and an `onSubmit` that talks to its own backend, plus\n * an optional `contextSlot` for app-specific context (user, current URL, …).\n *\n * **A screenshot can be pasted straight in.** Ctrl/Cmd+V anywhere in the dialog\n * takes an image off the clipboard and makes it the attachment, through the same\n * validation and the same preview as the file picker. It is the gesture the two\n * ways in did not cover: `onCaptureScreenshot` snapshots the *whole* app view,\n * and the file picker needs a file — so somebody who wanted to show one panel,\n * or one region of one, had to save a crop to disk first and then find it again\n * (Steering Design feedback #39). The clipboard is where a region snip already\n * is on every platform.\n */\nexport function FeedbackDialog({\n open,\n onClose,\n categories,\n category,\n onCategoryChange,\n labels,\n onSubmit,\n submitting = false,\n contextSlot,\n attachmentAccept = DEFAULT_ATTACHMENT_ACCEPT,\n maxAttachmentBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onAttachmentError,\n onCaptureScreenshot,\n}: {\n open: boolean;\n onClose: () => void;\n categories: FeedbackCategoryOption[];\n category: string;\n onCategoryChange: (value: string) => void;\n labels: FeedbackDialogLabels;\n onSubmit: (data: FeedbackSubmission) => void | Promise<void>;\n submitting?: boolean;\n contextSlot?: ReactNode;\n attachmentAccept?: string[];\n maxAttachmentBytes?: number;\n onAttachmentError?: (kind: \"type\" | \"size\") => void;\n /**\n * Optional: capture a screenshot of the underlying app view and return it as a\n * File. When provided, a \"Capture screenshot\" button is shown next to \"Add\n * attachment\"; the returned file is fed through the same validation + preview.\n */\n onCaptureScreenshot?: () => Promise<File | null>;\n}) {\n const [title, setTitle] = useState(\"\");\n const [body, setBody] = useState(\"\");\n const [attachment, setAttachment] = useState<File | null>(null);\n const isMobile = useMediaQuery(PHONE_QUERY, false);\n\n // Reset the form whenever the dialog is (re)opened.\n useEffect(() => {\n if (open) {\n setTitle(\"\");\n setBody(\"\");\n setAttachment(null);\n }\n }, [open]);\n\n const canSubmit = !!title && !!body && !submitting;\n const trySubmit = () => {\n if (canSubmit) void onSubmit({ title, body, category, attachment });\n };\n\n if (!open) return null;\n\n const categoryField = (\n <Select label={labels.category} value={category} onChange={(e) => onCategoryChange(e.target.value)}>\n {categories.map((c) => (\n <option key={c.value} value={c.value}>\n {c.label}\n </option>\n ))}\n </Select>\n );\n // On a phone the subject leads as a heading rather than as the second of two\n // identical boxes under a select (Keksdose feedback #179). `variant=\"display\"`\n // is a no-op above the phone breakpoint, so the desktop dialog is unchanged.\n const subjectField = (\n <Input label={labels.subject} value={title} onChange={(e) => setTitle(e.target.value)} variant=\"display\" />\n );\n const bodyField = (\n <Textarea rows={5} label={labels.body} value={body} onChange={(e) => setBody(e.target.value)} />\n );\n const attachmentField = (\n <FeedbackAttachmentField\n value={attachment}\n onChange={setAttachment}\n labels={labels}\n accept={attachmentAccept}\n maxBytes={maxAttachmentBytes}\n onError={onAttachmentError}\n onCaptureScreenshot={onCaptureScreenshot}\n // On `document`, not on the panel: the Modal focuses its own panel on open\n // and traps Tab inside it, so while this dialog is up every paste in the\n // page is meant for it — including the one made with nothing in\n // particular focused, which never reaches a React `onPaste` on a child.\n documentPaste={open}\n />\n );\n\n return (\n <Modal\n onClose={onClose}\n // The one dialog whose subject is the page behind it (Keksdose dev#460: \"Make\n // the feedback dialog draggable so I can see behind it if it blocks\n // something\") — you are describing what is under it while you type.\n draggable\n onKeyDown={(e) => {\n if ((e.ctrlKey || e.metaKey) && e.key === \"Enter\") {\n e.preventDefault();\n trySubmit();\n }\n }}\n >\n {/* Also the drag handle: pressing anywhere on the panel's own chrome moves it,\n and the heading is the strip a user reaches for. */}\n <h2 className=\"mb-3 text-lg font-semibold\">{labels.title}</h2>\n <div className=\"space-y-3\">\n {isMobile ? (\n // Phone shape, the same reasoning as the transaction editor's (#177):\n // the desktop order is the DATA MODEL's order — category, then subject,\n // then body — but nobody opens this dialog to pick a category. What they\n // came to do is say the thing, so that leads; the classification and the\n // evidence follow, a tier down.\n <>\n {subjectField}\n {bodyField}\n <div className=\"space-y-3 border-t border-[var(--border)] pt-3\">\n {categoryField}\n {attachmentField}\n {contextSlot}\n </div>\n </>\n ) : (\n <>\n {categoryField}\n {subjectField}\n {bodyField}\n {attachmentField}\n {contextSlot}\n </>\n )}\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"text-xs text-[var(--text-placeholder)]\">{labels.submitHint}</div>\n <div className=\"flex gap-2\">\n <Button variant=\"ghost\" onClick={onClose}>\n {labels.cancel}\n </Button>\n <Button onClick={trySubmit} disabled={!canSubmit}>\n {labels.save}\n </Button>\n </div>\n </div>\n </div>\n </Modal>\n );\n}\n"],"mappings":"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","names":[]}
@@ -38,11 +38,12 @@ type FeedbackCategory = "CRASH" | "BUG" | "IDEA" | "QUESTION" | "OTHER";
38
38
  /**
39
39
  * A glyph and a tone per status.
40
40
  *
41
- * Colour alone is not a label — two of these are a violet and an indigo apart —
42
- * so every status carries its own shape as well. Declaration order is chain
43
- * order, and {@link FEEDBACK_STATUS_ORDER} is derived from it rather than
44
- * restated, because a restated list cannot be checked for exhaustiveness: an
45
- * eighth status would break the build here and leave a literal seven long.
41
+ * Colour alone is not a label — two of these sit a hue apart, and a consumer who
42
+ * moves `--brand` can park it beside any of the others — so every status carries
43
+ * its own shape as well. Declaration order is chain order, and
44
+ * {@link FEEDBACK_STATUS_ORDER} is derived from it rather than restated, because
45
+ * a restated list cannot be checked for exhaustiveness: an eighth status would
46
+ * break the build here and leave a literal seven long.
46
47
  */
47
48
  declare const FEEDBACK_STATUS_META: Record<FeedbackStatus, {
48
49
  icon: LucideIcon;
@@ -90,7 +91,7 @@ declare function selectableFeedbackStatuses(_current: FeedbackStatus): FeedbackS
90
91
  *
91
92
  * A category cell that is one grey pill holding the raw enum makes a CRASH —
92
93
  * filed automatically, by somebody staring at a broken page right now — read
93
- * exactly like a QUESTION, and sit unnoticed in the queue. Red plus its own
94
+ * exactly like a QUESTION, and sit unnoticed in the queue. `--danger` plus its own
94
95
  * glyph is what makes that impossible; the hand-filed categories stay
95
96
  * deliberately quiet so that the loud one means something.
96
97
  *
@@ -163,8 +164,28 @@ declare function FeedbackStatusTransitions({ status, statuses, canEdit, onPick,
163
164
  * One component because all three are the same gesture: a draft that is not
164
165
  * committed until it is saved. Ctrl/⌘+Enter submits, which is the same shortcut
165
166
  * the compose dialog next door uses, so the habit carries across the feature.
167
+ *
168
+ * ## `initial` seeds the draft; it does not own it (changed 2026-09-22)
169
+ *
170
+ * This used to re-seed from `initial` in an effect, and that effect threw away
171
+ * whatever was in the box — **and the picture attached to it** — every time the
172
+ * prop arrived with a different value. `initial` is normally the saved note, so
173
+ * the list refreshing under the editor, or the owner re-deriving the same string,
174
+ * was enough: a triager halfway through a reply, with a screenshot picked out,
175
+ * lost both and got the saved text back with no way to undo it.
176
+ *
177
+ * A draft belongs to the thing being edited, so switching to a different note is a
178
+ * different editor — say so with a `key`:
179
+ *
180
+ * ```tsx
181
+ * <FeedbackNoteEditor key={note.id} initial={note.body} … />
182
+ * ```
183
+ *
184
+ * Where the id is not in hand at the call site, pass {@link resetKey} instead and
185
+ * the editor re-seeds when THAT changes. Either way the decision is the caller's,
186
+ * which is the point: the editor cannot tell a new subject from a new render.
166
187
  */
167
- declare function FeedbackNoteEditor({ initial, pending, onSave, onCancel, saveLabel, cancelLabel, placeholder, rows, attachment, }: {
188
+ declare function FeedbackNoteEditor({ initial, pending, onSave, onCancel, saveLabel, cancelLabel, placeholder, rows, attachment, resetKey, }: {
168
189
  initial: string;
169
190
  pending: boolean;
170
191
  onSave: (value: string, attachment?: File | null) => void;
@@ -181,6 +202,11 @@ declare function FeedbackNoteEditor({ initial, pending, onSave, onCancel, saveLa
181
202
  * *outcome* editor beside it wants, since an outcome is the answer rather
182
203
  * than the evidence. */
183
204
  attachment?: FeedbackNoteAttachment;
205
+ /** Change this to say "the editor is now editing something ELSE", and the draft
206
+ * and its attachment are dropped and re-seeded from `initial`. For call sites
207
+ * that cannot put a `key` on the editor (see the note above). Leave it out and
208
+ * the draft is never thrown away behind the user's back. */
209
+ resetKey?: string | number;
184
210
  }): react.JSX.Element;
185
211
  /** What {@link FeedbackNoteEditor} needs in order to offer a picture with the
186
212
  * note: the same four things {@link FeedbackAttachmentField} takes, so the
@@ -1,5 +1,6 @@
1
+ "use client";
1
2
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState } from "react";
3
+ import { useRef, useState } from "react";
3
4
  import {
4
5
  Ban,
5
6
  Bug,
@@ -21,44 +22,52 @@ import { FeedbackAttachmentField } from "./feedback-attachment.js";
21
22
  const FEEDBACK_STATUS_META = {
22
23
  OPEN: {
23
24
  icon: Inbox,
24
- activeBg: "bg-slate-200 dark:bg-slate-700",
25
- activeText: "text-slate-900 dark:text-white"
25
+ activeBg: "bg-[var(--bg-active)]",
26
+ activeText: "text-[var(--text-primary)]"
26
27
  },
27
28
  IN_PROGRESS: {
28
29
  icon: Wrench,
29
- activeBg: "bg-amber-100 dark:bg-amber-500/20",
30
- activeText: "text-amber-700 dark:text-amber-300"
30
+ activeBg: "bg-[var(--warning-bg)]",
31
+ activeText: "text-[var(--warning)]"
31
32
  },
32
33
  // Where solved work is parked, waiting on the person who reported it. Its own
33
34
  // colour, because "somebody has to check this" is a state you want to find by
34
35
  // scanning rather than by reading.
35
36
  IN_EVALUATION: {
36
37
  icon: Eye,
37
- activeBg: "bg-sky-100 dark:bg-sky-500/20",
38
- activeText: "text-sky-700 dark:text-sky-300"
38
+ activeBg: "bg-[var(--info-bg)]",
39
+ activeText: "text-[var(--info)]"
39
40
  },
40
- // Resolved but only checkable on a deployed build — indigo, so it reads as
41
- // "waiting on something" rather than as a done or a refusal.
41
+ // Resolved but only checkable on a deployed build — the SOFT brand tint, so it
42
+ // reads as "waiting on something" rather than as a done or a refusal, and it
43
+ // follows whatever the consumer's brand is instead of pinning an indigo island
44
+ // beside it.
42
45
  NEEDS_LIVE_TEST: {
43
46
  icon: CloudUpload,
44
- activeBg: "bg-indigo-100 dark:bg-indigo-500/20",
45
- activeText: "text-indigo-700 dark:text-indigo-300"
47
+ activeBg: "bg-[var(--brand-bg)]",
48
+ activeText: "text-[var(--brand-muted)]"
46
49
  },
47
- // Parked on purpose. Muted, not red: it is not a refusal.
50
+ // Parked on purpose. Muted, not `--danger`: it is not a refusal.
48
51
  POSTPONED: {
49
52
  icon: PauseCircle,
50
- activeBg: "bg-slate-200 dark:bg-slate-700",
51
- activeText: "text-slate-600 dark:text-slate-300"
53
+ activeBg: "bg-[var(--bg-active)]",
54
+ activeText: "text-[var(--text-secondary)]"
52
55
  },
56
+ // The one status with nowhere to move to: the vocabulary has a brand, an info, a
57
+ // warning and a danger, and nothing that means "succeeded". Deliberately NOT
58
+ // borrowed from a family that means something else — `--status-synced` is a
59
+ // FIELD's trip to the database rather than an outcome, and tokens.css says so in
60
+ // as many words. Green beside WONT_DO's red is the classic CVD-unsafe pair, which
61
+ // is why both carry a glyph and neither leans on colour alone.
53
62
  DONE: {
54
63
  icon: CheckCircle2,
55
- activeBg: "bg-emerald-100 dark:bg-emerald-500/20",
56
- activeText: "text-emerald-700 dark:text-emerald-300"
64
+ activeBg: "bg-[var(--success-bg)]",
65
+ activeText: "text-[var(--success)]"
57
66
  },
58
67
  WONT_DO: {
59
68
  icon: Ban,
60
- activeBg: "bg-rose-100 dark:bg-rose-500/20",
61
- activeText: "text-rose-700 dark:text-rose-300"
69
+ activeBg: "bg-[var(--danger-bg)]",
70
+ activeText: "text-[var(--danger)]"
62
71
  }
63
72
  };
64
73
  const FEEDBACK_STATUS_ORDER = Object.keys(FEEDBACK_STATUS_META);
@@ -88,28 +97,28 @@ function selectableFeedbackStatuses(_current) {
88
97
  const FEEDBACK_CATEGORY_META = {
89
98
  CRASH: {
90
99
  icon: OctagonAlert,
91
- badgeBg: "bg-red-100 dark:bg-red-500/20",
92
- badgeText: "text-red-700 dark:text-red-300"
100
+ badgeBg: "bg-[var(--danger-bg)]",
101
+ badgeText: "text-[var(--danger)]"
93
102
  },
94
103
  BUG: {
95
104
  icon: Bug,
96
- badgeBg: "bg-amber-100 dark:bg-amber-500/20",
97
- badgeText: "text-amber-700 dark:text-amber-300"
105
+ badgeBg: "bg-[var(--warning-bg)]",
106
+ badgeText: "text-[var(--warning)]"
98
107
  },
99
108
  IDEA: {
100
109
  icon: Lightbulb,
101
- badgeBg: "bg-slate-100 dark:bg-slate-800",
102
- badgeText: "text-slate-700 dark:text-slate-300"
110
+ badgeBg: "bg-[var(--bg-surface-2)]",
111
+ badgeText: "text-[var(--text-secondary)]"
103
112
  },
104
113
  QUESTION: {
105
114
  icon: HelpCircle,
106
- badgeBg: "bg-slate-100 dark:bg-slate-800",
107
- badgeText: "text-slate-700 dark:text-slate-300"
115
+ badgeBg: "bg-[var(--bg-surface-2)]",
116
+ badgeText: "text-[var(--text-secondary)]"
108
117
  },
109
118
  OTHER: {
110
119
  icon: MoreHorizontal,
111
- badgeBg: "bg-slate-100 dark:bg-slate-800",
112
- badgeText: "text-slate-700 dark:text-slate-300"
120
+ badgeBg: "bg-[var(--bg-surface-2)]",
121
+ badgeText: "text-[var(--text-secondary)]"
113
122
  }
114
123
  };
115
124
  const FEEDBACK_CATEGORY_ORDER = Object.keys(FEEDBACK_CATEGORY_META);
@@ -197,7 +206,7 @@ function FeedbackStatusTransitions({
197
206
  "transition-colors",
198
207
  variant === "icon" ? cn(
199
208
  "flex size-7 items-center justify-center rounded",
200
- active ? cn(meta.activeBg, meta.activeText) : "text-slate-400 hover:text-slate-700 dark:text-slate-500 dark:hover:text-slate-200"
209
+ active ? cn(meta.activeBg, meta.activeText) : "text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]"
201
210
  ) : cn(
202
211
  "inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs font-medium",
203
212
  active ? cn(meta.activeBg, meta.activeText, "border-transparent") : "border-[var(--border)] bg-transparent text-[var(--text-primary)] hover:bg-[var(--bg-surface-2)]"
@@ -230,15 +239,18 @@ function FeedbackNoteEditor({
230
239
  cancelLabel,
231
240
  placeholder,
232
241
  rows = 3,
233
- attachment
242
+ attachment,
243
+ resetKey
234
244
  }) {
235
245
  const [draft, setDraft] = useState(initial);
236
246
  const [file, setFile] = useState(null);
237
- const root = useRef(null);
238
- useEffect(() => {
247
+ const [seededFor, setSeededFor] = useState(resetKey);
248
+ if (resetKey !== seededFor) {
249
+ setSeededFor(resetKey);
239
250
  setDraft(initial);
240
251
  setFile(null);
241
- }, [initial]);
252
+ }
253
+ const root = useRef(null);
242
254
  const submit = () => onSave(draft, file);
243
255
  return /* @__PURE__ */ jsxs(
244
256
  "div",
@@ -252,7 +264,7 @@ function FeedbackNoteEditor({
252
264
  }
253
265
  },
254
266
  children: [
255
- placeholder && /* @__PURE__ */ jsx("p", { className: "text-xs text-slate-500 dark:text-slate-400", children: placeholder }),
267
+ placeholder && /* @__PURE__ */ jsx("p", { className: "text-xs text-[var(--text-muted)]", children: placeholder }),
256
268
  /* @__PURE__ */ jsx(Textarea, { rows, value: draft, onChange: (event) => setDraft(event.target.value) }),
257
269
  attachment && /* @__PURE__ */ jsx(
258
270
  FeedbackAttachmentField,
@@ -282,7 +294,7 @@ function FeedbackDetailSection({
282
294
  }) {
283
295
  return /* @__PURE__ */ jsxs("div", { children: [
284
296
  /* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-center justify-between gap-2", children: [
285
- /* @__PURE__ */ jsx("div", { className: "text-xs font-medium uppercase tracking-wide text-slate-500 dark:text-slate-400", children: title }),
297
+ /* @__PURE__ */ jsx("div", { className: "text-xs font-medium uppercase tracking-wide text-[var(--text-muted)]", children: title }),
286
298
  action
287
299
  ] }),
288
300
  children
@@ -292,8 +304,8 @@ function FeedbackDetail({ children }) {
292
304
  return /* @__PURE__ */ jsx("div", { className: "space-y-3 text-sm", children });
293
305
  }
294
306
  function FeedbackProse({ children, empty }) {
295
- if (!children) return /* @__PURE__ */ jsx("p", { className: "text-slate-400 dark:text-slate-600", children: empty ?? "\u2014" });
296
- return /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap text-slate-700 dark:text-slate-300", children });
307
+ if (!children) return /* @__PURE__ */ jsx("p", { className: "text-[var(--text-placeholder)]", children: empty ?? "\u2014" });
308
+ return /* @__PURE__ */ jsx("p", { className: "whitespace-pre-wrap text-[var(--text-secondary)]", children });
297
309
  }
298
310
  export {
299
311
  FEEDBACK_CATEGORY_META,