@eifi1/ui-kit 0.4.1 → 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 +89 -19
  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 +74 -19
  63. package/dist/components/dropdown.js +72 -22
  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 +21 -7
  75. package/dist/components/file-dropzone.js +28 -7
  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 +21 -2
  105. package/dist/components/number-input.js +2 -1
  106. package/dist/components/number-input.js.map +1 -1
  107. package/dist/components/numpad-sheet.d.ts +25 -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 +5 -3
  176. package/dist/components/user-avatar.js +4 -2
  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 +14 -1
  270. package/dist/theme/palette-presets.js +231 -3
  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 +66 -13
  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
@@ -0,0 +1,246 @@
1
+ import { cn } from "../lib/cn";
2
+ import { useEffect, useRef, useState, type RefObject } from "react";
3
+ import { Camera, FileText, Paperclip, X } from "lucide-react";
4
+ import { Button } from "../components/ui";
5
+ import { useKitFileLabels } from "../i18n/kit-labels";
6
+ import type { FeedbackAttachmentLabels } from "./feedback-dialog";
7
+
8
+ export const DEFAULT_ATTACHMENT_ACCEPT = ["image/png", "image/jpeg", "image/webp", "image/gif"];
9
+ export const DEFAULT_MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
10
+
11
+ /**
12
+ * Picking one picture: the file dialog, a capture of the app view, or a paste.
13
+ *
14
+ * Lifted out of {@link FeedbackDialog} so the *reply* half of the feature can
15
+ * have it too (Steering Design feedback #128). A report is a conversation — it
16
+ * is filed, it is answered, and the reporter sends it back saying that is not
17
+ * what they meant — and the screenshot showing what they mean is taken at
18
+ * whichever of those points they looked. Only the first of them had a way to
19
+ * attach one, so everything after it had to be described in words.
20
+ *
21
+ * **Three ways in, and they are three because no one of them covers the others.**
22
+ * The file dialog needs a file on disk; `onCaptureScreenshot` snapshots the
23
+ * *whole* app view; and the clipboard is where a region snip already is on every
24
+ * platform (Steering Design feedback #39). Somebody who wanted to show one panel
25
+ * had to save a crop and then find it again.
26
+ *
27
+ * `documentPaste` and `pasteFrom` decide where the paste is listened for, and
28
+ * it is a real choice rather than a flag with a default. A modal traps focus,
29
+ * so while it is up every paste in the page is meant for it — including one
30
+ * made with nothing in particular focused, which never reaches a React
31
+ * `onPaste` on a child: that is `documentPaste`. An editor **inline on a
32
+ * page** is not that: the page around it has its own fields, so it listens
33
+ * within its own subtree and a paste elsewhere stays where it was aimed. But
34
+ * "its own subtree" has to include the text box the paste is actually made in,
35
+ * and that box is this field's *sibling*, not its child — a paste in it bubbles
36
+ * to their common parent and never through here. `pasteFrom` is that parent
37
+ * (Steering Design feedback #140): the element whose subtree is listened to,
38
+ * handed in by whoever renders both the box and this field side by side.
39
+ */
40
+ export function FeedbackAttachmentField({
41
+ value,
42
+ onChange,
43
+ labels,
44
+ accept = DEFAULT_ATTACHMENT_ACCEPT,
45
+ maxBytes = DEFAULT_MAX_ATTACHMENT_BYTES,
46
+ onError,
47
+ onCaptureScreenshot,
48
+ documentPaste = false,
49
+ pasteFrom,
50
+ className,
51
+ }: {
52
+ value: File | null;
53
+ onChange: (file: File | null) => void;
54
+ labels: FeedbackAttachmentLabels;
55
+ accept?: string[];
56
+ maxBytes?: number;
57
+ onError?: (kind: "type" | "size") => void;
58
+ /** Snapshot the app view behind this and return it as a File. A "Capture
59
+ * screenshot" button appears only when it is given. */
60
+ onCaptureScreenshot?: () => Promise<File | null>;
61
+ /** Listen for the paste on `document` rather than on this field's own
62
+ * subtree. For a modal, which owns the whole page while it is up. */
63
+ documentPaste?: boolean;
64
+ /** Listen for the paste within this element's subtree rather than this
65
+ * field's own — for a field standing *beside* the text box a paste is made
66
+ * in, whose common parent is where the event bubbles to. Ignored when
67
+ * `documentPaste` is set. */
68
+ pasteFrom?: RefObject<HTMLElement | null>;
69
+ className?: string;
70
+ }) {
71
+ const [preview, setPreview] = useState<string | null>(null);
72
+ const [capturing, setCapturing] = useState(false);
73
+ const fileInputRef = useRef<HTMLInputElement>(null);
74
+ // `file.size` from `<UiKitProvider labels>`, formatted in its locale — see FileDropzone.
75
+ const fileText = useKitFileLabels();
76
+
77
+ // Object-URL preview lifecycle (create on change, revoke on cleanup).
78
+ useEffect(() => {
79
+ if (!value) {
80
+ setPreview(null);
81
+ return;
82
+ }
83
+ const url = URL.createObjectURL(value);
84
+ setPreview(url);
85
+ return () => URL.revokeObjectURL(url);
86
+ }, [value]);
87
+
88
+ const pick = (file: File | undefined | null) => {
89
+ if (!file) return;
90
+ if (!accept.includes(file.type)) {
91
+ onError?.("type");
92
+ return;
93
+ }
94
+ if (file.size > maxBytes) {
95
+ onError?.("size");
96
+ return;
97
+ }
98
+ onChange(file);
99
+ };
100
+
101
+ // The paste handler is registered once rather than per render, so it reads
102
+ // `pick` — which closes over props that change identity on every render — out
103
+ // of a ref rather than out of its own dependency list.
104
+ const latest = useRef(pick);
105
+ useEffect(() => {
106
+ latest.current = pick;
107
+ });
108
+
109
+ const takeImage = (clipboard: DataTransfer | null, stop: () => void) => {
110
+ const items = Array.from(clipboard?.items ?? []);
111
+ const image = items.find((item) => item.kind === "file" && item.type.startsWith("image/"));
112
+ const file = image?.getAsFile();
113
+ if (!file) return;
114
+ // Only once there IS an image: a paste of text into a field must stay a
115
+ // paste of text, and a clipboard holding both is a copy whose text half is
116
+ // what the field was focused for.
117
+ stop();
118
+ // Clipboard images arrive named "image.png" at best and unnamed at worst,
119
+ // and the name is what the inbox shows beside the thumbnail. A name that
120
+ // says where it came from is more use than the browser's.
121
+ latest.current(new File([file], pastedName(file.type), { type: file.type }));
122
+ };
123
+
124
+ // The element listened on, where it is not this field's own subtree: the
125
+ // document for a modal, the given parent for an inline editor. Read inside
126
+ // the effect rather than in render, because a ref's `current` is only set
127
+ // once the parent has mounted — which it has by the time effects run.
128
+ useEffect(() => {
129
+ const target: EventTarget | null = documentPaste ? document : (pasteFrom?.current ?? null);
130
+ if (!target) return;
131
+ const onPaste = (event: Event) => {
132
+ const clipboard = (event as ClipboardEvent).clipboardData;
133
+ takeImage(clipboard, () => event.preventDefault());
134
+ };
135
+ target.addEventListener("paste", onPaste);
136
+ return () => target.removeEventListener("paste", onPaste);
137
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- takeImage reads `latest`, which is a ref
138
+ }, [documentPaste, pasteFrom]);
139
+ const listensElsewhere = documentPaste || pasteFrom !== undefined;
140
+
141
+ return (
142
+ <div
143
+ // `relative` for the same reason as FileDropzone: the `sr-only` file input below
144
+ // is `position: absolute`, and without a positioned ancestor it is laid out
145
+ // against the initial containing block — extending the DOCUMENT height to its own
146
+ // offset and producing a phantom second scrollbar on any long page.
147
+ className={cn("relative", className)}
148
+ onPaste={
149
+ listensElsewhere
150
+ ? undefined
151
+ : (event) => takeImage(event.clipboardData, () => event.preventDefault())
152
+ }
153
+ >
154
+ {labels.attachment && (
155
+ <div className="mb-1 text-xs font-medium uppercase tracking-wide text-[var(--text-muted)]">
156
+ {labels.attachment}
157
+ </div>
158
+ )}
159
+ {value ? (
160
+ <div className="flex items-start gap-2">
161
+ {value.type.startsWith("image/") && preview ? (
162
+ <img
163
+ src={preview}
164
+ alt={value.name}
165
+ className="h-20 w-20 rounded border border-[var(--border)] object-cover"
166
+ />
167
+ ) : (
168
+ // Non-image attachments (PDF, text) can't preview as an <img>, so
169
+ // show a neutral file tile with the name/size beside it instead.
170
+ <div className="flex h-20 w-20 items-center justify-center rounded border border-[var(--border)] text-[var(--text-placeholder)]">
171
+ <FileText className="size-8" />
172
+ </div>
173
+ )}
174
+ <div className="min-w-0 flex-1">
175
+ <div className="truncate text-sm text-[var(--text-secondary)]">{value.name}</div>
176
+ <div className="text-xs text-[var(--text-muted)]">
177
+ {fileText.size(value.size)}
178
+ </div>
179
+ </div>
180
+ <button
181
+ type="button"
182
+ onClick={() => onChange(null)}
183
+ aria-label={labels.attachmentRemove}
184
+ className="rounded p-1.5 text-[var(--text-placeholder)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-secondary)]"
185
+ >
186
+ <X className="size-4" />
187
+ </button>
188
+ </div>
189
+ ) : (
190
+ <div className="space-y-1.5">
191
+ <div className="flex flex-wrap gap-2">
192
+ <Button type="button" variant="secondary" onClick={() => fileInputRef.current?.click()}>
193
+ <Paperclip className="size-4" /> {labels.attachmentAdd}
194
+ </Button>
195
+ {onCaptureScreenshot && (
196
+ <Button
197
+ type="button"
198
+ variant="secondary"
199
+ disabled={capturing}
200
+ onClick={() => {
201
+ if (capturing) return;
202
+ setCapturing(true);
203
+ void onCaptureScreenshot()
204
+ .then((file) => {
205
+ if (file) pick(file);
206
+ })
207
+ .finally(() => setCapturing(false));
208
+ }}
209
+ >
210
+ <Camera className="size-4" />{" "}
211
+ {capturing ? "…" : (labels.attachmentCapture ?? "Capture screenshot")}
212
+ </Button>
213
+ )}
214
+ </div>
215
+ {/* Said out loud, because a gesture with no affordance is a gesture
216
+ nobody finds. */}
217
+ <p className="text-xs text-[var(--text-muted)]">
218
+ {labels.attachmentPaste ?? "…or paste a screenshot from the clipboard."}
219
+ </p>
220
+ </div>
221
+ )}
222
+ <input
223
+ ref={fileInputRef}
224
+ type="file"
225
+ accept={accept.join(",")}
226
+ className="sr-only"
227
+ onChange={(e) => {
228
+ pick(e.target.files?.[0]);
229
+ e.target.value = "";
230
+ }}
231
+ />
232
+ </div>
233
+ );
234
+ }
235
+
236
+ /** What a pasted image is called once it is an attachment.
237
+ *
238
+ * The extension is read off the mime type rather than assumed to be `.png`:
239
+ * Safari puts TIFF on the clipboard and a file called `pasted.png` that is not
240
+ * a PNG is one the receiving end opens wrong. */
241
+ export function pastedName(type: string): string {
242
+ const subtype = type.split("/")[1] ?? "png";
243
+ // `image/svg+xml` and friends carry a suffix that is not part of the
244
+ // extension, and none of them are in the accepted list anyway.
245
+ return `pasted.${subtype.split("+")[0]}`;
246
+ }
@@ -0,0 +1,209 @@
1
+ import { useEffect, useState } from "react";
2
+ import type { ReactNode } from "react";
3
+ import { Button, Input, PHONE_QUERY, Select, Textarea } from "../components/ui";
4
+ import { Modal } from "../components/modal";
5
+ import { useMediaQuery } from "../hooks/use-media-query";
6
+ import {
7
+ DEFAULT_ATTACHMENT_ACCEPT,
8
+ DEFAULT_MAX_ATTACHMENT_BYTES,
9
+ FeedbackAttachmentField,
10
+ } from "./feedback-attachment";
11
+
12
+ export interface FeedbackCategoryOption {
13
+ value: string;
14
+ label: string;
15
+ }
16
+
17
+ export interface FeedbackAttachmentLabels {
18
+ /** The heading over the field. Optional: a note editor puts the buttons
19
+ * straight under its textarea, where a second heading is noise. */
20
+ attachment?: string;
21
+ attachmentAdd: string;
22
+ /** Label for the "capture screenshot" button. Optional — falls back to an English default. */
23
+ attachmentCapture?: string;
24
+ /** The line under the attachment buttons saying a screenshot can be pasted
25
+ * straight in. Optional — falls back to an English default. */
26
+ attachmentPaste?: string;
27
+ attachmentRemove: string;
28
+ }
29
+
30
+ export interface FeedbackDialogLabels extends FeedbackAttachmentLabels {
31
+ title: string;
32
+ category: string;
33
+ subject: string;
34
+ body: string;
35
+ attachment: string;
36
+ submitHint: string;
37
+ cancel: string;
38
+ save: string;
39
+ }
40
+
41
+ export interface FeedbackSubmission {
42
+ title: string;
43
+ body: string;
44
+ category: string;
45
+ attachment: File | null;
46
+ }
47
+
48
+ /**
49
+ * The generic feedback form dialog: category + subject + body + an optional image
50
+ * attachment, with Ctrl/Cmd+Enter to submit. Domain-free — the app supplies the
51
+ * category options, labels and an `onSubmit` that talks to its own backend, plus
52
+ * an optional `contextSlot` for app-specific context (user, current URL, …).
53
+ *
54
+ * **A screenshot can be pasted straight in.** Ctrl/Cmd+V anywhere in the dialog
55
+ * takes an image off the clipboard and makes it the attachment, through the same
56
+ * validation and the same preview as the file picker. It is the gesture the two
57
+ * ways in did not cover: `onCaptureScreenshot` snapshots the *whole* app view,
58
+ * and the file picker needs a file — so somebody who wanted to show one panel,
59
+ * or one region of one, had to save a crop to disk first and then find it again
60
+ * (Steering Design feedback #39). The clipboard is where a region snip already
61
+ * is on every platform.
62
+ */
63
+ export function FeedbackDialog({
64
+ open,
65
+ onClose,
66
+ categories,
67
+ category,
68
+ onCategoryChange,
69
+ labels,
70
+ onSubmit,
71
+ submitting = false,
72
+ contextSlot,
73
+ attachmentAccept = DEFAULT_ATTACHMENT_ACCEPT,
74
+ maxAttachmentBytes = DEFAULT_MAX_ATTACHMENT_BYTES,
75
+ onAttachmentError,
76
+ onCaptureScreenshot,
77
+ }: {
78
+ open: boolean;
79
+ onClose: () => void;
80
+ categories: FeedbackCategoryOption[];
81
+ category: string;
82
+ onCategoryChange: (value: string) => void;
83
+ labels: FeedbackDialogLabels;
84
+ onSubmit: (data: FeedbackSubmission) => void | Promise<void>;
85
+ submitting?: boolean;
86
+ contextSlot?: ReactNode;
87
+ attachmentAccept?: string[];
88
+ maxAttachmentBytes?: number;
89
+ onAttachmentError?: (kind: "type" | "size") => void;
90
+ /**
91
+ * Optional: capture a screenshot of the underlying app view and return it as a
92
+ * File. When provided, a "Capture screenshot" button is shown next to "Add
93
+ * attachment"; the returned file is fed through the same validation + preview.
94
+ */
95
+ onCaptureScreenshot?: () => Promise<File | null>;
96
+ }) {
97
+ const [title, setTitle] = useState("");
98
+ const [body, setBody] = useState("");
99
+ const [attachment, setAttachment] = useState<File | null>(null);
100
+ const isMobile = useMediaQuery(PHONE_QUERY, false);
101
+
102
+ // Reset the form whenever the dialog is (re)opened.
103
+ useEffect(() => {
104
+ if (open) {
105
+ setTitle("");
106
+ setBody("");
107
+ setAttachment(null);
108
+ }
109
+ }, [open]);
110
+
111
+ const canSubmit = !!title && !!body && !submitting;
112
+ const trySubmit = () => {
113
+ if (canSubmit) void onSubmit({ title, body, category, attachment });
114
+ };
115
+
116
+ if (!open) return null;
117
+
118
+ const categoryField = (
119
+ <Select label={labels.category} value={category} onChange={(e) => onCategoryChange(e.target.value)}>
120
+ {categories.map((c) => (
121
+ <option key={c.value} value={c.value}>
122
+ {c.label}
123
+ </option>
124
+ ))}
125
+ </Select>
126
+ );
127
+ // On a phone the subject leads as a heading rather than as the second of two
128
+ // identical boxes under a select (Keksdose feedback #179). `variant="display"`
129
+ // is a no-op above the phone breakpoint, so the desktop dialog is unchanged.
130
+ const subjectField = (
131
+ <Input label={labels.subject} value={title} onChange={(e) => setTitle(e.target.value)} variant="display" />
132
+ );
133
+ const bodyField = (
134
+ <Textarea rows={5} label={labels.body} value={body} onChange={(e) => setBody(e.target.value)} />
135
+ );
136
+ const attachmentField = (
137
+ <FeedbackAttachmentField
138
+ value={attachment}
139
+ onChange={setAttachment}
140
+ labels={labels}
141
+ accept={attachmentAccept}
142
+ maxBytes={maxAttachmentBytes}
143
+ onError={onAttachmentError}
144
+ onCaptureScreenshot={onCaptureScreenshot}
145
+ // On `document`, not on the panel: the Modal focuses its own panel on open
146
+ // and traps Tab inside it, so while this dialog is up every paste in the
147
+ // page is meant for it — including the one made with nothing in
148
+ // particular focused, which never reaches a React `onPaste` on a child.
149
+ documentPaste={open}
150
+ />
151
+ );
152
+
153
+ return (
154
+ <Modal
155
+ onClose={onClose}
156
+ // The one dialog whose subject is the page behind it (Keksdose dev#460: "Make
157
+ // the feedback dialog draggable so I can see behind it if it blocks
158
+ // something") — you are describing what is under it while you type.
159
+ draggable
160
+ onKeyDown={(e) => {
161
+ if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
162
+ e.preventDefault();
163
+ trySubmit();
164
+ }
165
+ }}
166
+ >
167
+ {/* Also the drag handle: pressing anywhere on the panel's own chrome moves it,
168
+ and the heading is the strip a user reaches for. */}
169
+ <h2 className="mb-3 text-lg font-semibold">{labels.title}</h2>
170
+ <div className="space-y-3">
171
+ {isMobile ? (
172
+ // Phone shape, the same reasoning as the transaction editor's (#177):
173
+ // the desktop order is the DATA MODEL's order — category, then subject,
174
+ // then body — but nobody opens this dialog to pick a category. What they
175
+ // came to do is say the thing, so that leads; the classification and the
176
+ // evidence follow, a tier down.
177
+ <>
178
+ {subjectField}
179
+ {bodyField}
180
+ <div className="space-y-3 border-t border-[var(--border)] pt-3">
181
+ {categoryField}
182
+ {attachmentField}
183
+ {contextSlot}
184
+ </div>
185
+ </>
186
+ ) : (
187
+ <>
188
+ {categoryField}
189
+ {subjectField}
190
+ {bodyField}
191
+ {attachmentField}
192
+ {contextSlot}
193
+ </>
194
+ )}
195
+ <div className="flex items-center justify-between gap-2">
196
+ <div className="text-xs text-[var(--text-placeholder)]">{labels.submitHint}</div>
197
+ <div className="flex gap-2">
198
+ <Button variant="ghost" onClick={onClose}>
199
+ {labels.cancel}
200
+ </Button>
201
+ <Button onClick={trySubmit} disabled={!canSubmit}>
202
+ {labels.save}
203
+ </Button>
204
+ </div>
205
+ </div>
206
+ </div>
207
+ </Modal>
208
+ );
209
+ }