@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
@@ -0,0 +1,615 @@
1
+ import { useRef, useState } from "react";
2
+ import type { ReactNode } from "react";
3
+ import {
4
+ Ban,
5
+ Bug,
6
+ CheckCircle2,
7
+ CloudUpload,
8
+ Eye,
9
+ HelpCircle,
10
+ Inbox,
11
+ Lightbulb,
12
+ MoreHorizontal,
13
+ OctagonAlert,
14
+ PauseCircle,
15
+ Wrench,
16
+ } from "lucide-react";
17
+ import type { LucideIcon } from "lucide-react";
18
+ import { cn } from "../lib/cn";
19
+ import { Button, Textarea } from "../components/ui";
20
+ import { Tooltip } from "../components/tooltip";
21
+ import { FeedbackAttachmentField } from "./feedback-attachment";
22
+ import type { FeedbackAttachmentLabels } from "./feedback-dialog";
23
+
24
+ /**
25
+ * The feedback **inbox**, as the parts two apps were each writing separately.
26
+ *
27
+ * `feedback-dialog.tsx` next door is the other half — the form somebody files a
28
+ * report with — and the split between them is the same one: this owns the
29
+ * *vocabulary*, the *policy* about how a report moves, and the *look*; the app
30
+ * owns the data, the API and every string.
31
+ *
32
+ * It exists because both apps had grown their own copy. Keksdose's is the one
33
+ * this is lifted from, comments and all, because it is the one that had been
34
+ * argued with users for a year — the glyph per status, the two sizes of the
35
+ * status control, what a row offers from where it currently stands. Steering
36
+ * Design's was thinner in every one of those places and read as a different
37
+ * product for no reason anyone chose.
38
+ *
39
+ * **What is deliberately NOT here.** The two apps store a report differently and
40
+ * are meant to: one keeps the reporter's context in a JSON column and the
41
+ * screenshot in an object store, the other keeps a `page_path` and the bytes in
42
+ * the row. So there is no `Feedback` type in this file and nothing here takes
43
+ * one. Every component takes the values it draws, and the detail panel is a
44
+ * *shell* the app fills — which is what lets each keep the shape that suits it
45
+ * without either of them inventing a second look for a status pill.
46
+ */
47
+
48
+ /** The seven states a report can be in.
49
+ *
50
+ * Shared value for value across both apps on purpose: it is what lets one habit,
51
+ * and one agent prompt, work on either repo. Four are a chain and three sit off
52
+ * it — see {@link visibleFeedbackStatuses} for what that buys. */
53
+ export type FeedbackStatus =
54
+ | "OPEN"
55
+ | "IN_PROGRESS"
56
+ | "IN_EVALUATION"
57
+ | "NEEDS_LIVE_TEST"
58
+ | "POSTPONED"
59
+ | "DONE"
60
+ | "WONT_DO";
61
+
62
+ /** What a report is about. `CRASH` is filed by an error boundary and never
63
+ * chosen, which is exactly why it is loud below and absent from every picker. */
64
+ export type FeedbackCategory = "CRASH" | "BUG" | "IDEA" | "QUESTION" | "OTHER";
65
+
66
+ /**
67
+ * A glyph and a tone per status.
68
+ *
69
+ * Colour alone is not a label — two of these sit a hue apart, and a consumer who
70
+ * moves `--brand` can park it beside any of the others — so every status carries
71
+ * its own shape as well. Declaration order is chain order, and
72
+ * {@link FEEDBACK_STATUS_ORDER} is derived from it rather than restated, because
73
+ * a restated list cannot be checked for exhaustiveness: an eighth status would
74
+ * break the build here and leave a literal seven long.
75
+ */
76
+ export const FEEDBACK_STATUS_META: Record<
77
+ FeedbackStatus,
78
+ { icon: LucideIcon; activeBg: string; activeText: string }
79
+ > = {
80
+ OPEN: {
81
+ icon: Inbox,
82
+ activeBg: "bg-[var(--bg-active)]",
83
+ activeText: "text-[var(--text-primary)]",
84
+ },
85
+ IN_PROGRESS: {
86
+ icon: Wrench,
87
+ activeBg: "bg-[var(--warning-bg)]",
88
+ activeText: "text-[var(--warning)]",
89
+ },
90
+ // Where solved work is parked, waiting on the person who reported it. Its own
91
+ // colour, because "somebody has to check this" is a state you want to find by
92
+ // scanning rather than by reading.
93
+ IN_EVALUATION: {
94
+ icon: Eye,
95
+ activeBg: "bg-[var(--info-bg)]",
96
+ activeText: "text-[var(--info)]",
97
+ },
98
+ // Resolved but only checkable on a deployed build — the SOFT brand tint, so it
99
+ // reads as "waiting on something" rather than as a done or a refusal, and it
100
+ // follows whatever the consumer's brand is instead of pinning an indigo island
101
+ // beside it.
102
+ NEEDS_LIVE_TEST: {
103
+ icon: CloudUpload,
104
+ activeBg: "bg-[var(--brand-bg)]",
105
+ activeText: "text-[var(--brand-muted)]",
106
+ },
107
+ // Parked on purpose. Muted, not `--danger`: it is not a refusal.
108
+ POSTPONED: {
109
+ icon: PauseCircle,
110
+ activeBg: "bg-[var(--bg-active)]",
111
+ activeText: "text-[var(--text-secondary)]",
112
+ },
113
+ // The one status with nowhere to move to: the vocabulary has a brand, an info, a
114
+ // warning and a danger, and nothing that means "succeeded". Deliberately NOT
115
+ // borrowed from a family that means something else — `--status-synced` is a
116
+ // FIELD's trip to the database rather than an outcome, and tokens.css says so in
117
+ // as many words. Green beside WONT_DO's red is the classic CVD-unsafe pair, which
118
+ // is why both carry a glyph and neither leans on colour alone.
119
+ DONE: {
120
+ icon: CheckCircle2,
121
+ activeBg: "bg-[var(--success-bg)]",
122
+ activeText: "text-[var(--success)]",
123
+ },
124
+ WONT_DO: {
125
+ icon: Ban,
126
+ activeBg: "bg-[var(--danger-bg)]",
127
+ activeText: "text-[var(--danger)]",
128
+ },
129
+ };
130
+
131
+ /** Chain order, then the three off it. The order a picker offers, and the rank a
132
+ * status column sorts by — so a list sorted by status reads as a queue rather
133
+ * than as an alphabet. */
134
+ export const FEEDBACK_STATUS_ORDER = Object.keys(FEEDBACK_STATUS_META) as FeedbackStatus[];
135
+
136
+ /** States that sit OFF the linear chain: reachable from anywhere, leading
137
+ * nowhere by themselves. `WONT_DO` has always worked this way; `POSTPONED`
138
+ * ("not now") and `NEEDS_LIVE_TEST` ("resolved, but only provable on a deployed
139
+ * build") are the same shape — a row can be parked or handed to the next deploy
140
+ * from any point, and comes back to the chain when somebody picks a real state
141
+ * for it.
142
+ *
143
+ * Kept out of the forward chain deliberately: putting them in it would mean an
144
+ * "advance" gesture could park an item, and every row would have to pass
145
+ * through them to reach `DONE`. */
146
+ const OFF_CHAIN: FeedbackStatus[] = ["NEEDS_LIVE_TEST", "POSTPONED", "WONT_DO"];
147
+
148
+ const FORWARD_CHAIN: FeedbackStatus[] = ["OPEN", "IN_PROGRESS", "IN_EVALUATION", "DONE"];
149
+
150
+ /** The next status along the chain, or null at the end (or off it, as `WONT_DO`
151
+ * is). What a swipe-to-advance gesture commits. */
152
+ export function nextFeedbackStatus(current: FeedbackStatus): FeedbackStatus | null {
153
+ const at = FORWARD_CHAIN.indexOf(current);
154
+ return at !== -1 && at < FORWARD_CHAIN.length - 1 ? FORWARD_CHAIN[at + 1] : null;
155
+ }
156
+
157
+ /**
158
+ * The steps that make sense from where a row currently stands.
159
+ *
160
+ * For the compact control in a table cell: it is a glanceable triage affordance,
161
+ * and a row of seven icons in a cell is noise. One step forward, one step back
162
+ * (so an item can be sent back for rework), and any of the three off-chain
163
+ * verdicts — which is what makes those usable as verdicts at all.
164
+ *
165
+ * The two parking states are not dead ends: each offers the step that resumes
166
+ * work and the ones that close it, but deliberately **not each other** —
167
+ * "postponed" and "waiting on a deploy" are different answers to different
168
+ * questions, and a row moving between them directly is a re-triage, which starts
169
+ * by picking the work back up.
170
+ */
171
+ export function visibleFeedbackStatuses(current: FeedbackStatus): FeedbackStatus[] {
172
+ const set = new Set<FeedbackStatus>([current]);
173
+ const at = FORWARD_CHAIN.indexOf(current);
174
+ if (at !== -1) {
175
+ if (at < FORWARD_CHAIN.length - 1) set.add(FORWARD_CHAIN[at + 1]);
176
+ if (at > 0) set.add(FORWARD_CHAIN[at - 1]);
177
+ for (const off of OFF_CHAIN) set.add(off);
178
+ } else if (current !== "WONT_DO") {
179
+ set.add("IN_PROGRESS");
180
+ set.add("DONE");
181
+ set.add("WONT_DO");
182
+ }
183
+ // WONT_DO falls through with nothing added: it stays terminal, reopened by a
184
+ // note from its author rather than by a status pill.
185
+ return FEEDBACK_STATUS_ORDER.filter((status) => set.has(status));
186
+ }
187
+
188
+ /**
189
+ * Every status, in chain order — what an EXPANDED row offers.
190
+ *
191
+ * {@link visibleFeedbackStatuses} narrows the choice to the steps that make
192
+ * sense from where a row is, which is right for a table cell. Once the row is
193
+ * open in front of you, that same narrowing turns every non-adjacent move into a
194
+ * walk: OPEN to DONE meant three round trips, and WONT_DO could not be left at
195
+ * all except through a rework note.
196
+ *
197
+ * Takes a `current` it does not use, so the two policies read as one pair at the
198
+ * call sites and a future rule ("terminal rows still cannot jump to X") has a
199
+ * place to live.
200
+ */
201
+ export function selectableFeedbackStatuses(_current: FeedbackStatus): FeedbackStatus[] {
202
+ return [...FEEDBACK_STATUS_ORDER];
203
+ }
204
+
205
+ /**
206
+ * Per-category badge treatment, the counterpart to the status meta above.
207
+ *
208
+ * A category cell that is one grey pill holding the raw enum makes a CRASH —
209
+ * filed automatically, by somebody staring at a broken page right now — read
210
+ * exactly like a QUESTION, and sit unnoticed in the queue. `--danger` plus its own
211
+ * glyph is what makes that impossible; the hand-filed categories stay
212
+ * deliberately quiet so that the loud one means something.
213
+ *
214
+ * Declaration order is triage order, CRASH first.
215
+ */
216
+ export const FEEDBACK_CATEGORY_META: Record<
217
+ FeedbackCategory,
218
+ { icon: LucideIcon; badgeBg: string; badgeText: string }
219
+ > = {
220
+ CRASH: {
221
+ icon: OctagonAlert,
222
+ badgeBg: "bg-[var(--danger-bg)]",
223
+ badgeText: "text-[var(--danger)]",
224
+ },
225
+ BUG: {
226
+ icon: Bug,
227
+ badgeBg: "bg-[var(--warning-bg)]",
228
+ badgeText: "text-[var(--warning)]",
229
+ },
230
+ IDEA: {
231
+ icon: Lightbulb,
232
+ badgeBg: "bg-[var(--bg-surface-2)]",
233
+ badgeText: "text-[var(--text-secondary)]",
234
+ },
235
+ QUESTION: {
236
+ icon: HelpCircle,
237
+ badgeBg: "bg-[var(--bg-surface-2)]",
238
+ badgeText: "text-[var(--text-secondary)]",
239
+ },
240
+ OTHER: {
241
+ icon: MoreHorizontal,
242
+ badgeBg: "bg-[var(--bg-surface-2)]",
243
+ badgeText: "text-[var(--text-secondary)]",
244
+ },
245
+ };
246
+
247
+ /** Triage order, CRASH first — also a category column's sort key and the option
248
+ * order in its filter. Sorting by the raw enum is alphabetical, which parks
249
+ * CRASH between BUG and IDEA. */
250
+ export const FEEDBACK_CATEGORY_ORDER = Object.keys(FEEDBACK_CATEGORY_META) as FeedbackCategory[];
251
+
252
+ /** A category's rank for a column sort. Anything this build has never heard of —
253
+ * an older client against a newer API — sorts LAST rather than to `indexOf`'s
254
+ * −1, which would rank it above CRASH. */
255
+ export function feedbackCategoryRank(category: FeedbackCategory): number {
256
+ const at = FEEDBACK_CATEGORY_ORDER.indexOf(category);
257
+ return at === -1 ? FEEDBACK_CATEGORY_ORDER.length : at;
258
+ }
259
+
260
+ /**
261
+ * The category as a badge.
262
+ *
263
+ * Falls back to OTHER's treatment for a category this build has never heard of,
264
+ * so an older client against a newer API degrades to a readable neutral pill
265
+ * instead of throwing on `meta.icon` — a crash report must not be able to cause
266
+ * one. The *label* is the caller's, and for the same reason it needs the same
267
+ * guard: pass what OTHER is called if you cannot name the value you were given.
268
+ */
269
+ export function FeedbackCategoryBadge({
270
+ category,
271
+ label,
272
+ compact = false,
273
+ className,
274
+ }: {
275
+ category: FeedbackCategory;
276
+ label: ReactNode;
277
+ compact?: boolean;
278
+ className?: string;
279
+ }) {
280
+ // Annotated `| undefined` because the Record's index signature promises a hit
281
+ // for every FeedbackCategory, and at runtime the API can hand us one that is not.
282
+ const known: (typeof FEEDBACK_CATEGORY_META)["OTHER"] | undefined = FEEDBACK_CATEGORY_META[category];
283
+ const meta = known ?? FEEDBACK_CATEGORY_META.OTHER;
284
+ const Icon = meta.icon;
285
+ return (
286
+ <span
287
+ className={cn(
288
+ "inline-flex items-center gap-1 rounded font-medium",
289
+ compact ? "px-1.5 py-0.5 text-[11px]" : "px-2 py-0.5 text-xs",
290
+ meta.badgeBg,
291
+ meta.badgeText,
292
+ className,
293
+ )}
294
+ >
295
+ <Icon className={compact ? "size-3" : "size-3.5"} aria-hidden />
296
+ {label}
297
+ </span>
298
+ );
299
+ }
300
+
301
+ /** Where a report stands, as a badge — for the places that only *report* the
302
+ * status rather than offering to change it. */
303
+ export function FeedbackStatusBadge({
304
+ status,
305
+ label,
306
+ className,
307
+ }: {
308
+ status: FeedbackStatus;
309
+ label: ReactNode;
310
+ className?: string;
311
+ }) {
312
+ const meta = FEEDBACK_STATUS_META[status] ?? FEEDBACK_STATUS_META.OPEN;
313
+ const Icon = meta.icon;
314
+ return (
315
+ <span
316
+ className={cn(
317
+ "inline-flex items-center gap-1 whitespace-nowrap rounded px-1.5 py-0.5 text-[11px] font-medium",
318
+ meta.activeBg,
319
+ meta.activeText,
320
+ className,
321
+ )}
322
+ >
323
+ <Icon className="size-3" aria-hidden />
324
+ {label}
325
+ </span>
326
+ );
327
+ }
328
+
329
+ /**
330
+ * A row of statuses to switch a report to — either a table column's icon row or
331
+ * an expanded panel's labelled pills.
332
+ *
333
+ * One component for both, because everything except the class strings is the
334
+ * same decision: which one is current, and when the buttons are disabled. Two
335
+ * copies had already drifted — only the icon variant carried `aria-label` and
336
+ * `aria-pressed`, so the pill row was unlabelled for a screen reader.
337
+ *
338
+ * WHICH statuses to offer is the caller's, not this component's:
339
+ * {@link visibleFeedbackStatuses} for a compact column,
340
+ * {@link selectableFeedbackStatuses} for an expanded row. Passing the list keeps
341
+ * that policy readable at the two call sites instead of hiding it behind
342
+ * `variant`.
343
+ */
344
+ export function FeedbackStatusTransitions({
345
+ status,
346
+ statuses,
347
+ canEdit,
348
+ onPick,
349
+ variant,
350
+ label,
351
+ className,
352
+ }: {
353
+ status: FeedbackStatus;
354
+ statuses: FeedbackStatus[];
355
+ canEdit: boolean;
356
+ onPick: (status: FeedbackStatus) => void;
357
+ variant: "icon" | "pill";
358
+ /** What each status is called, translated by the app. */
359
+ label: (status: FeedbackStatus) => string;
360
+ className?: string;
361
+ }) {
362
+ return (
363
+ <div
364
+ className={cn(variant === "icon" ? "flex items-center gap-0.5" : "flex flex-wrap gap-1.5", className)}
365
+ // A status control inside a clickable row must not also open the row.
366
+ onClick={(event) => event.stopPropagation()}
367
+ >
368
+ {statuses.map((value) => {
369
+ const meta = FEEDBACK_STATUS_META[value];
370
+ const Icon = meta.icon;
371
+ const active = status === value;
372
+ const name = label(value);
373
+ const button = (
374
+ <button
375
+ key={value}
376
+ type="button"
377
+ disabled={!canEdit || active}
378
+ onClick={() => onPick(value)}
379
+ aria-label={name}
380
+ aria-pressed={active}
381
+ className={cn(
382
+ "transition-colors",
383
+ variant === "icon"
384
+ ? cn(
385
+ "flex size-7 items-center justify-center rounded",
386
+ active
387
+ ? cn(meta.activeBg, meta.activeText)
388
+ : "text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]",
389
+ )
390
+ : cn(
391
+ "inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs font-medium",
392
+ active
393
+ ? cn(meta.activeBg, meta.activeText, "border-transparent")
394
+ : "border-[var(--border)] bg-transparent text-[var(--text-primary)] hover:bg-[var(--bg-surface-2)]",
395
+ ),
396
+ // Both stay unclickable, but only the read-only case is DIMMED: the
397
+ // current status is the one thing in the row that has to be legible
398
+ // at a glance, and among seven pills a faded active one reads as
399
+ // "unavailable" rather than as "this is where the row stands".
400
+ (!canEdit || active) && "cursor-default",
401
+ !canEdit && "opacity-60",
402
+ )}
403
+ >
404
+ <Icon className={variant === "icon" ? "size-4" : "size-3.5"} aria-hidden />
405
+ {variant === "pill" && name}
406
+ </button>
407
+ );
408
+ // The icon row has no visible label, so it needs the tooltip; the pills
409
+ // carry theirs inline.
410
+ //
411
+ // ⚠️ ABOVE, and portalled (Keksdose live #339). `side="bottom"` put the bubble
412
+ // straight over the NEXT ROW's icons — in a queue you triage by running down it
413
+ // with the pointer, that is the row you are about to reach, and it was covered
414
+ // every time you paused on one. Above covers the row you have just left instead.
415
+ //
416
+ // `portal` is not cosmetic here. An absolutely-positioned bubble is clipped by
417
+ // any ancestor that scrolls — this cell lives inside the DataTable's own
418
+ // scroller — and it cannot flip: `placeTooltip` is what moves the bubble below
419
+ // the icon for the FIRST row, where there is no room above it, instead of
420
+ // painting it off the top of the list.
421
+ return variant === "icon" ? (
422
+ <Tooltip key={value} label={name} side="top" portal>
423
+ {button}
424
+ </Tooltip>
425
+ ) : (
426
+ button
427
+ );
428
+ })}
429
+ </div>
430
+ );
431
+ }
432
+
433
+ /**
434
+ * A textarea and its two buttons — for an outcome, for a note sent back with a
435
+ * report, for a body being corrected.
436
+ *
437
+ * One component because all three are the same gesture: a draft that is not
438
+ * committed until it is saved. Ctrl/⌘+Enter submits, which is the same shortcut
439
+ * the compose dialog next door uses, so the habit carries across the feature.
440
+ *
441
+ * ## `initial` seeds the draft; it does not own it (changed 2026-09-22)
442
+ *
443
+ * This used to re-seed from `initial` in an effect, and that effect threw away
444
+ * whatever was in the box — **and the picture attached to it** — every time the
445
+ * prop arrived with a different value. `initial` is normally the saved note, so
446
+ * the list refreshing under the editor, or the owner re-deriving the same string,
447
+ * was enough: a triager halfway through a reply, with a screenshot picked out,
448
+ * lost both and got the saved text back with no way to undo it.
449
+ *
450
+ * A draft belongs to the thing being edited, so switching to a different note is a
451
+ * different editor — say so with a `key`:
452
+ *
453
+ * ```tsx
454
+ * <FeedbackNoteEditor key={note.id} initial={note.body} … />
455
+ * ```
456
+ *
457
+ * Where the id is not in hand at the call site, pass {@link resetKey} instead and
458
+ * the editor re-seeds when THAT changes. Either way the decision is the caller's,
459
+ * which is the point: the editor cannot tell a new subject from a new render.
460
+ */
461
+ export function FeedbackNoteEditor({
462
+ initial,
463
+ pending,
464
+ onSave,
465
+ onCancel,
466
+ saveLabel,
467
+ cancelLabel,
468
+ placeholder,
469
+ rows = 3,
470
+ attachment,
471
+ resetKey,
472
+ }: {
473
+ initial: string;
474
+ pending: boolean;
475
+ onSave: (value: string, attachment?: File | null) => void;
476
+ onCancel: () => void;
477
+ saveLabel: ReactNode;
478
+ cancelLabel: ReactNode;
479
+ /** A line above the field saying what to write, not an in-field placeholder:
480
+ * a hint that disappears the moment somebody starts typing is a hint that is
481
+ * gone exactly when it is being followed. */
482
+ placeholder?: ReactNode;
483
+ rows?: number;
484
+ /** Offer a picture with the note (Steering Design feedback #128). Omitted,
485
+ * the editor is exactly the text box it always was — which is what the
486
+ * *outcome* editor beside it wants, since an outcome is the answer rather
487
+ * than the evidence. */
488
+ attachment?: FeedbackNoteAttachment;
489
+ /** Change this to say "the editor is now editing something ELSE", and the draft
490
+ * and its attachment are dropped and re-seeded from `initial`. For call sites
491
+ * that cannot put a `key` on the editor (see the note above). Leave it out and
492
+ * the draft is never thrown away behind the user's back. */
493
+ resetKey?: string | number;
494
+ }) {
495
+ const [draft, setDraft] = useState(initial);
496
+ const [file, setFile] = useState<File | null>(null);
497
+ // Adjusted during render rather than in an effect: an effect would paint the old
498
+ // draft first and then replace it, and — far worse — it is a second definition of
499
+ // when a draft dies that the caller cannot see. React re-runs this component
500
+ // immediately, before anything is committed to the screen.
501
+ const [seededFor, setSeededFor] = useState(resetKey);
502
+ if (resetKey !== seededFor) {
503
+ setSeededFor(resetKey);
504
+ setDraft(initial);
505
+ setFile(null);
506
+ }
507
+ // The editor's own root, which is where a paste made in the text box bubbles
508
+ // to: the attachment field below is the box's sibling, so a paste in the box
509
+ // never passes through the field's own subtree. It listens here instead
510
+ // (Steering Design feedback #140).
511
+ const root = useRef<HTMLDivElement>(null);
512
+ const submit = () => onSave(draft, file);
513
+ return (
514
+ <div
515
+ ref={root}
516
+ className="space-y-2"
517
+ onKeyDown={(event) => {
518
+ if ((event.ctrlKey || event.metaKey) && event.key === "Enter" && !pending) {
519
+ event.preventDefault();
520
+ submit();
521
+ }
522
+ }}
523
+ >
524
+ {placeholder && <p className="text-xs text-[var(--text-muted)]">{placeholder}</p>}
525
+ <Textarea rows={rows} value={draft} onChange={(event) => setDraft(event.target.value)} />
526
+ {attachment && (
527
+ <FeedbackAttachmentField
528
+ value={file}
529
+ onChange={setFile}
530
+ labels={attachment.labels}
531
+ accept={attachment.accept}
532
+ maxBytes={attachment.maxBytes}
533
+ onError={attachment.onError}
534
+ onCaptureScreenshot={attachment.onCaptureScreenshot}
535
+ // Within THIS editor's subtree, not on `document`: the editor sits
536
+ // inline on a page that has other fields, so a paste made in one of
537
+ // them is meant for that one. The textarea above is where the caret
538
+ // already is, and it is the field's sibling — so the field is told
539
+ // to listen on their common parent, where the paste bubbles to.
540
+ pasteFrom={root}
541
+ />
542
+ )}
543
+ <div className="flex justify-end gap-2">
544
+ <Button variant="ghost" onClick={onCancel}>
545
+ {cancelLabel}
546
+ </Button>
547
+ <Button variant="brand" disabled={pending} onClick={submit}>
548
+ {saveLabel}
549
+ </Button>
550
+ </div>
551
+ </div>
552
+ );
553
+ }
554
+
555
+ /** What {@link FeedbackNoteEditor} needs in order to offer a picture with the
556
+ * note: the same four things {@link FeedbackAttachmentField} takes, so the
557
+ * reply path is held to the app's own limits rather than to the defaults. */
558
+ export interface FeedbackNoteAttachment {
559
+ labels: FeedbackAttachmentLabels;
560
+ accept?: string[];
561
+ maxBytes?: number;
562
+ onError?: (kind: "type" | "size") => void;
563
+ onCaptureScreenshot?: () => Promise<File | null>;
564
+ }
565
+
566
+ /**
567
+ * One labelled block of an opened report.
568
+ *
569
+ * The detail panel is a stack of these, and it is a stack rather than a
570
+ * component with fixed fields on purpose: the two apps keep a report's context
571
+ * and its screenshot in genuinely different places, and a shell that insisted on
572
+ * both would force one of them to invent a shape it does not have. What is
573
+ * shared is what a section *looks* like — and that is all that made the two
574
+ * panels read as different products.
575
+ */
576
+ export function FeedbackDetailSection({
577
+ title,
578
+ action,
579
+ children,
580
+ }: {
581
+ title: ReactNode;
582
+ /** Something on the title's own line — an edit button, a resolved-at date. */
583
+ action?: ReactNode;
584
+ children: ReactNode;
585
+ }) {
586
+ return (
587
+ <div>
588
+ <div className="mb-1 flex items-center justify-between gap-2">
589
+ <div className="text-xs font-medium uppercase tracking-wide text-[var(--text-muted)]">
590
+ {title}
591
+ </div>
592
+ {action}
593
+ </div>
594
+ {children}
595
+ </div>
596
+ );
597
+ }
598
+
599
+ /** The stack a detail panel is. Here so the spacing between sections is decided
600
+ * once rather than by whichever app was written second. */
601
+ export function FeedbackDetail({ children }: { children: ReactNode }) {
602
+ return <div className="space-y-3 text-sm">{children}</div>;
603
+ }
604
+
605
+ /** Prose inside a section — a body, an outcome, a note.
606
+ *
607
+ * `whitespace-pre-wrap` is the whole component: a report is written in
608
+ * paragraphs, and a note appended to it later is separated by blank lines that
609
+ * carry the entire "this arrived after the answer" reading. */
610
+ export function FeedbackProse({ children, empty }: { children?: string; empty?: ReactNode }) {
611
+ if (!children) return <p className="text-[var(--text-placeholder)]">{empty ?? "—"}</p>;
612
+ return (
613
+ <p className="whitespace-pre-wrap text-[var(--text-secondary)]">{children}</p>
614
+ );
615
+ }
@@ -0,0 +1,8 @@
1
+ // `@eifi1/ui-kit/feedback` — the report form and the parts an inbox is built from.
2
+ //
3
+ // A re-slicing of the main barrel, not a new API. The kit owns the status vocabulary,
4
+ // the transition policy and the look; each app still wires its own API, columns,
5
+ // strings and permissions — see the note at the top of feedback/feedback-inbox.tsx.
6
+ export * from "./feedback/feedback-attachment";
7
+ export * from "./feedback/feedback-dialog";
8
+ export * from "./feedback/feedback-inbox";