@eifi1/ui-kit 0.51.3 → 0.51.5

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 (780) hide show
  1. package/README.md +6 -6
  2. package/dist/account/account-settings-labels.js.map +1 -1
  3. package/dist/account/account-settings.js +11 -9
  4. package/dist/account/account-settings.js.map +1 -1
  5. package/dist/account/delete-account-setting.js +56 -65
  6. package/dist/account/delete-account-setting.js.map +1 -1
  7. package/dist/account/email-change-setting-labels.js.map +1 -1
  8. package/dist/account/email-change-setting.js.map +1 -1
  9. package/dist/account/passkeys-setting.js.map +1 -1
  10. package/dist/account/sessions-setting.js.map +1 -1
  11. package/dist/admin/admin-action-confirm.js +1 -3
  12. package/dist/admin/admin-action-confirm.js.map +1 -1
  13. package/dist/admin/admin-action-log.js.map +1 -1
  14. package/dist/admin/admin-area-labels.js +4 -1
  15. package/dist/admin/admin-area-labels.js.map +1 -1
  16. package/dist/admin/admin-overview.js +19 -11
  17. package/dist/admin/admin-overview.js.map +1 -1
  18. package/dist/admin/admin-sections.js.map +1 -1
  19. package/dist/admin/admin-tool-links.d.ts +1 -1
  20. package/dist/admin/admin-tool-links.js +7 -1
  21. package/dist/admin/admin-tool-links.js.map +1 -1
  22. package/dist/admin/invitations-panel.js +2 -13
  23. package/dist/admin/invitations-panel.js.map +1 -1
  24. package/dist/admin/mail-test-card.js.map +1 -1
  25. package/dist/admin/reviewer-scope-editor.js +4 -11
  26. package/dist/admin/reviewer-scope-editor.js.map +1 -1
  27. package/dist/admin/role-select.js.map +1 -1
  28. package/dist/admin/user-roster.js +1 -9
  29. package/dist/admin/user-roster.js.map +1 -1
  30. package/dist/auth/accept-invitation.js.map +1 -1
  31. package/dist/auth/auth-errors.js.map +1 -1
  32. package/dist/auth/forgot-password-form.js +23 -31
  33. package/dist/auth/forgot-password-form.js.map +1 -1
  34. package/dist/auth/register-form.js.map +1 -1
  35. package/dist/auth/reset-password-form.js.map +1 -1
  36. package/dist/auth/sign-in-form.js +13 -5
  37. package/dist/auth/sign-in-form.js.map +1 -1
  38. package/dist/auth/verify-email.js +1 -8
  39. package/dist/auth/verify-email.js.map +1 -1
  40. package/dist/billing/billing-banner.js.map +1 -1
  41. package/dist/billing/billing-labels.js.map +1 -1
  42. package/dist/billing/checkout-processing.js.map +1 -1
  43. package/dist/billing/plan-card.js +91 -85
  44. package/dist/billing/plan-card.js.map +1 -1
  45. package/dist/billing/plan-limit-toast.js.map +1 -1
  46. package/dist/billing/plan-limit.js.map +1 -1
  47. package/dist/billing/plan-price.js.map +1 -1
  48. package/dist/billing/plan-switch.js.map +1 -1
  49. package/dist/billing/subscription-actions.js.map +1 -1
  50. package/dist/bin/eifi1-pay-page.mjs +6 -1
  51. package/dist/components/account-chips.js.map +1 -1
  52. package/dist/components/alert-banner.js +1 -5
  53. package/dist/components/alert-banner.js.map +1 -1
  54. package/dist/components/amount-input.js +29 -12
  55. package/dist/components/amount-input.js.map +1 -1
  56. package/dist/components/authed-image.js.map +1 -1
  57. package/dist/components/autocomplete.js.map +1 -1
  58. package/dist/components/bulk-action-bar.js +1 -4
  59. package/dist/components/bulk-action-bar.js.map +1 -1
  60. package/dist/components/button-group.js.map +1 -1
  61. package/dist/components/button-look.js.map +1 -1
  62. package/dist/components/button.js +1 -7
  63. package/dist/components/button.js.map +1 -1
  64. package/dist/components/calculator.js +5 -5
  65. package/dist/components/calculator.js.map +1 -1
  66. package/dist/components/calendar-heatmap.js +4 -10
  67. package/dist/components/calendar-heatmap.js.map +1 -1
  68. package/dist/components/card.js +1 -8
  69. package/dist/components/card.js.map +1 -1
  70. package/dist/components/chart-zoom-core.js +3 -17
  71. package/dist/components/chart-zoom-core.js.map +1 -1
  72. package/dist/components/chart.d.ts +1 -1
  73. package/dist/components/chart.js +4 -22
  74. package/dist/components/chart.js.map +1 -1
  75. package/dist/components/checkbox-group.js +1 -4
  76. package/dist/components/checkbox-group.js.map +1 -1
  77. package/dist/components/chip-input.js +4 -1
  78. package/dist/components/chip-input.js.map +1 -1
  79. package/dist/components/chip.js +10 -5
  80. package/dist/components/chip.js.map +1 -1
  81. package/dist/components/choice-card.js +58 -74
  82. package/dist/components/choice-card.js.map +1 -1
  83. package/dist/components/column-mapper-labels.js.map +1 -1
  84. package/dist/components/column-mapper.js +167 -227
  85. package/dist/components/column-mapper.js.map +1 -1
  86. package/dist/components/combobox-core.js +5 -9
  87. package/dist/components/combobox-core.js.map +1 -1
  88. package/dist/components/combobox.js +1 -4
  89. package/dist/components/combobox.js.map +1 -1
  90. package/dist/components/confirm-dialog.js.map +1 -1
  91. package/dist/components/copy-button.js.map +1 -1
  92. package/dist/components/country-select.d.ts +5 -4
  93. package/dist/components/country-select.js +3 -19
  94. package/dist/components/country-select.js.map +1 -1
  95. package/dist/components/currency-option-list.js +13 -11
  96. package/dist/components/currency-option-list.js.map +1 -1
  97. package/dist/components/currency-select.js +1 -6
  98. package/dist/components/currency-select.js.map +1 -1
  99. package/dist/components/danger-confirm.js.map +1 -1
  100. package/dist/components/data-table-cells.js.map +1 -1
  101. package/dist/components/data-table-column-settings.js.map +1 -1
  102. package/dist/components/data-table-filter-popover.js +2 -18
  103. package/dist/components/data-table-filter-popover.js.map +1 -1
  104. package/dist/components/data-table-filters.js.map +1 -1
  105. package/dist/components/data-table-fit.js +7 -5
  106. package/dist/components/data-table-fit.js.map +1 -1
  107. package/dist/components/data-table-groups.js +3 -1
  108. package/dist/components/data-table-groups.js.map +1 -1
  109. package/dist/components/data-table-head.js +49 -57
  110. package/dist/components/data-table-head.js.map +1 -1
  111. package/dist/components/data-table-mobile.js +31 -44
  112. package/dist/components/data-table-mobile.js.map +1 -1
  113. package/dist/components/data-table-pagination.js.map +1 -1
  114. package/dist/components/data-table-row-actions.js +19 -16
  115. package/dist/components/data-table-row-actions.js.map +1 -1
  116. package/dist/components/data-table-row.js.map +1 -1
  117. package/dist/components/data-table-shared.js.map +1 -1
  118. package/dist/components/data-table-sort.js.map +1 -1
  119. package/dist/components/data-table-virtual.js +4 -1
  120. package/dist/components/data-table-virtual.js.map +1 -1
  121. package/dist/components/data-table.d.ts +8 -0
  122. package/dist/components/data-table.js +30 -39
  123. package/dist/components/data-table.js.map +1 -1
  124. package/dist/components/date-picker.js +1 -6
  125. package/dist/components/date-picker.js.map +1 -1
  126. package/dist/components/date-range-picker.js.map +1 -1
  127. package/dist/components/description-list.js +10 -10
  128. package/dist/components/description-list.js.map +1 -1
  129. package/dist/components/dialog-frame.js +9 -2
  130. package/dist/components/dialog-frame.js.map +1 -1
  131. package/dist/components/disclosure.js +17 -27
  132. package/dist/components/disclosure.js.map +1 -1
  133. package/dist/components/dropdown.js +25 -35
  134. package/dist/components/dropdown.js.map +1 -1
  135. package/dist/components/empty-state.js.map +1 -1
  136. package/dist/components/entity-combobox.js.map +1 -1
  137. package/dist/components/error-boundary.js.map +1 -1
  138. package/dist/components/field-base.js +5 -5
  139. package/dist/components/field-base.js.map +1 -1
  140. package/dist/components/field-parts.d.ts +1 -1
  141. package/dist/components/field-parts.js +10 -14
  142. package/dist/components/field-parts.js.map +1 -1
  143. package/dist/components/field-sync.js +8 -10
  144. package/dist/components/field-sync.js.map +1 -1
  145. package/dist/components/file-button.js.map +1 -1
  146. package/dist/components/file-dropzone.js.map +1 -1
  147. package/dist/components/floating-panel.js +4 -1
  148. package/dist/components/floating-panel.js.map +1 -1
  149. package/dist/components/focus-ring.js.map +1 -1
  150. package/dist/components/form-actions.js.map +1 -1
  151. package/dist/components/grouped-picker.js +11 -12
  152. package/dist/components/grouped-picker.js.map +1 -1
  153. package/dist/components/hover-menu.js +1 -4
  154. package/dist/components/hover-menu.js.map +1 -1
  155. package/dist/components/iban-input-labels.js.map +1 -1
  156. package/dist/components/icon-button.js +4 -1
  157. package/dist/components/icon-button.js.map +1 -1
  158. package/dist/components/icon-picker.js +6 -10
  159. package/dist/components/icon-picker.js.map +1 -1
  160. package/dist/components/image-grid.js +41 -50
  161. package/dist/components/image-grid.js.map +1 -1
  162. package/dist/components/inline-edit-field.js.map +1 -1
  163. package/dist/components/inline-entity-combobox.js +6 -12
  164. package/dist/components/inline-entity-combobox.js.map +1 -1
  165. package/dist/components/input.js +14 -2
  166. package/dist/components/input.js.map +1 -1
  167. package/dist/components/jump-calendar.js +1 -7
  168. package/dist/components/jump-calendar.js.map +1 -1
  169. package/dist/components/legal-labels.js +8 -2
  170. package/dist/components/legal-labels.js.map +1 -1
  171. package/dist/components/legal-page.d.ts +18 -1
  172. package/dist/components/legal-page.js +27 -2
  173. package/dist/components/legal-page.js.map +1 -1
  174. package/dist/components/legal.d.ts +40 -5
  175. package/dist/components/legal.js +41 -5
  176. package/dist/components/legal.js.map +1 -1
  177. package/dist/components/lightbox.js.map +1 -1
  178. package/dist/components/line-items.js +26 -36
  179. package/dist/components/line-items.js.map +1 -1
  180. package/dist/components/list-labels.js +3 -1
  181. package/dist/components/list-labels.js.map +1 -1
  182. package/dist/components/list.d.ts +18 -1
  183. package/dist/components/list.js +54 -13
  184. package/dist/components/list.js.map +1 -1
  185. package/dist/components/measured-grid.js +29 -53
  186. package/dist/components/measured-grid.js.map +1 -1
  187. package/dist/components/menu-item.js.map +1 -1
  188. package/dist/components/mini-calendar.js +13 -31
  189. package/dist/components/mini-calendar.js.map +1 -1
  190. package/dist/components/money-field.js.map +1 -1
  191. package/dist/components/month-picker.js +10 -21
  192. package/dist/components/month-picker.js.map +1 -1
  193. package/dist/components/multi-entity-combobox.js +3 -16
  194. package/dist/components/multi-entity-combobox.js.map +1 -1
  195. package/dist/components/multi-select.js.map +1 -1
  196. package/dist/components/nav-pills.js +10 -1
  197. package/dist/components/nav-pills.js.map +1 -1
  198. package/dist/components/number-field.js.map +1 -1
  199. package/dist/components/number-input.js +1 -7
  200. package/dist/components/number-input.js.map +1 -1
  201. package/dist/components/numpad-sheet.js +1 -4
  202. package/dist/components/numpad-sheet.js.map +1 -1
  203. package/dist/components/one-time-code-input.js +1 -7
  204. package/dist/components/one-time-code-input.js.map +1 -1
  205. package/dist/components/page-contents.js +13 -22
  206. package/dist/components/page-contents.js.map +1 -1
  207. package/dist/components/page-header.js +1 -11
  208. package/dist/components/page-header.js.map +1 -1
  209. package/dist/components/password-strength-labels.js.map +1 -1
  210. package/dist/components/password-strength.js +2 -8
  211. package/dist/components/password-strength.js.map +1 -1
  212. package/dist/components/phone-input.js +42 -57
  213. package/dist/components/phone-input.js.map +1 -1
  214. package/dist/components/picker-sheet.js.map +1 -1
  215. package/dist/components/pie-chart.js +1 -8
  216. package/dist/components/pie-chart.js.map +1 -1
  217. package/dist/components/popover.js.map +1 -1
  218. package/dist/components/progress-bar.js +16 -9
  219. package/dist/components/progress-bar.js.map +1 -1
  220. package/dist/components/qr-code.js +1 -8
  221. package/dist/components/qr-code.js.map +1 -1
  222. package/dist/components/row-actions.js +5 -1
  223. package/dist/components/row-actions.js.map +1 -1
  224. package/dist/components/search-field.js.map +1 -1
  225. package/dist/components/select.js +28 -38
  226. package/dist/components/select.js.map +1 -1
  227. package/dist/components/separator.js +1 -5
  228. package/dist/components/separator.js.map +1 -1
  229. package/dist/components/series-chart-axes.js +4 -1
  230. package/dist/components/series-chart-axes.js.map +1 -1
  231. package/dist/components/series-chart-budget.js.map +1 -1
  232. package/dist/components/series-chart-ticks.js.map +1 -1
  233. package/dist/components/series-chart.js +29 -45
  234. package/dist/components/series-chart.js.map +1 -1
  235. package/dist/components/settings-fields.js +21 -17
  236. package/dist/components/settings-fields.js.map +1 -1
  237. package/dist/components/share-card.js +4 -9
  238. package/dist/components/share-card.js.map +1 -1
  239. package/dist/components/sign-chip.js.map +1 -1
  240. package/dist/components/signature-pad.js +4 -13
  241. package/dist/components/signature-pad.js.map +1 -1
  242. package/dist/components/signed-amount.js +12 -5
  243. package/dist/components/signed-amount.js.map +1 -1
  244. package/dist/components/skeleton.js +2 -17
  245. package/dist/components/skeleton.js.map +1 -1
  246. package/dist/components/slider.js +13 -36
  247. package/dist/components/slider.js.map +1 -1
  248. package/dist/components/sparkline.js.map +1 -1
  249. package/dist/components/stat-tile.js +14 -10
  250. package/dist/components/stat-tile.js.map +1 -1
  251. package/dist/components/swatch-picker.js +14 -4
  252. package/dist/components/swatch-picker.js.map +1 -1
  253. package/dist/components/swipeable-row.d.ts +1 -1
  254. package/dist/components/swipeable-row.js +1 -8
  255. package/dist/components/swipeable-row.js.map +1 -1
  256. package/dist/components/table-skeleton.js +4 -1
  257. package/dist/components/table-skeleton.js.map +1 -1
  258. package/dist/components/table.js +12 -9
  259. package/dist/components/table.js.map +1 -1
  260. package/dist/components/tabs.js +5 -24
  261. package/dist/components/tabs.js.map +1 -1
  262. package/dist/components/text-link.js.map +1 -1
  263. package/dist/components/text.js.map +1 -1
  264. package/dist/components/tile-radio.js +1 -7
  265. package/dist/components/tile-radio.js.map +1 -1
  266. package/dist/components/time-input.js.map +1 -1
  267. package/dist/components/toast.js.map +1 -1
  268. package/dist/components/toggle-group.js +6 -1
  269. package/dist/components/toggle-group.js.map +1 -1
  270. package/dist/components/toggle-legend.js +2 -11
  271. package/dist/components/toggle-legend.js.map +1 -1
  272. package/dist/components/tooltip.js +2 -22
  273. package/dist/components/tooltip.js.map +1 -1
  274. package/dist/components/translation-review-editor.js +1 -4
  275. package/dist/components/translation-review-editor.js.map +1 -1
  276. package/dist/components/translation-review-parts.d.ts +1 -1
  277. package/dist/components/translation-review-parts.js +7 -1
  278. package/dist/components/translation-review-parts.js.map +1 -1
  279. package/dist/components/translation-review.js +18 -7
  280. package/dist/components/translation-review.js.map +1 -1
  281. package/dist/components/tree-view.js +14 -10
  282. package/dist/components/tree-view.js.map +1 -1
  283. package/dist/components/treemap.js +1 -13
  284. package/dist/components/treemap.js.map +1 -1
  285. package/dist/components/trigger-aria.js.map +1 -1
  286. package/dist/components/use-table-state.js +14 -11
  287. package/dist/components/use-table-state.js.map +1 -1
  288. package/dist/components/user-avatar.js +3 -26
  289. package/dist/components/user-avatar.js.map +1 -1
  290. package/dist/demo/demo-start.js +8 -4
  291. package/dist/demo/demo-start.js.map +1 -1
  292. package/dist/eslint/index.js +1 -1
  293. package/dist/feedback/feedback-attachment-labels.js.map +1 -1
  294. package/dist/feedback/feedback-attachment.js +1 -8
  295. package/dist/feedback/feedback-attachment.js.map +1 -1
  296. package/dist/feedback/feedback-capture.d.ts +1 -1
  297. package/dist/feedback/feedback-capture.js +3 -1
  298. package/dist/feedback/feedback-capture.js.map +1 -1
  299. package/dist/feedback/feedback-comment-thread.js +27 -13
  300. package/dist/feedback/feedback-comment-thread.js.map +1 -1
  301. package/dist/feedback/feedback-context.js +1 -4
  302. package/dist/feedback/feedback-context.js.map +1 -1
  303. package/dist/feedback/feedback-dialog.js.map +1 -1
  304. package/dist/feedback/feedback-inbox.js.map +1 -1
  305. package/dist/feedback/feedback-public-list.js +7 -1
  306. package/dist/feedback/feedback-public-list.js.map +1 -1
  307. package/dist/feedback/feedback-record.js +1 -5
  308. package/dist/feedback/feedback-record.js.map +1 -1
  309. package/dist/feedback/feedback-replies.js +25 -10
  310. package/dist/feedback/feedback-replies.js.map +1 -1
  311. package/dist/feedback/feedback-row-detail.js.map +1 -1
  312. package/dist/feedback/feedback-status-undo.js.map +1 -1
  313. package/dist/feedback/feedback-submit-dialog.js.map +1 -1
  314. package/dist/feedback/feedback-table.d.ts +3 -1
  315. package/dist/feedback/feedback-table.js +22 -5
  316. package/dist/feedback/feedback-table.js.map +1 -1
  317. package/dist/feedback/feedback-thread.js +2 -13
  318. package/dist/feedback/feedback-thread.js.map +1 -1
  319. package/dist/feedback/feedback-vocabulary.js.map +1 -1
  320. package/dist/feedback/feedback-vote-button.js.map +1 -1
  321. package/dist/hooks/use-anchored-rect.js.map +1 -1
  322. package/dist/hooks/use-document-title.js.map +1 -1
  323. package/dist/hooks/use-file-drop.js.map +1 -1
  324. package/dist/hooks/use-file-picker-labels.js.map +1 -1
  325. package/dist/hooks/use-file-picker.js.map +1 -1
  326. package/dist/hooks/use-focus-trap.js.map +1 -1
  327. package/dist/hooks/use-overlay-history.js +10 -5
  328. package/dist/hooks/use-overlay-history.js.map +1 -1
  329. package/dist/hooks/use-row-swipe.d.ts +1 -1
  330. package/dist/hooks/use-row-swipe.js +1 -6
  331. package/dist/hooks/use-row-swipe.js.map +1 -1
  332. package/dist/i18n/defaults.js +1 -4
  333. package/dist/i18n/defaults.js.map +1 -1
  334. package/dist/i18n/german.js +15 -4
  335. package/dist/i18n/german.js.map +1 -1
  336. package/dist/i18n/kit-labels.js +2 -8
  337. package/dist/i18n/kit-labels.js.map +1 -1
  338. package/dist/i18n/languages.js.map +1 -1
  339. package/dist/i18n/locales/en.js +19 -4
  340. package/dist/i18n/locales/en.js.map +2 -2
  341. package/dist/i18n/locales/es.js +15 -4
  342. package/dist/i18n/locales/es.js.map +1 -1
  343. package/dist/i18n/locales/fr.js +15 -4
  344. package/dist/i18n/locales/fr.js.map +1 -1
  345. package/dist/i18n/locales/hu.js +15 -4
  346. package/dist/i18n/locales/hu.js.map +1 -1
  347. package/dist/i18n/locales/it.js +15 -4
  348. package/dist/i18n/locales/it.js.map +1 -1
  349. package/dist/i18n/locales/zh.js +11 -3
  350. package/dist/i18n/locales/zh.js.map +1 -1
  351. package/dist/i18n/plurals.js.map +1 -1
  352. package/dist/i18n/review.js +15 -7
  353. package/dist/i18n/review.js.map +1 -1
  354. package/dist/i18n/shared-labels.js.map +1 -1
  355. package/dist/index.d.ts +17 -17
  356. package/dist/index.js +40 -23
  357. package/dist/index.js.map +1 -1
  358. package/dist/landing/access.js +1 -4
  359. package/dist/landing/access.js.map +1 -1
  360. package/dist/landing/landing-sections.js +35 -38
  361. package/dist/landing/landing-sections.js.map +1 -1
  362. package/dist/landing/page-seo.js.map +1 -1
  363. package/dist/lib/calc.js.map +1 -1
  364. package/dist/lib/clipboard-minus.js.map +1 -1
  365. package/dist/lib/dates.js.map +1 -1
  366. package/dist/lib/decimal-marks.js.map +1 -1
  367. package/dist/lib/device-settings.js +2 -1
  368. package/dist/lib/device-settings.js.map +1 -1
  369. package/dist/lib/direction.js.map +1 -1
  370. package/dist/lib/iban.js.map +1 -1
  371. package/dist/lib/logger.js +1 -5
  372. package/dist/lib/logger.js.map +1 -1
  373. package/dist/lib/person-name.js.map +1 -1
  374. package/dist/lib/phone.js.map +1 -1
  375. package/dist/lib/qr-encode.js +344 -8
  376. package/dist/lib/qr-encode.js.map +1 -1
  377. package/dist/lib/session-refresh.js.map +1 -1
  378. package/dist/lib/table-text.js +15 -3
  379. package/dist/lib/table-text.js.map +1 -1
  380. package/dist/lib/text-match.d.ts +20 -0
  381. package/dist/lib/text-match.js +11 -1
  382. package/dist/lib/text-match.js.map +1 -1
  383. package/dist/lib/translation-review.js.map +1 -1
  384. package/dist/notifications/notification-inbox.js +63 -44
  385. package/dist/notifications/notification-inbox.js.map +1 -1
  386. package/dist/notifications/notification-parts.js.map +1 -1
  387. package/dist/notifications/notification-types-setting.js.map +1 -1
  388. package/dist/notifications/notifications-labels.js.map +1 -1
  389. package/dist/notifications/push-device-setting.js +6 -1
  390. package/dist/notifications/push-device-setting.js.map +1 -1
  391. package/dist/notifications/push-devices-setting.js.map +1 -1
  392. package/dist/notifications/push-worker.js.map +1 -1
  393. package/dist/notifications/quiet-hours-setting.js.map +1 -1
  394. package/dist/notifications/use-push-device.js +4 -1
  395. package/dist/notifications/use-push-device.js.map +1 -1
  396. package/dist/offline/pwa-update-prompt.d.ts +1 -1
  397. package/dist/offline/pwa-update-prompt.js +7 -1
  398. package/dist/offline/pwa-update-prompt.js.map +1 -1
  399. package/dist/offline/query-persistence.js.map +1 -1
  400. package/dist/offline.d.ts +1 -1
  401. package/dist/offline.js +6 -1
  402. package/dist/offline.js.map +1 -1
  403. package/dist/pay/pay.css +8 -1
  404. package/dist/rhf/fields.d.ts +1 -2
  405. package/dist/rhf/fields.js +1 -10
  406. package/dist/rhf/fields.js.map +1 -1
  407. package/dist/rhf/form-core.js +1 -4
  408. package/dist/rhf/form-core.js.map +1 -1
  409. package/dist/rhf/line-items.js.map +1 -1
  410. package/dist/rhf/use-rhf-wizard-step.js.map +1 -1
  411. package/dist/search/command-palette.js +71 -62
  412. package/dist/search/command-palette.js.map +1 -1
  413. package/dist/search/global-search.js +34 -5
  414. package/dist/search/global-search.js.map +1 -1
  415. package/dist/search/search-index.js.map +1 -1
  416. package/dist/settings/device-reset-setting.js +1 -6
  417. package/dist/settings/device-reset-setting.js.map +1 -1
  418. package/dist/settings/settings-heading.js.map +1 -1
  419. package/dist/settings/settings-layout.js +9 -20
  420. package/dist/settings/settings-layout.js.map +1 -1
  421. package/dist/settings/use-settings-focus.d.ts +7 -0
  422. package/dist/settings/use-settings-focus.js +2 -0
  423. package/dist/settings/use-settings-focus.js.map +1 -1
  424. package/dist/settings/use-settings-route.js.map +1 -1
  425. package/dist/shell/app-shell.js +62 -31
  426. package/dist/shell/app-shell.js.map +1 -1
  427. package/dist/shell/auth-layout.js +1 -10
  428. package/dist/shell/auth-layout.js.map +1 -1
  429. package/dist/shell/dev-menu.d.ts +5 -0
  430. package/dist/shell/dev-menu.js +14 -7
  431. package/dist/shell/dev-menu.js.map +1 -1
  432. package/dist/shell/option-switcher-menu.js.map +1 -1
  433. package/dist/shell/sync-status-indicator.js +7 -6
  434. package/dist/shell/sync-status-indicator.js.map +1 -1
  435. package/dist/shell/top-bar-parts-labels.js.map +1 -1
  436. package/dist/shell/topbar-action-menu.d.ts +14 -1
  437. package/dist/shell/topbar-action-menu.js +85 -34
  438. package/dist/shell/topbar-action-menu.js.map +1 -1
  439. package/dist/shell/topbar-controls.js +1 -10
  440. package/dist/shell/topbar-controls.js.map +1 -1
  441. package/dist/shell/tour-menu.js +9 -1
  442. package/dist/shell/tour-menu.js.map +1 -1
  443. package/dist/shell.d.ts +1 -1
  444. package/dist/shell.js +2 -1
  445. package/dist/shell.js.map +1 -1
  446. package/dist/sync/db.js.map +1 -1
  447. package/dist/sync/engine.js +24 -6
  448. package/dist/sync/engine.js.map +1 -1
  449. package/dist/sync.d.ts +2 -2
  450. package/dist/sync.js +7 -1
  451. package/dist/sync.js.map +1 -1
  452. package/dist/testing/css-variables.js.map +1 -1
  453. package/dist/testing/index.d.ts +1 -1
  454. package/dist/testing/index.js +1 -3
  455. package/dist/testing/index.js.map +1 -1
  456. package/dist/testing/react.js.map +1 -1
  457. package/dist/testing/viewport.js +7 -4
  458. package/dist/testing/viewport.js.map +1 -1
  459. package/dist/theme/contrast-tokens.js +13 -2
  460. package/dist/theme/contrast-tokens.js.map +1 -1
  461. package/dist/theme/palette-derive.d.ts +1 -1
  462. package/dist/theme/palette-derive.js +18 -29
  463. package/dist/theme/palette-derive.js.map +1 -1
  464. package/dist/theme/palette-presets.js.map +1 -1
  465. package/dist/theme/palette-store.js.map +1 -1
  466. package/dist/theme/text-size.js.map +1 -1
  467. package/dist/theme/theme-store.js +1 -3
  468. package/dist/theme/theme-store.js.map +1 -1
  469. package/dist/tour/tour-catalogue.js.map +1 -1
  470. package/dist/tour/tour-dock.js +5 -1
  471. package/dist/tour/tour-dock.js.map +1 -1
  472. package/dist/tour/tour-engine.js +1 -4
  473. package/dist/tour/tour-engine.js.map +1 -1
  474. package/dist/tour/tour.js +21 -12
  475. package/dist/tour/tour.js.map +1 -1
  476. package/dist/tour/use-tour-progress.js.map +1 -1
  477. package/dist/tour/use-tour-url-sync.js.map +1 -1
  478. package/dist/wizard/stepper-nav.js +3 -24
  479. package/dist/wizard/stepper-nav.js.map +1 -1
  480. package/dist/wizard/types.js.map +1 -1
  481. package/dist/wizard/use-wizard.js +24 -28
  482. package/dist/wizard/use-wizard.js.map +1 -1
  483. package/dist/wizard/validation.js.map +1 -1
  484. package/dist/wizard/wizard-context.js.map +1 -1
  485. package/dist/wizard/wizard-step.d.ts +1 -1
  486. package/dist/wizard/wizard-step.js +1 -8
  487. package/dist/wizard/wizard-step.js.map +1 -1
  488. package/dist/wizard/wizard-summary.js.map +1 -1
  489. package/package.json +5 -2
  490. package/src/account/account-settings-labels.ts +1 -2
  491. package/src/account/account-settings.tsx +11 -17
  492. package/src/account/delete-account-setting.tsx +1 -6
  493. package/src/account/email-change-setting-labels.ts +1 -2
  494. package/src/account/email-change-setting.tsx +3 -1
  495. package/src/account/passkeys-setting.tsx +1 -3
  496. package/src/account/sessions-setting.tsx +4 -2
  497. package/src/admin/admin-action-confirm.tsx +4 -7
  498. package/src/admin/admin-action-log.tsx +29 -29
  499. package/src/admin/admin-area-labels.ts +4 -1
  500. package/src/admin/admin-overview.tsx +8 -2
  501. package/src/admin/admin-sections.tsx +3 -1
  502. package/src/admin/admin-tool-links.tsx +7 -1
  503. package/src/admin/invitations-panel.tsx +5 -11
  504. package/src/admin/mail-test-card.tsx +1 -2
  505. package/src/admin/reviewer-scope-editor.tsx +4 -9
  506. package/src/admin/role-select.tsx +4 -2
  507. package/src/admin/user-roster.tsx +2 -11
  508. package/src/auth/accept-invitation.tsx +1 -7
  509. package/src/auth/auth-errors.ts +1 -6
  510. package/src/auth/forgot-password-form.tsx +2 -7
  511. package/src/auth/register-form.tsx +7 -8
  512. package/src/auth/reset-password-form.tsx +2 -5
  513. package/src/auth/sign-in-form.tsx +15 -14
  514. package/src/auth/verify-email.tsx +2 -7
  515. package/src/billing/billing-banner.tsx +3 -7
  516. package/src/billing/billing-labels.ts +6 -4
  517. package/src/billing/checkout-processing.ts +0 -1
  518. package/src/billing/plan-card.tsx +9 -8
  519. package/src/billing/plan-limit-toast.tsx +1 -2
  520. package/src/billing/plan-limit.ts +1 -2
  521. package/src/billing/plan-price.ts +1 -4
  522. package/src/billing/plan-switch.tsx +1 -2
  523. package/src/billing/subscription-actions.tsx +5 -2
  524. package/src/components/account-chips.tsx +1 -7
  525. package/src/components/alert-banner.tsx +2 -8
  526. package/src/components/amount-input.tsx +206 -178
  527. package/src/components/authed-image.tsx +7 -4
  528. package/src/components/autocomplete.tsx +7 -9
  529. package/src/components/bulk-action-bar.tsx +2 -7
  530. package/src/components/button-group.tsx +1 -2
  531. package/src/components/button-look.ts +6 -17
  532. package/src/components/button.tsx +1 -4
  533. package/src/components/calculator.tsx +6 -9
  534. package/src/components/calendar-heatmap.tsx +5 -10
  535. package/src/components/card.tsx +9 -13
  536. package/src/components/chart-zoom-core.tsx +6 -33
  537. package/src/components/chart.tsx +8 -31
  538. package/src/components/checkbox-group.tsx +1 -4
  539. package/src/components/chip-input.tsx +6 -6
  540. package/src/components/chip.tsx +14 -26
  541. package/src/components/choice-card.tsx +4 -18
  542. package/src/components/column-mapper-labels.ts +3 -10
  543. package/src/components/column-mapper.tsx +33 -122
  544. package/src/components/combobox-core.tsx +17 -47
  545. package/src/components/combobox.tsx +2 -6
  546. package/src/components/confirm-dialog.tsx +1 -3
  547. package/src/components/copy-button.tsx +2 -4
  548. package/src/components/country-select.tsx +9 -21
  549. package/src/components/currency-option-list.tsx +16 -18
  550. package/src/components/currency-select.tsx +2 -7
  551. package/src/components/danger-confirm.tsx +8 -9
  552. package/src/components/data-table-cells.tsx +1 -2
  553. package/src/components/data-table-column-settings.tsx +1 -3
  554. package/src/components/data-table-filter-popover.tsx +5 -19
  555. package/src/components/data-table-filters.ts +1 -6
  556. package/src/components/data-table-fit.ts +7 -7
  557. package/src/components/data-table-groups.tsx +3 -1
  558. package/src/components/data-table-head.tsx +32 -46
  559. package/src/components/data-table-mobile.tsx +99 -108
  560. package/src/components/data-table-pagination.tsx +2 -6
  561. package/src/components/data-table-row-actions.tsx +25 -24
  562. package/src/components/data-table-row.tsx +16 -17
  563. package/src/components/data-table-shared.tsx +10 -16
  564. package/src/components/data-table-sort.ts +1 -2
  565. package/src/components/data-table-virtual.ts +9 -4
  566. package/src/components/data-table.tsx +497 -501
  567. package/src/components/date-picker.tsx +1 -7
  568. package/src/components/date-range-picker.tsx +3 -13
  569. package/src/components/description-list.tsx +7 -13
  570. package/src/components/dialog-frame.tsx +12 -3
  571. package/src/components/disclosure.tsx +8 -12
  572. package/src/components/dropdown.tsx +3 -11
  573. package/src/components/empty-state.tsx +1 -3
  574. package/src/components/entity-combobox.tsx +2 -6
  575. package/src/components/error-boundary.tsx +7 -7
  576. package/src/components/field-base.tsx +8 -14
  577. package/src/components/field-parts.tsx +12 -23
  578. package/src/components/field-sync.tsx +7 -11
  579. package/src/components/file-button.tsx +7 -3
  580. package/src/components/file-dropzone.tsx +7 -12
  581. package/src/components/floating-panel.tsx +44 -51
  582. package/src/components/focus-ring.ts +1 -2
  583. package/src/components/form-actions.tsx +1 -4
  584. package/src/components/grouped-picker.tsx +21 -16
  585. package/src/components/hover-menu.tsx +1 -4
  586. package/src/components/iban-input-labels.ts +1 -2
  587. package/src/components/icon-button.tsx +46 -48
  588. package/src/components/icon-picker.tsx +9 -20
  589. package/src/components/image-grid.tsx +1 -6
  590. package/src/components/inline-edit-field.tsx +1 -5
  591. package/src/components/inline-entity-combobox.tsx +11 -25
  592. package/src/components/input.tsx +14 -2
  593. package/src/components/jump-calendar.tsx +2 -6
  594. package/src/components/legal-labels.ts +8 -3
  595. package/src/components/legal-page.tsx +56 -6
  596. package/src/components/legal.tsx +83 -10
  597. package/src/components/lightbox.tsx +1 -3
  598. package/src/components/line-items.tsx +7 -15
  599. package/src/components/list-labels.ts +2 -0
  600. package/src/components/list.tsx +81 -27
  601. package/src/components/measured-grid.tsx +8 -30
  602. package/src/components/menu-item.tsx +1 -3
  603. package/src/components/mini-calendar.tsx +129 -159
  604. package/src/components/money-field.tsx +4 -2
  605. package/src/components/month-picker.tsx +14 -21
  606. package/src/components/multi-entity-combobox.tsx +6 -26
  607. package/src/components/multi-select.tsx +5 -9
  608. package/src/components/nav-pills.tsx +10 -2
  609. package/src/components/number-field.tsx +18 -15
  610. package/src/components/number-input.tsx +3 -19
  611. package/src/components/numpad-sheet.tsx +9 -13
  612. package/src/components/one-time-code-input.tsx +6 -11
  613. package/src/components/page-contents.tsx +2 -9
  614. package/src/components/page-header.tsx +5 -10
  615. package/src/components/password-strength-labels.ts +1 -2
  616. package/src/components/password-strength.tsx +6 -9
  617. package/src/components/phone-input.tsx +2 -13
  618. package/src/components/picker-sheet.tsx +2 -5
  619. package/src/components/pie-chart.tsx +6 -12
  620. package/src/components/popover.tsx +2 -1
  621. package/src/components/progress-bar.tsx +16 -25
  622. package/src/components/qr-code.tsx +2 -9
  623. package/src/components/row-actions.tsx +9 -1
  624. package/src/components/search-field.tsx +4 -5
  625. package/src/components/select.tsx +2 -10
  626. package/src/components/separator.tsx +1 -5
  627. package/src/components/series-chart-axes.ts +5 -5
  628. package/src/components/series-chart-budget.ts +1 -4
  629. package/src/components/series-chart-ticks.ts +1 -4
  630. package/src/components/series-chart.tsx +43 -83
  631. package/src/components/settings-fields.tsx +20 -15
  632. package/src/components/share-card.tsx +6 -14
  633. package/src/components/sign-chip.tsx +16 -16
  634. package/src/components/signature-pad.tsx +21 -13
  635. package/src/components/signed-amount.tsx +6 -14
  636. package/src/components/skeleton.tsx +2 -13
  637. package/src/components/slider.tsx +9 -20
  638. package/src/components/sparkline.tsx +2 -13
  639. package/src/components/stat-tile.tsx +13 -24
  640. package/src/components/swatch-picker.tsx +8 -3
  641. package/src/components/swipeable-row.tsx +2 -11
  642. package/src/components/table-skeleton.tsx +4 -1
  643. package/src/components/table.tsx +12 -8
  644. package/src/components/tabs.tsx +24 -39
  645. package/src/components/text-link.tsx +6 -6
  646. package/src/components/text.tsx +1 -2
  647. package/src/components/tile-radio.tsx +6 -19
  648. package/src/components/time-input.tsx +4 -5
  649. package/src/components/toast.tsx +3 -12
  650. package/src/components/toggle-group.tsx +9 -9
  651. package/src/components/toggle-legend.tsx +4 -16
  652. package/src/components/tooltip.tsx +9 -48
  653. package/src/components/translation-review-editor.tsx +4 -4
  654. package/src/components/translation-review-parts.tsx +7 -1
  655. package/src/components/translation-review.tsx +15 -16
  656. package/src/components/tree-view.tsx +17 -20
  657. package/src/components/treemap.tsx +3 -15
  658. package/src/components/trigger-aria.ts +1 -4
  659. package/src/components/use-table-state.ts +16 -17
  660. package/src/components/user-avatar.tsx +4 -21
  661. package/src/demo/demo-start.tsx +8 -4
  662. package/src/feedback/feedback-attachment-labels.ts +2 -1
  663. package/src/feedback/feedback-attachment.tsx +16 -29
  664. package/src/feedback/feedback-capture.ts +3 -1
  665. package/src/feedback/feedback-comment-thread.tsx +9 -6
  666. package/src/feedback/feedback-context.tsx +1 -4
  667. package/src/feedback/feedback-dialog.tsx +3 -4
  668. package/src/feedback/feedback-inbox.tsx +3 -9
  669. package/src/feedback/feedback-public-list.tsx +7 -1
  670. package/src/feedback/feedback-record.ts +6 -14
  671. package/src/feedback/feedback-replies.tsx +10 -5
  672. package/src/feedback/feedback-row-detail.tsx +2 -5
  673. package/src/feedback/feedback-status-undo.ts +1 -4
  674. package/src/feedback/feedback-submit-dialog.tsx +4 -4
  675. package/src/feedback/feedback-table.tsx +29 -9
  676. package/src/feedback/feedback-thread.tsx +4 -16
  677. package/src/feedback/feedback-vocabulary.ts +72 -82
  678. package/src/feedback/feedback-vote-button.tsx +9 -8
  679. package/src/hooks/use-anchored-rect.ts +1 -4
  680. package/src/hooks/use-document-title.ts +3 -1
  681. package/src/hooks/use-file-drop.ts +1 -3
  682. package/src/hooks/use-file-picker-labels.ts +1 -2
  683. package/src/hooks/use-file-picker.tsx +1 -5
  684. package/src/hooks/use-focus-trap.ts +1 -2
  685. package/src/hooks/use-overlay-history.ts +10 -5
  686. package/src/hooks/use-row-swipe.ts +1 -6
  687. package/src/i18n/defaults.ts +1 -4
  688. package/src/i18n/german.ts +42 -50
  689. package/src/i18n/kit-labels.tsx +5 -20
  690. package/src/i18n/languages.ts +2 -8
  691. package/src/i18n/locales/en.ts +15 -31
  692. package/src/i18n/locales/es.ts +44 -50
  693. package/src/i18n/locales/fr.ts +43 -52
  694. package/src/i18n/locales/hu.ts +35 -38
  695. package/src/i18n/locales/it.ts +44 -53
  696. package/src/i18n/locales/zh.ts +23 -33
  697. package/src/i18n/plurals.ts +4 -1
  698. package/src/i18n/review.ts +20 -7
  699. package/src/i18n/shared-labels.ts +1 -2
  700. package/src/index.ts +107 -32
  701. package/src/landing/access.ts +2 -7
  702. package/src/landing/landing-sections.tsx +14 -15
  703. package/src/landing/page-seo.ts +1 -3
  704. package/src/lib/calc.ts +4 -1
  705. package/src/lib/clipboard-minus.ts +4 -1
  706. package/src/lib/dates.ts +2 -10
  707. package/src/lib/decimal-marks.ts +1 -3
  708. package/src/lib/device-settings.ts +2 -1
  709. package/src/lib/direction.ts +1 -3
  710. package/src/lib/iban.ts +89 -9
  711. package/src/lib/logger.ts +1 -5
  712. package/src/lib/person-name.ts +1 -3
  713. package/src/lib/phone.ts +5 -8
  714. package/src/lib/qr-encode.ts +32 -8
  715. package/src/lib/session-refresh.ts +5 -6
  716. package/src/lib/table-text.ts +24 -6
  717. package/src/lib/text-match.ts +43 -1
  718. package/src/lib/translation-review.ts +1 -3
  719. package/src/notifications/notification-inbox.tsx +17 -4
  720. package/src/notifications/notification-parts.tsx +1 -2
  721. package/src/notifications/notification-types-setting.tsx +19 -18
  722. package/src/notifications/notifications-labels.ts +1 -2
  723. package/src/notifications/push-device-setting.tsx +7 -1
  724. package/src/notifications/push-devices-setting.tsx +7 -3
  725. package/src/notifications/push-worker.ts +10 -2
  726. package/src/notifications/quiet-hours-setting.tsx +4 -1
  727. package/src/notifications/use-push-device.ts +4 -1
  728. package/src/offline/pwa-update-prompt.tsx +7 -1
  729. package/src/offline/query-persistence.ts +9 -6
  730. package/src/offline.ts +6 -1
  731. package/src/rhf/fields.tsx +50 -19
  732. package/src/rhf/form-core.tsx +2 -6
  733. package/src/rhf/line-items.tsx +5 -7
  734. package/src/rhf/use-rhf-wizard-step.ts +3 -12
  735. package/src/search/command-palette.tsx +15 -23
  736. package/src/search/global-search.tsx +38 -8
  737. package/src/search/search-index.ts +5 -1
  738. package/src/settings/device-reset-setting.tsx +5 -8
  739. package/src/settings/settings-heading.tsx +3 -1
  740. package/src/settings/settings-layout.tsx +6 -13
  741. package/src/settings/use-settings-focus.ts +8 -0
  742. package/src/settings/use-settings-route.ts +4 -3
  743. package/src/shell/app-shell.tsx +48 -24
  744. package/src/shell/auth-layout.tsx +1 -6
  745. package/src/shell/dev-menu.tsx +18 -6
  746. package/src/shell/option-switcher-menu.tsx +1 -3
  747. package/src/shell/sync-status-indicator.tsx +14 -12
  748. package/src/shell/top-bar-parts-labels.ts +1 -3
  749. package/src/shell/topbar-action-menu.tsx +130 -45
  750. package/src/shell/topbar-controls.tsx +5 -19
  751. package/src/shell/tour-menu.tsx +9 -1
  752. package/src/shell.ts +1 -1
  753. package/src/sync/db.ts +5 -1
  754. package/src/sync/engine.ts +44 -12
  755. package/src/sync.ts +16 -2
  756. package/src/testing/css-variables.ts +1 -2
  757. package/src/testing/index.ts +1 -6
  758. package/src/testing/react.ts +1 -2
  759. package/src/testing/viewport.ts +9 -9
  760. package/src/theme/contrast-tokens.ts +13 -2
  761. package/src/theme/palette-derive.ts +24 -50
  762. package/src/theme/palette-presets.ts +0 -1
  763. package/src/theme/palette-store.ts +1 -4
  764. package/src/theme/text-size.ts +1 -1
  765. package/src/theme/theme-store.ts +2 -6
  766. package/src/tour/tour-catalogue.tsx +4 -2
  767. package/src/tour/tour-dock.ts +5 -1
  768. package/src/tour/tour-engine.ts +2 -6
  769. package/src/tour/tour.tsx +28 -12
  770. package/src/tour/use-tour-progress.ts +8 -2
  771. package/src/tour/use-tour-url-sync.ts +3 -1
  772. package/src/vite-env.d.ts +1 -4
  773. package/src/wizard/stepper-nav.tsx +10 -35
  774. package/src/wizard/types.ts +1 -4
  775. package/src/wizard/use-wizard.ts +28 -43
  776. package/src/wizard/validation.ts +1 -5
  777. package/src/wizard/wizard-context.tsx +2 -6
  778. package/src/wizard/wizard-step.tsx +1 -8
  779. package/src/wizard/wizard-summary.tsx +2 -6
  780. package/tokens.css +393 -152
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/feedback/feedback-capture.ts"],"sourcesContent":["/**\n * `captureAppScreenshot` — the picture of the app a feedback report attaches (0.27.0,\n * docs/feedback-harmonization.md §5).\n *\n * Lifted from keksdose `frontend/src/features/feedback/capture-screenshot.ts` (live #271),\n * which is the canon: Kurvenschmiede carried a second copy (`capture-page.ts`) with none of\n * its speed-ups, and kastlan had no capture at all. Each app wrote the same three awkward\n * decisions — which element, which library, what to do when it throws — and they drifted\n * the moment there were two copies (Kurvenschmiede feedback #128 says exactly that).\n *\n * WHY IT IS FAST (keksdose live #271: *\"Capture screenshot — That takes ages in my mobile\n * phone\"*). `modern-screenshot` deep-clones the subtree and inlines every node's computed\n * style onto the clone, because the final image is an SVG `foreignObject` that carries no\n * stylesheets. By default it walks the WHOLE computed style (~340 longhands in Chrome) per\n * element; keksdose's /transactions has ~4,800 elements under `#root` at phone width, so\n * that was ~1.6 million `getPropertyValue` calls before a pixel was drawn — 21–52 s on a\n * desktop CPU, 153–160 s with the CPU throttled 4×. Two levers fixed it, both kept here:\n *\n * 1. {@link CAPTURE_STYLE_PROPERTIES} — copy the ~140 longhands a Tailwind v4 app built\n * on this kit actually uses. Alone: 21 s → 1.8 s.\n * 2. {@link inViewport} drops `display: none` subtrees — on a phone most of the\n * document, since `DataTable` renders both layouts and hides one — and anything\n * wholly outside the viewport. Together: 27–52 s → 0.4–1.1 s, the picture\n * 0.05% of pixels different (antialiasing).\n *\n * Plus WebP at 0.9 (222 KB → 46 KB, an upload a phone makes on a mobile connection) with\n * the file named from what the canvas ACTUALLY produced — a browser without WebP encodes\n * PNG, and a \"screenshot.webp\" that is a PNG is refused by the server on its type — and\n * `font: false`, since the app's faces are same-origin and already in the page.\n *\n * WHY THE PEER IS LOADED DYNAMICALLY. `modern-screenshot` is an OPTIONAL peer\n * (`peerDependenciesMeta.optional`, like `sonner`), and this module is re-exported by the\n * barrel: a static import would make every app that imports a Button install it. It is\n * imported inside the function, so an app pays nothing until somebody presses \"Capture\n * screenshot\", and an app without it gets a REJECTED promise — which the submit dialog\n * turns into the \"Could not capture a screenshot\" toast (`feedbackToast.captureFailed`).\n *\n * WHAT IS NOT HERE: Kurvenschmiede's `features: { restoreScrollPosition: true }` (its\n * feedback #153: a capture of a scrolled `<main>` came out as the TOP of the page). The\n * viewport filter already answers that case — what is scrolled out of view is dropped, so\n * what remains is what was on screen, give or take the part of the first row that was\n * scrolled past (checked in Chromium, 0.27.0: an inner `<main>` scrolled to row 20 captures\n * rows 20–26). Both together are worse than either: the clone is translated by the scroll\n * offset AFTER the rows above were dropped, and the picture came out blank below the\n * header (same check).\n */\n\n/**\n * The computed properties copied onto the clone (keksdose `capture-screenshot.ts:94`).\n *\n * LONGHANDS only: `getPropertyValue()` resolves a shorthand to `\"\"` whenever its longhands\n * disagree, so `\"border\"` would silently drop every one-sided border. Chosen by what\n * Tailwind v4 emits rather than by what looks complete: `font-variant-numeric` because\n * money is `tabular-nums`, the SVG paint properties because every icon is a lucide `<svg>`\n * inheriting `stroke: currentColor`.\n */\nconst CAPTURE_STYLE_PROPERTIES: readonly string[] = [\n // box\n \"display\",\n \"position\",\n \"top\",\n \"right\",\n \"bottom\",\n \"left\",\n \"width\",\n \"height\",\n \"min-width\",\n \"min-height\",\n \"max-width\",\n \"max-height\",\n \"box-sizing\",\n \"margin-top\",\n \"margin-right\",\n \"margin-bottom\",\n \"margin-left\",\n \"padding-top\",\n \"padding-right\",\n \"padding-bottom\",\n \"padding-left\",\n \"overflow-x\",\n \"overflow-y\",\n \"z-index\",\n \"float\",\n \"clear\",\n \"aspect-ratio\",\n \"inset-inline-start\",\n \"inset-inline-end\",\n // flex + grid\n \"flex-direction\",\n \"flex-wrap\",\n \"flex-grow\",\n \"flex-shrink\",\n \"flex-basis\",\n \"align-items\",\n \"align-self\",\n \"align-content\",\n \"justify-content\",\n \"justify-items\",\n \"justify-self\",\n \"row-gap\",\n \"column-gap\",\n \"order\",\n \"grid-template-columns\",\n \"grid-template-rows\",\n \"grid-column-start\",\n \"grid-column-end\",\n \"grid-row-start\",\n \"grid-row-end\",\n \"grid-auto-flow\",\n \"grid-auto-rows\",\n \"grid-auto-columns\",\n // type\n \"color\",\n \"font-family\",\n \"font-size\",\n \"font-weight\",\n \"font-style\",\n \"font-variant-numeric\",\n \"font-feature-settings\",\n \"line-height\",\n \"letter-spacing\",\n \"word-spacing\",\n \"text-align\",\n \"text-indent\",\n \"text-transform\",\n \"text-overflow\",\n \"text-decoration-line\",\n \"text-decoration-color\",\n \"text-decoration-style\",\n \"text-decoration-thickness\",\n \"text-underline-offset\",\n \"white-space\",\n \"word-break\",\n \"overflow-wrap\",\n \"vertical-align\",\n \"direction\",\n \"list-style-type\",\n \"list-style-position\",\n \"-webkit-line-clamp\",\n \"-webkit-box-orient\",\n \"-webkit-text-fill-color\",\n // paint\n \"background-color\",\n \"background-image\",\n \"background-position\",\n \"background-repeat\",\n \"background-size\",\n \"background-clip\",\n \"background-origin\",\n \"opacity\",\n \"visibility\",\n \"box-shadow\",\n \"filter\",\n \"backdrop-filter\",\n \"mix-blend-mode\",\n \"clip-path\",\n // borders + outline\n \"border-top-width\",\n \"border-right-width\",\n \"border-bottom-width\",\n \"border-left-width\",\n \"border-top-style\",\n \"border-right-style\",\n \"border-bottom-style\",\n \"border-left-style\",\n \"border-top-color\",\n \"border-right-color\",\n \"border-bottom-color\",\n \"border-left-color\",\n \"border-top-left-radius\",\n \"border-top-right-radius\",\n \"border-bottom-right-radius\",\n \"border-bottom-left-radius\",\n \"outline-width\",\n \"outline-style\",\n \"outline-color\",\n \"outline-offset\",\n // transform\n \"transform\",\n \"transform-origin\",\n \"rotate\",\n \"scale\",\n \"translate\",\n // tables\n \"border-collapse\",\n \"border-spacing\",\n \"table-layout\",\n \"caption-side\",\n // replaced content + svg\n \"object-fit\",\n \"object-position\",\n \"fill\",\n \"fill-opacity\",\n \"fill-rule\",\n \"stroke\",\n \"stroke-opacity\",\n \"stroke-width\",\n \"stroke-linecap\",\n \"stroke-linejoin\",\n \"stroke-dasharray\",\n \"stroke-dashoffset\",\n \"shape-rendering\",\n \"paint-order\",\n];\n\n/** The extension that belongs to what the canvas actually produced (keksdose\n * `capture-screenshot.ts:247`). */\nconst EXTENSIONS: Record<string, string> = {\n \"image/webp\": \"webp\",\n \"image/jpeg\": \"jpg\",\n \"image/png\": \"png\",\n};\n\n/**\n * Keep a node only if it can paint inside the viewport (keksdose `capture-screenshot.ts:267`).\n *\n * - `display: none` goes, with its subtree — it paints nothing and no descendant can.\n * `<style>` and `<link>` are computed `display: none` too but are NOT decoration: a\n * stylesheet inside the captured subtree still applies inside the `foreignObject`.\n * `visibility: hidden` is deliberately not checked: a descendant may set it back.\n * - A box wholly outside the viewport goes: the root is viewport-sized (the app scrolls an\n * inner `<main>`), so it would have been clipped away unseen.\n * - A ZERO box is kept: `display: contents` reports 0×0 while its children paint, and the\n * library's filter drops a rejected node's children too.\n */\nfunction inViewport(node: Node): boolean {\n if (node.nodeType !== Node.ELEMENT_NODE) return true;\n const el = node as Element;\n const tag = el.tagName;\n if (tag !== \"STYLE\" && tag !== \"LINK\") {\n const display = el.ownerDocument?.defaultView?.getComputedStyle(el).display;\n if (display === \"none\") return false;\n }\n const rect = el.getBoundingClientRect?.();\n if (!rect) return true;\n if (rect.width === 0 && rect.height === 0) return true;\n return !(rect.bottom <= 0 || rect.right <= 0 || rect.top >= window.innerHeight || rect.left >= window.innerWidth);\n}\n\n/** What `captureAppScreenshot` takes. */\nexport interface CaptureAppScreenshotOptions {\n /**\n * The id of the element to capture. Default `\"root\"` — the app's mount point, which is\n * right on purpose: the kit's `Modal` portals to `document.body` as a SIBLING of `#root`,\n * so capturing `#root` leaves the feedback dialog and its backdrop out of the picture\n * without a single filter rule.\n */\n rootId?: string;\n}\n\n/**\n * Snapshot the app view behind the feedback dialog, as a `File` ready for the upload —\n * `screenshot.webp`, or `screenshot.png` where the browser cannot encode WebP.\n *\n * - Resolves `null` when there is no element with `rootId` — nothing to capture.\n * - **Rejects** when the optional peer `modern-screenshot` is not installed (the error\n * says so), or when the capture itself fails. It never swallows: what to say about a\n * failure is the caller's — `useFeedbackSubmit` toasts `feedbackToast.captureFailed`.\n *\n * Call it from the \"Capture screenshot\" button's own click and nowhere else (keksdose\n * checked: running it at dialog OPEN was suspected and was never the cause of the wait).\n */\nexport async function captureAppScreenshot({ rootId = \"root\" }: CaptureAppScreenshotOptions = {}): Promise<File | null> {\n let domToBlob: (typeof import(\"modern-screenshot\"))[\"domToBlob\"];\n try {\n ({ domToBlob } = await import(\"modern-screenshot\"));\n } catch (cause) {\n throw new Error(\n \"@eifi1/ui-kit: captureAppScreenshot needs the optional peer `modern-screenshot` — install it (npm i modern-screenshot).\",\n { cause },\n );\n }\n const el = document.getElementById(rootId);\n if (!el) return null;\n const blob = await domToBlob(el, {\n // Falls back to PNG where WebP cannot be encoded; the File is named from the blob.\n type: \"image/webp\",\n quality: 0.9,\n font: false,\n includeStyleProperties: [...CAPTURE_STYLE_PROPERTIES],\n filter: inViewport,\n });\n const type = blob.type || \"image/png\";\n return new File([blob], `screenshot.${EXTENSIONS[type] ?? \"png\"}`, { type });\n}\n"],"mappings":";AAwDA,MAAM,2BAA8C;AAAA;AAAA,EAElD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAIA,MAAM,aAAqC;AAAA,EACzC,cAAc;AAAA,EACd,cAAc;AAAA,EACd,aAAa;AACf;AAcA,SAAS,WAAW,MAAqB;AACvC,MAAI,KAAK,aAAa,KAAK,aAAc,QAAO;AAChD,QAAM,KAAK;AACX,QAAM,MAAM,GAAG;AACf,MAAI,QAAQ,WAAW,QAAQ,QAAQ;AACrC,UAAM,UAAU,GAAG,eAAe,aAAa,iBAAiB,EAAE,EAAE;AACpE,QAAI,YAAY,OAAQ,QAAO;AAAA,EACjC;AACA,QAAM,OAAO,GAAG,wBAAwB;AACxC,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,KAAK,UAAU,KAAK,KAAK,WAAW,EAAG,QAAO;AAClD,SAAO,EAAE,KAAK,UAAU,KAAK,KAAK,SAAS,KAAK,KAAK,OAAO,OAAO,eAAe,KAAK,QAAQ,OAAO;AACxG;AAyBA,eAAsB,qBAAqB,EAAE,SAAS,OAAO,IAAiC,CAAC,GAAyB;AACtH,MAAI;AACJ,MAAI;AACF,KAAC,EAAE,UAAU,IAAI,MAAM,OAAO,mBAAmB;AAAA,EACnD,SAAS,OAAO;AACd,UAAM,IAAI;AAAA,MACR;AAAA,MACA,EAAE,MAAM;AAAA,IACV;AAAA,EACF;AACA,QAAM,KAAK,SAAS,eAAe,MAAM;AACzC,MAAI,CAAC,GAAI,QAAO;AAChB,QAAM,OAAO,MAAM,UAAU,IAAI;AAAA;AAAA,IAE/B,MAAM;AAAA,IACN,SAAS;AAAA,IACT,MAAM;AAAA,IACN,wBAAwB,CAAC,GAAG,wBAAwB;AAAA,IACpD,QAAQ;AAAA,EACV,CAAC;AACD,QAAM,OAAO,KAAK,QAAQ;AAC1B,SAAO,IAAI,KAAK,CAAC,IAAI,GAAG,cAAc,WAAW,IAAI,KAAK,KAAK,IAAI,EAAE,KAAK,CAAC;AAC7E;","names":[]}
1
+ {"version":3,"sources":["../../src/feedback/feedback-capture.ts"],"sourcesContent":["/**\n * `captureAppScreenshot` — the picture of the app a feedback report attaches (0.27.0,\n * docs/feedback-harmonization.md §5).\n *\n * Lifted from keksdose `frontend/src/features/feedback/capture-screenshot.ts` (live #271),\n * which is the canon: Kurvenschmiede carried a second copy (`capture-page.ts`) with none of\n * its speed-ups, and kastlan had no capture at all. Each app wrote the same three awkward\n * decisions — which element, which library, what to do when it throws — and they drifted\n * the moment there were two copies (Kurvenschmiede feedback #128 says exactly that).\n *\n * WHY IT IS FAST (keksdose live #271: *\"Capture screenshot — That takes ages in my mobile\n * phone\"*). `modern-screenshot` deep-clones the subtree and inlines every node's computed\n * style onto the clone, because the final image is an SVG `foreignObject` that carries no\n * stylesheets. By default it walks the WHOLE computed style (~340 longhands in Chrome) per\n * element; keksdose's /transactions has ~4,800 elements under `#root` at phone width, so\n * that was ~1.6 million `getPropertyValue` calls before a pixel was drawn — 21–52 s on a\n * desktop CPU, 153–160 s with the CPU throttled 4×. Two levers fixed it, both kept here:\n *\n * 1. {@link CAPTURE_STYLE_PROPERTIES} — copy the ~140 longhands a Tailwind v4 app built\n * on this kit actually uses. Alone: 21 s → 1.8 s.\n * 2. {@link inViewport} drops `display: none` subtrees — on a phone most of the\n * document, since `DataTable` renders both layouts and hides one — and anything\n * wholly outside the viewport. Together: 27–52 s → 0.4–1.1 s, the picture\n * 0.05% of pixels different (antialiasing).\n *\n * Plus WebP at 0.9 (222 KB → 46 KB, an upload a phone makes on a mobile connection) with\n * the file named from what the canvas ACTUALLY produced — a browser without WebP encodes\n * PNG, and a \"screenshot.webp\" that is a PNG is refused by the server on its type — and\n * `font: false`, since the app's faces are same-origin and already in the page.\n *\n * WHY THE PEER IS LOADED DYNAMICALLY. `modern-screenshot` is an OPTIONAL peer\n * (`peerDependenciesMeta.optional`, like `sonner`), and this module is re-exported by the\n * barrel: a static import would make every app that imports a Button install it. It is\n * imported inside the function, so an app pays nothing until somebody presses \"Capture\n * screenshot\", and an app without it gets a REJECTED promise — which the submit dialog\n * turns into the \"Could not capture a screenshot\" toast (`feedbackToast.captureFailed`).\n *\n * WHAT IS NOT HERE: Kurvenschmiede's `features: { restoreScrollPosition: true }` (its\n * feedback #153: a capture of a scrolled `<main>` came out as the TOP of the page). The\n * viewport filter already answers that case — what is scrolled out of view is dropped, so\n * what remains is what was on screen, give or take the part of the first row that was\n * scrolled past (checked in Chromium, 0.27.0: an inner `<main>` scrolled to row 20 captures\n * rows 20–26). Both together are worse than either: the clone is translated by the scroll\n * offset AFTER the rows above were dropped, and the picture came out blank below the\n * header (same check).\n */\n\n/**\n * The computed properties copied onto the clone (keksdose `capture-screenshot.ts:94`).\n *\n * LONGHANDS only: `getPropertyValue()` resolves a shorthand to `\"\"` whenever its longhands\n * disagree, so `\"border\"` would silently drop every one-sided border. Chosen by what\n * Tailwind v4 emits rather than by what looks complete: `font-variant-numeric` because\n * money is `tabular-nums`, the SVG paint properties because every icon is a lucide `<svg>`\n * inheriting `stroke: currentColor`.\n */\nconst CAPTURE_STYLE_PROPERTIES: readonly string[] = [\n // box\n \"display\",\n \"position\",\n \"top\",\n \"right\",\n \"bottom\",\n \"left\",\n \"width\",\n \"height\",\n \"min-width\",\n \"min-height\",\n \"max-width\",\n \"max-height\",\n \"box-sizing\",\n \"margin-top\",\n \"margin-right\",\n \"margin-bottom\",\n \"margin-left\",\n \"padding-top\",\n \"padding-right\",\n \"padding-bottom\",\n \"padding-left\",\n \"overflow-x\",\n \"overflow-y\",\n \"z-index\",\n \"float\",\n \"clear\",\n \"aspect-ratio\",\n \"inset-inline-start\",\n \"inset-inline-end\",\n // flex + grid\n \"flex-direction\",\n \"flex-wrap\",\n \"flex-grow\",\n \"flex-shrink\",\n \"flex-basis\",\n \"align-items\",\n \"align-self\",\n \"align-content\",\n \"justify-content\",\n \"justify-items\",\n \"justify-self\",\n \"row-gap\",\n \"column-gap\",\n \"order\",\n \"grid-template-columns\",\n \"grid-template-rows\",\n \"grid-column-start\",\n \"grid-column-end\",\n \"grid-row-start\",\n \"grid-row-end\",\n \"grid-auto-flow\",\n \"grid-auto-rows\",\n \"grid-auto-columns\",\n // type\n \"color\",\n \"font-family\",\n \"font-size\",\n \"font-weight\",\n \"font-style\",\n \"font-variant-numeric\",\n \"font-feature-settings\",\n \"line-height\",\n \"letter-spacing\",\n \"word-spacing\",\n \"text-align\",\n \"text-indent\",\n \"text-transform\",\n \"text-overflow\",\n \"text-decoration-line\",\n \"text-decoration-color\",\n \"text-decoration-style\",\n \"text-decoration-thickness\",\n \"text-underline-offset\",\n \"white-space\",\n \"word-break\",\n \"overflow-wrap\",\n \"vertical-align\",\n \"direction\",\n \"list-style-type\",\n \"list-style-position\",\n \"-webkit-line-clamp\",\n \"-webkit-box-orient\",\n \"-webkit-text-fill-color\",\n // paint\n \"background-color\",\n \"background-image\",\n \"background-position\",\n \"background-repeat\",\n \"background-size\",\n \"background-clip\",\n \"background-origin\",\n \"opacity\",\n \"visibility\",\n \"box-shadow\",\n \"filter\",\n \"backdrop-filter\",\n \"mix-blend-mode\",\n \"clip-path\",\n // borders + outline\n \"border-top-width\",\n \"border-right-width\",\n \"border-bottom-width\",\n \"border-left-width\",\n \"border-top-style\",\n \"border-right-style\",\n \"border-bottom-style\",\n \"border-left-style\",\n \"border-top-color\",\n \"border-right-color\",\n \"border-bottom-color\",\n \"border-left-color\",\n \"border-top-left-radius\",\n \"border-top-right-radius\",\n \"border-bottom-right-radius\",\n \"border-bottom-left-radius\",\n \"outline-width\",\n \"outline-style\",\n \"outline-color\",\n \"outline-offset\",\n // transform\n \"transform\",\n \"transform-origin\",\n \"rotate\",\n \"scale\",\n \"translate\",\n // tables\n \"border-collapse\",\n \"border-spacing\",\n \"table-layout\",\n \"caption-side\",\n // replaced content + svg\n \"object-fit\",\n \"object-position\",\n \"fill\",\n \"fill-opacity\",\n \"fill-rule\",\n \"stroke\",\n \"stroke-opacity\",\n \"stroke-width\",\n \"stroke-linecap\",\n \"stroke-linejoin\",\n \"stroke-dasharray\",\n \"stroke-dashoffset\",\n \"shape-rendering\",\n \"paint-order\",\n];\n\n/** The extension that belongs to what the canvas actually produced (keksdose\n * `capture-screenshot.ts:247`). */\nconst EXTENSIONS: Record<string, string> = {\n \"image/webp\": \"webp\",\n \"image/jpeg\": \"jpg\",\n \"image/png\": \"png\",\n};\n\n/**\n * Keep a node only if it can paint inside the viewport (keksdose `capture-screenshot.ts:267`).\n *\n * - `display: none` goes, with its subtree — it paints nothing and no descendant can.\n * `<style>` and `<link>` are computed `display: none` too but are NOT decoration: a\n * stylesheet inside the captured subtree still applies inside the `foreignObject`.\n * `visibility: hidden` is deliberately not checked: a descendant may set it back.\n * - A box wholly outside the viewport goes: the root is viewport-sized (the app scrolls an\n * inner `<main>`), so it would have been clipped away unseen.\n * - A ZERO box is kept: `display: contents` reports 0×0 while its children paint, and the\n * library's filter drops a rejected node's children too.\n */\nfunction inViewport(node: Node): boolean {\n if (node.nodeType !== Node.ELEMENT_NODE) return true;\n const el = node as Element;\n const tag = el.tagName;\n if (tag !== \"STYLE\" && tag !== \"LINK\") {\n const display = el.ownerDocument?.defaultView?.getComputedStyle(el).display;\n if (display === \"none\") return false;\n }\n const rect = el.getBoundingClientRect?.();\n if (!rect) return true;\n if (rect.width === 0 && rect.height === 0) return true;\n return !(rect.bottom <= 0 || rect.right <= 0 || rect.top >= window.innerHeight || rect.left >= window.innerWidth);\n}\n\n/** What `captureAppScreenshot` takes. */\nexport interface CaptureAppScreenshotOptions {\n /**\n * The id of the element to capture. Default `\"root\"` — the app's mount point, which is\n * right on purpose: the kit's `Modal` portals to `document.body` as a SIBLING of `#root`,\n * so capturing `#root` leaves the feedback dialog and its backdrop out of the picture\n * without a single filter rule.\n */\n rootId?: string;\n}\n\n/**\n * Snapshot the app view behind the feedback dialog, as a `File` ready for the upload —\n * `screenshot.webp`, or `screenshot.png` where the browser cannot encode WebP.\n *\n * - Resolves `null` when there is no element with `rootId` — nothing to capture.\n * - **Rejects** when the optional peer `modern-screenshot` is not installed (the error\n * says so), or when the capture itself fails. It never swallows: what to say about a\n * failure is the caller's — `useFeedbackSubmit` toasts `feedbackToast.captureFailed`.\n *\n * Call it from the \"Capture screenshot\" button's own click and nowhere else (keksdose\n * checked: running it at dialog OPEN was suspected and was never the cause of the wait).\n */\nexport async function captureAppScreenshot({\n rootId = \"root\",\n}: CaptureAppScreenshotOptions = {}): Promise<File | null> {\n let domToBlob: (typeof import(\"modern-screenshot\"))[\"domToBlob\"];\n try {\n ({ domToBlob } = await import(\"modern-screenshot\"));\n } catch (cause) {\n throw new Error(\n \"@eifi1/ui-kit: captureAppScreenshot needs the optional peer `modern-screenshot` — install it (npm i modern-screenshot).\",\n { cause },\n );\n }\n const el = document.getElementById(rootId);\n if (!el) return null;\n const blob = await domToBlob(el, {\n // Falls back to PNG where WebP cannot be encoded; the File is named from the blob.\n type: \"image/webp\",\n quality: 0.9,\n font: false,\n includeStyleProperties: [...CAPTURE_STYLE_PROPERTIES],\n filter: inViewport,\n });\n const type = blob.type || \"image/png\";\n return new File([blob], `screenshot.${EXTENSIONS[type] ?? \"png\"}`, { type });\n}\n"],"mappings":";AAwDA,MAAM,2BAA8C;AAAA;AAAA,EAElD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAIA,MAAM,aAAqC;AAAA,EACzC,cAAc;AAAA,EACd,cAAc;AAAA,EACd,aAAa;AACf;AAcA,SAAS,WAAW,MAAqB;AACvC,MAAI,KAAK,aAAa,KAAK,aAAc,QAAO;AAChD,QAAM,KAAK;AACX,QAAM,MAAM,GAAG;AACf,MAAI,QAAQ,WAAW,QAAQ,QAAQ;AACrC,UAAM,UAAU,GAAG,eAAe,aAAa,iBAAiB,EAAE,EAAE;AACpE,QAAI,YAAY,OAAQ,QAAO;AAAA,EACjC;AACA,QAAM,OAAO,GAAG,wBAAwB;AACxC,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI,KAAK,UAAU,KAAK,KAAK,WAAW,EAAG,QAAO;AAClD,SAAO,EAAE,KAAK,UAAU,KAAK,KAAK,SAAS,KAAK,KAAK,OAAO,OAAO,eAAe,KAAK,QAAQ,OAAO;AACxG;AAyBA,eAAsB,qBAAqB;AAAA,EACzC,SAAS;AACX,IAAiC,CAAC,GAAyB;AACzD,MAAI;AACJ,MAAI;AACF,KAAC,EAAE,UAAU,IAAI,MAAM,OAAO,mBAAmB;AAAA,EACnD,SAAS,OAAO;AACd,UAAM,IAAI;AAAA,MACR;AAAA,MACA,EAAE,MAAM;AAAA,IACV;AAAA,EACF;AACA,QAAM,KAAK,SAAS,eAAe,MAAM;AACzC,MAAI,CAAC,GAAI,QAAO;AAChB,QAAM,OAAO,MAAM,UAAU,IAAI;AAAA;AAAA,IAE/B,MAAM;AAAA,IACN,SAAS;AAAA,IACT,MAAM;AAAA,IACN,wBAAwB,CAAC,GAAG,wBAAwB;AAAA,IACpD,QAAQ;AAAA,EACV,CAAC;AACD,QAAM,OAAO,KAAK,QAAQ;AAC1B,SAAO,IAAI,KAAK,CAAC,IAAI,GAAG,cAAc,WAAW,IAAI,KAAK,KAAK,IAAI,EAAE,KAAK,CAAC;AAC7E;","names":[]}
@@ -63,14 +63,21 @@ function FeedbackCommentThread({
63
63
  createdAt: reply.created_at,
64
64
  body: reply.body,
65
65
  attachments: reply.attachment_url ? [{ name: attachmentName(reply.attachment_url), url: reply.attachment_url }] : void 0,
66
- badge: /* @__PURE__ */ jsxs("span", { className: "inline-flex min-w-0 max-w-full flex-wrap items-center gap-1", "data-feedback-entry-kind": "rework", children: [
67
- /* @__PURE__ */ jsx(FeedbackReworkEntryChip, {}),
68
- reply.status_to && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1", "data-feedback-reply-status": reply.status_to, children: [
69
- /* @__PURE__ */ jsx("span", { className: "sr-only", children: replyLabels.statusChanged }),
70
- /* @__PURE__ */ jsx("span", { "aria-hidden": true, children: "\u2192" }),
71
- /* @__PURE__ */ jsx(FeedbackStatusBadge, { status: reply.status_to })
72
- ] })
73
- ] })
66
+ badge: /* @__PURE__ */ jsxs(
67
+ "span",
68
+ {
69
+ className: "inline-flex min-w-0 max-w-full flex-wrap items-center gap-1",
70
+ "data-feedback-entry-kind": "rework",
71
+ children: [
72
+ /* @__PURE__ */ jsx(FeedbackReworkEntryChip, {}),
73
+ reply.status_to && /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1", "data-feedback-reply-status": reply.status_to, children: [
74
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: replyLabels.statusChanged }),
75
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, children: "\u2192" }),
76
+ /* @__PURE__ */ jsx(FeedbackStatusBadge, { status: reply.status_to })
77
+ ] })
78
+ ]
79
+ }
80
+ )
74
81
  }
75
82
  });
76
83
  return;
@@ -86,11 +93,18 @@ function FeedbackCommentThread({
86
93
  // 0.47 (§12): an admin reply's file, where the view carries it.
87
94
  attachments: reply.attachment_url ? [{ name: attachmentName(reply.attachment_url), url: reply.attachment_url }] : void 0,
88
95
  staff: true,
89
- badge: reply.status_to ? /* @__PURE__ */ jsxs("span", { className: "inline-flex min-w-0 max-w-full flex-wrap items-center gap-1", "data-feedback-reply-status": reply.status_to, children: [
90
- /* @__PURE__ */ jsx("span", { className: "sr-only", children: replyLabels.statusChanged }),
91
- /* @__PURE__ */ jsx("span", { "aria-hidden": true, children: "\u2192" }),
92
- /* @__PURE__ */ jsx(FeedbackStatusBadge, { status: reply.status_to })
93
- ] }) : void 0
96
+ badge: reply.status_to ? /* @__PURE__ */ jsxs(
97
+ "span",
98
+ {
99
+ className: "inline-flex min-w-0 max-w-full flex-wrap items-center gap-1",
100
+ "data-feedback-reply-status": reply.status_to,
101
+ children: [
102
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: replyLabels.statusChanged }),
103
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, children: "\u2192" }),
104
+ /* @__PURE__ */ jsx(FeedbackStatusBadge, { status: reply.status_to })
105
+ ]
106
+ }
107
+ ) : void 0
94
108
  }
95
109
  });
96
110
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/feedback/feedback-comment-thread.tsx"],"sourcesContent":["import { useContext } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { EyeOff } from \"lucide-react\";\nimport { Button } from \"../components/button\";\nimport { useConfirm } from \"../components/confirm-dialog\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport type { AuthedFetcher } from \"../hooks/use-authed-src\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { FeedbackStatusBadge } from \"./feedback-inbox\";\nimport type { FeedbackComment, FeedbackReply } from \"./feedback-public-types\";\nimport { DEFAULT_FEEDBACK_PUBLIC_LABELS } from \"./feedback-public-labels\";\nimport { FEEDBACK_COMMENT_MAX, attachmentName } from \"./feedback-record\";\nimport { FeedbackEntryFile, useFeedbackEntryUpload, type FeedbackEntryUploadOptions } from \"./feedback-entry-file\";\nimport { useFeedbackRepliesLabels } from \"./feedback-replies\";\nimport { FeedbackRepliesDrawnAboveContext } from \"./feedback-replies-context\";\nimport { FeedbackReworkEntryChip } from \"./feedback-rework-entry-chip\";\nimport { ChatComposer, FeedbackThread } from \"./feedback-thread\";\nimport type { FeedbackThreadMessage } from \"./feedback-thread\";\nimport { DEFAULT_FEEDBACK_COMMENTS_LABELS } from \"./feedback-thread-parts-labels\";\n\n/**\n * A report's conversation as everyone who may read it sees it\n * (docs/feedback-harmonization.md §9.8): the admin's replies and other people's\n * comments, each its own row, oldest first — merged by time, on the\n * {@link FeedbackThread} look — and the box to add one.\n *\n * - **Replies** are drawn as staff posts with the status they set (\"→ Done\"); a reply\n * without an author is \"Admin\" (§9.7).\n * - **Reworks** (0.40, §10.3, `kind: \"rework\"`) are the report's author's: the\n * **Rework** chip, \"→ status\" when the entry moved one (a migrated one did not), the\n * file when the view carries it; at the reading end for the author ({@link\n * FeedbackCommentThreadProps.isAuthor}), and a `null` name is \"A user\", never \"Admin\".\n * - **Comments**: the viewer's own at the reading end (\"You\"); someone else's under the\n * first name the server sent, `null` as \"A user\" (§9.5, `feedbackPublic.anonymous`).\n * - **Hidden** (§9.8): others see \"Hidden by an admin\" and no name, no text; the writer\n * sees their text with that line; the admin sees it struck through, with **Unhide**.\n * An admin has **Hide** on every visible comment; the writer **Delete** on their own,\n * after a confirm (it is a hard delete). Nobody edits a comment.\n * - **The composer** (1–4 000 characters) only with `onComment`: left out for a reader\n * who may not write (a demo, a CRASH report — the app knows). Since 0.47 (§12) with\n * `onUpload` it takes one file as the rework note does — pasted into the box,\n * captured or picked — uploaded first, its URL `onComment`'s second argument.\n * - **Files** (0.47, §12): a comment's or an entry's file under its text, a picture\n * INLINE — a thumbnail through `fetcher` that opens full size — any other file a\n * download ({@link FeedbackEntryFile}). A veiled comment shows none.\n *\n * Delete asks through `useConfirm`, so a thread with `onDelete` and a comment of the\n * viewer's needs the app's `ConfirmProvider` (each app mounts one already).\n */\n\n/* ── Labels ───────────────────────────────────────────────────────────────── */\n\n/** The `feedbackComments` namespace of `<UiKitProvider labels>`. A `null` name is\n * `feedbackPublic.anonymous`, the public pages' word, so the two say the same. */\nexport interface FeedbackCommentsLabels {\n /** The thread's accessible name. de-CH: \"Kommentare\" */\n comments: string;\n /** Nothing yet. de-CH: \"Noch keine Kommentare\" */\n empty: string;\n /** In place of a hidden comment. de-CH: \"Von einem Admin ausgeblendet\" */\n hidden: string;\n /** The admin's button. de-CH: \"Ausblenden\" */\n hide: string;\n /** The admin's button on a hidden comment. de-CH: \"Einblenden\" */\n unhide: string;\n /** The writer's button. de-CH: \"Löschen\" */\n delete: string;\n /** The confirm's question. de-CH: \"Diesen Kommentar löschen?\" */\n deleteTitle: string;\n /** The confirm's line. de-CH: \"Er wird für alle entfernt. Das kann nicht rückgängig\n * gemacht werden.\" */\n deleteBody: string;\n /** The box's accessible name. de-CH: \"Kommentar schreiben\" */\n field: string;\n /** de-CH: \"Kommentar schreiben…\" */\n placeholder: string;\n /** The send button. de-CH: \"Kommentieren\" */\n send: string;\n}\n\nexport { DEFAULT_FEEDBACK_COMMENTS_LABELS };\n\n/* ── The thread ───────────────────────────────────────────────────────────── */\n\nexport interface FeedbackCommentThreadProps extends FeedbackEntryUploadOptions {\n /** The report's replies (§9.7), oldest first. Left out or `[]`: none.\n *\n * Inside `FeedbackRowDetail`'s `comments` they are dropped (0.38.1): the detail's\n * Replies section draws them already, and the thread there is the comments alone —\n * so passing them there cannot draw them twice. */\n replies?: readonly FeedbackReply[];\n /** `GET /feedback/{id}/comments`, oldest first. */\n comments: readonly FeedbackComment[];\n /** The viewer is the app's admin: Hide / Unhide, the hidden text struck through. */\n isAdmin: boolean;\n /** 0.40: the viewer filed the report (the record's `mine`) — its rework entries are\n * theirs, drawn at the reading end as \"You\". Default `false`. */\n isAuthor?: boolean;\n /** `POST /feedback/{id}/comments`. Return the promise: the box clears when it\n * resolves, and keeps the draft when it rejects. A failure is the app's to toast —\n * the server's `detail`, else `feedbackToast.commentFailed` (0.38.1). Left out, no\n * composer. 0.47 (§12): `attachmentUrl` is the one file uploaded with it (with\n * `onUpload`), `undefined` for none — `attachment_url` in the body (server-kit 0.18). */\n onComment?: (body: string, attachmentUrl?: string) => void | Promise<unknown>;\n /** 0.47: the app's authenticated GET (`AuthedImage`'s) for the files in the thread.\n * Left out, the browser's own fetch with same-origin cookies. */\n fetcher?: AuthedFetcher | null;\n /** `PATCH /feedback/{id}/comments/{comment_id} {hidden}` — the admin's. A rejection\n * is caught (0.38.1) and is the app's to toast (`feedbackToast.updateFailed`). Left\n * out, no Hide / Unhide. */\n onHide?: (comment: FeedbackComment, hidden: boolean) => void | Promise<unknown>;\n /** `DELETE /feedback/{id}/comments/{comment_id}` — the writer's own, after the\n * confirm. Left out, no Delete. */\n onDelete?: (comment: FeedbackComment) => void | Promise<unknown>;\n /** A comment is being sent. */\n pending?: boolean;\n /** The comments are loading. */\n loading?: boolean;\n /** Comment, Hide and Delete COMMIT — see Button's `commit`. */\n commit?: CommitScope;\n /** \"Now\" for the relative times — pin it in tests and stories. */\n now?: Date | number;\n labels?: LabelOverride<FeedbackCommentsLabels>;\n className?: string;\n}\n\ntype Entry = { at: number; order: number; message: FeedbackThreadMessage };\n\n/** The admin's Hide / Unhide as a promise — a sync throw or a `void` return included —\n * so the click can catch it as Delete's does. */\nconst hideComment = async (\n onHide: NonNullable<FeedbackCommentThreadProps[\"onHide\"]>,\n comment: FeedbackComment,\n) => {\n await onHide(comment, !comment.hidden);\n};\n\nconst instant = (iso: string) => {\n const ms = Date.parse(iso);\n return Number.isNaN(ms) ? 0 : ms;\n};\n\nexport function FeedbackCommentThread({\n replies: repliesProp = [],\n comments,\n isAdmin,\n isAuthor = false,\n onComment,\n onHide,\n onDelete,\n pending = false,\n loading = false,\n commit,\n now,\n fetcher,\n onUpload,\n onCaptureScreenshot,\n attachment,\n labels: labelsProp,\n className,\n}: FeedbackCommentThreadProps) {\n const { composerAttachment, upload, uploading } = useFeedbackEntryUpload({\n onUpload,\n onCaptureScreenshot,\n attachment,\n });\n const labels = useKitLabels(\"feedbackComments\", DEFAULT_FEEDBACK_COMMENTS_LABELS, labelsProp);\n const anonymous = useKitLabels(\"feedbackPublic\", DEFAULT_FEEDBACK_PUBLIC_LABELS).anonymous;\n const replyLabels = useFeedbackRepliesLabels();\n const repliesDrawnAbove = useContext(FeedbackRepliesDrawnAboveContext);\n const replies = repliesDrawnAbove ? [] : repliesProp;\n\n const entries: Entry[] = [];\n replies.forEach((reply, order) => {\n if (reply.kind === \"rework\") {\n entries.push({\n at: instant(reply.created_at),\n order,\n message: {\n id: `reply-${reply.id}`,\n own: isAuthor,\n author: isAuthor ? undefined : (reply.author_name ?? anonymous),\n createdAt: reply.created_at,\n body: reply.body,\n attachments: reply.attachment_url\n ? [{ name: attachmentName(reply.attachment_url), url: reply.attachment_url }]\n : undefined,\n badge: (\n <span className=\"inline-flex min-w-0 max-w-full flex-wrap items-center gap-1\" data-feedback-entry-kind=\"rework\">\n <FeedbackReworkEntryChip />\n {reply.status_to && (\n <span className=\"inline-flex items-center gap-1\" data-feedback-reply-status={reply.status_to}>\n <span className=\"sr-only\">{replyLabels.statusChanged}</span>\n <span aria-hidden>→</span>\n <FeedbackStatusBadge status={reply.status_to} />\n </span>\n )}\n </span>\n ),\n },\n });\n return;\n }\n entries.push({\n at: instant(reply.created_at),\n order,\n message: {\n id: `reply-${reply.id}`,\n author: reply.author_name ?? replyLabels.admin,\n createdAt: reply.created_at,\n body: reply.body,\n // 0.47 (§12): an admin reply's file, where the view carries it.\n attachments: reply.attachment_url\n ? [{ name: attachmentName(reply.attachment_url), url: reply.attachment_url }]\n : undefined,\n staff: true,\n badge: reply.status_to ? (\n <span className=\"inline-flex min-w-0 max-w-full flex-wrap items-center gap-1\" data-feedback-reply-status={reply.status_to}>\n <span className=\"sr-only\">{replyLabels.statusChanged}</span>\n <span aria-hidden>→</span>\n <FeedbackStatusBadge status={reply.status_to} />\n </span>\n ) : undefined,\n },\n });\n });\n comments.forEach((comment, index) => {\n // Shown as hidden — no name, no text — to anyone but its writer and an admin.\n const veiled = comment.hidden && !comment.mine && !isAdmin;\n const actions: ReactNode[] = [];\n if (comment.hidden) {\n actions.push(\n <span key=\"hidden\" className=\"inline-flex items-center gap-1\" data-feedback-comment-hidden=\"\">\n <EyeOff className=\"size-3\" aria-hidden />\n {labels.hidden}\n </span>,\n );\n }\n if (isAdmin && onHide) {\n actions.push(\n <Button\n key=\"hide\"\n type=\"button\"\n variant=\"link\"\n tone=\"muted\"\n size=\"sm\"\n commit={commit}\n // Caught as Delete's is (0.38.1): a rejection is the app's to toast — its\n // mutation's `onError` — and must not surface as an unhandled one.\n onClick={() => void hideComment(onHide, comment).catch(() => {})}\n >\n {comment.hidden ? labels.unhide : labels.hide}\n </Button>,\n );\n }\n if (comment.mine && onDelete) {\n actions.push(\n <DeleteCommentButton key=\"delete\" comment={comment} onDelete={onDelete} commit={commit} labels={labels} />,\n );\n }\n entries.push({\n at: instant(comment.created_at),\n // Replies first on a tie, then the comments in their order.\n order: replies.length + index,\n message: {\n id: `comment-${comment.id}`,\n own: comment.mine,\n author: veiled ? undefined : comment.mine ? undefined : (comment.author_name ?? anonymous),\n createdAt: comment.created_at,\n body: veiled ? \"\" : (comment.body ?? \"\"),\n // 0.47 (§12): its one file; none when veiled (the server sends none either).\n attachments:\n !veiled && comment.attachment_url\n ? [{ name: attachmentName(comment.attachment_url), url: comment.attachment_url }]\n : undefined,\n staff: !veiled && comment.staff,\n struck: comment.hidden && isAdmin && !veiled,\n footer: actions.length > 0 ? actions : undefined,\n },\n });\n });\n entries.sort((a, b) => a.at - b.at || a.order - b.order);\n\n return (\n <div className={className} data-feedback-comment-thread=\"\">\n <FeedbackThread\n messages={entries.map((entry) => entry.message)}\n loading={loading}\n now={now}\n renderAttachment={(file) => (file.url ? <FeedbackEntryFile url={file.url} fetcher={fetcher} /> : file.name)}\n labels={{ thread: labels.comments, empty: labels.empty }}\n />\n {onComment && (\n <ChatComposer\n className=\"mt-3\"\n onSend={async (body, file) => {\n const url = await upload(file);\n // The second argument only with a file, so a text comment calls as before 0.47.\n return url === undefined ? onComment(body) : onComment(body, url);\n }}\n pending={pending || uploading}\n attachment={composerAttachment}\n commit={commit}\n maxLength={FEEDBACK_COMMENT_MAX}\n showCount\n labels={{ field: labels.field, placeholder: labels.placeholder, send: labels.send }}\n />\n )}\n </div>\n );\n}\n\n/** The writer's Delete: a confirm (danger tone, focus on Cancel), then `onDelete`. Its\n * own component, so `useConfirm` — which needs a `ConfirmProvider` — is only called\n * where there is something to delete. */\nfunction DeleteCommentButton({\n comment,\n onDelete,\n commit,\n labels,\n}: {\n comment: FeedbackComment;\n onDelete: (comment: FeedbackComment) => void | Promise<unknown>;\n commit?: CommitScope;\n labels: FeedbackCommentsLabels;\n}) {\n const confirm = useConfirm();\n const remove = async () => {\n const ok = await confirm({\n title: labels.deleteTitle,\n body: labels.deleteBody,\n confirmLabel: labels.delete,\n tone: \"danger\",\n commit,\n });\n if (ok) await onDelete(comment);\n };\n return (\n <Button\n type=\"button\"\n variant=\"link\"\n tone=\"danger\"\n size=\"sm\"\n commit={commit}\n onClick={() => void remove().catch(() => {})}\n >\n {labels.delete}\n </Button>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/feedback/feedback-comment-thread.tsx"],"sourcesContent":["import { useContext } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { EyeOff } from \"lucide-react\";\nimport { Button } from \"../components/button\";\nimport { useConfirm } from \"../components/confirm-dialog\";\nimport type { CommitScope } from \"../components/write-lock\";\nimport type { AuthedFetcher } from \"../hooks/use-authed-src\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { FeedbackStatusBadge } from \"./feedback-inbox\";\nimport type { FeedbackComment, FeedbackReply } from \"./feedback-public-types\";\nimport { DEFAULT_FEEDBACK_PUBLIC_LABELS } from \"./feedback-public-labels\";\nimport { FEEDBACK_COMMENT_MAX, attachmentName } from \"./feedback-record\";\nimport { FeedbackEntryFile, useFeedbackEntryUpload, type FeedbackEntryUploadOptions } from \"./feedback-entry-file\";\nimport { useFeedbackRepliesLabels } from \"./feedback-replies\";\nimport { FeedbackRepliesDrawnAboveContext } from \"./feedback-replies-context\";\nimport { FeedbackReworkEntryChip } from \"./feedback-rework-entry-chip\";\nimport { ChatComposer, FeedbackThread } from \"./feedback-thread\";\nimport type { FeedbackThreadMessage } from \"./feedback-thread\";\nimport { DEFAULT_FEEDBACK_COMMENTS_LABELS } from \"./feedback-thread-parts-labels\";\n\n/**\n * A report's conversation as everyone who may read it sees it\n * (docs/feedback-harmonization.md §9.8): the admin's replies and other people's\n * comments, each its own row, oldest first — merged by time, on the\n * {@link FeedbackThread} look — and the box to add one.\n *\n * - **Replies** are drawn as staff posts with the status they set (\"→ Done\"); a reply\n * without an author is \"Admin\" (§9.7).\n * - **Reworks** (0.40, §10.3, `kind: \"rework\"`) are the report's author's: the\n * **Rework** chip, \"→ status\" when the entry moved one (a migrated one did not), the\n * file when the view carries it; at the reading end for the author ({@link\n * FeedbackCommentThreadProps.isAuthor}), and a `null` name is \"A user\", never \"Admin\".\n * - **Comments**: the viewer's own at the reading end (\"You\"); someone else's under the\n * first name the server sent, `null` as \"A user\" (§9.5, `feedbackPublic.anonymous`).\n * - **Hidden** (§9.8): others see \"Hidden by an admin\" and no name, no text; the writer\n * sees their text with that line; the admin sees it struck through, with **Unhide**.\n * An admin has **Hide** on every visible comment; the writer **Delete** on their own,\n * after a confirm (it is a hard delete). Nobody edits a comment.\n * - **The composer** (1–4 000 characters) only with `onComment`: left out for a reader\n * who may not write (a demo, a CRASH report — the app knows). Since 0.47 (§12) with\n * `onUpload` it takes one file as the rework note does — pasted into the box,\n * captured or picked — uploaded first, its URL `onComment`'s second argument.\n * - **Files** (0.47, §12): a comment's or an entry's file under its text, a picture\n * INLINE — a thumbnail through `fetcher` that opens full size — any other file a\n * download ({@link FeedbackEntryFile}). A veiled comment shows none.\n *\n * Delete asks through `useConfirm`, so a thread with `onDelete` and a comment of the\n * viewer's needs the app's `ConfirmProvider` (each app mounts one already).\n */\n\n/* ── Labels ───────────────────────────────────────────────────────────────── */\n\n/** The `feedbackComments` namespace of `<UiKitProvider labels>`. A `null` name is\n * `feedbackPublic.anonymous`, the public pages' word, so the two say the same. */\nexport interface FeedbackCommentsLabels {\n /** The thread's accessible name. de-CH: \"Kommentare\" */\n comments: string;\n /** Nothing yet. de-CH: \"Noch keine Kommentare\" */\n empty: string;\n /** In place of a hidden comment. de-CH: \"Von einem Admin ausgeblendet\" */\n hidden: string;\n /** The admin's button. de-CH: \"Ausblenden\" */\n hide: string;\n /** The admin's button on a hidden comment. de-CH: \"Einblenden\" */\n unhide: string;\n /** The writer's button. de-CH: \"Löschen\" */\n delete: string;\n /** The confirm's question. de-CH: \"Diesen Kommentar löschen?\" */\n deleteTitle: string;\n /** The confirm's line. de-CH: \"Er wird für alle entfernt. Das kann nicht rückgängig\n * gemacht werden.\" */\n deleteBody: string;\n /** The box's accessible name. de-CH: \"Kommentar schreiben\" */\n field: string;\n /** de-CH: \"Kommentar schreiben…\" */\n placeholder: string;\n /** The send button. de-CH: \"Kommentieren\" */\n send: string;\n}\n\nexport { DEFAULT_FEEDBACK_COMMENTS_LABELS };\n\n/* ── The thread ───────────────────────────────────────────────────────────── */\n\nexport interface FeedbackCommentThreadProps extends FeedbackEntryUploadOptions {\n /** The report's replies (§9.7), oldest first. Left out or `[]`: none.\n *\n * Inside `FeedbackRowDetail`'s `comments` they are dropped (0.38.1): the detail's\n * Replies section draws them already, and the thread there is the comments alone —\n * so passing them there cannot draw them twice. */\n replies?: readonly FeedbackReply[];\n /** `GET /feedback/{id}/comments`, oldest first. */\n comments: readonly FeedbackComment[];\n /** The viewer is the app's admin: Hide / Unhide, the hidden text struck through. */\n isAdmin: boolean;\n /** 0.40: the viewer filed the report (the record's `mine`) — its rework entries are\n * theirs, drawn at the reading end as \"You\". Default `false`. */\n isAuthor?: boolean;\n /** `POST /feedback/{id}/comments`. Return the promise: the box clears when it\n * resolves, and keeps the draft when it rejects. A failure is the app's to toast —\n * the server's `detail`, else `feedbackToast.commentFailed` (0.38.1). Left out, no\n * composer. 0.47 (§12): `attachmentUrl` is the one file uploaded with it (with\n * `onUpload`), `undefined` for none — `attachment_url` in the body (server-kit 0.18). */\n onComment?: (body: string, attachmentUrl?: string) => void | Promise<unknown>;\n /** 0.47: the app's authenticated GET (`AuthedImage`'s) for the files in the thread.\n * Left out, the browser's own fetch with same-origin cookies. */\n fetcher?: AuthedFetcher | null;\n /** `PATCH /feedback/{id}/comments/{comment_id} {hidden}` — the admin's. A rejection\n * is caught (0.38.1) and is the app's to toast (`feedbackToast.updateFailed`). Left\n * out, no Hide / Unhide. */\n onHide?: (comment: FeedbackComment, hidden: boolean) => void | Promise<unknown>;\n /** `DELETE /feedback/{id}/comments/{comment_id}` — the writer's own, after the\n * confirm. Left out, no Delete. */\n onDelete?: (comment: FeedbackComment) => void | Promise<unknown>;\n /** A comment is being sent. */\n pending?: boolean;\n /** The comments are loading. */\n loading?: boolean;\n /** Comment, Hide and Delete COMMIT — see Button's `commit`. */\n commit?: CommitScope;\n /** \"Now\" for the relative times — pin it in tests and stories. */\n now?: Date | number;\n labels?: LabelOverride<FeedbackCommentsLabels>;\n className?: string;\n}\n\ntype Entry = { at: number; order: number; message: FeedbackThreadMessage };\n\n/** The admin's Hide / Unhide as a promise — a sync throw or a `void` return included —\n * so the click can catch it as Delete's does. */\nconst hideComment = async (onHide: NonNullable<FeedbackCommentThreadProps[\"onHide\"]>, comment: FeedbackComment) => {\n await onHide(comment, !comment.hidden);\n};\n\nconst instant = (iso: string) => {\n const ms = Date.parse(iso);\n return Number.isNaN(ms) ? 0 : ms;\n};\n\nexport function FeedbackCommentThread({\n replies: repliesProp = [],\n comments,\n isAdmin,\n isAuthor = false,\n onComment,\n onHide,\n onDelete,\n pending = false,\n loading = false,\n commit,\n now,\n fetcher,\n onUpload,\n onCaptureScreenshot,\n attachment,\n labels: labelsProp,\n className,\n}: FeedbackCommentThreadProps) {\n const { composerAttachment, upload, uploading } = useFeedbackEntryUpload({\n onUpload,\n onCaptureScreenshot,\n attachment,\n });\n const labels = useKitLabels(\"feedbackComments\", DEFAULT_FEEDBACK_COMMENTS_LABELS, labelsProp);\n const anonymous = useKitLabels(\"feedbackPublic\", DEFAULT_FEEDBACK_PUBLIC_LABELS).anonymous;\n const replyLabels = useFeedbackRepliesLabels();\n const repliesDrawnAbove = useContext(FeedbackRepliesDrawnAboveContext);\n const replies = repliesDrawnAbove ? [] : repliesProp;\n\n const entries: Entry[] = [];\n replies.forEach((reply, order) => {\n if (reply.kind === \"rework\") {\n entries.push({\n at: instant(reply.created_at),\n order,\n message: {\n id: `reply-${reply.id}`,\n own: isAuthor,\n author: isAuthor ? undefined : (reply.author_name ?? anonymous),\n createdAt: reply.created_at,\n body: reply.body,\n attachments: reply.attachment_url\n ? [{ name: attachmentName(reply.attachment_url), url: reply.attachment_url }]\n : undefined,\n badge: (\n <span\n className=\"inline-flex min-w-0 max-w-full flex-wrap items-center gap-1\"\n data-feedback-entry-kind=\"rework\"\n >\n <FeedbackReworkEntryChip />\n {reply.status_to && (\n <span className=\"inline-flex items-center gap-1\" data-feedback-reply-status={reply.status_to}>\n <span className=\"sr-only\">{replyLabels.statusChanged}</span>\n <span aria-hidden>→</span>\n <FeedbackStatusBadge status={reply.status_to} />\n </span>\n )}\n </span>\n ),\n },\n });\n return;\n }\n entries.push({\n at: instant(reply.created_at),\n order,\n message: {\n id: `reply-${reply.id}`,\n author: reply.author_name ?? replyLabels.admin,\n createdAt: reply.created_at,\n body: reply.body,\n // 0.47 (§12): an admin reply's file, where the view carries it.\n attachments: reply.attachment_url\n ? [{ name: attachmentName(reply.attachment_url), url: reply.attachment_url }]\n : undefined,\n staff: true,\n badge: reply.status_to ? (\n <span\n className=\"inline-flex min-w-0 max-w-full flex-wrap items-center gap-1\"\n data-feedback-reply-status={reply.status_to}\n >\n <span className=\"sr-only\">{replyLabels.statusChanged}</span>\n <span aria-hidden>→</span>\n <FeedbackStatusBadge status={reply.status_to} />\n </span>\n ) : undefined,\n },\n });\n });\n comments.forEach((comment, index) => {\n // Shown as hidden — no name, no text — to anyone but its writer and an admin.\n const veiled = comment.hidden && !comment.mine && !isAdmin;\n const actions: ReactNode[] = [];\n if (comment.hidden) {\n actions.push(\n <span key=\"hidden\" className=\"inline-flex items-center gap-1\" data-feedback-comment-hidden=\"\">\n <EyeOff className=\"size-3\" aria-hidden />\n {labels.hidden}\n </span>,\n );\n }\n if (isAdmin && onHide) {\n actions.push(\n <Button\n key=\"hide\"\n type=\"button\"\n variant=\"link\"\n tone=\"muted\"\n size=\"sm\"\n commit={commit}\n // Caught as Delete's is (0.38.1): a rejection is the app's to toast — its\n // mutation's `onError` — and must not surface as an unhandled one.\n onClick={() => void hideComment(onHide, comment).catch(() => {})}\n >\n {comment.hidden ? labels.unhide : labels.hide}\n </Button>,\n );\n }\n if (comment.mine && onDelete) {\n actions.push(\n <DeleteCommentButton key=\"delete\" comment={comment} onDelete={onDelete} commit={commit} labels={labels} />,\n );\n }\n entries.push({\n at: instant(comment.created_at),\n // Replies first on a tie, then the comments in their order.\n order: replies.length + index,\n message: {\n id: `comment-${comment.id}`,\n own: comment.mine,\n author: veiled ? undefined : comment.mine ? undefined : (comment.author_name ?? anonymous),\n createdAt: comment.created_at,\n body: veiled ? \"\" : (comment.body ?? \"\"),\n // 0.47 (§12): its one file; none when veiled (the server sends none either).\n attachments:\n !veiled && comment.attachment_url\n ? [{ name: attachmentName(comment.attachment_url), url: comment.attachment_url }]\n : undefined,\n staff: !veiled && comment.staff,\n struck: comment.hidden && isAdmin && !veiled,\n footer: actions.length > 0 ? actions : undefined,\n },\n });\n });\n entries.sort((a, b) => a.at - b.at || a.order - b.order);\n\n return (\n <div className={className} data-feedback-comment-thread=\"\">\n <FeedbackThread\n messages={entries.map((entry) => entry.message)}\n loading={loading}\n now={now}\n renderAttachment={(file) => (file.url ? <FeedbackEntryFile url={file.url} fetcher={fetcher} /> : file.name)}\n labels={{ thread: labels.comments, empty: labels.empty }}\n />\n {onComment && (\n <ChatComposer\n className=\"mt-3\"\n onSend={async (body, file) => {\n const url = await upload(file);\n // The second argument only with a file, so a text comment calls as before 0.47.\n return url === undefined ? onComment(body) : onComment(body, url);\n }}\n pending={pending || uploading}\n attachment={composerAttachment}\n commit={commit}\n maxLength={FEEDBACK_COMMENT_MAX}\n showCount\n labels={{ field: labels.field, placeholder: labels.placeholder, send: labels.send }}\n />\n )}\n </div>\n );\n}\n\n/** The writer's Delete: a confirm (danger tone, focus on Cancel), then `onDelete`. Its\n * own component, so `useConfirm` — which needs a `ConfirmProvider` — is only called\n * where there is something to delete. */\nfunction DeleteCommentButton({\n comment,\n onDelete,\n commit,\n labels,\n}: {\n comment: FeedbackComment;\n onDelete: (comment: FeedbackComment) => void | Promise<unknown>;\n commit?: CommitScope;\n labels: FeedbackCommentsLabels;\n}) {\n const confirm = useConfirm();\n const remove = async () => {\n const ok = await confirm({\n title: labels.deleteTitle,\n body: labels.deleteBody,\n confirmLabel: labels.delete,\n tone: \"danger\",\n commit,\n });\n if (ok) await onDelete(comment);\n };\n return (\n <Button\n type=\"button\"\n variant=\"link\"\n tone=\"danger\"\n size=\"sm\"\n commit={commit}\n onClick={() => void remove().catch(() => {})}\n >\n {labels.delete}\n </Button>\n );\n}\n"],"mappings":";AA6Lc,cAEE,YAFF;AA7Ld,SAAS,kBAAkB;AAE3B,SAAS,cAAc;AACvB,SAAS,cAAc;AACvB,SAAS,kBAAkB;AAG3B,SAAS,oBAAwC;AACjD,SAAS,2BAA2B;AAEpC,SAAS,sCAAsC;AAC/C,SAAS,sBAAsB,sBAAsB;AACrD,SAAS,mBAAmB,8BAA+D;AAC3F,SAAS,gCAAgC;AACzC,SAAS,wCAAwC;AACjD,SAAS,+BAA+B;AACxC,SAAS,cAAc,sBAAsB;AAE7C,SAAS,wCAAwC;AAgHjD,MAAM,cAAc,OAAO,QAA2D,YAA6B;AACjH,QAAM,OAAO,SAAS,CAAC,QAAQ,MAAM;AACvC;AAEA,MAAM,UAAU,CAAC,QAAgB;AAC/B,QAAM,KAAK,KAAK,MAAM,GAAG;AACzB,SAAO,OAAO,MAAM,EAAE,IAAI,IAAI;AAChC;AAEO,SAAS,sBAAsB;AAAA,EACpC,SAAS,cAAc,CAAC;AAAA,EACxB;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AACF,GAA+B;AAC7B,QAAM,EAAE,oBAAoB,QAAQ,UAAU,IAAI,uBAAuB;AAAA,IACvE;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACD,QAAM,SAAS,aAAa,oBAAoB,kCAAkC,UAAU;AAC5F,QAAM,YAAY,aAAa,kBAAkB,8BAA8B,EAAE;AACjF,QAAM,cAAc,yBAAyB;AAC7C,QAAM,oBAAoB,WAAW,gCAAgC;AACrE,QAAM,UAAU,oBAAoB,CAAC,IAAI;AAEzC,QAAM,UAAmB,CAAC;AAC1B,UAAQ,QAAQ,CAAC,OAAO,UAAU;AAChC,QAAI,MAAM,SAAS,UAAU;AAC3B,cAAQ,KAAK;AAAA,QACX,IAAI,QAAQ,MAAM,UAAU;AAAA,QAC5B;AAAA,QACA,SAAS;AAAA,UACP,IAAI,SAAS,MAAM,EAAE;AAAA,UACrB,KAAK;AAAA,UACL,QAAQ,WAAW,SAAa,MAAM,eAAe;AAAA,UACrD,WAAW,MAAM;AAAA,UACjB,MAAM,MAAM;AAAA,UACZ,aAAa,MAAM,iBACf,CAAC,EAAE,MAAM,eAAe,MAAM,cAAc,GAAG,KAAK,MAAM,eAAe,CAAC,IAC1E;AAAA,UACJ,OACE;AAAA,YAAC;AAAA;AAAA,cACC,WAAU;AAAA,cACV,4BAAyB;AAAA,cAEzB;AAAA,oCAAC,2BAAwB;AAAA,gBACxB,MAAM,aACL,qBAAC,UAAK,WAAU,kCAAiC,8BAA4B,MAAM,WACjF;AAAA,sCAAC,UAAK,WAAU,WAAW,sBAAY,eAAc;AAAA,kBACrD,oBAAC,UAAK,eAAW,MAAC,oBAAC;AAAA,kBACnB,oBAAC,uBAAoB,QAAQ,MAAM,WAAW;AAAA,mBAChD;AAAA;AAAA;AAAA,UAEJ;AAAA,QAEJ;AAAA,MACF,CAAC;AACD;AAAA,IACF;AACA,YAAQ,KAAK;AAAA,MACX,IAAI,QAAQ,MAAM,UAAU;AAAA,MAC5B;AAAA,MACA,SAAS;AAAA,QACP,IAAI,SAAS,MAAM,EAAE;AAAA,QACrB,QAAQ,MAAM,eAAe,YAAY;AAAA,QACzC,WAAW,MAAM;AAAA,QACjB,MAAM,MAAM;AAAA;AAAA,QAEZ,aAAa,MAAM,iBACf,CAAC,EAAE,MAAM,eAAe,MAAM,cAAc,GAAG,KAAK,MAAM,eAAe,CAAC,IAC1E;AAAA,QACJ,OAAO;AAAA,QACP,OAAO,MAAM,YACX;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,8BAA4B,MAAM;AAAA,YAElC;AAAA,kCAAC,UAAK,WAAU,WAAW,sBAAY,eAAc;AAAA,cACrD,oBAAC,UAAK,eAAW,MAAC,oBAAC;AAAA,cACnB,oBAAC,uBAAoB,QAAQ,MAAM,WAAW;AAAA;AAAA;AAAA,QAChD,IACE;AAAA,MACN;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACD,WAAS,QAAQ,CAAC,SAAS,UAAU;AAEnC,UAAM,SAAS,QAAQ,UAAU,CAAC,QAAQ,QAAQ,CAAC;AACnD,UAAM,UAAuB,CAAC;AAC9B,QAAI,QAAQ,QAAQ;AAClB,cAAQ;AAAA,QACN,qBAAC,UAAkB,WAAU,kCAAiC,gCAA6B,IACzF;AAAA,8BAAC,UAAO,WAAU,UAAS,eAAW,MAAC;AAAA,UACtC,OAAO;AAAA,aAFA,QAGV;AAAA,MACF;AAAA,IACF;AACA,QAAI,WAAW,QAAQ;AACrB,cAAQ;AAAA,QACN;AAAA,UAAC;AAAA;AAAA,YAEC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,MAAK;AAAA,YACL,MAAK;AAAA,YACL;AAAA,YAGA,SAAS,MAAM,KAAK,YAAY,QAAQ,OAAO,EAAE,MAAM,MAAM;AAAA,YAAC,CAAC;AAAA,YAE9D,kBAAQ,SAAS,OAAO,SAAS,OAAO;AAAA;AAAA,UAVrC;AAAA,QAWN;AAAA,MACF;AAAA,IACF;AACA,QAAI,QAAQ,QAAQ,UAAU;AAC5B,cAAQ;AAAA,QACN,oBAAC,uBAAiC,SAAkB,UAAoB,QAAgB,UAA/D,QAA+E;AAAA,MAC1G;AAAA,IACF;AACA,YAAQ,KAAK;AAAA,MACX,IAAI,QAAQ,QAAQ,UAAU;AAAA;AAAA,MAE9B,OAAO,QAAQ,SAAS;AAAA,MACxB,SAAS;AAAA,QACP,IAAI,WAAW,QAAQ,EAAE;AAAA,QACzB,KAAK,QAAQ;AAAA,QACb,QAAQ,SAAS,SAAY,QAAQ,OAAO,SAAa,QAAQ,eAAe;AAAA,QAChF,WAAW,QAAQ;AAAA,QACnB,MAAM,SAAS,KAAM,QAAQ,QAAQ;AAAA;AAAA,QAErC,aACE,CAAC,UAAU,QAAQ,iBACf,CAAC,EAAE,MAAM,eAAe,QAAQ,cAAc,GAAG,KAAK,QAAQ,eAAe,CAAC,IAC9E;AAAA,QACN,OAAO,CAAC,UAAU,QAAQ;AAAA,QAC1B,QAAQ,QAAQ,UAAU,WAAW,CAAC;AAAA,QACtC,QAAQ,QAAQ,SAAS,IAAI,UAAU;AAAA,MACzC;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACD,UAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK;AAEvD,SACE,qBAAC,SAAI,WAAsB,gCAA6B,IACtD;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,UAAU,QAAQ,IAAI,CAAC,UAAU,MAAM,OAAO;AAAA,QAC9C;AAAA,QACA;AAAA,QACA,kBAAkB,CAAC,SAAU,KAAK,MAAM,oBAAC,qBAAkB,KAAK,KAAK,KAAK,SAAkB,IAAK,KAAK;AAAA,QACtG,QAAQ,EAAE,QAAQ,OAAO,UAAU,OAAO,OAAO,MAAM;AAAA;AAAA,IACzD;AAAA,IACC,aACC;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV,QAAQ,OAAO,MAAM,SAAS;AAC5B,gBAAM,MAAM,MAAM,OAAO,IAAI;AAE7B,iBAAO,QAAQ,SAAY,UAAU,IAAI,IAAI,UAAU,MAAM,GAAG;AAAA,QAClE;AAAA,QACA,SAAS,WAAW;AAAA,QACpB,YAAY;AAAA,QACZ;AAAA,QACA,WAAW;AAAA,QACX,WAAS;AAAA,QACT,QAAQ,EAAE,OAAO,OAAO,OAAO,aAAa,OAAO,aAAa,MAAM,OAAO,KAAK;AAAA;AAAA,IACpF;AAAA,KAEJ;AAEJ;AAKA,SAAS,oBAAoB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,UAAU,WAAW;AAC3B,QAAM,SAAS,YAAY;AACzB,UAAM,KAAK,MAAM,QAAQ;AAAA,MACvB,OAAO,OAAO;AAAA,MACd,MAAM,OAAO;AAAA,MACb,cAAc,OAAO;AAAA,MACrB,MAAM;AAAA,MACN;AAAA,IACF,CAAC;AACD,QAAI,GAAI,OAAM,SAAS,OAAO;AAAA,EAChC;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,MAAK;AAAA,MACL;AAAA,MACA,SAAS,MAAM,KAAK,OAAO,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,MAE1C,iBAAO;AAAA;AAAA,EACV;AAEJ;","names":[]}
@@ -52,10 +52,7 @@ function FeedbackContextBox({
52
52
  return /* @__PURE__ */ jsxs(
53
53
  "div",
54
54
  {
55
- className: cn(
56
- "space-y-1 rounded-md border border-subtle bg-surface-2 px-3 py-2 text-xs text-muted",
57
- className
58
- ),
55
+ className: cn("space-y-1 rounded-md border border-subtle bg-surface-2 px-3 py-2 text-xs text-muted", className),
59
56
  children: [
60
57
  /* @__PURE__ */ jsx("div", { className: "[overflow-wrap:anywhere]", children: lead === null ? line : /* @__PURE__ */ jsxs(Fragment, { children: [
61
58
  /* @__PURE__ */ jsx("span", { className: "font-medium text-secondary", children: lead }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/feedback/feedback-context.tsx"],"sourcesContent":["import { Checkbox } from \"../components/checkbox\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport type { FeedbackContext, FeedbackEnvironment } from \"./feedback-record\";\nimport { DEFAULT_FEEDBACK_CONTEXT_LABELS } from \"./feedback-context-labels\";\n\n/**\n * The reporter context of a feedback report: the box the submit dialog shows, and the\n * `context` object `POST /feedback` sends (0.27.0, docs/feedback-harmonization.md §3.2,\n * §4.2). One file for both, so what the box SAYS will be sent and what IS sent cannot\n * drift apart.\n *\n * Each app had written both halves itself (keksdose `use-feedback-dialog.tsx:105` and\n * `:222`, kastlan `feedback-button.tsx:80`, Kurvenschmiede `use-feedback-dialog.tsx:124`),\n * and they had drifted: kastlan sent six keys with `user_email: null` and no version,\n * Kurvenschmiede a `page_path` column and a sentence where keksdose had a labelled box,\n * and only keksdose showed the full URL it was about to send.\n */\n\n/* ── Who is filing ───────────────────────────────────────────────────────── */\n\n/**\n * The person filing, as the app knows them. Snake case on purpose: keksdose's and\n * Kurvenschmiede's user objects already have this shape (`id`, `email`, `display_name`),\n * so they pass their `user` as it is; kastlan maps its token's `sub` and name parts.\n *\n * A SNAPSHOT for the report and the box. The server should overwrite the three `user_*`\n * keys from the session (§3.2; Kurvenschmiede does), so nobody can file in someone else's\n * name — this is what the reporter is SHOWN, not proof of who they are.\n */\nexport interface FeedbackSubmitter {\n id?: number | null;\n email?: string | null;\n display_name?: string | null;\n}\n\n/** \"Ada Example (ada@example.com)\", the name alone, the email alone, or \"—\" when the app\n * knows neither (keksdose `use-feedback-dialog.tsx:228`). */\nfunction submitterLine(user: FeedbackSubmitter | null | undefined): string {\n const name = user?.display_name?.trim();\n const email = user?.email?.trim();\n if (name && email) return `${name} (${email})`;\n return name || email || \"—\";\n}\n\n/* ── The object (§3.2) ───────────────────────────────────────────────────── */\n\n/** What {@link feedbackContext} is built from. */\nexport interface FeedbackContextInput {\n /** The submitter, or `null` when signed out / unknown — the three `user_*` keys are then\n * `null`, as keksdose sends them. */\n user?: FeedbackSubmitter | null;\n /** The box's \"Attach current page URL\" checkbox. `false` withholds `url` and `route`\n * (both `\"\"`); nothing else is withheld — `origin` says which copy of the app, which is\n * not personal, and triage needs it first (keksdose `use-feedback-dialog.tsx:110`). */\n attachUrl: boolean;\n /** `location.href` when the dialog OPENED — the page the report is about, not the one\n * the reporter may have navigated to while typing. */\n url: string;\n /** Its pathname. Default: read off `url` (a path-only `url` works too). */\n route?: string;\n /** Default `location.origin`. */\n origin?: string;\n /** `\"prod\"` / `\"dev\"` / `\"local\"` — the app's own reading (keksdose\n * `app/deploy-environment.ts`). Left out of the object when not given. */\n environment?: FeedbackEnvironment | (string & {});\n /** The app build (`__APP_VERSION__`). Left out of the object when not given. */\n version?: string;\n /** Default `${innerWidth}x${innerHeight}` at the time of the call — \"406x816\". */\n viewport?: string;\n /** Default `navigator.userAgent`. */\n ua?: string;\n}\n\n/** The pathname of a full or path-only URL; `\"\"` for something that is neither. */\nfunction pathOf(url: string, base: string | undefined): string {\n if (!url) return \"\";\n try {\n return new URL(url, base || \"http://localhost\").pathname;\n } catch {\n return \"\";\n }\n}\n\n/**\n * The `context` object of `POST /feedback` (§3.2), keys in keksdose's order: `url`, `route`,\n * `origin`, `environment`, `user_id`, `user_email`, `user_display_name`, `viewport`, `ua`,\n * `version`.\n *\n * With `attachUrl: false`, `url` and `route` are `\"\"` — present and empty, as keksdose\n * sends them, so a reader can tell \"withheld\" from \"an old row without the key\".\n * `environment` and `version` are omitted when the app passes none (kastlan had no\n * version), rather than sent as `undefined`.\n *\n * Reads `window` / `navigator` only for the defaults it was not given, so it is a pure\n * function in a test that passes everything.\n */\nexport function feedbackContext(input: FeedbackContextInput): FeedbackContext {\n const { user, attachUrl, url } = input;\n const win = typeof window === \"undefined\" ? undefined : window;\n const origin = input.origin ?? win?.location.origin ?? \"\";\n const context: FeedbackContext = {\n url: attachUrl ? url : \"\",\n route: attachUrl ? (input.route ?? pathOf(url, origin || win?.location.href)) : \"\",\n origin,\n };\n if (input.environment !== undefined) context.environment = input.environment;\n context.user_id = user?.id ?? null;\n context.user_email = user?.email ?? null;\n context.user_display_name = user?.display_name ?? null;\n context.viewport = input.viewport ?? (win ? `${win.innerWidth}x${win.innerHeight}` : \"\");\n context.ua = input.ua ?? (typeof navigator === \"undefined\" ? \"\" : navigator.userAgent);\n if (input.version !== undefined) context.version = input.version;\n return context;\n}\n\n/* ── The box (§4.2) ──────────────────────────────────────────────────────── */\n\n/**\n * `feedbackContext` — the context box's words. de-CH canon (keksdose `de-CH.json`):\n * `user` \"Nutzer\", `attachUrl` \"Aktuelle Seiten-URL anhängen\".\n */\nexport interface FeedbackContextLabels {\n /** The submitter line's label — the box draws \"**User:** Ada Example (ada@example.com)\".\n * The label alone: the colon between it and the person is the language's\n * `common.fieldValue` (fr \"Utilisateur : …\", zh \"用户:…\"). */\n user: string;\n /** The checkbox. The URL it is about is the line beneath it. */\n attachUrl: string;\n}\n\n// Defined in ./feedback-context-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_FEEDBACK_CONTEXT_LABELS };\n\nexport interface FeedbackContextBoxProps {\n /** Who is filing — the same value handed to {@link feedbackContext}. */\n user?: FeedbackSubmitter | null;\n /** The URL that goes with the report — the same `url` handed to {@link feedbackContext}. */\n url: string;\n /** The checkbox, on by default in the dialog (§4.2) — the same `attachUrl`. */\n attachUrl: boolean;\n onAttachUrlChange: (next: boolean) => void;\n /** Prop > `<UiKitProvider labels={{ feedbackContext }}>` > English. */\n labels?: LabelOverride<FeedbackContextLabels>;\n className?: string;\n}\n\n/**\n * The submit dialog's context box — the dialog's `contextSlot` (keksdose\n * `use-feedback-dialog.tsx:222`, §4.2): who is filing, and the page URL with a checkbox to\n * leave it out, ON by default.\n *\n * The URL stays visible when the box is off, struck through: the reporter sees what they\n * withheld rather than a line that vanished, and ticking it back on is the same glance.\n * Kurvenschmiede's argument for the checkbox is why it exists at all (its feedback #101):\n * the route is right for a bug and wrong for an idea about a screen one is not on — filed\n * against whatever screen happened to be open, the queue reads as if it were about that\n * screen.\n *\n * Nothing in here is the app's: hand it the values handed to {@link feedbackContext}, and\n * what the box shows is what is sent.\n */\nexport function FeedbackContextBox({\n user,\n url,\n attachUrl,\n onAttachUrlChange,\n labels: labelsProp,\n className,\n}: FeedbackContextBoxProps) {\n const labels = useKitLabels(\"feedbackContext\", DEFAULT_FEEDBACK_CONTEXT_LABELS, labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const submitter = submitterLine(user);\n // \"User: Ada …\" through `common.fieldValue`, so French gets its space before the colon\n // and Chinese its full-width one; the label and its punctuation are drawn bold, as\n // keksdose's box does. A `fieldValue` that does not end in the value (an app's own\n // order) is shown whole, unbolded, rather than cut in the wrong place.\n const line = common.fieldValue(labels.user, submitter);\n const lead = line.endsWith(submitter) ? line.slice(0, line.length - submitter.length) : null;\n return (\n <div\n className={cn(\n \"space-y-1 rounded-md border border-subtle bg-surface-2 px-3 py-2 text-xs text-muted\",\n className,\n )}\n >\n <div className=\"[overflow-wrap:anywhere]\">\n {lead === null ? (\n line\n ) : (\n <>\n <span className=\"font-medium text-secondary\">{lead}</span>\n {submitter}\n </>\n )}\n </div>\n <Checkbox\n checked={attachUrl}\n onCheckedChange={onAttachUrlChange}\n label={<span className=\"text-xs font-medium text-secondary\">{labels.attachUrl}</span>}\n description={\n url ? (\n // `break-all`: a URL is one unbreakable word, and at 390px a long query string\n // would otherwise push the dialog sideways.\n <span\n data-withheld={attachUrl ? undefined : \"\"}\n className={cn(\"break-all text-xs\", !attachUrl && \"line-through opacity-60\")}\n >\n {url}\n </span>\n ) : undefined\n }\n />\n </div>\n );\n}\n"],"mappings":";AA8LU,mBACE,KADF;AA9LV,SAAS,gBAAgB;AACzB,SAAS,uBAAuB,oBAAwC;AACxE,SAAS,UAAU;AAEnB,SAAS,uCAAuC;AAkChD,SAAS,cAAc,MAAoD;AACzE,QAAM,OAAO,MAAM,cAAc,KAAK;AACtC,QAAM,QAAQ,MAAM,OAAO,KAAK;AAChC,MAAI,QAAQ,MAAO,QAAO,GAAG,IAAI,KAAK,KAAK;AAC3C,SAAO,QAAQ,SAAS;AAC1B;AAgCA,SAAS,OAAO,KAAa,MAAkC;AAC7D,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI;AACF,WAAO,IAAI,IAAI,KAAK,QAAQ,kBAAkB,EAAE;AAAA,EAClD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAeO,SAAS,gBAAgB,OAA8C;AAC5E,QAAM,EAAE,MAAM,WAAW,IAAI,IAAI;AACjC,QAAM,MAAM,OAAO,WAAW,cAAc,SAAY;AACxD,QAAM,SAAS,MAAM,UAAU,KAAK,SAAS,UAAU;AACvD,QAAM,UAA2B;AAAA,IAC/B,KAAK,YAAY,MAAM;AAAA,IACvB,OAAO,YAAa,MAAM,SAAS,OAAO,KAAK,UAAU,KAAK,SAAS,IAAI,IAAK;AAAA,IAChF;AAAA,EACF;AACA,MAAI,MAAM,gBAAgB,OAAW,SAAQ,cAAc,MAAM;AACjE,UAAQ,UAAU,MAAM,MAAM;AAC9B,UAAQ,aAAa,MAAM,SAAS;AACpC,UAAQ,oBAAoB,MAAM,gBAAgB;AAClD,UAAQ,WAAW,MAAM,aAAa,MAAM,GAAG,IAAI,UAAU,IAAI,IAAI,WAAW,KAAK;AACrF,UAAQ,KAAK,MAAM,OAAO,OAAO,cAAc,cAAc,KAAK,UAAU;AAC5E,MAAI,MAAM,YAAY,OAAW,SAAQ,UAAU,MAAM;AACzD,SAAO;AACT;AAgDO,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AACF,GAA4B;AAC1B,QAAM,SAAS,aAAa,mBAAmB,iCAAiC,UAAU;AAC1F,QAAM,SAAS,aAAa,UAAU,qBAAqB;AAC3D,QAAM,YAAY,cAAc,IAAI;AAKpC,QAAM,OAAO,OAAO,WAAW,OAAO,MAAM,SAAS;AACrD,QAAM,OAAO,KAAK,SAAS,SAAS,IAAI,KAAK,MAAM,GAAG,KAAK,SAAS,UAAU,MAAM,IAAI;AACxF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT;AAAA,QACA;AAAA,MACF;AAAA,MAEA;AAAA,4BAAC,SAAI,WAAU,4BACZ,mBAAS,OACR,OAEA,iCACE;AAAA,8BAAC,UAAK,WAAU,8BAA8B,gBAAK;AAAA,UAClD;AAAA,WACH,GAEJ;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,SAAS;AAAA,YACT,iBAAiB;AAAA,YACjB,OAAO,oBAAC,UAAK,WAAU,sCAAsC,iBAAO,WAAU;AAAA,YAC9E,aACE;AAAA;AAAA;AAAA,cAGE;AAAA,gBAAC;AAAA;AAAA,kBACC,iBAAe,YAAY,SAAY;AAAA,kBACvC,WAAW,GAAG,qBAAqB,CAAC,aAAa,yBAAyB;AAAA,kBAEzE;AAAA;AAAA,cACH;AAAA,gBACE;AAAA;AAAA,QAER;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/feedback/feedback-context.tsx"],"sourcesContent":["import { Checkbox } from \"../components/checkbox\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport type { FeedbackContext, FeedbackEnvironment } from \"./feedback-record\";\nimport { DEFAULT_FEEDBACK_CONTEXT_LABELS } from \"./feedback-context-labels\";\n\n/**\n * The reporter context of a feedback report: the box the submit dialog shows, and the\n * `context` object `POST /feedback` sends (0.27.0, docs/feedback-harmonization.md §3.2,\n * §4.2). One file for both, so what the box SAYS will be sent and what IS sent cannot\n * drift apart.\n *\n * Each app had written both halves itself (keksdose `use-feedback-dialog.tsx:105` and\n * `:222`, kastlan `feedback-button.tsx:80`, Kurvenschmiede `use-feedback-dialog.tsx:124`),\n * and they had drifted: kastlan sent six keys with `user_email: null` and no version,\n * Kurvenschmiede a `page_path` column and a sentence where keksdose had a labelled box,\n * and only keksdose showed the full URL it was about to send.\n */\n\n/* ── Who is filing ───────────────────────────────────────────────────────── */\n\n/**\n * The person filing, as the app knows them. Snake case on purpose: keksdose's and\n * Kurvenschmiede's user objects already have this shape (`id`, `email`, `display_name`),\n * so they pass their `user` as it is; kastlan maps its token's `sub` and name parts.\n *\n * A SNAPSHOT for the report and the box. The server should overwrite the three `user_*`\n * keys from the session (§3.2; Kurvenschmiede does), so nobody can file in someone else's\n * name — this is what the reporter is SHOWN, not proof of who they are.\n */\nexport interface FeedbackSubmitter {\n id?: number | null;\n email?: string | null;\n display_name?: string | null;\n}\n\n/** \"Ada Example (ada@example.com)\", the name alone, the email alone, or \"—\" when the app\n * knows neither (keksdose `use-feedback-dialog.tsx:228`). */\nfunction submitterLine(user: FeedbackSubmitter | null | undefined): string {\n const name = user?.display_name?.trim();\n const email = user?.email?.trim();\n if (name && email) return `${name} (${email})`;\n return name || email || \"—\";\n}\n\n/* ── The object (§3.2) ───────────────────────────────────────────────────── */\n\n/** What {@link feedbackContext} is built from. */\nexport interface FeedbackContextInput {\n /** The submitter, or `null` when signed out / unknown — the three `user_*` keys are then\n * `null`, as keksdose sends them. */\n user?: FeedbackSubmitter | null;\n /** The box's \"Attach current page URL\" checkbox. `false` withholds `url` and `route`\n * (both `\"\"`); nothing else is withheld — `origin` says which copy of the app, which is\n * not personal, and triage needs it first (keksdose `use-feedback-dialog.tsx:110`). */\n attachUrl: boolean;\n /** `location.href` when the dialog OPENED — the page the report is about, not the one\n * the reporter may have navigated to while typing. */\n url: string;\n /** Its pathname. Default: read off `url` (a path-only `url` works too). */\n route?: string;\n /** Default `location.origin`. */\n origin?: string;\n /** `\"prod\"` / `\"dev\"` / `\"local\"` — the app's own reading (keksdose\n * `app/deploy-environment.ts`). Left out of the object when not given. */\n environment?: FeedbackEnvironment | (string & {});\n /** The app build (`__APP_VERSION__`). Left out of the object when not given. */\n version?: string;\n /** Default `${innerWidth}x${innerHeight}` at the time of the call — \"406x816\". */\n viewport?: string;\n /** Default `navigator.userAgent`. */\n ua?: string;\n}\n\n/** The pathname of a full or path-only URL; `\"\"` for something that is neither. */\nfunction pathOf(url: string, base: string | undefined): string {\n if (!url) return \"\";\n try {\n return new URL(url, base || \"http://localhost\").pathname;\n } catch {\n return \"\";\n }\n}\n\n/**\n * The `context` object of `POST /feedback` (§3.2), keys in keksdose's order: `url`, `route`,\n * `origin`, `environment`, `user_id`, `user_email`, `user_display_name`, `viewport`, `ua`,\n * `version`.\n *\n * With `attachUrl: false`, `url` and `route` are `\"\"` — present and empty, as keksdose\n * sends them, so a reader can tell \"withheld\" from \"an old row without the key\".\n * `environment` and `version` are omitted when the app passes none (kastlan had no\n * version), rather than sent as `undefined`.\n *\n * Reads `window` / `navigator` only for the defaults it was not given, so it is a pure\n * function in a test that passes everything.\n */\nexport function feedbackContext(input: FeedbackContextInput): FeedbackContext {\n const { user, attachUrl, url } = input;\n const win = typeof window === \"undefined\" ? undefined : window;\n const origin = input.origin ?? win?.location.origin ?? \"\";\n const context: FeedbackContext = {\n url: attachUrl ? url : \"\",\n route: attachUrl ? (input.route ?? pathOf(url, origin || win?.location.href)) : \"\",\n origin,\n };\n if (input.environment !== undefined) context.environment = input.environment;\n context.user_id = user?.id ?? null;\n context.user_email = user?.email ?? null;\n context.user_display_name = user?.display_name ?? null;\n context.viewport = input.viewport ?? (win ? `${win.innerWidth}x${win.innerHeight}` : \"\");\n context.ua = input.ua ?? (typeof navigator === \"undefined\" ? \"\" : navigator.userAgent);\n if (input.version !== undefined) context.version = input.version;\n return context;\n}\n\n/* ── The box (§4.2) ──────────────────────────────────────────────────────── */\n\n/**\n * `feedbackContext` — the context box's words. de-CH canon (keksdose `de-CH.json`):\n * `user` \"Nutzer\", `attachUrl` \"Aktuelle Seiten-URL anhängen\".\n */\nexport interface FeedbackContextLabels {\n /** The submitter line's label — the box draws \"**User:** Ada Example (ada@example.com)\".\n * The label alone: the colon between it and the person is the language's\n * `common.fieldValue` (fr \"Utilisateur : …\", zh \"用户:…\"). */\n user: string;\n /** The checkbox. The URL it is about is the line beneath it. */\n attachUrl: string;\n}\n\n// Defined in ./feedback-context-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_FEEDBACK_CONTEXT_LABELS };\n\nexport interface FeedbackContextBoxProps {\n /** Who is filing — the same value handed to {@link feedbackContext}. */\n user?: FeedbackSubmitter | null;\n /** The URL that goes with the report — the same `url` handed to {@link feedbackContext}. */\n url: string;\n /** The checkbox, on by default in the dialog (§4.2) — the same `attachUrl`. */\n attachUrl: boolean;\n onAttachUrlChange: (next: boolean) => void;\n /** Prop > `<UiKitProvider labels={{ feedbackContext }}>` > English. */\n labels?: LabelOverride<FeedbackContextLabels>;\n className?: string;\n}\n\n/**\n * The submit dialog's context box — the dialog's `contextSlot` (keksdose\n * `use-feedback-dialog.tsx:222`, §4.2): who is filing, and the page URL with a checkbox to\n * leave it out, ON by default.\n *\n * The URL stays visible when the box is off, struck through: the reporter sees what they\n * withheld rather than a line that vanished, and ticking it back on is the same glance.\n * Kurvenschmiede's argument for the checkbox is why it exists at all (its feedback #101):\n * the route is right for a bug and wrong for an idea about a screen one is not on — filed\n * against whatever screen happened to be open, the queue reads as if it were about that\n * screen.\n *\n * Nothing in here is the app's: hand it the values handed to {@link feedbackContext}, and\n * what the box shows is what is sent.\n */\nexport function FeedbackContextBox({\n user,\n url,\n attachUrl,\n onAttachUrlChange,\n labels: labelsProp,\n className,\n}: FeedbackContextBoxProps) {\n const labels = useKitLabels(\"feedbackContext\", DEFAULT_FEEDBACK_CONTEXT_LABELS, labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const submitter = submitterLine(user);\n // \"User: Ada …\" through `common.fieldValue`, so French gets its space before the colon\n // and Chinese its full-width one; the label and its punctuation are drawn bold, as\n // keksdose's box does. A `fieldValue` that does not end in the value (an app's own\n // order) is shown whole, unbolded, rather than cut in the wrong place.\n const line = common.fieldValue(labels.user, submitter);\n const lead = line.endsWith(submitter) ? line.slice(0, line.length - submitter.length) : null;\n return (\n <div\n className={cn(\"space-y-1 rounded-md border border-subtle bg-surface-2 px-3 py-2 text-xs text-muted\", className)}\n >\n <div className=\"[overflow-wrap:anywhere]\">\n {lead === null ? (\n line\n ) : (\n <>\n <span className=\"font-medium text-secondary\">{lead}</span>\n {submitter}\n </>\n )}\n </div>\n <Checkbox\n checked={attachUrl}\n onCheckedChange={onAttachUrlChange}\n label={<span className=\"text-xs font-medium text-secondary\">{labels.attachUrl}</span>}\n description={\n url ? (\n // `break-all`: a URL is one unbreakable word, and at 390px a long query string\n // would otherwise push the dialog sideways.\n <span\n data-withheld={attachUrl ? undefined : \"\"}\n className={cn(\"break-all text-xs\", !attachUrl && \"line-through opacity-60\")}\n >\n {url}\n </span>\n ) : undefined\n }\n />\n </div>\n );\n}\n"],"mappings":";AA2LU,mBACE,KADF;AA3LV,SAAS,gBAAgB;AACzB,SAAS,uBAAuB,oBAAwC;AACxE,SAAS,UAAU;AAEnB,SAAS,uCAAuC;AAkChD,SAAS,cAAc,MAAoD;AACzE,QAAM,OAAO,MAAM,cAAc,KAAK;AACtC,QAAM,QAAQ,MAAM,OAAO,KAAK;AAChC,MAAI,QAAQ,MAAO,QAAO,GAAG,IAAI,KAAK,KAAK;AAC3C,SAAO,QAAQ,SAAS;AAC1B;AAgCA,SAAS,OAAO,KAAa,MAAkC;AAC7D,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI;AACF,WAAO,IAAI,IAAI,KAAK,QAAQ,kBAAkB,EAAE;AAAA,EAClD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAeO,SAAS,gBAAgB,OAA8C;AAC5E,QAAM,EAAE,MAAM,WAAW,IAAI,IAAI;AACjC,QAAM,MAAM,OAAO,WAAW,cAAc,SAAY;AACxD,QAAM,SAAS,MAAM,UAAU,KAAK,SAAS,UAAU;AACvD,QAAM,UAA2B;AAAA,IAC/B,KAAK,YAAY,MAAM;AAAA,IACvB,OAAO,YAAa,MAAM,SAAS,OAAO,KAAK,UAAU,KAAK,SAAS,IAAI,IAAK;AAAA,IAChF;AAAA,EACF;AACA,MAAI,MAAM,gBAAgB,OAAW,SAAQ,cAAc,MAAM;AACjE,UAAQ,UAAU,MAAM,MAAM;AAC9B,UAAQ,aAAa,MAAM,SAAS;AACpC,UAAQ,oBAAoB,MAAM,gBAAgB;AAClD,UAAQ,WAAW,MAAM,aAAa,MAAM,GAAG,IAAI,UAAU,IAAI,IAAI,WAAW,KAAK;AACrF,UAAQ,KAAK,MAAM,OAAO,OAAO,cAAc,cAAc,KAAK,UAAU;AAC5E,MAAI,MAAM,YAAY,OAAW,SAAQ,UAAU,MAAM;AACzD,SAAO;AACT;AAgDO,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AACF,GAA4B;AAC1B,QAAM,SAAS,aAAa,mBAAmB,iCAAiC,UAAU;AAC1F,QAAM,SAAS,aAAa,UAAU,qBAAqB;AAC3D,QAAM,YAAY,cAAc,IAAI;AAKpC,QAAM,OAAO,OAAO,WAAW,OAAO,MAAM,SAAS;AACrD,QAAM,OAAO,KAAK,SAAS,SAAS,IAAI,KAAK,MAAM,GAAG,KAAK,SAAS,UAAU,MAAM,IAAI;AACxF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,GAAG,uFAAuF,SAAS;AAAA,MAE9G;AAAA,4BAAC,SAAI,WAAU,4BACZ,mBAAS,OACR,OAEA,iCACE;AAAA,8BAAC,UAAK,WAAU,8BAA8B,gBAAK;AAAA,UAClD;AAAA,WACH,GAEJ;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,SAAS;AAAA,YACT,iBAAiB;AAAA,YACjB,OAAO,oBAAC,UAAK,WAAU,sCAAsC,iBAAO,WAAU;AAAA,YAC9E,aACE;AAAA;AAAA;AAAA,cAGE;AAAA,gBAAC;AAAA;AAAA,kBACC,iBAAe,YAAY,SAAY;AAAA,kBACvC,WAAW,GAAG,qBAAqB,CAAC,aAAa,yBAAyB;AAAA,kBAEzE;AAAA;AAAA,cACH;AAAA,gBACE;AAAA;AAAA,QAER;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/feedback/feedback-dialog.tsx"],"sourcesContent":["import { useId, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Button } from \"../components/button\";\nimport { Input } from \"../components/input\";\nimport { Select } from \"../components/select\";\nimport { Textarea } from \"../components/textarea\";\nimport type { ButtonSize, ButtonVariant } from \"../components/button-look\";\nimport { Modal } from \"../components/modal\";\nimport { isApplePlatform } from \"../hooks/use-hotkey\";\nimport { usePhoneLayout } from \"../hooks/use-breakpoint\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport {\n DEFAULT_ATTACHMENT_ACCEPT,\n DEFAULT_FEEDBACK_ATTACHMENT_LABELS,\n DEFAULT_MAX_ATTACHMENT_BYTES,\n FeedbackAttachmentField,\n type FeedbackAttachmentError,\n type FeedbackAttachmentErrorInfo,\n} from \"./feedback-attachment\";\nimport { DEFAULT_FEEDBACK_DIALOG_LABELS } from \"./feedback-dialog-labels\";\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 the provider's\n * `feedbackAttachment.attachmentCapture`, then English. */\n attachmentCapture?: string;\n /** The line under the attachment buttons saying a screenshot can be pasted\n * straight in. Optional — falls back to the provider's `feedbackAttachment`, then English. */\n attachmentPaste?: string;\n attachmentRemove: string;\n /** `multiple` mode (0.15.5): the chip list's accessible name — and, in\n * `<FeedbackDialog attachments=\"multiple\">`, the heading over it (0.16.0). Optional,\n * as are the keys below — they fall back to the provider's `feedbackAttachment`,\n * then English. */\n attachmentList?: string;\n /** `multiple` mode (0.16.0): the screenshot slot's chip title, in place of its file name. */\n attachmentScreenshot?: string;\n /** `multiple` mode: one chip's remove button, given the file's name. */\n attachmentRemoveFile?: (name: string) => string;\n /** `multiple` mode: the line shown once `max` files are attached. */\n attachmentLimit?: (max: number) => string;\n /** `refs` mode (0.22.0): the second line of a chip whose upload is still running. */\n attachmentUploading?: string;\n}\n\nexport interface FeedbackDialogLabels extends FeedbackAttachmentLabels {\n title: string;\n category: string;\n subject: string;\n body: string;\n attachment: string;\n /** See {@link FeedbackDialogTextLabels.submitHint}. */\n submitHint: FeedbackSubmitHint;\n cancel: string;\n save: string;\n}\n\n/** The submit hint: a string, or the line for the platform (`apple`: Mac, iPhone, iPad). */\nexport type FeedbackSubmitHint = string | ((apple: boolean) => string);\n\n/**\n * The dialog's own strings — the `feedbackDialog` namespace of `<UiKitProvider\n * labels>` (0.12.0). The attachment keys are not repeated here: they are the\n * `feedbackAttachment` namespace's, which the field inside already reads.\n *\n * Before, every key was required at the call site, so each app restated ten strings\n * wherever it mounted the dialog (kastlan feedback-button.tsx built the object inline).\n */\nexport interface FeedbackDialogTextLabels {\n title: string;\n category: string;\n subject: string;\n body: string;\n /** The body's label when `requireBody={false}`: \"What happened? (optional)\".\n * Optional, so a complete `UiKitLabels` typed before 0.14.2 still compiles. */\n bodyOptional?: string;\n /** The heading over the attachment buttons. */\n attachment: string;\n /**\n * The line beside the buttons naming the shortcut. Since 0.27.0 a function of the\n * platform, like `form.submitShortcut`: `apple` is true on a Mac, iPhone or iPad, where\n * the key is Cmd — \"⌘ Enter to send\" there, keksdose's \"Ctrl+Enter to send\" elsewhere\n * (§7.12). A plain string is still taken and shown everywhere, as before 0.27.0.\n */\n submitHint: FeedbackSubmitHint;\n cancel: string;\n save: string;\n /** 0.38 (docs/feedback-harmonization.md §9.2 decision 6): the line under the body when\n * reports are public — `useFeedbackSubmit({ publicNotice: true })` shows it. Optional,\n * so a complete `UiKitLabels` typed before 0.38 still compiles. de-CH \"Andere Personen\n * sehen Titel und Text (nie Ihre Screenshots oder Dateien).\" */\n publicNotice?: string;\n}\n\n// Defined in ./feedback-dialog-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_FEEDBACK_DIALOG_LABELS };\n\nexport interface FeedbackSubmission {\n title: string;\n body: string;\n category: string;\n attachment: File | null;\n}\n\n/**\n * What `onSubmit` gets under `attachments=\"multiple\"` (0.15.5): the screenshot and\n * the picked/pasted files apart, because an app files them differently — keksdose\n * attaches the screenshot to the report and the photos as its evidence list. No\n * `attachment`: there is no one file it could name.\n */\nexport interface FeedbackMultipleSubmission {\n title: string;\n body: string;\n category: string;\n /** What `onCaptureScreenshot` returned, or `null` — at most one. */\n screenshot: File | null;\n /** Picked and pasted files, in the order they were added; at most `maxAttachments`. */\n attachments: File[];\n}\n\ninterface FeedbackDialogBaseProps {\n open: boolean;\n onClose: () => void;\n categories: FeedbackCategoryOption[];\n category: string;\n onCategoryChange: (value: string) => void;\n /** Prop > `<UiKitProvider labels={{ feedbackDialog, feedbackAttachment }}>` >\n * English. Optional since 0.12.0; a whole `FeedbackDialogLabels` still fits. */\n labels?: LabelOverride<FeedbackDialogLabels>;\n submitting?: boolean;\n contextSlot?: ReactNode;\n attachmentAccept?: string[];\n maxAttachmentBytes?: number;\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 * `false`: a report with only a subject can be sent, and the body's label says it is\n * optional (`bodyOptional`, unless `labels.body` is passed). Default `true`, the\n * dialog's behaviour until 0.14.2 (keksdose K1: its backend takes title-only reports).\n */\n requireBody?: boolean;\n /**\n * The attachment field's add and capture buttons: its `buttonVariant` (0.25.0),\n * Button's own `variant`. Default `\"secondary\"`, the look they always had.\n *\n * The field took `buttonVariant` / `buttonSize` in 0.24.0 (keksdose's support chat),\n * but the dialog builds its field itself and passed neither, so a host that wanted\n * quieter buttons here — a dialog that already ends in a row of Cancel / Send — had\n * no way to say so short of rebuilding the dialog. Named after the dialog's other\n * attachment options (`attachmentAccept`, `onAttachmentError`) and handed through\n * unchanged, in both `attachments` modes.\n */\n attachmentButtonVariant?: ButtonVariant;\n /** The attachment field's `buttonSize` (0.25.0) — with {@link attachmentButtonVariant}.\n * Default `\"md\"`; `\"sm\"` draws the paperclip and camera at 14px. */\n attachmentButtonSize?: ButtonSize;\n /**\n * A muted line under the body saying who will read it (0.38, docs/feedback-harmonization.md\n * §9.2 decision 6) — the kit's sentence is `feedbackDialog.publicNotice`, \"Other users\n * will see the title and text (never your screenshots or files)\", which\n * `useFeedbackSubmit({ publicNotice: true })` passes. Attached to the body field with\n * `aria-describedby`, so it is heard where the text is written. Left out: no line.\n */\n publicNotice?: ReactNode;\n}\n\n/** One attachment — a screenshot, a picked or a pasted image; a second replaces\n * nothing, it has to wait for the first to be removed. The default. */\nexport interface FeedbackDialogSingleProps extends FeedbackDialogBaseProps {\n attachments?: \"single\";\n onSubmit: (data: FeedbackSubmission) => void | Promise<void>;\n /** `info` (0.23.0) names the refused file and the limit — see\n * {@link FeedbackAttachmentErrorInfo}. */\n onAttachmentError?: (kind: \"type\" | \"size\", info: FeedbackAttachmentErrorInfo) => void;\n /** Multiple mode only — refused here rather than silently ignored. */\n maxAttachments?: never;\n}\n\n/**\n * One screenshot plus up to `maxAttachments` picked or pasted files (0.15.5, keksdose\n * dev#578: \"pasted two photos, the second overwrote the first\"). The capture button is\n * offered while there is no screenshot yet; the add button while there is room.\n */\nexport interface FeedbackDialogMultipleProps extends FeedbackDialogBaseProps {\n attachments: \"multiple\";\n /** Default `DEFAULT_MAX_ATTACHMENTS` (5). The screenshot does not count. */\n maxAttachments?: number;\n onSubmit: (data: FeedbackMultipleSubmission) => void | Promise<void>;\n /** `\"count\"`: more files arrived at once than there was room for; the surplus was\n * dropped. `info` (0.23.0) names the refused file(s) and the limit. */\n onAttachmentError?: (kind: FeedbackAttachmentError, info: FeedbackAttachmentErrorInfo) => void;\n}\n\nexport type FeedbackDialogProps = FeedbackDialogSingleProps | FeedbackDialogMultipleProps;\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 *\n * **`attachments=\"multiple\"`** (0.15.5) takes several: every paste and pick adds a\n * chip, and `onSubmit` gets `{ screenshot, attachments }` ({@link\n * FeedbackMultipleSubmission}) instead of `attachment`. Opt-in, so a caller typed\n * against the one-file submission keeps compiling and behaving as before.\n */\nexport function FeedbackDialog(props: FeedbackDialogProps) {\n const {\n open,\n onClose,\n categories,\n category,\n onCategoryChange,\n labels: labelsProp,\n submitting = false,\n contextSlot,\n attachmentAccept = DEFAULT_ATTACHMENT_ACCEPT,\n maxAttachmentBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onCaptureScreenshot,\n requireBody = true,\n attachmentButtonVariant,\n attachmentButtonSize,\n publicNotice,\n } = props;\n const [title, setTitle] = useState(\"\");\n const [body, setBody] = useState(\"\");\n const [attachment, setAttachment] = useState<File | null>(null);\n // `attachments=\"multiple\"` only: the capture, apart from the picked/pasted files.\n const [screenshot, setScreenshot] = useState<File | null>(null);\n const [files, setFiles] = useState<File[]>([]);\n const isMobile = usePhoneLayout();\n const noticeId = useId();\n const hasNotice = publicNotice !== undefined && publicNotice !== null && publicNotice !== false && publicNotice !== \"\";\n const labels = useKitLabels(\"feedbackDialog\", DEFAULT_FEEDBACK_DIALOG_LABELS, labelsProp);\n const attachmentText = useKitLabels(\"feedbackAttachment\", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);\n // Under `multiple` the heading is `attachmentList` — a screenshot AND photos — which\n // also names the chip list beneath it: one string, so the two cannot disagree. Its old\n // second name, `feedbackDialog.attachments`, went in 0.34.\n const listHeading =\n labelsProp?.attachmentList ??\n attachmentText.attachmentList ??\n DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentList!;\n // The attachment field resolves its own keys from `feedbackAttachment`; it gets the\n // prop's (so a caller's `attachmentAdd` still wins) plus the resolved heading.\n const attachmentLabels =\n props.attachments === \"multiple\"\n ? { ...labelsProp, attachment: listHeading, attachmentList: listHeading }\n : { ...labelsProp, attachment: labels.attachment };\n\n // Reset the form whenever the dialog is (re)opened — during render, on the\n // closed-to-open transition, so the old draft never paints for a frame.\n const [wasOpen, setWasOpen] = useState(open);\n if (open !== wasOpen) {\n setWasOpen(open);\n if (open) {\n setTitle(\"\");\n setBody(\"\");\n setAttachment(null);\n setScreenshot(null);\n setFiles([]);\n }\n }\n\n // Trimmed: a subject or body of spaces is empty (keksdose's API answers a blank title\n // with a 422, and a blank report is no report).\n const canSubmit = !!title.trim() && (!requireBody || !!body.trim()) && !submitting;\n const trySubmit = () => {\n if (!canSubmit) return;\n if (props.attachments === \"multiple\") {\n void props.onSubmit({ title, body, category, screenshot, attachments: files });\n } else {\n void props.onSubmit({ title, body, category, attachment });\n }\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 bodyTextarea = (\n <Textarea\n rows={5}\n label={requireBody ? labels.body : (labelsProp?.body ?? labels.bodyOptional ?? labels.body)}\n value={body}\n onChange={(e) => setBody(e.target.value)}\n aria-describedby={hasNotice ? noticeId : undefined}\n />\n );\n // 0.38: who will read it, right under what they are writing (§9.2 decision 6).\n const bodyField = hasNotice ? (\n <div className=\"space-y-1\">\n {bodyTextarea}\n <p id={noticeId} className=\"text-xs text-muted\" data-feedback-public-notice=\"\">\n {publicNotice}\n </p>\n </div>\n ) : (\n bodyTextarea\n );\n const fieldProps = {\n labels: attachmentLabels,\n accept: attachmentAccept,\n maxBytes: maxAttachmentBytes,\n onCaptureScreenshot,\n // Left undefined they are the field's own defaults (secondary / md).\n buttonVariant: attachmentButtonVariant,\n buttonSize: attachmentButtonSize,\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 const attachmentField =\n props.attachments === \"multiple\" ? (\n <FeedbackAttachmentField\n {...fieldProps}\n multiple\n value={files}\n onChange={setFiles}\n max={props.maxAttachments}\n // The capture goes to its own slot, so it is offered once and never crowds\n // out a photo; a paste is a photo (a region snip is chosen evidence, not \"the\n // screenshot\").\n screenshot={screenshot}\n onScreenshotChange={setScreenshot}\n onError={props.onAttachmentError}\n />\n ) : (\n <FeedbackAttachmentField\n {...fieldProps}\n value={attachment}\n onChange={setAttachment}\n onError={props.onAttachmentError}\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-subtle 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-placeholder\">\n {typeof labels.submitHint === \"function\" ? labels.submitHint(isApplePlatform()) : labels.submitHint}\n </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 { useId, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Button } from \"../components/button\";\nimport { Input } from \"../components/input\";\nimport { Select } from \"../components/select\";\nimport { Textarea } from \"../components/textarea\";\nimport type { ButtonSize, ButtonVariant } from \"../components/button-look\";\nimport { Modal } from \"../components/modal\";\nimport { isApplePlatform } from \"../hooks/use-hotkey\";\nimport { usePhoneLayout } from \"../hooks/use-breakpoint\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport {\n DEFAULT_ATTACHMENT_ACCEPT,\n DEFAULT_FEEDBACK_ATTACHMENT_LABELS,\n DEFAULT_MAX_ATTACHMENT_BYTES,\n FeedbackAttachmentField,\n type FeedbackAttachmentError,\n type FeedbackAttachmentErrorInfo,\n} from \"./feedback-attachment\";\nimport { DEFAULT_FEEDBACK_DIALOG_LABELS } from \"./feedback-dialog-labels\";\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 the provider's\n * `feedbackAttachment.attachmentCapture`, then English. */\n attachmentCapture?: string;\n /** The line under the attachment buttons saying a screenshot can be pasted\n * straight in. Optional — falls back to the provider's `feedbackAttachment`, then English. */\n attachmentPaste?: string;\n attachmentRemove: string;\n /** `multiple` mode (0.15.5): the chip list's accessible name — and, in\n * `<FeedbackDialog attachments=\"multiple\">`, the heading over it (0.16.0). Optional,\n * as are the keys below — they fall back to the provider's `feedbackAttachment`,\n * then English. */\n attachmentList?: string;\n /** `multiple` mode (0.16.0): the screenshot slot's chip title, in place of its file name. */\n attachmentScreenshot?: string;\n /** `multiple` mode: one chip's remove button, given the file's name. */\n attachmentRemoveFile?: (name: string) => string;\n /** `multiple` mode: the line shown once `max` files are attached. */\n attachmentLimit?: (max: number) => string;\n /** `refs` mode (0.22.0): the second line of a chip whose upload is still running. */\n attachmentUploading?: string;\n}\n\nexport interface FeedbackDialogLabels extends FeedbackAttachmentLabels {\n title: string;\n category: string;\n subject: string;\n body: string;\n attachment: string;\n /** See {@link FeedbackDialogTextLabels.submitHint}. */\n submitHint: FeedbackSubmitHint;\n cancel: string;\n save: string;\n}\n\n/** The submit hint: a string, or the line for the platform (`apple`: Mac, iPhone, iPad). */\nexport type FeedbackSubmitHint = string | ((apple: boolean) => string);\n\n/**\n * The dialog's own strings — the `feedbackDialog` namespace of `<UiKitProvider\n * labels>` (0.12.0). The attachment keys are not repeated here: they are the\n * `feedbackAttachment` namespace's, which the field inside already reads.\n *\n * Before, every key was required at the call site, so each app restated ten strings\n * wherever it mounted the dialog (kastlan feedback-button.tsx built the object inline).\n */\nexport interface FeedbackDialogTextLabels {\n title: string;\n category: string;\n subject: string;\n body: string;\n /** The body's label when `requireBody={false}`: \"What happened? (optional)\".\n * Optional, so a complete `UiKitLabels` typed before 0.14.2 still compiles. */\n bodyOptional?: string;\n /** The heading over the attachment buttons. */\n attachment: string;\n /**\n * The line beside the buttons naming the shortcut. Since 0.27.0 a function of the\n * platform, like `form.submitShortcut`: `apple` is true on a Mac, iPhone or iPad, where\n * the key is Cmd — \"⌘ Enter to send\" there, keksdose's \"Ctrl+Enter to send\" elsewhere\n * (§7.12). A plain string is still taken and shown everywhere, as before 0.27.0.\n */\n submitHint: FeedbackSubmitHint;\n cancel: string;\n save: string;\n /** 0.38 (docs/feedback-harmonization.md §9.2 decision 6): the line under the body when\n * reports are public — `useFeedbackSubmit({ publicNotice: true })` shows it. Optional,\n * so a complete `UiKitLabels` typed before 0.38 still compiles. de-CH \"Andere Personen\n * sehen Titel und Text (nie Ihre Screenshots oder Dateien).\" */\n publicNotice?: string;\n}\n\n// Defined in ./feedback-dialog-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_FEEDBACK_DIALOG_LABELS };\n\nexport interface FeedbackSubmission {\n title: string;\n body: string;\n category: string;\n attachment: File | null;\n}\n\n/**\n * What `onSubmit` gets under `attachments=\"multiple\"` (0.15.5): the screenshot and\n * the picked/pasted files apart, because an app files them differently — keksdose\n * attaches the screenshot to the report and the photos as its evidence list. No\n * `attachment`: there is no one file it could name.\n */\nexport interface FeedbackMultipleSubmission {\n title: string;\n body: string;\n category: string;\n /** What `onCaptureScreenshot` returned, or `null` — at most one. */\n screenshot: File | null;\n /** Picked and pasted files, in the order they were added; at most `maxAttachments`. */\n attachments: File[];\n}\n\ninterface FeedbackDialogBaseProps {\n open: boolean;\n onClose: () => void;\n categories: FeedbackCategoryOption[];\n category: string;\n onCategoryChange: (value: string) => void;\n /** Prop > `<UiKitProvider labels={{ feedbackDialog, feedbackAttachment }}>` >\n * English. Optional since 0.12.0; a whole `FeedbackDialogLabels` still fits. */\n labels?: LabelOverride<FeedbackDialogLabels>;\n submitting?: boolean;\n contextSlot?: ReactNode;\n attachmentAccept?: string[];\n maxAttachmentBytes?: number;\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 * `false`: a report with only a subject can be sent, and the body's label says it is\n * optional (`bodyOptional`, unless `labels.body` is passed). Default `true`, the\n * dialog's behaviour until 0.14.2 (keksdose K1: its backend takes title-only reports).\n */\n requireBody?: boolean;\n /**\n * The attachment field's add and capture buttons: its `buttonVariant` (0.25.0),\n * Button's own `variant`. Default `\"secondary\"`, the look they always had.\n *\n * The field took `buttonVariant` / `buttonSize` in 0.24.0 (keksdose's support chat),\n * but the dialog builds its field itself and passed neither, so a host that wanted\n * quieter buttons here — a dialog that already ends in a row of Cancel / Send — had\n * no way to say so short of rebuilding the dialog. Named after the dialog's other\n * attachment options (`attachmentAccept`, `onAttachmentError`) and handed through\n * unchanged, in both `attachments` modes.\n */\n attachmentButtonVariant?: ButtonVariant;\n /** The attachment field's `buttonSize` (0.25.0) — with {@link attachmentButtonVariant}.\n * Default `\"md\"`; `\"sm\"` draws the paperclip and camera at 14px. */\n attachmentButtonSize?: ButtonSize;\n /**\n * A muted line under the body saying who will read it (0.38, docs/feedback-harmonization.md\n * §9.2 decision 6) — the kit's sentence is `feedbackDialog.publicNotice`, \"Other users\n * will see the title and text (never your screenshots or files)\", which\n * `useFeedbackSubmit({ publicNotice: true })` passes. Attached to the body field with\n * `aria-describedby`, so it is heard where the text is written. Left out: no line.\n */\n publicNotice?: ReactNode;\n}\n\n/** One attachment — a screenshot, a picked or a pasted image; a second replaces\n * nothing, it has to wait for the first to be removed. The default. */\nexport interface FeedbackDialogSingleProps extends FeedbackDialogBaseProps {\n attachments?: \"single\";\n onSubmit: (data: FeedbackSubmission) => void | Promise<void>;\n /** `info` (0.23.0) names the refused file and the limit — see\n * {@link FeedbackAttachmentErrorInfo}. */\n onAttachmentError?: (kind: \"type\" | \"size\", info: FeedbackAttachmentErrorInfo) => void;\n /** Multiple mode only — refused here rather than silently ignored. */\n maxAttachments?: never;\n}\n\n/**\n * One screenshot plus up to `maxAttachments` picked or pasted files (0.15.5, keksdose\n * dev#578: \"pasted two photos, the second overwrote the first\"). The capture button is\n * offered while there is no screenshot yet; the add button while there is room.\n */\nexport interface FeedbackDialogMultipleProps extends FeedbackDialogBaseProps {\n attachments: \"multiple\";\n /** Default `DEFAULT_MAX_ATTACHMENTS` (5). The screenshot does not count. */\n maxAttachments?: number;\n onSubmit: (data: FeedbackMultipleSubmission) => void | Promise<void>;\n /** `\"count\"`: more files arrived at once than there was room for; the surplus was\n * dropped. `info` (0.23.0) names the refused file(s) and the limit. */\n onAttachmentError?: (kind: FeedbackAttachmentError, info: FeedbackAttachmentErrorInfo) => void;\n}\n\nexport type FeedbackDialogProps = FeedbackDialogSingleProps | FeedbackDialogMultipleProps;\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 *\n * **`attachments=\"multiple\"`** (0.15.5) takes several: every paste and pick adds a\n * chip, and `onSubmit` gets `{ screenshot, attachments }` ({@link\n * FeedbackMultipleSubmission}) instead of `attachment`. Opt-in, so a caller typed\n * against the one-file submission keeps compiling and behaving as before.\n */\nexport function FeedbackDialog(props: FeedbackDialogProps) {\n const {\n open,\n onClose,\n categories,\n category,\n onCategoryChange,\n labels: labelsProp,\n submitting = false,\n contextSlot,\n attachmentAccept = DEFAULT_ATTACHMENT_ACCEPT,\n maxAttachmentBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onCaptureScreenshot,\n requireBody = true,\n attachmentButtonVariant,\n attachmentButtonSize,\n publicNotice,\n } = props;\n const [title, setTitle] = useState(\"\");\n const [body, setBody] = useState(\"\");\n const [attachment, setAttachment] = useState<File | null>(null);\n // `attachments=\"multiple\"` only: the capture, apart from the picked/pasted files.\n const [screenshot, setScreenshot] = useState<File | null>(null);\n const [files, setFiles] = useState<File[]>([]);\n const isMobile = usePhoneLayout();\n const noticeId = useId();\n const hasNotice =\n publicNotice !== undefined && publicNotice !== null && publicNotice !== false && publicNotice !== \"\";\n const labels = useKitLabels(\"feedbackDialog\", DEFAULT_FEEDBACK_DIALOG_LABELS, labelsProp);\n const attachmentText = useKitLabels(\"feedbackAttachment\", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);\n // Under `multiple` the heading is `attachmentList` — a screenshot AND photos — which\n // also names the chip list beneath it: one string, so the two cannot disagree. Its old\n // second name, `feedbackDialog.attachments`, went in 0.34.\n const listHeading =\n labelsProp?.attachmentList ?? attachmentText.attachmentList ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentList!;\n // The attachment field resolves its own keys from `feedbackAttachment`; it gets the\n // prop's (so a caller's `attachmentAdd` still wins) plus the resolved heading.\n const attachmentLabels =\n props.attachments === \"multiple\"\n ? { ...labelsProp, attachment: listHeading, attachmentList: listHeading }\n : { ...labelsProp, attachment: labels.attachment };\n\n // Reset the form whenever the dialog is (re)opened — during render, on the\n // closed-to-open transition, so the old draft never paints for a frame.\n const [wasOpen, setWasOpen] = useState(open);\n if (open !== wasOpen) {\n setWasOpen(open);\n if (open) {\n setTitle(\"\");\n setBody(\"\");\n setAttachment(null);\n setScreenshot(null);\n setFiles([]);\n }\n }\n\n // Trimmed: a subject or body of spaces is empty (keksdose's API answers a blank title\n // with a 422, and a blank report is no report).\n const canSubmit = !!title.trim() && (!requireBody || !!body.trim()) && !submitting;\n const trySubmit = () => {\n if (!canSubmit) return;\n if (props.attachments === \"multiple\") {\n void props.onSubmit({ title, body, category, screenshot, attachments: files });\n } else {\n void props.onSubmit({ title, body, category, attachment });\n }\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 bodyTextarea = (\n <Textarea\n rows={5}\n label={requireBody ? labels.body : (labelsProp?.body ?? labels.bodyOptional ?? labels.body)}\n value={body}\n onChange={(e) => setBody(e.target.value)}\n aria-describedby={hasNotice ? noticeId : undefined}\n />\n );\n // 0.38: who will read it, right under what they are writing (§9.2 decision 6).\n const bodyField = hasNotice ? (\n <div className=\"space-y-1\">\n {bodyTextarea}\n <p id={noticeId} className=\"text-xs text-muted\" data-feedback-public-notice=\"\">\n {publicNotice}\n </p>\n </div>\n ) : (\n bodyTextarea\n );\n const fieldProps = {\n labels: attachmentLabels,\n accept: attachmentAccept,\n maxBytes: maxAttachmentBytes,\n onCaptureScreenshot,\n // Left undefined they are the field's own defaults (secondary / md).\n buttonVariant: attachmentButtonVariant,\n buttonSize: attachmentButtonSize,\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 const attachmentField =\n props.attachments === \"multiple\" ? (\n <FeedbackAttachmentField\n {...fieldProps}\n multiple\n value={files}\n onChange={setFiles}\n max={props.maxAttachments}\n // The capture goes to its own slot, so it is offered once and never crowds\n // out a photo; a paste is a photo (a region snip is chosen evidence, not \"the\n // screenshot\").\n screenshot={screenshot}\n onScreenshotChange={setScreenshot}\n onError={props.onAttachmentError}\n />\n ) : (\n <FeedbackAttachmentField\n {...fieldProps}\n value={attachment}\n onChange={setAttachment}\n onError={props.onAttachmentError}\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-subtle 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-placeholder\">\n {typeof labels.submitHint === \"function\" ? labels.submitHint(isApplePlatform()) : labels.submitHint}\n </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 +1 @@
1
- {"version":3,"sources":["../../src/feedback/feedback-inbox.tsx"],"sourcesContent":["import { useId, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { Button } from \"../components/button\";\nimport { Textarea } from \"../components/textarea\";\nimport type { ButtonSize, ButtonVariant } from \"../components/button-look\";\nimport { Tooltip } from \"../components/tooltip\";\nimport { useCommitReason, useWriteLock, type CommitScope } from \"../components/write-lock\";\nimport { FeedbackAttachmentField, type FeedbackAttachmentErrorInfo } from \"./feedback-attachment\";\nimport type { FeedbackAttachmentLabels } from \"./feedback-dialog\";\nimport { useFeedbackCategoryLabels, useFeedbackStatusLabels } from \"./feedback-labels\";\nimport {\n FEEDBACK_CATEGORY_META,\n FEEDBACK_STATUS_META,\n type FeedbackCategory,\n type FeedbackStatus,\n} from \"./feedback-vocabulary\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\n// The words module, not ./feedback-row-detail: that one imports this file.\nimport { DEFAULT_FEEDBACK_DETAIL_LABELS } from \"./feedback-row-detail-labels\";\n\n/**\n * The feedback **inbox**, as the parts two apps were each writing separately.\n *\n * `feedback-dialog.tsx` next door is the other half — the form somebody files a\n * report with — and the split between them is the same one: the kit owns the\n * *vocabulary*, the *policy* about how a report moves (both in\n * feedback-vocabulary.ts since 0.34, re-exported below), and the *look* (here);\n * the app owns the data, the API and every string.\n *\n * It exists because both apps had grown their own copy. Keksdose's is the one\n * this is lifted from, comments and all, because it is the one that had been\n * argued with users for a year — the glyph per status, the two sizes of the\n * status control, what a row offers from where it currently stands. Steering\n * Design's was thinner in every one of those places and read as a different\n * product for no reason anyone chose.\n *\n * **The report itself lives next door (0.27.0).** This file used to say there\n * was deliberately no `Feedback` type here: the apps stored a report differently\n * (a context JSON column and an object store in one, a `page_path` and the bytes\n * in the row in another). Marcel's feedback round (2026-10-04,\n * docs/feedback-harmonization.md §5) gave all three apps one contract, so the\n * shape is now shared — `FeedbackRecord` / `FeedbackContext` in\n * feedback-record.ts, with the constants and body helpers. The components HERE\n * still take the values they draw rather than a row, which keeps them usable\n * from any surface; the detail panel stays a *shell* the row detail fills.\n *\n * **The words are the kit's too (0.27.0).** The status and category names are\n * the `feedbackStatus` / `feedbackCategory` namespaces (feedback-labels.ts,\n * keksdose's wording), read by default by the badges and the transitions below;\n * a `label` the caller passes still wins.\n */\n\n// The vocabulary — statuses, categories, their glyphs and orders, the transitions — is\n// feedback-vocabulary.ts since 0.34 (MOD-1): data the top bar's feedback menu imports\n// without this file's components. Re-exported, so every name is still found here.\nexport {\n FEEDBACK_CATEGORY_META,\n FEEDBACK_CATEGORY_ORDER,\n FEEDBACK_STATUS_META,\n FEEDBACK_STATUS_ORDER,\n feedbackCategoryRank,\n nextFeedbackStatus,\n selectableFeedbackStatuses,\n visibleFeedbackStatuses,\n} from \"./feedback-vocabulary\";\nexport type { FeedbackCategory, FeedbackStatus } from \"./feedback-vocabulary\";\n\n/**\n * The category as a badge.\n *\n * Falls back to OTHER's treatment for a category this build has never heard of,\n * so an older client against a newer API degrades to a readable neutral pill\n * instead of throwing on `meta.icon` — a crash report must not be able to cause\n * one. The *label* gets the same guard: by default the `feedbackCategory`\n * namespace's word (0.27.0), and for a value it has no word for, what OTHER is\n * called.\n */\nexport function FeedbackCategoryBadge({\n category,\n label,\n compact = false,\n className,\n}: {\n category: FeedbackCategory;\n /** Over the `feedbackCategory` label (0.27.0: optional — before, every app passed\n * its own translation). */\n label?: ReactNode;\n compact?: boolean;\n className?: string;\n}) {\n const names = useFeedbackCategoryLabels();\n // Annotated `| undefined` because the Record's index signature promises a hit\n // for every FeedbackCategory, and at runtime the API can hand us one that is not.\n const known: (typeof FEEDBACK_CATEGORY_META)[\"OTHER\"] | undefined = FEEDBACK_CATEGORY_META[category];\n const meta = known ?? FEEDBACK_CATEGORY_META.OTHER;\n const Icon = meta.icon;\n return (\n <span\n className={cn(\n \"inline-flex items-center gap-1 rounded font-medium\",\n compact ? \"px-1.5 py-0.5 text-caption\" : \"px-2 py-0.5 text-xs\",\n meta.badgeBg,\n meta.badgeText,\n className,\n )}\n >\n <Icon className={compact ? \"size-3\" : \"size-3.5\"} aria-hidden />\n {label ?? (known ? names[category] : names.OTHER)}\n </span>\n );\n}\n\n/** Where a report stands, as a badge — for the places that only *report* the\n * status rather than offering to change it. Named by the `feedbackStatus`\n * namespace unless `label` says otherwise (0.27.0); a status this build has never\n * heard of shows OPEN's look and its own raw value, never OPEN's name. */\nexport function FeedbackStatusBadge({\n status,\n label,\n className,\n}: {\n status: FeedbackStatus;\n /** Over the `feedbackStatus` label (0.27.0: optional). */\n label?: ReactNode;\n className?: string;\n}) {\n const names = useFeedbackStatusLabels();\n const meta = FEEDBACK_STATUS_META[status] ?? FEEDBACK_STATUS_META.OPEN;\n const name: string | undefined = names[status];\n const Icon = meta.icon;\n return (\n <span\n className={cn(\n \"inline-flex items-center gap-1 whitespace-nowrap rounded px-1.5 py-0.5 text-caption font-medium\",\n meta.activeBg,\n meta.activeText,\n className,\n )}\n >\n <Icon className=\"size-3\" aria-hidden />\n {label ?? name ?? status}\n </span>\n );\n}\n\n/**\n * A row of statuses to switch a report to — either a table column's icon row or\n * an expanded panel's labelled pills.\n *\n * One component for both, because everything except the class strings is the\n * same decision: which one is current, and when the buttons are disabled. Two\n * copies had already drifted — only the icon variant carried `aria-label` and\n * `aria-pressed`, so the pill row was unlabelled for a screen reader.\n *\n * WHICH statuses to offer is the caller's, not this component's:\n * {@link visibleFeedbackStatuses} for a compact column,\n * {@link selectableFeedbackStatuses} for an expanded row. Passing the list keeps\n * that policy readable at the two call sites instead of hiding it behind\n * `variant`.\n */\nexport function FeedbackStatusTransitions({\n status,\n statuses,\n canEdit,\n onPick,\n variant,\n label,\n commit,\n disabledReason: ownDisabledReason,\n className,\n}: {\n status: FeedbackStatus;\n statuses: FeedbackStatus[];\n canEdit: boolean;\n onPick: (status: FeedbackStatus) => void;\n variant: \"icon\" | \"pill\";\n /** What each status is called. Default (0.27.0): the `feedbackStatus` namespace —\n * before, every app passed its own translation. */\n label?: (status: FeedbackStatus) => string;\n /**\n * Every button here SAVES the moment it is pressed (0.27.0) — opt in and, under a\n * locked {@link WriteLockProvider}, the row stays as it is and says why: each status\n * the row could move to is `aria-disabled` but focusable, the click does nothing, and\n * the lock's reason is in the kit Tooltip and the button's description — the\n * `disabledReason` path `Button` takes. Off by default: a feedback inbox is not what\n * every app's lock is about (keksdose's shell lock is a read-only demo BUDGET, and a\n * report is not budget data).\n */\n commit?: CommitScope;\n /** Why the statuses cannot be changed right now, the same way (a lock's own reason\n * wins over it). Only read while `canEdit`: a read-only row is a picture of the\n * workflow, not a refused action. */\n disabledReason?: ReactNode;\n className?: string;\n}) {\n const names = useFeedbackStatusLabels();\n const nameOf = label ?? ((value: FeedbackStatus) => names[value]);\n const reason = useCommitReason(commit, ownDisabledReason);\n const locked = canEdit && reason !== undefined && reason !== null && reason !== false && reason !== \"\";\n const reasonId = useId();\n return (\n // Not a control: it only stops clicks on the buttons inside from bubbling to\n // the row. The buttons are the keyboard path, and Enter/Space on them fires\n // their own click, which lands here the same way.\n // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- propagation guard around real buttons\n <div\n className={cn(variant === \"icon\" ? \"flex items-center gap-0.5\" : \"flex flex-wrap gap-1.5\", className)}\n // A status control inside a clickable row must not also open the row.\n onClick={(event) => event.stopPropagation()}\n >\n {locked && (\n <span id={reasonId} hidden>\n {reason}\n </span>\n )}\n {statuses.map((value) => {\n const meta = FEEDBACK_STATUS_META[value];\n const Icon = meta.icon;\n const active = status === value;\n const name = nameOf(value);\n // Locked: focusable and described, never firing (see `commit`). The current\n // status stays natively disabled — it was never something to press.\n const refused = locked && !active;\n const button = (\n <button\n key={value}\n type=\"button\"\n disabled={refused ? undefined : !canEdit || active}\n aria-disabled={refused || undefined}\n aria-describedby={refused ? reasonId : undefined}\n onClick={() => {\n if (!refused) onPick(value);\n }}\n aria-label={name}\n aria-pressed={active}\n className={cn(\n \"transition-colors\",\n variant === \"icon\"\n ? cn(\n \"flex size-7 items-center justify-center rounded\",\n active\n ? cn(meta.activeBg, meta.activeText)\n : \"text-placeholder hover:text-secondary\",\n )\n : cn(\n \"inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs font-medium\",\n active\n ? cn(meta.activeBg, meta.activeText, \"border-transparent\")\n : // The translucent ink on hover (0.33, B′), as Button's secondary:\n // the well is the page in light, and these pills sit on either.\n \"border-subtle bg-transparent text-primary hover:bg-[color-mix(in_srgb,var(--text-primary)_7%,transparent)]\",\n ),\n // Both stay unclickable, but only the read-only case is DIMMED: the\n // current status is the one thing in the row that has to be legible\n // at a glance, and among eight pills a faded active one reads as\n // \"unavailable\" rather than as \"this is where the row stands\".\n (!canEdit || active) && \"cursor-default\",\n !canEdit && \"opacity-60\",\n refused && \"cursor-not-allowed opacity-50\",\n )}\n >\n <Icon className={variant === \"icon\" ? \"size-4\" : \"size-3.5\"} aria-hidden />\n {variant === \"pill\" && name}\n </button>\n );\n // Locked, the bubble says WHY in both variants; the icon's name is still its\n // `aria-label`, and the reason its description — the hidden copy above, which\n // stays put, rather than the bubble (as on `Button`: the FRAGMENT keeps Tooltip\n // from adding the bubble as a second, coming-and-going description).\n if (refused) {\n return (\n <Tooltip key={value} label={reason} side=\"top\" portal={variant === \"icon\" || undefined}>\n <>{button}</>\n </Tooltip>\n );\n }\n // The icon row has no visible label, so it needs the tooltip; the pills\n // carry theirs inline.\n //\n // ⚠️ ABOVE, and portalled (Keksdose live #339). `side=\"bottom\"` put the bubble\n // straight over the NEXT ROW's icons — in a queue you triage by running down it\n // with the pointer, that is the row you are about to reach, and it was covered\n // every time you paused on one. Above covers the row you have just left instead.\n //\n // `portal` is not cosmetic here. An absolutely-positioned bubble is clipped by\n // any ancestor that scrolls — this cell lives inside the DataTable's own\n // scroller — and it cannot flip: `placeTooltip` is what moves the bubble below\n // the icon for the FIRST row, where there is no room above it, instead of\n // painting it off the top of the list.\n return variant === \"icon\" ? (\n <Tooltip key={value} label={name} side=\"top\" portal>\n {button}\n </Tooltip>\n ) : (\n button\n );\n })}\n </div>\n );\n}\n\n/**\n * A textarea and its two buttons — for an outcome, for a note sent back with a\n * report, for a body being corrected.\n *\n * One component because all three are the same gesture: a draft that is not\n * committed until it is saved. Ctrl/⌘+Enter submits, which is the same shortcut\n * the compose dialog next door uses, so the habit carries across the feature.\n *\n * ## `initial` seeds the draft; it does not own it (changed 2026-09-22)\n *\n * This used to re-seed from `initial` in an effect, and that effect threw away\n * whatever was in the box — **and the picture attached to it** — every time the\n * prop arrived with a different value. `initial` is normally the saved note, so\n * the list refreshing under the editor, or the owner re-deriving the same string,\n * was enough: a triager halfway through a reply, with a screenshot picked out,\n * lost both and got the saved text back with no way to undo it.\n *\n * A draft belongs to the thing being edited, so switching to a different note is a\n * different editor — say so with a `key`:\n *\n * ```tsx\n * <FeedbackNoteEditor key={note.id} initial={note.body} … />\n * ```\n *\n * Where the id is not in hand at the call site, pass {@link resetKey} instead and\n * the editor re-seeds when THAT changes. Either way the decision is the caller's,\n * which is the point: the editor cannot tell a new subject from a new render.\n */\nexport function FeedbackNoteEditor({\n initial,\n pending,\n onSave,\n onCancel,\n saveLabel,\n cancelLabel,\n placeholder,\n label,\n rows = 3,\n attachment,\n resetKey,\n required = false,\n commit,\n}: {\n initial: string;\n pending: boolean;\n onSave: (value: string, attachment?: File | null) => void;\n onCancel: () => void;\n saveLabel: ReactNode;\n cancelLabel: ReactNode;\n /** A line above the field saying what to write, not an in-field placeholder:\n * a hint that disappears the moment somebody starts typing is a hint that is\n * gone exactly when it is being followed. */\n placeholder?: ReactNode;\n /** The box's accessible name (0.34). Without it the `placeholder` line names the\n * box, and without that the `feedbackDetail` namespace's `note` (\"Note\") does —\n * a box with neither was read out as just \"edit text\" (the showcase's axe check).\n * Given with a `placeholder`, this names the box and the line describes it. */\n label?: string;\n rows?: number;\n /** Offer a picture with the note (Steering Design feedback #128). Omitted,\n * the editor is exactly the text box it always was — which is what the\n * *outcome* editor beside it wants, since an outcome is the answer rather\n * than the evidence. */\n attachment?: FeedbackNoteAttachment;\n /** Change this to say \"the editor is now editing something ELSE\", and the draft\n * and its attachment are dropped and re-seeded from `initial`. For call sites\n * that cannot put a `key` on the editor (see the note above). Leave it out and\n * the draft is never thrown away behind the user's back. */\n resetKey?: string | number;\n /**\n * A blank note cannot be saved (0.27.0): Save stays disabled — and Ctrl/⌘+Enter does\n * nothing — until the trimmed text is non-empty, and the box is `aria-required`. For\n * the rework note, which the feedback contract requires (§3.4: a file alone cannot\n * be sent; keksdose `feedback-page.tsx:577` dropped the click silently instead, so a\n * press on Send with an empty box looked like a lost send). Default `false`: an\n * outcome or a description may be cleared on purpose.\n */\n required?: boolean;\n /** Save COMMITS (0.27.0): under a locked `WriteLockProvider` it is disabled the\n * `disabledReason` way, with the lock's reason, and Ctrl/⌘+Enter saves nothing. The\n * text box stays editable — nothing in it reaches the server until the save. */\n commit?: CommitScope;\n}) {\n const [draft, setDraft] = useState(initial);\n const [file, setFile] = useState<File | null>(null);\n // Adjusted during render rather than in an effect: an effect would paint the old\n // draft first and then replace it, and — far worse — it is a second definition of\n // when a draft dies that the caller cannot see. React re-runs this component\n // immediately, before anything is committed to the screen.\n const [seededFor, setSeededFor] = useState(resetKey);\n if (resetKey !== seededFor) {\n setSeededFor(resetKey);\n setDraft(initial);\n setFile(null);\n }\n // The editor's own root, which is where a paste made in the text box bubbles\n // to: the attachment field below is the box's sibling, so a paste in the box\n // never passes through the field's own subtree. It listens here instead\n // (Steering Design feedback #140).\n const root = useRef<HTMLDivElement>(null);\n const hintId = useId();\n const detailText = useKitLabels(\"feedbackDetail\", DEFAULT_FEEDBACK_DETAIL_LABELS);\n // The lock as the save button sees it, with its scope (§12.36): `commit &&\n // lock.locked` read a `{ except }` scope as `true`.\n const lock = useWriteLock(commit ?? false);\n const blank = required && !draft.trim();\n // The shortcut obeys what the button shows: no save while one is in flight, while the\n // note a `required` editor needs is missing, or while a `commit` save is locked.\n const canSubmit = !pending && !blank && !lock.locked;\n const submit = () => onSave(draft, file);\n return (\n // Not a control: a delegated Ctrl/Cmd+Enter shortcut for the textarea inside,\n // which is what holds focus. The save button is the non-shortcut path.\n // eslint-disable-next-line jsx-a11y/no-static-element-interactions -- delegated keyboard shortcut for the focused field\n <div\n ref={root}\n className=\"space-y-2\"\n onKeyDown={(event) => {\n if ((event.ctrlKey || event.metaKey) && event.key === \"Enter\" && canSubmit) {\n event.preventDefault();\n submit();\n }\n }}\n >\n {/* The line names the box (0.27.0): it is the only text that says what to write\n in it, and an unnamed textarea is read out as just \"edit text\". */}\n {placeholder && (\n <p id={hintId} className=\"text-xs text-muted\">\n {placeholder}\n </p>\n )}\n <Textarea\n rows={rows}\n value={draft}\n onChange={(event) => setDraft(event.target.value)}\n // Never nameless (round 0.34): the caller's `label`, else the line, else the\n // `feedbackDetail` namespace's `note`. With both, the line describes the box.\n aria-label={label ?? (placeholder ? undefined : detailText.note)}\n aria-labelledby={placeholder && label === undefined ? hintId : undefined}\n aria-describedby={placeholder && label !== undefined ? hintId : undefined}\n aria-required={required || undefined}\n />\n {attachment && (\n <FeedbackAttachmentField\n value={file}\n onChange={setFile}\n // A pause while the save is in flight (the field's G5b `disabled`): the note\n // goes out with the file AS IT WAS at the press, so a chip removed or added\n // mid-send would be a lie about what was sent.\n disabled={pending}\n labels={attachment.labels}\n accept={attachment.accept}\n maxBytes={attachment.maxBytes}\n onError={attachment.onError}\n onCaptureScreenshot={attachment.onCaptureScreenshot}\n buttonVariant={attachment.buttonVariant}\n buttonSize={attachment.buttonSize}\n // Within THIS editor's subtree, not on `document`: the editor sits\n // inline on a page that has other fields, so a paste made in one of\n // them is meant for that one. The textarea above is where the caret\n // already is, and it is the field's sibling — so the field is told\n // to listen on their common parent, where the paste bubbles to.\n pasteFrom={root}\n />\n )}\n <div className=\"flex justify-end gap-2\">\n <Button variant=\"ghost\" onClick={onCancel}>\n {cancelLabel}\n </Button>\n <Button variant=\"accent\" disabled={pending || blank} commit={commit} onClick={submit}>\n {saveLabel}\n </Button>\n </div>\n </div>\n );\n}\n\n/** What {@link FeedbackNoteEditor} needs in order to offer a picture with the\n * note: the same things {@link FeedbackAttachmentField} takes, so the reply path is\n * held to the app's own limits — and drawn in its own look — rather than the\n * defaults. */\nexport interface FeedbackNoteAttachment {\n /** Over the field's own `feedbackAttachment` namespace. Optional since 0.27.0 — the\n * field has read the provider's words since 0.7.0, so an app with a catalogue has\n * nothing to restate. */\n labels?: LabelOverride<FeedbackAttachmentLabels>;\n accept?: string[];\n maxBytes?: number;\n /** `info` (0.23.0) names the refused file and the limit. */\n onError?: (kind: \"type\" | \"size\", info: FeedbackAttachmentErrorInfo) => void;\n onCaptureScreenshot?: () => Promise<File | null>;\n /**\n * The add and capture buttons' look (0.25.0) — the field's `buttonVariant` (0.24.0),\n * Button's own `variant`. Default `\"secondary\"`, as before.\n *\n * The note editor sits inline in a triage panel, and its buttons end in a ghost\n * Cancel and a brand Save: two bordered full-size attachment buttons above them read\n * as a second form. The field could already be told otherwise; the editor builds it\n * itself and had nowhere to say so.\n */\n buttonVariant?: ButtonVariant;\n /** The add and capture buttons' size (0.25.0) — the field's `buttonSize`. Default\n * `\"md\"`; `\"sm\"` draws their icons at 14px. */\n buttonSize?: ButtonSize;\n}\n\n/**\n * One labelled block of an opened report.\n *\n * The detail panel is a stack of these, and it is a stack rather than a\n * component with fixed fields on purpose: the two apps keep a report's context\n * and its screenshot in genuinely different places, and a shell that insisted on\n * both would force one of them to invent a shape it does not have. What is\n * shared is what a section *looks* like — and that is all that made the two\n * panels read as different products.\n */\nexport function FeedbackDetailSection({\n title,\n action,\n children,\n}: {\n title: ReactNode;\n /** Something on the title's own line — an edit button, a resolved-at date. */\n action?: ReactNode;\n children: ReactNode;\n}) {\n return (\n <div>\n <div className=\"mb-1 flex items-center justify-between gap-2\">\n <div className=\"text-xs font-medium uppercase tracking-wide text-muted\">\n {title}\n </div>\n {action}\n </div>\n {children}\n </div>\n );\n}\n\n/** The stack a detail panel is. Here so the spacing between sections is decided\n * once rather than by whichever app was written second. */\nexport function FeedbackDetail({ children, className }: { children: ReactNode; className?: string }) {\n return <div className={cn(\"space-y-3 text-sm\", className)}>{children}</div>;\n}\n\n/** Prose inside a section — a body, an outcome, a note.\n *\n * `whitespace-pre-wrap` is the whole component: a report is written in\n * paragraphs, and a note appended to it later is separated by blank lines that\n * carry the entire \"this arrived after the answer\" reading. */\nexport function FeedbackProse({ children, empty }: { children?: string; empty?: ReactNode }) {\n if (!children) return <p className=\"text-placeholder\">{empty ?? \"—\"}</p>;\n return (\n <p className=\"whitespace-pre-wrap text-secondary\">{children}</p>\n );\n}\n"],"mappings":"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","names":["FEEDBACK_CATEGORY_META","FEEDBACK_STATUS_META"]}
1
+ {"version":3,"sources":["../../src/feedback/feedback-inbox.tsx"],"sourcesContent":["import { useId, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { Button } from \"../components/button\";\nimport { Textarea } from \"../components/textarea\";\nimport type { ButtonSize, ButtonVariant } from \"../components/button-look\";\nimport { Tooltip } from \"../components/tooltip\";\nimport { useCommitReason, useWriteLock, type CommitScope } from \"../components/write-lock\";\nimport { FeedbackAttachmentField, type FeedbackAttachmentErrorInfo } from \"./feedback-attachment\";\nimport type { FeedbackAttachmentLabels } from \"./feedback-dialog\";\nimport { useFeedbackCategoryLabels, useFeedbackStatusLabels } from \"./feedback-labels\";\nimport {\n FEEDBACK_CATEGORY_META,\n FEEDBACK_STATUS_META,\n type FeedbackCategory,\n type FeedbackStatus,\n} from \"./feedback-vocabulary\";\nimport { useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\n// The words module, not ./feedback-row-detail: that one imports this file.\nimport { DEFAULT_FEEDBACK_DETAIL_LABELS } from \"./feedback-row-detail-labels\";\n\n/**\n * The feedback **inbox**, as the parts two apps were each writing separately.\n *\n * `feedback-dialog.tsx` next door is the other half — the form somebody files a\n * report with — and the split between them is the same one: the kit owns the\n * *vocabulary*, the *policy* about how a report moves (both in\n * feedback-vocabulary.ts since 0.34, re-exported below), and the *look* (here);\n * the app owns the data, the API and every string.\n *\n * It exists because both apps had grown their own copy. Keksdose's is the one\n * this is lifted from, comments and all, because it is the one that had been\n * argued with users for a year — the glyph per status, the two sizes of the\n * status control, what a row offers from where it currently stands. Steering\n * Design's was thinner in every one of those places and read as a different\n * product for no reason anyone chose.\n *\n * **The report itself lives next door (0.27.0).** This file used to say there\n * was deliberately no `Feedback` type here: the apps stored a report differently\n * (a context JSON column and an object store in one, a `page_path` and the bytes\n * in the row in another). Marcel's feedback round (2026-10-04,\n * docs/feedback-harmonization.md §5) gave all three apps one contract, so the\n * shape is now shared — `FeedbackRecord` / `FeedbackContext` in\n * feedback-record.ts, with the constants and body helpers. The components HERE\n * still take the values they draw rather than a row, which keeps them usable\n * from any surface; the detail panel stays a *shell* the row detail fills.\n *\n * **The words are the kit's too (0.27.0).** The status and category names are\n * the `feedbackStatus` / `feedbackCategory` namespaces (feedback-labels.ts,\n * keksdose's wording), read by default by the badges and the transitions below;\n * a `label` the caller passes still wins.\n */\n\n// The vocabulary — statuses, categories, their glyphs and orders, the transitions — is\n// feedback-vocabulary.ts since 0.34 (MOD-1): data the top bar's feedback menu imports\n// without this file's components. Re-exported, so every name is still found here.\nexport {\n FEEDBACK_CATEGORY_META,\n FEEDBACK_CATEGORY_ORDER,\n FEEDBACK_STATUS_META,\n FEEDBACK_STATUS_ORDER,\n feedbackCategoryRank,\n nextFeedbackStatus,\n selectableFeedbackStatuses,\n visibleFeedbackStatuses,\n} from \"./feedback-vocabulary\";\nexport type { FeedbackCategory, FeedbackStatus } from \"./feedback-vocabulary\";\n\n/**\n * The category as a badge.\n *\n * Falls back to OTHER's treatment for a category this build has never heard of,\n * so an older client against a newer API degrades to a readable neutral pill\n * instead of throwing on `meta.icon` — a crash report must not be able to cause\n * one. The *label* gets the same guard: by default the `feedbackCategory`\n * namespace's word (0.27.0), and for a value it has no word for, what OTHER is\n * called.\n */\nexport function FeedbackCategoryBadge({\n category,\n label,\n compact = false,\n className,\n}: {\n category: FeedbackCategory;\n /** Over the `feedbackCategory` label (0.27.0: optional — before, every app passed\n * its own translation). */\n label?: ReactNode;\n compact?: boolean;\n className?: string;\n}) {\n const names = useFeedbackCategoryLabels();\n // Annotated `| undefined` because the Record's index signature promises a hit\n // for every FeedbackCategory, and at runtime the API can hand us one that is not.\n const known: (typeof FEEDBACK_CATEGORY_META)[\"OTHER\"] | undefined = FEEDBACK_CATEGORY_META[category];\n const meta = known ?? FEEDBACK_CATEGORY_META.OTHER;\n const Icon = meta.icon;\n return (\n <span\n className={cn(\n \"inline-flex items-center gap-1 rounded font-medium\",\n compact ? \"px-1.5 py-0.5 text-caption\" : \"px-2 py-0.5 text-xs\",\n meta.badgeBg,\n meta.badgeText,\n className,\n )}\n >\n <Icon className={compact ? \"size-3\" : \"size-3.5\"} aria-hidden />\n {label ?? (known ? names[category] : names.OTHER)}\n </span>\n );\n}\n\n/** Where a report stands, as a badge — for the places that only *report* the\n * status rather than offering to change it. Named by the `feedbackStatus`\n * namespace unless `label` says otherwise (0.27.0); a status this build has never\n * heard of shows OPEN's look and its own raw value, never OPEN's name. */\nexport function FeedbackStatusBadge({\n status,\n label,\n className,\n}: {\n status: FeedbackStatus;\n /** Over the `feedbackStatus` label (0.27.0: optional). */\n label?: ReactNode;\n className?: string;\n}) {\n const names = useFeedbackStatusLabels();\n const meta = FEEDBACK_STATUS_META[status] ?? FEEDBACK_STATUS_META.OPEN;\n const name: string | undefined = names[status];\n const Icon = meta.icon;\n return (\n <span\n className={cn(\n \"inline-flex items-center gap-1 whitespace-nowrap rounded px-1.5 py-0.5 text-caption font-medium\",\n meta.activeBg,\n meta.activeText,\n className,\n )}\n >\n <Icon className=\"size-3\" aria-hidden />\n {label ?? name ?? status}\n </span>\n );\n}\n\n/**\n * A row of statuses to switch a report to — either a table column's icon row or\n * an expanded panel's labelled pills.\n *\n * One component for both, because everything except the class strings is the\n * same decision: which one is current, and when the buttons are disabled. Two\n * copies had already drifted — only the icon variant carried `aria-label` and\n * `aria-pressed`, so the pill row was unlabelled for a screen reader.\n *\n * WHICH statuses to offer is the caller's, not this component's:\n * {@link visibleFeedbackStatuses} for a compact column,\n * {@link selectableFeedbackStatuses} for an expanded row. Passing the list keeps\n * that policy readable at the two call sites instead of hiding it behind\n * `variant`.\n */\nexport function FeedbackStatusTransitions({\n status,\n statuses,\n canEdit,\n onPick,\n variant,\n label,\n commit,\n disabledReason: ownDisabledReason,\n className,\n}: {\n status: FeedbackStatus;\n statuses: FeedbackStatus[];\n canEdit: boolean;\n onPick: (status: FeedbackStatus) => void;\n variant: \"icon\" | \"pill\";\n /** What each status is called. Default (0.27.0): the `feedbackStatus` namespace —\n * before, every app passed its own translation. */\n label?: (status: FeedbackStatus) => string;\n /**\n * Every button here SAVES the moment it is pressed (0.27.0) — opt in and, under a\n * locked {@link WriteLockProvider}, the row stays as it is and says why: each status\n * the row could move to is `aria-disabled` but focusable, the click does nothing, and\n * the lock's reason is in the kit Tooltip and the button's description — the\n * `disabledReason` path `Button` takes. Off by default: a feedback inbox is not what\n * every app's lock is about (keksdose's shell lock is a read-only demo BUDGET, and a\n * report is not budget data).\n */\n commit?: CommitScope;\n /** Why the statuses cannot be changed right now, the same way (a lock's own reason\n * wins over it). Only read while `canEdit`: a read-only row is a picture of the\n * workflow, not a refused action. */\n disabledReason?: ReactNode;\n className?: string;\n}) {\n const names = useFeedbackStatusLabels();\n const nameOf = label ?? ((value: FeedbackStatus) => names[value]);\n const reason = useCommitReason(commit, ownDisabledReason);\n const locked = canEdit && reason !== undefined && reason !== null && reason !== false && reason !== \"\";\n const reasonId = useId();\n return (\n // Not a control: it only stops clicks on the buttons inside from bubbling to\n // the row. The buttons are the keyboard path, and Enter/Space on them fires\n // their own click, which lands here the same way.\n // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- propagation guard around real buttons\n <div\n className={cn(variant === \"icon\" ? \"flex items-center gap-0.5\" : \"flex flex-wrap gap-1.5\", className)}\n // A status control inside a clickable row must not also open the row.\n onClick={(event) => event.stopPropagation()}\n >\n {locked && (\n <span id={reasonId} hidden>\n {reason}\n </span>\n )}\n {statuses.map((value) => {\n const meta = FEEDBACK_STATUS_META[value];\n const Icon = meta.icon;\n const active = status === value;\n const name = nameOf(value);\n // Locked: focusable and described, never firing (see `commit`). The current\n // status stays natively disabled — it was never something to press.\n const refused = locked && !active;\n const button = (\n <button\n key={value}\n type=\"button\"\n disabled={refused ? undefined : !canEdit || active}\n aria-disabled={refused || undefined}\n aria-describedby={refused ? reasonId : undefined}\n onClick={() => {\n if (!refused) onPick(value);\n }}\n aria-label={name}\n aria-pressed={active}\n className={cn(\n \"transition-colors\",\n variant === \"icon\"\n ? cn(\n \"flex size-7 items-center justify-center rounded\",\n active ? cn(meta.activeBg, meta.activeText) : \"text-placeholder hover:text-secondary\",\n )\n : cn(\n \"inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs font-medium\",\n active\n ? cn(meta.activeBg, meta.activeText, \"border-transparent\")\n : // The translucent ink on hover (0.33, B′), as Button's secondary:\n // the well is the page in light, and these pills sit on either.\n \"border-subtle bg-transparent text-primary hover:bg-[color-mix(in_srgb,var(--text-primary)_7%,transparent)]\",\n ),\n // Both stay unclickable, but only the read-only case is DIMMED: the\n // current status is the one thing in the row that has to be legible\n // at a glance, and among eight pills a faded active one reads as\n // \"unavailable\" rather than as \"this is where the row stands\".\n (!canEdit || active) && \"cursor-default\",\n !canEdit && \"opacity-60\",\n refused && \"cursor-not-allowed opacity-50\",\n )}\n >\n <Icon className={variant === \"icon\" ? \"size-4\" : \"size-3.5\"} aria-hidden />\n {variant === \"pill\" && name}\n </button>\n );\n // Locked, the bubble says WHY in both variants; the icon's name is still its\n // `aria-label`, and the reason its description — the hidden copy above, which\n // stays put, rather than the bubble (as on `Button`: the FRAGMENT keeps Tooltip\n // from adding the bubble as a second, coming-and-going description).\n if (refused) {\n return (\n <Tooltip key={value} label={reason} side=\"top\" portal={variant === \"icon\" || undefined}>\n <>{button}</>\n </Tooltip>\n );\n }\n // The icon row has no visible label, so it needs the tooltip; the pills\n // carry theirs inline.\n //\n // ⚠️ ABOVE, and portalled (Keksdose live #339). `side=\"bottom\"` put the bubble\n // straight over the NEXT ROW's icons — in a queue you triage by running down it\n // with the pointer, that is the row you are about to reach, and it was covered\n // every time you paused on one. Above covers the row you have just left instead.\n //\n // `portal` is not cosmetic here. An absolutely-positioned bubble is clipped by\n // any ancestor that scrolls — this cell lives inside the DataTable's own\n // scroller — and it cannot flip: `placeTooltip` is what moves the bubble below\n // the icon for the FIRST row, where there is no room above it, instead of\n // painting it off the top of the list.\n return variant === \"icon\" ? (\n <Tooltip key={value} label={name} side=\"top\" portal>\n {button}\n </Tooltip>\n ) : (\n button\n );\n })}\n </div>\n );\n}\n\n/**\n * A textarea and its two buttons — for an outcome, for a note sent back with a\n * report, for a body being corrected.\n *\n * One component because all three are the same gesture: a draft that is not\n * committed until it is saved. Ctrl/⌘+Enter submits, which is the same shortcut\n * the compose dialog next door uses, so the habit carries across the feature.\n *\n * ## `initial` seeds the draft; it does not own it (changed 2026-09-22)\n *\n * This used to re-seed from `initial` in an effect, and that effect threw away\n * whatever was in the box — **and the picture attached to it** — every time the\n * prop arrived with a different value. `initial` is normally the saved note, so\n * the list refreshing under the editor, or the owner re-deriving the same string,\n * was enough: a triager halfway through a reply, with a screenshot picked out,\n * lost both and got the saved text back with no way to undo it.\n *\n * A draft belongs to the thing being edited, so switching to a different note is a\n * different editor — say so with a `key`:\n *\n * ```tsx\n * <FeedbackNoteEditor key={note.id} initial={note.body} … />\n * ```\n *\n * Where the id is not in hand at the call site, pass {@link resetKey} instead and\n * the editor re-seeds when THAT changes. Either way the decision is the caller's,\n * which is the point: the editor cannot tell a new subject from a new render.\n */\nexport function FeedbackNoteEditor({\n initial,\n pending,\n onSave,\n onCancel,\n saveLabel,\n cancelLabel,\n placeholder,\n label,\n rows = 3,\n attachment,\n resetKey,\n required = false,\n commit,\n}: {\n initial: string;\n pending: boolean;\n onSave: (value: string, attachment?: File | null) => void;\n onCancel: () => void;\n saveLabel: ReactNode;\n cancelLabel: ReactNode;\n /** A line above the field saying what to write, not an in-field placeholder:\n * a hint that disappears the moment somebody starts typing is a hint that is\n * gone exactly when it is being followed. */\n placeholder?: ReactNode;\n /** The box's accessible name (0.34). Without it the `placeholder` line names the\n * box, and without that the `feedbackDetail` namespace's `note` (\"Note\") does —\n * a box with neither was read out as just \"edit text\" (the showcase's axe check).\n * Given with a `placeholder`, this names the box and the line describes it. */\n label?: string;\n rows?: number;\n /** Offer a picture with the note (Steering Design feedback #128). Omitted,\n * the editor is exactly the text box it always was — which is what the\n * *outcome* editor beside it wants, since an outcome is the answer rather\n * than the evidence. */\n attachment?: FeedbackNoteAttachment;\n /** Change this to say \"the editor is now editing something ELSE\", and the draft\n * and its attachment are dropped and re-seeded from `initial`. For call sites\n * that cannot put a `key` on the editor (see the note above). Leave it out and\n * the draft is never thrown away behind the user's back. */\n resetKey?: string | number;\n /**\n * A blank note cannot be saved (0.27.0): Save stays disabled — and Ctrl/⌘+Enter does\n * nothing — until the trimmed text is non-empty, and the box is `aria-required`. For\n * the rework note, which the feedback contract requires (§3.4: a file alone cannot\n * be sent; keksdose `feedback-page.tsx:577` dropped the click silently instead, so a\n * press on Send with an empty box looked like a lost send). Default `false`: an\n * outcome or a description may be cleared on purpose.\n */\n required?: boolean;\n /** Save COMMITS (0.27.0): under a locked `WriteLockProvider` it is disabled the\n * `disabledReason` way, with the lock's reason, and Ctrl/⌘+Enter saves nothing. The\n * text box stays editable — nothing in it reaches the server until the save. */\n commit?: CommitScope;\n}) {\n const [draft, setDraft] = useState(initial);\n const [file, setFile] = useState<File | null>(null);\n // Adjusted during render rather than in an effect: an effect would paint the old\n // draft first and then replace it, and — far worse — it is a second definition of\n // when a draft dies that the caller cannot see. React re-runs this component\n // immediately, before anything is committed to the screen.\n const [seededFor, setSeededFor] = useState(resetKey);\n if (resetKey !== seededFor) {\n setSeededFor(resetKey);\n setDraft(initial);\n setFile(null);\n }\n // The editor's own root, which is where a paste made in the text box bubbles\n // to: the attachment field below is the box's sibling, so a paste in the box\n // never passes through the field's own subtree. It listens here instead\n // (Steering Design feedback #140).\n const root = useRef<HTMLDivElement>(null);\n const hintId = useId();\n const detailText = useKitLabels(\"feedbackDetail\", DEFAULT_FEEDBACK_DETAIL_LABELS);\n // The lock as the save button sees it, with its scope (§12.36): `commit &&\n // lock.locked` read a `{ except }` scope as `true`.\n const lock = useWriteLock(commit ?? false);\n const blank = required && !draft.trim();\n // The shortcut obeys what the button shows: no save while one is in flight, while the\n // note a `required` editor needs is missing, or while a `commit` save is locked.\n const canSubmit = !pending && !blank && !lock.locked;\n const submit = () => onSave(draft, file);\n return (\n // Not a control: a delegated Ctrl/Cmd+Enter shortcut for the textarea inside,\n // which is what holds focus. The save button is the non-shortcut path.\n // eslint-disable-next-line jsx-a11y/no-static-element-interactions -- delegated keyboard shortcut for the focused field\n <div\n ref={root}\n className=\"space-y-2\"\n onKeyDown={(event) => {\n if ((event.ctrlKey || event.metaKey) && event.key === \"Enter\" && canSubmit) {\n event.preventDefault();\n submit();\n }\n }}\n >\n {/* The line names the box (0.27.0): it is the only text that says what to write\n in it, and an unnamed textarea is read out as just \"edit text\". */}\n {placeholder && (\n <p id={hintId} className=\"text-xs text-muted\">\n {placeholder}\n </p>\n )}\n <Textarea\n rows={rows}\n value={draft}\n onChange={(event) => setDraft(event.target.value)}\n // Never nameless (round 0.34): the caller's `label`, else the line, else the\n // `feedbackDetail` namespace's `note`. With both, the line describes the box.\n aria-label={label ?? (placeholder ? undefined : detailText.note)}\n aria-labelledby={placeholder && label === undefined ? hintId : undefined}\n aria-describedby={placeholder && label !== undefined ? hintId : undefined}\n aria-required={required || undefined}\n />\n {attachment && (\n <FeedbackAttachmentField\n value={file}\n onChange={setFile}\n // A pause while the save is in flight (the field's G5b `disabled`): the note\n // goes out with the file AS IT WAS at the press, so a chip removed or added\n // mid-send would be a lie about what was sent.\n disabled={pending}\n labels={attachment.labels}\n accept={attachment.accept}\n maxBytes={attachment.maxBytes}\n onError={attachment.onError}\n onCaptureScreenshot={attachment.onCaptureScreenshot}\n buttonVariant={attachment.buttonVariant}\n buttonSize={attachment.buttonSize}\n // Within THIS editor's subtree, not on `document`: the editor sits\n // inline on a page that has other fields, so a paste made in one of\n // them is meant for that one. The textarea above is where the caret\n // already is, and it is the field's sibling — so the field is told\n // to listen on their common parent, where the paste bubbles to.\n pasteFrom={root}\n />\n )}\n <div className=\"flex justify-end gap-2\">\n <Button variant=\"ghost\" onClick={onCancel}>\n {cancelLabel}\n </Button>\n <Button variant=\"accent\" disabled={pending || blank} commit={commit} onClick={submit}>\n {saveLabel}\n </Button>\n </div>\n </div>\n );\n}\n\n/** What {@link FeedbackNoteEditor} needs in order to offer a picture with the\n * note: the same things {@link FeedbackAttachmentField} takes, so the reply path is\n * held to the app's own limits — and drawn in its own look — rather than the\n * defaults. */\nexport interface FeedbackNoteAttachment {\n /** Over the field's own `feedbackAttachment` namespace. Optional since 0.27.0 — the\n * field has read the provider's words since 0.7.0, so an app with a catalogue has\n * nothing to restate. */\n labels?: LabelOverride<FeedbackAttachmentLabels>;\n accept?: string[];\n maxBytes?: number;\n /** `info` (0.23.0) names the refused file and the limit. */\n onError?: (kind: \"type\" | \"size\", info: FeedbackAttachmentErrorInfo) => void;\n onCaptureScreenshot?: () => Promise<File | null>;\n /**\n * The add and capture buttons' look (0.25.0) — the field's `buttonVariant` (0.24.0),\n * Button's own `variant`. Default `\"secondary\"`, as before.\n *\n * The note editor sits inline in a triage panel, and its buttons end in a ghost\n * Cancel and a brand Save: two bordered full-size attachment buttons above them read\n * as a second form. The field could already be told otherwise; the editor builds it\n * itself and had nowhere to say so.\n */\n buttonVariant?: ButtonVariant;\n /** The add and capture buttons' size (0.25.0) — the field's `buttonSize`. Default\n * `\"md\"`; `\"sm\"` draws their icons at 14px. */\n buttonSize?: ButtonSize;\n}\n\n/**\n * One labelled block of an opened report.\n *\n * The detail panel is a stack of these, and it is a stack rather than a\n * component with fixed fields on purpose: the two apps keep a report's context\n * and its screenshot in genuinely different places, and a shell that insisted on\n * both would force one of them to invent a shape it does not have. What is\n * shared is what a section *looks* like — and that is all that made the two\n * panels read as different products.\n */\nexport function FeedbackDetailSection({\n title,\n action,\n children,\n}: {\n title: ReactNode;\n /** Something on the title's own line — an edit button, a resolved-at date. */\n action?: ReactNode;\n children: ReactNode;\n}) {\n return (\n <div>\n <div className=\"mb-1 flex items-center justify-between gap-2\">\n <div className=\"text-xs font-medium uppercase tracking-wide text-muted\">{title}</div>\n {action}\n </div>\n {children}\n </div>\n );\n}\n\n/** The stack a detail panel is. Here so the spacing between sections is decided\n * once rather than by whichever app was written second. */\nexport function FeedbackDetail({ children, className }: { children: ReactNode; className?: string }) {\n return <div className={cn(\"space-y-3 text-sm\", className)}>{children}</div>;\n}\n\n/** Prose inside a section — a body, an outcome, a note.\n *\n * `whitespace-pre-wrap` is the whole component: a report is written in\n * paragraphs, and a note appended to it later is separated by blank lines that\n * carry the entire \"this arrived after the answer\" reading. */\nexport function FeedbackProse({ children, empty }: { children?: string; empty?: ReactNode }) {\n if (!children) return <p className=\"text-placeholder\">{empty ?? \"—\"}</p>;\n return <p className=\"whitespace-pre-wrap text-secondary\">{children}</p>;\n}\n"],"mappings":"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","names":["FEEDBACK_CATEGORY_META","FEEDBACK_STATUS_META"]}
@@ -11,7 +11,13 @@ import { useKitLabels } from "../i18n/kit-labels.js";
11
11
  import { cn } from "../lib/cn.js";
12
12
  import { FILL_HEIGHT_CLASS } from "../shell/fill-height.js";
13
13
  import { EMPTY_FORMATTED_VALUE } from "../lib/format.js";
14
- import { FeedbackCategoryBadge, FeedbackDetail, FeedbackDetailSection, FeedbackProse, FeedbackStatusBadge } from "./feedback-inbox.js";
14
+ import {
15
+ FeedbackCategoryBadge,
16
+ FeedbackDetail,
17
+ FeedbackDetailSection,
18
+ FeedbackProse,
19
+ FeedbackStatusBadge
20
+ } from "./feedback-inbox.js";
15
21
  import { useFeedbackCategoryLabels } from "./feedback-labels.js";
16
22
  import { FeedbackCommentThread } from "./feedback-comment-thread.js";
17
23
  import { DEFAULT_FEEDBACK_PUBLIC_LABELS } from "./feedback-public-labels.js";