@eifi1/ui-kit 0.51.4 → 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 (757) hide show
  1. package/README.md +3 -3
  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 +1 -4
  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 +10 -8
  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.map +1 -1
  106. package/dist/components/data-table-groups.js +3 -1
  107. package/dist/components/data-table-groups.js.map +1 -1
  108. package/dist/components/data-table-head.js +44 -56
  109. package/dist/components/data-table-head.js.map +1 -1
  110. package/dist/components/data-table-mobile.js +31 -44
  111. package/dist/components/data-table-mobile.js.map +1 -1
  112. package/dist/components/data-table-pagination.js.map +1 -1
  113. package/dist/components/data-table-row-actions.js +19 -16
  114. package/dist/components/data-table-row-actions.js.map +1 -1
  115. package/dist/components/data-table-row.js.map +1 -1
  116. package/dist/components/data-table-shared.js.map +1 -1
  117. package/dist/components/data-table-sort.js.map +1 -1
  118. package/dist/components/data-table-virtual.js +4 -1
  119. package/dist/components/data-table-virtual.js.map +1 -1
  120. package/dist/components/data-table.js +30 -39
  121. package/dist/components/data-table.js.map +1 -1
  122. package/dist/components/date-picker.js +1 -6
  123. package/dist/components/date-picker.js.map +1 -1
  124. package/dist/components/date-range-picker.js.map +1 -1
  125. package/dist/components/description-list.js +10 -10
  126. package/dist/components/description-list.js.map +1 -1
  127. package/dist/components/dialog-frame.js +9 -2
  128. package/dist/components/dialog-frame.js.map +1 -1
  129. package/dist/components/disclosure.js +17 -27
  130. package/dist/components/disclosure.js.map +1 -1
  131. package/dist/components/dropdown.js +25 -35
  132. package/dist/components/dropdown.js.map +1 -1
  133. package/dist/components/empty-state.js.map +1 -1
  134. package/dist/components/entity-combobox.js.map +1 -1
  135. package/dist/components/error-boundary.js.map +1 -1
  136. package/dist/components/field-base.js +5 -5
  137. package/dist/components/field-base.js.map +1 -1
  138. package/dist/components/field-parts.d.ts +1 -1
  139. package/dist/components/field-parts.js +10 -14
  140. package/dist/components/field-parts.js.map +1 -1
  141. package/dist/components/field-sync.js +8 -10
  142. package/dist/components/field-sync.js.map +1 -1
  143. package/dist/components/file-button.js.map +1 -1
  144. package/dist/components/file-dropzone.js.map +1 -1
  145. package/dist/components/floating-panel.js +4 -1
  146. package/dist/components/floating-panel.js.map +1 -1
  147. package/dist/components/focus-ring.js.map +1 -1
  148. package/dist/components/form-actions.js.map +1 -1
  149. package/dist/components/grouped-picker.js +6 -8
  150. package/dist/components/grouped-picker.js.map +1 -1
  151. package/dist/components/hover-menu.js +1 -4
  152. package/dist/components/hover-menu.js.map +1 -1
  153. package/dist/components/iban-input-labels.js.map +1 -1
  154. package/dist/components/icon-button.js +4 -1
  155. package/dist/components/icon-button.js.map +1 -1
  156. package/dist/components/icon-picker.js +2 -7
  157. package/dist/components/icon-picker.js.map +1 -1
  158. package/dist/components/image-grid.js +41 -50
  159. package/dist/components/image-grid.js.map +1 -1
  160. package/dist/components/inline-edit-field.js.map +1 -1
  161. package/dist/components/inline-entity-combobox.js +2 -8
  162. package/dist/components/inline-entity-combobox.js.map +1 -1
  163. package/dist/components/input.js +14 -2
  164. package/dist/components/input.js.map +1 -1
  165. package/dist/components/jump-calendar.js +1 -7
  166. package/dist/components/jump-calendar.js.map +1 -1
  167. package/dist/components/legal-labels.js.map +1 -1
  168. package/dist/components/legal-page.js +14 -2
  169. package/dist/components/legal-page.js.map +1 -1
  170. package/dist/components/legal.js +12 -2
  171. package/dist/components/legal.js.map +1 -1
  172. package/dist/components/lightbox.js.map +1 -1
  173. package/dist/components/line-items.js +26 -36
  174. package/dist/components/line-items.js.map +1 -1
  175. package/dist/components/list.js +22 -10
  176. package/dist/components/list.js.map +1 -1
  177. package/dist/components/measured-grid.js +29 -53
  178. package/dist/components/measured-grid.js.map +1 -1
  179. package/dist/components/menu-item.js.map +1 -1
  180. package/dist/components/mini-calendar.js +13 -31
  181. package/dist/components/mini-calendar.js.map +1 -1
  182. package/dist/components/money-field.js.map +1 -1
  183. package/dist/components/month-picker.js +10 -21
  184. package/dist/components/month-picker.js.map +1 -1
  185. package/dist/components/multi-entity-combobox.js +3 -16
  186. package/dist/components/multi-entity-combobox.js.map +1 -1
  187. package/dist/components/multi-select.js.map +1 -1
  188. package/dist/components/nav-pills.js +10 -1
  189. package/dist/components/nav-pills.js.map +1 -1
  190. package/dist/components/number-field.js.map +1 -1
  191. package/dist/components/number-input.js +1 -7
  192. package/dist/components/number-input.js.map +1 -1
  193. package/dist/components/numpad-sheet.js +1 -4
  194. package/dist/components/numpad-sheet.js.map +1 -1
  195. package/dist/components/one-time-code-input.js +1 -7
  196. package/dist/components/one-time-code-input.js.map +1 -1
  197. package/dist/components/page-contents.js +13 -22
  198. package/dist/components/page-contents.js.map +1 -1
  199. package/dist/components/page-header.js +1 -11
  200. package/dist/components/page-header.js.map +1 -1
  201. package/dist/components/password-strength-labels.js.map +1 -1
  202. package/dist/components/password-strength.js +2 -8
  203. package/dist/components/password-strength.js.map +1 -1
  204. package/dist/components/phone-input.js +42 -57
  205. package/dist/components/phone-input.js.map +1 -1
  206. package/dist/components/picker-sheet.js.map +1 -1
  207. package/dist/components/pie-chart.js +1 -8
  208. package/dist/components/pie-chart.js.map +1 -1
  209. package/dist/components/popover.js.map +1 -1
  210. package/dist/components/progress-bar.js +16 -9
  211. package/dist/components/progress-bar.js.map +1 -1
  212. package/dist/components/qr-code.js +1 -8
  213. package/dist/components/qr-code.js.map +1 -1
  214. package/dist/components/row-actions.js +5 -1
  215. package/dist/components/row-actions.js.map +1 -1
  216. package/dist/components/search-field.js.map +1 -1
  217. package/dist/components/select.js +28 -38
  218. package/dist/components/select.js.map +1 -1
  219. package/dist/components/separator.js +1 -5
  220. package/dist/components/separator.js.map +1 -1
  221. package/dist/components/series-chart-axes.js +4 -1
  222. package/dist/components/series-chart-axes.js.map +1 -1
  223. package/dist/components/series-chart-budget.js.map +1 -1
  224. package/dist/components/series-chart-ticks.js.map +1 -1
  225. package/dist/components/series-chart.js +29 -45
  226. package/dist/components/series-chart.js.map +1 -1
  227. package/dist/components/settings-fields.js +21 -17
  228. package/dist/components/settings-fields.js.map +1 -1
  229. package/dist/components/share-card.js +4 -9
  230. package/dist/components/share-card.js.map +1 -1
  231. package/dist/components/sign-chip.js.map +1 -1
  232. package/dist/components/signature-pad.js +4 -13
  233. package/dist/components/signature-pad.js.map +1 -1
  234. package/dist/components/signed-amount.js +12 -5
  235. package/dist/components/signed-amount.js.map +1 -1
  236. package/dist/components/skeleton.js +2 -17
  237. package/dist/components/skeleton.js.map +1 -1
  238. package/dist/components/slider.js +13 -36
  239. package/dist/components/slider.js.map +1 -1
  240. package/dist/components/sparkline.js.map +1 -1
  241. package/dist/components/stat-tile.js +14 -10
  242. package/dist/components/stat-tile.js.map +1 -1
  243. package/dist/components/swatch-picker.js +14 -4
  244. package/dist/components/swatch-picker.js.map +1 -1
  245. package/dist/components/swipeable-row.d.ts +1 -1
  246. package/dist/components/swipeable-row.js +1 -8
  247. package/dist/components/swipeable-row.js.map +1 -1
  248. package/dist/components/table-skeleton.js +4 -1
  249. package/dist/components/table-skeleton.js.map +1 -1
  250. package/dist/components/table.js +12 -9
  251. package/dist/components/table.js.map +1 -1
  252. package/dist/components/tabs.js +5 -24
  253. package/dist/components/tabs.js.map +1 -1
  254. package/dist/components/text-link.js.map +1 -1
  255. package/dist/components/text.js.map +1 -1
  256. package/dist/components/tile-radio.js +1 -7
  257. package/dist/components/tile-radio.js.map +1 -1
  258. package/dist/components/time-input.js.map +1 -1
  259. package/dist/components/toast.js.map +1 -1
  260. package/dist/components/toggle-group.js +6 -1
  261. package/dist/components/toggle-group.js.map +1 -1
  262. package/dist/components/toggle-legend.js +2 -11
  263. package/dist/components/toggle-legend.js.map +1 -1
  264. package/dist/components/tooltip.js +2 -22
  265. package/dist/components/tooltip.js.map +1 -1
  266. package/dist/components/translation-review-editor.js +1 -4
  267. package/dist/components/translation-review-editor.js.map +1 -1
  268. package/dist/components/translation-review-parts.d.ts +1 -1
  269. package/dist/components/translation-review-parts.js +7 -1
  270. package/dist/components/translation-review-parts.js.map +1 -1
  271. package/dist/components/translation-review.js +18 -7
  272. package/dist/components/translation-review.js.map +1 -1
  273. package/dist/components/tree-view.js +14 -10
  274. package/dist/components/tree-view.js.map +1 -1
  275. package/dist/components/treemap.js +1 -13
  276. package/dist/components/treemap.js.map +1 -1
  277. package/dist/components/trigger-aria.js.map +1 -1
  278. package/dist/components/use-table-state.js +14 -11
  279. package/dist/components/use-table-state.js.map +1 -1
  280. package/dist/components/user-avatar.js +3 -26
  281. package/dist/components/user-avatar.js.map +1 -1
  282. package/dist/demo/demo-start.js +8 -4
  283. package/dist/demo/demo-start.js.map +1 -1
  284. package/dist/eslint/index.js +1 -1
  285. package/dist/feedback/feedback-attachment-labels.js.map +1 -1
  286. package/dist/feedback/feedback-attachment.js +1 -8
  287. package/dist/feedback/feedback-attachment.js.map +1 -1
  288. package/dist/feedback/feedback-capture.d.ts +1 -1
  289. package/dist/feedback/feedback-capture.js +3 -1
  290. package/dist/feedback/feedback-capture.js.map +1 -1
  291. package/dist/feedback/feedback-comment-thread.js +27 -13
  292. package/dist/feedback/feedback-comment-thread.js.map +1 -1
  293. package/dist/feedback/feedback-context.js +1 -4
  294. package/dist/feedback/feedback-context.js.map +1 -1
  295. package/dist/feedback/feedback-dialog.js.map +1 -1
  296. package/dist/feedback/feedback-inbox.js.map +1 -1
  297. package/dist/feedback/feedback-public-list.js +7 -1
  298. package/dist/feedback/feedback-public-list.js.map +1 -1
  299. package/dist/feedback/feedback-record.js +1 -5
  300. package/dist/feedback/feedback-record.js.map +1 -1
  301. package/dist/feedback/feedback-replies.js +25 -10
  302. package/dist/feedback/feedback-replies.js.map +1 -1
  303. package/dist/feedback/feedback-row-detail.js.map +1 -1
  304. package/dist/feedback/feedback-status-undo.js.map +1 -1
  305. package/dist/feedback/feedback-submit-dialog.js.map +1 -1
  306. package/dist/feedback/feedback-table.js +4 -1
  307. package/dist/feedback/feedback-table.js.map +1 -1
  308. package/dist/feedback/feedback-thread.js +2 -13
  309. package/dist/feedback/feedback-thread.js.map +1 -1
  310. package/dist/feedback/feedback-vocabulary.js.map +1 -1
  311. package/dist/feedback/feedback-vote-button.js.map +1 -1
  312. package/dist/hooks/use-anchored-rect.js.map +1 -1
  313. package/dist/hooks/use-document-title.js.map +1 -1
  314. package/dist/hooks/use-file-drop.js.map +1 -1
  315. package/dist/hooks/use-file-picker-labels.js.map +1 -1
  316. package/dist/hooks/use-file-picker.js.map +1 -1
  317. package/dist/hooks/use-focus-trap.js.map +1 -1
  318. package/dist/hooks/use-overlay-history.js +10 -5
  319. package/dist/hooks/use-overlay-history.js.map +1 -1
  320. package/dist/hooks/use-row-swipe.d.ts +1 -1
  321. package/dist/hooks/use-row-swipe.js +1 -6
  322. package/dist/hooks/use-row-swipe.js.map +1 -1
  323. package/dist/i18n/defaults.js +1 -4
  324. package/dist/i18n/defaults.js.map +1 -1
  325. package/dist/i18n/german.js +4 -1
  326. package/dist/i18n/german.js.map +1 -1
  327. package/dist/i18n/kit-labels.js +2 -8
  328. package/dist/i18n/kit-labels.js.map +1 -1
  329. package/dist/i18n/languages.js.map +1 -1
  330. package/dist/i18n/locales/en.js +4 -1
  331. package/dist/i18n/locales/en.js.map +2 -2
  332. package/dist/i18n/locales/es.js +4 -1
  333. package/dist/i18n/locales/es.js.map +1 -1
  334. package/dist/i18n/locales/fr.js +4 -1
  335. package/dist/i18n/locales/fr.js.map +1 -1
  336. package/dist/i18n/locales/hu.js +4 -1
  337. package/dist/i18n/locales/hu.js.map +1 -1
  338. package/dist/i18n/locales/it.js +4 -1
  339. package/dist/i18n/locales/it.js.map +1 -1
  340. package/dist/i18n/locales/zh.js.map +1 -1
  341. package/dist/i18n/plurals.js.map +1 -1
  342. package/dist/i18n/review.js +13 -7
  343. package/dist/i18n/review.js.map +1 -1
  344. package/dist/i18n/shared-labels.js.map +1 -1
  345. package/dist/index.d.ts +16 -16
  346. package/dist/index.js +40 -23
  347. package/dist/index.js.map +1 -1
  348. package/dist/landing/access.js +1 -4
  349. package/dist/landing/access.js.map +1 -1
  350. package/dist/landing/landing-sections.js +35 -38
  351. package/dist/landing/landing-sections.js.map +1 -1
  352. package/dist/landing/page-seo.js.map +1 -1
  353. package/dist/lib/calc.js.map +1 -1
  354. package/dist/lib/clipboard-minus.js.map +1 -1
  355. package/dist/lib/dates.js.map +1 -1
  356. package/dist/lib/decimal-marks.js.map +1 -1
  357. package/dist/lib/device-settings.js +2 -1
  358. package/dist/lib/device-settings.js.map +1 -1
  359. package/dist/lib/direction.js.map +1 -1
  360. package/dist/lib/iban.js.map +1 -1
  361. package/dist/lib/logger.js +1 -5
  362. package/dist/lib/logger.js.map +1 -1
  363. package/dist/lib/person-name.js.map +1 -1
  364. package/dist/lib/phone.js.map +1 -1
  365. package/dist/lib/qr-encode.js +344 -8
  366. package/dist/lib/qr-encode.js.map +1 -1
  367. package/dist/lib/session-refresh.js.map +1 -1
  368. package/dist/lib/table-text.js +15 -3
  369. package/dist/lib/table-text.js.map +1 -1
  370. package/dist/lib/text-match.d.ts +4 -0
  371. package/dist/lib/text-match.js.map +1 -1
  372. package/dist/lib/translation-review.js.map +1 -1
  373. package/dist/notifications/notification-inbox.js +63 -44
  374. package/dist/notifications/notification-inbox.js.map +1 -1
  375. package/dist/notifications/notification-parts.js.map +1 -1
  376. package/dist/notifications/notification-types-setting.js.map +1 -1
  377. package/dist/notifications/notifications-labels.js.map +1 -1
  378. package/dist/notifications/push-device-setting.js +6 -1
  379. package/dist/notifications/push-device-setting.js.map +1 -1
  380. package/dist/notifications/push-devices-setting.js.map +1 -1
  381. package/dist/notifications/push-worker.js.map +1 -1
  382. package/dist/notifications/quiet-hours-setting.js.map +1 -1
  383. package/dist/notifications/use-push-device.js +4 -1
  384. package/dist/notifications/use-push-device.js.map +1 -1
  385. package/dist/offline/pwa-update-prompt.d.ts +1 -1
  386. package/dist/offline/pwa-update-prompt.js +7 -1
  387. package/dist/offline/pwa-update-prompt.js.map +1 -1
  388. package/dist/offline/query-persistence.js.map +1 -1
  389. package/dist/offline.d.ts +1 -1
  390. package/dist/offline.js +6 -1
  391. package/dist/offline.js.map +1 -1
  392. package/dist/pay/pay.css +8 -1
  393. package/dist/rhf/fields.d.ts +1 -2
  394. package/dist/rhf/fields.js +1 -10
  395. package/dist/rhf/fields.js.map +1 -1
  396. package/dist/rhf/form-core.js +1 -4
  397. package/dist/rhf/form-core.js.map +1 -1
  398. package/dist/rhf/line-items.js.map +1 -1
  399. package/dist/rhf/use-rhf-wizard-step.js.map +1 -1
  400. package/dist/search/command-palette.js +71 -62
  401. package/dist/search/command-palette.js.map +1 -1
  402. package/dist/search/global-search.js +34 -5
  403. package/dist/search/global-search.js.map +1 -1
  404. package/dist/search/search-index.js.map +1 -1
  405. package/dist/settings/device-reset-setting.js +1 -6
  406. package/dist/settings/device-reset-setting.js.map +1 -1
  407. package/dist/settings/settings-heading.js.map +1 -1
  408. package/dist/settings/settings-layout.js +9 -20
  409. package/dist/settings/settings-layout.js.map +1 -1
  410. package/dist/settings/use-settings-route.js.map +1 -1
  411. package/dist/shell/app-shell.js +62 -31
  412. package/dist/shell/app-shell.js.map +1 -1
  413. package/dist/shell/auth-layout.js +1 -10
  414. package/dist/shell/auth-layout.js.map +1 -1
  415. package/dist/shell/dev-menu.js +12 -6
  416. package/dist/shell/dev-menu.js.map +1 -1
  417. package/dist/shell/option-switcher-menu.js.map +1 -1
  418. package/dist/shell/sync-status-indicator.js +7 -6
  419. package/dist/shell/sync-status-indicator.js.map +1 -1
  420. package/dist/shell/top-bar-parts-labels.js.map +1 -1
  421. package/dist/shell/topbar-action-menu.js.map +1 -1
  422. package/dist/shell/topbar-controls.js +1 -10
  423. package/dist/shell/topbar-controls.js.map +1 -1
  424. package/dist/shell/tour-menu.js +9 -1
  425. package/dist/shell/tour-menu.js.map +1 -1
  426. package/dist/sync/db.js.map +1 -1
  427. package/dist/sync/engine.js +24 -6
  428. package/dist/sync/engine.js.map +1 -1
  429. package/dist/sync.d.ts +2 -2
  430. package/dist/sync.js +7 -1
  431. package/dist/sync.js.map +1 -1
  432. package/dist/testing/css-variables.js.map +1 -1
  433. package/dist/testing/index.d.ts +1 -1
  434. package/dist/testing/index.js +1 -3
  435. package/dist/testing/index.js.map +1 -1
  436. package/dist/testing/react.js.map +1 -1
  437. package/dist/testing/viewport.js +7 -4
  438. package/dist/testing/viewport.js.map +1 -1
  439. package/dist/theme/contrast-tokens.js +13 -2
  440. package/dist/theme/contrast-tokens.js.map +1 -1
  441. package/dist/theme/palette-derive.d.ts +1 -1
  442. package/dist/theme/palette-derive.js +18 -29
  443. package/dist/theme/palette-derive.js.map +1 -1
  444. package/dist/theme/palette-presets.js.map +1 -1
  445. package/dist/theme/palette-store.js.map +1 -1
  446. package/dist/theme/text-size.js.map +1 -1
  447. package/dist/theme/theme-store.js +1 -3
  448. package/dist/theme/theme-store.js.map +1 -1
  449. package/dist/tour/tour-catalogue.js.map +1 -1
  450. package/dist/tour/tour-dock.js +5 -1
  451. package/dist/tour/tour-dock.js.map +1 -1
  452. package/dist/tour/tour-engine.js +1 -4
  453. package/dist/tour/tour-engine.js.map +1 -1
  454. package/dist/tour/tour.js +21 -12
  455. package/dist/tour/tour.js.map +1 -1
  456. package/dist/tour/use-tour-progress.js.map +1 -1
  457. package/dist/tour/use-tour-url-sync.js.map +1 -1
  458. package/dist/wizard/stepper-nav.js +3 -24
  459. package/dist/wizard/stepper-nav.js.map +1 -1
  460. package/dist/wizard/types.js.map +1 -1
  461. package/dist/wizard/use-wizard.js +24 -28
  462. package/dist/wizard/use-wizard.js.map +1 -1
  463. package/dist/wizard/validation.js.map +1 -1
  464. package/dist/wizard/wizard-context.js.map +1 -1
  465. package/dist/wizard/wizard-step.d.ts +1 -1
  466. package/dist/wizard/wizard-step.js +1 -8
  467. package/dist/wizard/wizard-step.js.map +1 -1
  468. package/dist/wizard/wizard-summary.js.map +1 -1
  469. package/package.json +5 -2
  470. package/src/account/account-settings-labels.ts +1 -2
  471. package/src/account/account-settings.tsx +11 -17
  472. package/src/account/delete-account-setting.tsx +1 -6
  473. package/src/account/email-change-setting-labels.ts +1 -2
  474. package/src/account/email-change-setting.tsx +3 -1
  475. package/src/account/passkeys-setting.tsx +1 -3
  476. package/src/account/sessions-setting.tsx +4 -2
  477. package/src/admin/admin-action-confirm.tsx +4 -7
  478. package/src/admin/admin-action-log.tsx +29 -29
  479. package/src/admin/admin-area-labels.ts +4 -1
  480. package/src/admin/admin-overview.tsx +8 -2
  481. package/src/admin/admin-sections.tsx +3 -1
  482. package/src/admin/admin-tool-links.tsx +7 -1
  483. package/src/admin/invitations-panel.tsx +5 -11
  484. package/src/admin/mail-test-card.tsx +1 -2
  485. package/src/admin/reviewer-scope-editor.tsx +4 -9
  486. package/src/admin/role-select.tsx +4 -2
  487. package/src/admin/user-roster.tsx +2 -11
  488. package/src/auth/accept-invitation.tsx +1 -7
  489. package/src/auth/auth-errors.ts +1 -6
  490. package/src/auth/forgot-password-form.tsx +2 -7
  491. package/src/auth/register-form.tsx +7 -8
  492. package/src/auth/reset-password-form.tsx +2 -5
  493. package/src/auth/sign-in-form.tsx +15 -14
  494. package/src/auth/verify-email.tsx +2 -7
  495. package/src/billing/billing-banner.tsx +3 -7
  496. package/src/billing/billing-labels.ts +6 -4
  497. package/src/billing/checkout-processing.ts +0 -1
  498. package/src/billing/plan-card.tsx +9 -8
  499. package/src/billing/plan-limit-toast.tsx +1 -2
  500. package/src/billing/plan-limit.ts +1 -2
  501. package/src/billing/plan-price.ts +1 -4
  502. package/src/billing/plan-switch.tsx +1 -2
  503. package/src/billing/subscription-actions.tsx +5 -2
  504. package/src/components/account-chips.tsx +1 -7
  505. package/src/components/alert-banner.tsx +2 -8
  506. package/src/components/amount-input.tsx +206 -178
  507. package/src/components/authed-image.tsx +7 -4
  508. package/src/components/autocomplete.tsx +7 -9
  509. package/src/components/bulk-action-bar.tsx +2 -7
  510. package/src/components/button-group.tsx +1 -2
  511. package/src/components/button-look.ts +6 -17
  512. package/src/components/button.tsx +1 -4
  513. package/src/components/calculator.tsx +6 -9
  514. package/src/components/calendar-heatmap.tsx +5 -10
  515. package/src/components/card.tsx +9 -13
  516. package/src/components/chart-zoom-core.tsx +6 -33
  517. package/src/components/chart.tsx +8 -31
  518. package/src/components/checkbox-group.tsx +1 -4
  519. package/src/components/chip-input.tsx +6 -6
  520. package/src/components/chip.tsx +14 -26
  521. package/src/components/choice-card.tsx +4 -18
  522. package/src/components/column-mapper-labels.ts +3 -10
  523. package/src/components/column-mapper.tsx +33 -122
  524. package/src/components/combobox-core.tsx +12 -42
  525. package/src/components/combobox.tsx +2 -6
  526. package/src/components/confirm-dialog.tsx +1 -3
  527. package/src/components/copy-button.tsx +2 -4
  528. package/src/components/country-select.tsx +9 -21
  529. package/src/components/currency-option-list.tsx +13 -14
  530. package/src/components/currency-select.tsx +2 -7
  531. package/src/components/danger-confirm.tsx +8 -9
  532. package/src/components/data-table-cells.tsx +1 -2
  533. package/src/components/data-table-column-settings.tsx +1 -3
  534. package/src/components/data-table-filter-popover.tsx +5 -19
  535. package/src/components/data-table-filters.ts +1 -6
  536. package/src/components/data-table-fit.ts +1 -3
  537. package/src/components/data-table-groups.tsx +3 -1
  538. package/src/components/data-table-head.tsx +27 -45
  539. package/src/components/data-table-mobile.tsx +99 -108
  540. package/src/components/data-table-pagination.tsx +2 -6
  541. package/src/components/data-table-row-actions.tsx +25 -24
  542. package/src/components/data-table-row.tsx +16 -17
  543. package/src/components/data-table-shared.tsx +10 -16
  544. package/src/components/data-table-sort.ts +1 -2
  545. package/src/components/data-table-virtual.ts +9 -4
  546. package/src/components/data-table.tsx +489 -501
  547. package/src/components/date-picker.tsx +1 -7
  548. package/src/components/date-range-picker.tsx +3 -13
  549. package/src/components/description-list.tsx +7 -13
  550. package/src/components/dialog-frame.tsx +12 -3
  551. package/src/components/disclosure.tsx +8 -12
  552. package/src/components/dropdown.tsx +3 -11
  553. package/src/components/empty-state.tsx +1 -3
  554. package/src/components/entity-combobox.tsx +2 -6
  555. package/src/components/error-boundary.tsx +7 -7
  556. package/src/components/field-base.tsx +8 -14
  557. package/src/components/field-parts.tsx +12 -23
  558. package/src/components/field-sync.tsx +7 -11
  559. package/src/components/file-button.tsx +7 -3
  560. package/src/components/file-dropzone.tsx +7 -12
  561. package/src/components/floating-panel.tsx +44 -51
  562. package/src/components/focus-ring.ts +1 -2
  563. package/src/components/form-actions.tsx +1 -4
  564. package/src/components/grouped-picker.tsx +6 -8
  565. package/src/components/hover-menu.tsx +1 -4
  566. package/src/components/iban-input-labels.ts +1 -2
  567. package/src/components/icon-button.tsx +46 -48
  568. package/src/components/icon-picker.tsx +4 -17
  569. package/src/components/image-grid.tsx +1 -6
  570. package/src/components/inline-edit-field.tsx +1 -5
  571. package/src/components/inline-entity-combobox.tsx +6 -21
  572. package/src/components/input.tsx +14 -2
  573. package/src/components/jump-calendar.tsx +2 -6
  574. package/src/components/legal-labels.ts +1 -2
  575. package/src/components/legal-page.tsx +18 -7
  576. package/src/components/legal.tsx +14 -5
  577. package/src/components/lightbox.tsx +1 -3
  578. package/src/components/line-items.tsx +7 -15
  579. package/src/components/list.tsx +29 -25
  580. package/src/components/measured-grid.tsx +8 -30
  581. package/src/components/menu-item.tsx +1 -3
  582. package/src/components/mini-calendar.tsx +129 -159
  583. package/src/components/money-field.tsx +4 -2
  584. package/src/components/month-picker.tsx +14 -21
  585. package/src/components/multi-entity-combobox.tsx +6 -26
  586. package/src/components/multi-select.tsx +5 -9
  587. package/src/components/nav-pills.tsx +10 -2
  588. package/src/components/number-field.tsx +18 -15
  589. package/src/components/number-input.tsx +3 -19
  590. package/src/components/numpad-sheet.tsx +9 -13
  591. package/src/components/one-time-code-input.tsx +6 -11
  592. package/src/components/page-contents.tsx +2 -9
  593. package/src/components/page-header.tsx +5 -10
  594. package/src/components/password-strength-labels.ts +1 -2
  595. package/src/components/password-strength.tsx +6 -9
  596. package/src/components/phone-input.tsx +2 -13
  597. package/src/components/picker-sheet.tsx +2 -5
  598. package/src/components/pie-chart.tsx +6 -12
  599. package/src/components/popover.tsx +2 -1
  600. package/src/components/progress-bar.tsx +16 -25
  601. package/src/components/qr-code.tsx +2 -9
  602. package/src/components/row-actions.tsx +9 -1
  603. package/src/components/search-field.tsx +4 -5
  604. package/src/components/select.tsx +2 -10
  605. package/src/components/separator.tsx +1 -5
  606. package/src/components/series-chart-axes.ts +5 -5
  607. package/src/components/series-chart-budget.ts +1 -4
  608. package/src/components/series-chart-ticks.ts +1 -4
  609. package/src/components/series-chart.tsx +43 -83
  610. package/src/components/settings-fields.tsx +20 -15
  611. package/src/components/share-card.tsx +6 -14
  612. package/src/components/sign-chip.tsx +16 -16
  613. package/src/components/signature-pad.tsx +21 -13
  614. package/src/components/signed-amount.tsx +6 -14
  615. package/src/components/skeleton.tsx +2 -13
  616. package/src/components/slider.tsx +9 -20
  617. package/src/components/sparkline.tsx +2 -13
  618. package/src/components/stat-tile.tsx +13 -24
  619. package/src/components/swatch-picker.tsx +8 -3
  620. package/src/components/swipeable-row.tsx +2 -11
  621. package/src/components/table-skeleton.tsx +4 -1
  622. package/src/components/table.tsx +12 -8
  623. package/src/components/tabs.tsx +24 -39
  624. package/src/components/text-link.tsx +6 -6
  625. package/src/components/text.tsx +1 -2
  626. package/src/components/tile-radio.tsx +6 -19
  627. package/src/components/time-input.tsx +4 -5
  628. package/src/components/toast.tsx +3 -12
  629. package/src/components/toggle-group.tsx +9 -9
  630. package/src/components/toggle-legend.tsx +4 -16
  631. package/src/components/tooltip.tsx +9 -48
  632. package/src/components/translation-review-editor.tsx +4 -4
  633. package/src/components/translation-review-parts.tsx +7 -1
  634. package/src/components/translation-review.tsx +15 -16
  635. package/src/components/tree-view.tsx +17 -20
  636. package/src/components/treemap.tsx +3 -15
  637. package/src/components/trigger-aria.ts +1 -4
  638. package/src/components/use-table-state.ts +16 -17
  639. package/src/components/user-avatar.tsx +4 -21
  640. package/src/demo/demo-start.tsx +8 -4
  641. package/src/feedback/feedback-attachment-labels.ts +2 -1
  642. package/src/feedback/feedback-attachment.tsx +16 -29
  643. package/src/feedback/feedback-capture.ts +3 -1
  644. package/src/feedback/feedback-comment-thread.tsx +9 -6
  645. package/src/feedback/feedback-context.tsx +1 -4
  646. package/src/feedback/feedback-dialog.tsx +3 -4
  647. package/src/feedback/feedback-inbox.tsx +3 -9
  648. package/src/feedback/feedback-public-list.tsx +7 -1
  649. package/src/feedback/feedback-record.ts +6 -14
  650. package/src/feedback/feedback-replies.tsx +10 -5
  651. package/src/feedback/feedback-row-detail.tsx +2 -5
  652. package/src/feedback/feedback-status-undo.ts +1 -4
  653. package/src/feedback/feedback-submit-dialog.tsx +4 -4
  654. package/src/feedback/feedback-table.tsx +12 -8
  655. package/src/feedback/feedback-thread.tsx +4 -16
  656. package/src/feedback/feedback-vocabulary.ts +72 -82
  657. package/src/feedback/feedback-vote-button.tsx +9 -8
  658. package/src/hooks/use-anchored-rect.ts +1 -4
  659. package/src/hooks/use-document-title.ts +3 -1
  660. package/src/hooks/use-file-drop.ts +1 -3
  661. package/src/hooks/use-file-picker-labels.ts +1 -2
  662. package/src/hooks/use-file-picker.tsx +1 -5
  663. package/src/hooks/use-focus-trap.ts +1 -2
  664. package/src/hooks/use-overlay-history.ts +10 -5
  665. package/src/hooks/use-row-swipe.ts +1 -6
  666. package/src/i18n/defaults.ts +1 -4
  667. package/src/i18n/german.ts +33 -49
  668. package/src/i18n/kit-labels.tsx +5 -20
  669. package/src/i18n/languages.ts +2 -8
  670. package/src/i18n/locales/en.ts +11 -31
  671. package/src/i18n/locales/es.ts +35 -49
  672. package/src/i18n/locales/fr.ts +34 -51
  673. package/src/i18n/locales/hu.ts +26 -37
  674. package/src/i18n/locales/it.ts +35 -52
  675. package/src/i18n/locales/zh.ts +14 -32
  676. package/src/i18n/plurals.ts +4 -1
  677. package/src/i18n/review.ts +18 -7
  678. package/src/i18n/shared-labels.ts +1 -2
  679. package/src/index.ts +106 -32
  680. package/src/landing/access.ts +2 -7
  681. package/src/landing/landing-sections.tsx +14 -15
  682. package/src/landing/page-seo.ts +1 -3
  683. package/src/lib/calc.ts +4 -1
  684. package/src/lib/clipboard-minus.ts +4 -1
  685. package/src/lib/dates.ts +2 -10
  686. package/src/lib/decimal-marks.ts +1 -3
  687. package/src/lib/device-settings.ts +2 -1
  688. package/src/lib/direction.ts +1 -3
  689. package/src/lib/iban.ts +89 -9
  690. package/src/lib/logger.ts +1 -5
  691. package/src/lib/person-name.ts +1 -3
  692. package/src/lib/phone.ts +5 -8
  693. package/src/lib/qr-encode.ts +32 -8
  694. package/src/lib/session-refresh.ts +5 -6
  695. package/src/lib/table-text.ts +24 -6
  696. package/src/lib/text-match.ts +15 -3
  697. package/src/lib/translation-review.ts +1 -3
  698. package/src/notifications/notification-inbox.tsx +17 -4
  699. package/src/notifications/notification-parts.tsx +1 -2
  700. package/src/notifications/notification-types-setting.tsx +19 -18
  701. package/src/notifications/notifications-labels.ts +1 -2
  702. package/src/notifications/push-device-setting.tsx +7 -1
  703. package/src/notifications/push-devices-setting.tsx +7 -3
  704. package/src/notifications/push-worker.ts +10 -2
  705. package/src/notifications/quiet-hours-setting.tsx +4 -1
  706. package/src/notifications/use-push-device.ts +4 -1
  707. package/src/offline/pwa-update-prompt.tsx +7 -1
  708. package/src/offline/query-persistence.ts +9 -6
  709. package/src/offline.ts +6 -1
  710. package/src/rhf/fields.tsx +50 -19
  711. package/src/rhf/form-core.tsx +2 -6
  712. package/src/rhf/line-items.tsx +5 -7
  713. package/src/rhf/use-rhf-wizard-step.ts +3 -12
  714. package/src/search/command-palette.tsx +15 -23
  715. package/src/search/global-search.tsx +38 -8
  716. package/src/search/search-index.ts +5 -1
  717. package/src/settings/device-reset-setting.tsx +5 -8
  718. package/src/settings/settings-heading.tsx +3 -1
  719. package/src/settings/settings-layout.tsx +6 -13
  720. package/src/settings/use-settings-route.ts +4 -3
  721. package/src/shell/app-shell.tsx +48 -24
  722. package/src/shell/auth-layout.tsx +1 -6
  723. package/src/shell/dev-menu.tsx +12 -6
  724. package/src/shell/option-switcher-menu.tsx +1 -3
  725. package/src/shell/sync-status-indicator.tsx +14 -12
  726. package/src/shell/top-bar-parts-labels.ts +1 -3
  727. package/src/shell/topbar-action-menu.tsx +9 -8
  728. package/src/shell/topbar-controls.tsx +5 -19
  729. package/src/shell/tour-menu.tsx +9 -1
  730. package/src/sync/db.ts +5 -1
  731. package/src/sync/engine.ts +44 -12
  732. package/src/sync.ts +16 -2
  733. package/src/testing/css-variables.ts +1 -2
  734. package/src/testing/index.ts +1 -6
  735. package/src/testing/react.ts +1 -2
  736. package/src/testing/viewport.ts +9 -9
  737. package/src/theme/contrast-tokens.ts +13 -2
  738. package/src/theme/palette-derive.ts +24 -50
  739. package/src/theme/palette-presets.ts +0 -1
  740. package/src/theme/palette-store.ts +1 -4
  741. package/src/theme/text-size.ts +1 -1
  742. package/src/theme/theme-store.ts +2 -6
  743. package/src/tour/tour-catalogue.tsx +4 -2
  744. package/src/tour/tour-dock.ts +5 -1
  745. package/src/tour/tour-engine.ts +2 -6
  746. package/src/tour/tour.tsx +28 -12
  747. package/src/tour/use-tour-progress.ts +8 -2
  748. package/src/tour/use-tour-url-sync.ts +3 -1
  749. package/src/vite-env.d.ts +1 -4
  750. package/src/wizard/stepper-nav.tsx +10 -35
  751. package/src/wizard/types.ts +1 -4
  752. package/src/wizard/use-wizard.ts +28 -43
  753. package/src/wizard/validation.ts +1 -5
  754. package/src/wizard/wizard-context.tsx +2 -6
  755. package/src/wizard/wizard-step.tsx +1 -8
  756. package/src/wizard/wizard-summary.tsx +2 -6
  757. package/tokens.css +393 -152
@@ -22,27 +22,8 @@ function UserAvatar({ name, email, person, size = "md", badge, className, ...res
22
22
  const initials = (person ? personInitials(person, locale) : "") || avatarInitials(name, email);
23
23
  if (badge) {
24
24
  return /* @__PURE__ */ jsxs("span", { ...rest, className: "relative inline-flex shrink-0", children: [
25
- /* @__PURE__ */ jsx(
26
- "span",
27
- {
28
- "aria-hidden": true,
29
- className: cn(
30
- CHIP,
31
- AVATAR_SIZES[size],
32
- className
33
- ),
34
- children: initials
35
- }
36
- ),
37
- /* @__PURE__ */ jsx(
38
- StatusDot,
39
- {
40
- ring: true,
41
- tone: badge.tone ?? "danger",
42
- size: BADGE_SIZES[size],
43
- className: "absolute -end-0.5 -top-0.5"
44
- }
45
- ),
25
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: cn(CHIP, AVATAR_SIZES[size], className), children: initials }),
26
+ /* @__PURE__ */ jsx(StatusDot, { ring: true, tone: badge.tone ?? "danger", size: BADGE_SIZES[size], className: "absolute -end-0.5 -top-0.5" }),
46
27
  /* @__PURE__ */ jsx("span", { className: "sr-only", children: badge.label })
47
28
  ] });
48
29
  }
@@ -51,11 +32,7 @@ function UserAvatar({ name, email, person, size = "md", badge, className, ...res
51
32
  {
52
33
  "aria-hidden": true,
53
34
  ...rest,
54
- className: cn(
55
- CHIP,
56
- AVATAR_SIZES[size],
57
- className
58
- ),
35
+ className: cn(CHIP, AVATAR_SIZES[size], className),
59
36
  children: initials
60
37
  }
61
38
  );
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/user-avatar.tsx"],"sourcesContent":["import type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { personInitials } from \"../lib/person-name\";\nimport type { PersonName } from \"../lib/person-name\";\nimport { useKitLocale } from \"../i18n/kit-labels\";\nimport { StatusDot } from \"./status-dot\";\nimport type { StatusDotSize, StatusDotTone } from \"./status-dot\";\n\n/** Initials from a display name (first + last) or, failing that, an email —\n * e.g. \"Marcel Eifert\" → \"ME\", \"marcel@x.com\" → \"MA\". App-agnostic.\n *\n * \"Failing that\" means BLANK, not merely absent. This was `name ?? email ?? \"?\"`,\n * and `??` falls through on null/undefined alone — so an empty or whitespace-only\n * display name short-circuited the email it had in hand and landed on the guard\n * below, returning \"?\" for a user whose address says \"MA\" perfectly well. Keksdose\n * cannot reach it (its `display_name` has `min_length=1`), but this is exported as\n * app-agnostic, and any consumer whose profile name is optional free text got a wall\n * of \"?\" chips. The guard is what shows the empty case was foreseen; it just\n * answered it at the wrong end. */\nexport function avatarInitials(name?: string | null, email?: string | null): string {\n const source = [name, email].map((s) => s?.trim()).find(Boolean) ?? \"?\";\n const parts = source.split(/\\s+/).filter(Boolean);\n if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();\n return source.slice(0, 2).toUpperCase();\n}\n\nconst AVATAR_SIZES = {\n sm: \"size-7 text-caption\",\n md: \"size-8 text-xs\",\n lg: \"size-12 text-base\",\n} as const;\n\nconst CHIP =\n \"inline-flex shrink-0 items-center justify-center rounded-full bg-inverse font-semibold text-inverse\";\n\n/** The dot grows with the avatar: 8px on `sm`, 10px on `md` (keksdose's), 12px on `lg`. */\nconst BADGE_SIZES: Record<keyof typeof AVATAR_SIZES, StatusDotSize> = { sm: \"sm\", md: \"md\", lg: \"lg\" };\n\n/** A status dot in the avatar's top corner — an unread count, presence. */\nexport interface UserAvatarBadge {\n /** What the dot MEANS, for a screen reader (\"3 unread\"). Required: a coloured circle\n * says nothing to someone who cannot see it. */\n label: ReactNode;\n /** Default `danger`, the unread dot both apps draw by hand. */\n tone?: StatusDotTone;\n}\n\n/** A `<span>`'s props, minus `children`: the content is the initials this computes\n * from `person`/`name`/`email`, so there is nothing for a caller to put inside. */\nexport interface UserAvatarProps extends Omit<ComponentPropsWithRef<\"span\">, \"children\"> {\n name?: string | null;\n email?: string | null;\n /**\n * The person's first and last name (0.29.0, docs/auth-harmonization.md §3.2) —\n * preferred over `name` whenever either part is filled in. The initials follow the\n * READER's language, the provider's `locale`: \"AE\" for Ada Example, \"EA\" in Hungarian\n * (family name first), the family name's first character alone in Chinese. See\n * {@link personInitials}.\n *\n * `name` stays the fallback for an API that still sends only `display_name`, and\n * `email` after it: a person whose two parts are both blank falls through to them.\n * A migrated user (the whole old display name in `first`, no `last`, §3.3) keeps the\n * initials they had.\n */\n person?: PersonName | null;\n size?: keyof typeof AVATAR_SIZES;\n /**\n * A status dot in the top-end corner (the top-right in LTR, top-left in RTL), ringed\n * in the surface colour so it reads as its own mark (keksdose account-menu ~385,\n * top-bar :152). Its `label` is read as text where the dot sits — inside a button\n * named by its content it joins that name. In `TopBarActionMenu`'s `trigger` the\n * button is named by the menu's `aria-label` followed by this label: \"Account menu\n * 3 unread\". The initials stay hidden either way, so the person's name is NOT part\n * of it — put it in `aria-label` (\"Marcel Eifert, account menu\") if it should be.\n */\n badge?: UserAvatarBadge | null;\n}\n\n/**\n * A round initials avatar — the common user chip shared across apps (feedback\n * #333). Purely presentational; wrap it in a button for the account menu trigger.\n */\nexport function UserAvatar({ name, email, person, size = \"md\", badge, className, ...rest }: UserAvatarProps) {\n const locale = useKitLocale();\n const initials = (person ? personInitials(person, locale) : \"\") || avatarInitials(name, email);\n if (badge) {\n // A wrapper, because the chip itself is `aria-hidden` and the badge label must not\n // be. `className` stays on the chip (a size override still sizes the circle); the\n // other attributes go on the wrapper, which is what a caller addresses. `relative`\n // also contains the sr-only text.\n return (\n <span {...rest} className=\"relative inline-flex shrink-0\">\n <span\n aria-hidden\n className={cn(\n CHIP,\n AVATAR_SIZES[size],\n className,\n )}\n >\n {initials}\n </span>\n <StatusDot\n ring\n tone={badge.tone ?? \"danger\"}\n size={BADGE_SIZES[size]}\n className=\"absolute -end-0.5 -top-0.5\"\n />\n <span className=\"sr-only\">{badge.label}</span>\n </span>\n );\n }\n return (\n <span\n // `aria-hidden` sits BEFORE the spread, unlike the structural attributes on the\n // other components here, because it is a DEFAULT rather than an invariant. The\n // avatar is decorative next to the name it belongs to — which is where all three\n // apps render it — so hiding it stops a screen reader saying \"ME\" after the word\n // \"Marcel Eifert\". A caller who renders one ALONE (an assignee column, a\n // presence dot) needs the opposite, and now has it: `aria-hidden={false}` plus an\n // `aria-label` arrives through `...rest` and wins.\n aria-hidden\n {...rest}\n className={cn(\n CHIP,\n AVATAR_SIZES[size],\n className,\n )}\n >\n {initials}\n </span>\n );\n}\n"],"mappings":";AA2FM,SACE,KADF;AA1FN,SAAS,UAAU;AACnB,SAAS,sBAAsB;AAE/B,SAAS,oBAAoB;AAC7B,SAAS,iBAAiB;AAcnB,SAAS,eAAe,MAAsB,OAA+B;AAClF,QAAM,SAAS,CAAC,MAAM,KAAK,EAAE,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,EAAE,KAAK,OAAO,KAAK;AACpE,QAAM,QAAQ,OAAO,MAAM,KAAK,EAAE,OAAO,OAAO;AAChD,MAAI,MAAM,UAAU,EAAG,SAAQ,MAAM,CAAC,EAAE,CAAC,IAAI,MAAM,CAAC,EAAE,CAAC,GAAG,YAAY;AACtE,SAAO,OAAO,MAAM,GAAG,CAAC,EAAE,YAAY;AACxC;AAEA,MAAM,eAAe;AAAA,EACnB,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,OACJ;AAGF,MAAM,cAAgE,EAAE,IAAI,MAAM,IAAI,MAAM,IAAI,KAAK;AA8C9F,SAAS,WAAW,EAAE,MAAM,OAAO,QAAQ,OAAO,MAAM,OAAO,WAAW,GAAG,KAAK,GAAoB;AAC3G,QAAM,SAAS,aAAa;AAC5B,QAAM,YAAY,SAAS,eAAe,QAAQ,MAAM,IAAI,OAAO,eAAe,MAAM,KAAK;AAC7F,MAAI,OAAO;AAKT,WACE,qBAAC,UAAM,GAAG,MAAM,WAAU,iCACxB;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,eAAW;AAAA,UACX,WAAW;AAAA,YACT;AAAA,YACA,aAAa,IAAI;AAAA,YACjB;AAAA,UACF;AAAA,UAEC;AAAA;AAAA,MACH;AAAA,MACA;AAAA,QAAC;AAAA;AAAA,UACC,MAAI;AAAA,UACJ,MAAM,MAAM,QAAQ;AAAA,UACpB,MAAM,YAAY,IAAI;AAAA,UACtB,WAAU;AAAA;AAAA,MACZ;AAAA,MACA,oBAAC,UAAK,WAAU,WAAW,gBAAM,OAAM;AAAA,OACzC;AAAA,EAEJ;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MAQC,eAAW;AAAA,MACV,GAAG;AAAA,MACJ,WAAW;AAAA,QACT;AAAA,QACA,aAAa,IAAI;AAAA,QACjB;AAAA,MACF;AAAA,MAEC;AAAA;AAAA,EACH;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/components/user-avatar.tsx"],"sourcesContent":["import type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { personInitials } from \"../lib/person-name\";\nimport type { PersonName } from \"../lib/person-name\";\nimport { useKitLocale } from \"../i18n/kit-labels\";\nimport { StatusDot } from \"./status-dot\";\nimport type { StatusDotSize, StatusDotTone } from \"./status-dot\";\n\n/** Initials from a display name (first + last) or, failing that, an email —\n * e.g. \"Marcel Eifert\" → \"ME\", \"marcel@x.com\" → \"MA\". App-agnostic.\n *\n * \"Failing that\" means BLANK, not merely absent. This was `name ?? email ?? \"?\"`,\n * and `??` falls through on null/undefined alone — so an empty or whitespace-only\n * display name short-circuited the email it had in hand and landed on the guard\n * below, returning \"?\" for a user whose address says \"MA\" perfectly well. Keksdose\n * cannot reach it (its `display_name` has `min_length=1`), but this is exported as\n * app-agnostic, and any consumer whose profile name is optional free text got a wall\n * of \"?\" chips. The guard is what shows the empty case was foreseen; it just\n * answered it at the wrong end. */\nexport function avatarInitials(name?: string | null, email?: string | null): string {\n const source = [name, email].map((s) => s?.trim()).find(Boolean) ?? \"?\";\n const parts = source.split(/\\s+/).filter(Boolean);\n if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();\n return source.slice(0, 2).toUpperCase();\n}\n\nconst AVATAR_SIZES = {\n sm: \"size-7 text-caption\",\n md: \"size-8 text-xs\",\n lg: \"size-12 text-base\",\n} as const;\n\nconst CHIP = \"inline-flex shrink-0 items-center justify-center rounded-full bg-inverse font-semibold text-inverse\";\n\n/** The dot grows with the avatar: 8px on `sm`, 10px on `md` (keksdose's), 12px on `lg`. */\nconst BADGE_SIZES: Record<keyof typeof AVATAR_SIZES, StatusDotSize> = { sm: \"sm\", md: \"md\", lg: \"lg\" };\n\n/** A status dot in the avatar's top corner — an unread count, presence. */\nexport interface UserAvatarBadge {\n /** What the dot MEANS, for a screen reader (\"3 unread\"). Required: a coloured circle\n * says nothing to someone who cannot see it. */\n label: ReactNode;\n /** Default `danger`, the unread dot both apps draw by hand. */\n tone?: StatusDotTone;\n}\n\n/** A `<span>`'s props, minus `children`: the content is the initials this computes\n * from `person`/`name`/`email`, so there is nothing for a caller to put inside. */\nexport interface UserAvatarProps extends Omit<ComponentPropsWithRef<\"span\">, \"children\"> {\n name?: string | null;\n email?: string | null;\n /**\n * The person's first and last name (0.29.0, docs/auth-harmonization.md §3.2) —\n * preferred over `name` whenever either part is filled in. The initials follow the\n * READER's language, the provider's `locale`: \"AE\" for Ada Example, \"EA\" in Hungarian\n * (family name first), the family name's first character alone in Chinese. See\n * {@link personInitials}.\n *\n * `name` stays the fallback for an API that still sends only `display_name`, and\n * `email` after it: a person whose two parts are both blank falls through to them.\n * A migrated user (the whole old display name in `first`, no `last`, §3.3) keeps the\n * initials they had.\n */\n person?: PersonName | null;\n size?: keyof typeof AVATAR_SIZES;\n /**\n * A status dot in the top-end corner (the top-right in LTR, top-left in RTL), ringed\n * in the surface colour so it reads as its own mark (keksdose account-menu ~385,\n * top-bar :152). Its `label` is read as text where the dot sits — inside a button\n * named by its content it joins that name. In `TopBarActionMenu`'s `trigger` the\n * button is named by the menu's `aria-label` followed by this label: \"Account menu\n * 3 unread\". The initials stay hidden either way, so the person's name is NOT part\n * of it — put it in `aria-label` (\"Marcel Eifert, account menu\") if it should be.\n */\n badge?: UserAvatarBadge | null;\n}\n\n/**\n * A round initials avatar — the common user chip shared across apps (feedback\n * #333). Purely presentational; wrap it in a button for the account menu trigger.\n */\nexport function UserAvatar({ name, email, person, size = \"md\", badge, className, ...rest }: UserAvatarProps) {\n const locale = useKitLocale();\n const initials = (person ? personInitials(person, locale) : \"\") || avatarInitials(name, email);\n if (badge) {\n // A wrapper, because the chip itself is `aria-hidden` and the badge label must not\n // be. `className` stays on the chip (a size override still sizes the circle); the\n // other attributes go on the wrapper, which is what a caller addresses. `relative`\n // also contains the sr-only text.\n return (\n <span {...rest} className=\"relative inline-flex shrink-0\">\n <span aria-hidden className={cn(CHIP, AVATAR_SIZES[size], className)}>\n {initials}\n </span>\n <StatusDot ring tone={badge.tone ?? \"danger\"} size={BADGE_SIZES[size]} className=\"absolute -end-0.5 -top-0.5\" />\n <span className=\"sr-only\">{badge.label}</span>\n </span>\n );\n }\n return (\n <span\n // `aria-hidden` sits BEFORE the spread, unlike the structural attributes on the\n // other components here, because it is a DEFAULT rather than an invariant. The\n // avatar is decorative next to the name it belongs to — which is where all three\n // apps render it — so hiding it stops a screen reader saying \"ME\" after the word\n // \"Marcel Eifert\". A caller who renders one ALONE (an assignee column, a\n // presence dot) needs the opposite, and now has it: `aria-hidden={false}` plus an\n // `aria-label` arrives through `...rest` and wins.\n aria-hidden\n {...rest}\n className={cn(CHIP, AVATAR_SIZES[size], className)}\n >\n {initials}\n </span>\n );\n}\n"],"mappings":";AA0FM,SACE,KADF;AAzFN,SAAS,UAAU;AACnB,SAAS,sBAAsB;AAE/B,SAAS,oBAAoB;AAC7B,SAAS,iBAAiB;AAcnB,SAAS,eAAe,MAAsB,OAA+B;AAClF,QAAM,SAAS,CAAC,MAAM,KAAK,EAAE,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,EAAE,KAAK,OAAO,KAAK;AACpE,QAAM,QAAQ,OAAO,MAAM,KAAK,EAAE,OAAO,OAAO;AAChD,MAAI,MAAM,UAAU,EAAG,SAAQ,MAAM,CAAC,EAAE,CAAC,IAAI,MAAM,CAAC,EAAE,CAAC,GAAG,YAAY;AACtE,SAAO,OAAO,MAAM,GAAG,CAAC,EAAE,YAAY;AACxC;AAEA,MAAM,eAAe;AAAA,EACnB,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,MAAM,OAAO;AAGb,MAAM,cAAgE,EAAE,IAAI,MAAM,IAAI,MAAM,IAAI,KAAK;AA8C9F,SAAS,WAAW,EAAE,MAAM,OAAO,QAAQ,OAAO,MAAM,OAAO,WAAW,GAAG,KAAK,GAAoB;AAC3G,QAAM,SAAS,aAAa;AAC5B,QAAM,YAAY,SAAS,eAAe,QAAQ,MAAM,IAAI,OAAO,eAAe,MAAM,KAAK;AAC7F,MAAI,OAAO;AAKT,WACE,qBAAC,UAAM,GAAG,MAAM,WAAU,iCACxB;AAAA,0BAAC,UAAK,eAAW,MAAC,WAAW,GAAG,MAAM,aAAa,IAAI,GAAG,SAAS,GAChE,oBACH;AAAA,MACA,oBAAC,aAAU,MAAI,MAAC,MAAM,MAAM,QAAQ,UAAU,MAAM,YAAY,IAAI,GAAG,WAAU,8BAA6B;AAAA,MAC9G,oBAAC,UAAK,WAAU,WAAW,gBAAM,OAAM;AAAA,OACzC;AAAA,EAEJ;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MAQC,eAAW;AAAA,MACV,GAAG;AAAA,MACJ,WAAW,GAAG,MAAM,aAAa,IAAI,GAAG,SAAS;AAAA,MAEhD;AAAA;AAAA,EACH;AAEJ;","names":[]}
@@ -48,10 +48,14 @@ function DemoStart({
48
48
  const accessLink = useAccessAction(access, landingLabels);
49
49
  const [refusal, setRefusal] = useState(null);
50
50
  useNoIndex(noIndex);
51
- useOncePerKey(ONCE, () => start(), (_key, result) => {
52
- if (result.ok) onStarted(result.value);
53
- else setRefusal(refusalOf(result.error));
54
- });
51
+ useOncePerKey(
52
+ ONCE,
53
+ () => start(),
54
+ (_key, result) => {
55
+ if (result.ok) onStarted(result.value);
56
+ else setRefusal(refusalOf(result.error));
57
+ }
58
+ );
55
59
  return /* @__PURE__ */ jsxs("div", { ...rest, className: cn("flex flex-col items-center gap-4 py-4 text-center", className), children: [
56
60
  /* @__PURE__ */ jsx("div", { role: "status", "aria-live": "polite", className: "w-full", children: refusal === null ? /* @__PURE__ */ jsxs("p", { className: "flex items-center justify-center gap-2 text-sm text-secondary", children: [
57
61
  /* @__PURE__ */ jsx(Spinner, { label: null, className: "size-5" }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/demo/demo-start.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ComponentPropsWithRef } from \"react\";\n\nimport { cn } from \"../lib/cn\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { Button } from \"../components/button\";\nimport { Spinner } from \"../components/spinner\";\nimport { useNoIndex } from \"../hooks/use-noindex\";\nimport { authErrorCode, isRateLimited, retryAfterSeconds } from \"../auth/auth-errors\";\nimport { useOncePerKey } from \"../auth/status-parts\";\nimport { useAccessAction } from \"../landing/access\";\nimport type { AccessChoice } from \"../landing/access\";\nimport type { LandingLabels } from \"../landing/landing-labels\";\nimport { useDemoLabels } from \"./demo-labels\";\nimport type { DemoLabels } from \"./demo-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\n\n/** Why a start was refused, in the page's terms (§5.2). */\ntype Refusal =\n | { reason: \"rateLimited\"; minutes?: number }\n | { reason: \"capacity\" }\n | { reason: \"unavailable\" }\n | { reason: \"failed\" };\n\n/**\n * The server's answer to `POST /auth/demo-session`, read as a refusal (§5.1, §6.2):\n * `demo_rate_limited` (429, with `Retry-After`), `demo_capacity` (429, without),\n * `demo_disabled` (404) and `demo_not_ready` (503) by their codes; a bare 429 as the\n * per-IP window too; anything else is a plain failure.\n */\nfunction refusalOf(error: unknown): Refusal {\n const code = authErrorCode(error);\n if (code === \"demo_capacity\") return { reason: \"capacity\" };\n if (code === \"demo_disabled\" || code === \"demo_not_ready\") return { reason: \"unavailable\" };\n if (code === \"demo_rate_limited\" || (code === undefined && isRateLimited(error))) {\n const seconds = retryAfterSeconds(error);\n return { reason: \"rateLimited\", minutes: seconds ? Math.ceil(seconds / 60) : undefined };\n }\n return { reason: \"failed\" };\n}\n\nfunction messageOf(refusal: Refusal, labels: DemoLabels): string {\n switch (refusal.reason) {\n case \"rateLimited\":\n return labels.rateLimited(refusal.minutes);\n case \"capacity\":\n return labels.capacity;\n case \"unavailable\":\n return labels.unavailable;\n case \"failed\":\n return labels.failed;\n }\n}\n\n/** The one start a mount makes. */\nconst ONCE = \"demo\";\n\nexport interface DemoStartProps<T> extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n /**\n * Create the demo: the app's `POST /auth/demo-session {locale}`. Resolve with what it\n * answered (the `TokenResponse`, `refresh_token: null`, `expires_at`); reject with the\n * client's error — the codes are read out of it. Called once per mount.\n */\n start: () => Promise<T>;\n /** The demo exists: store the session (and, for kastlan, `kastlan.demoSession`) and\n * replace to the app's home. The page keeps saying \"Starting the demo…\" meanwhile. */\n onStarted: (session: T) => void;\n /** \"Request access\" beside a refusal. */\n access: AccessChoice;\n /** \"Back to the start page\". Default `/`. */\n backHref?: string;\n /** `false` adds no `noindex` — for a preview. Default `true`: `/demo` is never indexed\n * (§4.3). */\n noIndex?: boolean;\n labels?: LabelOverride<DemoLabels>;\n /** The access words (`landing` namespace) for this instance. */\n landingLabels?: Partial<LandingLabels>;\n}\n\n/**\n * The `/demo` page's content (docs/landing-demo-harmonization.md §5.2): it starts the\n * demo ONCE — StrictMode's double mount included, keksdose's ref guard — and says\n * \"Starting the demo…\" until the app has the session and has moved on.\n *\n * A refusal says why, in the visitor's words, and offers \"Request access\" and \"Back to\n * the start page\":\n *\n * | code | says |\n * |---|---|\n * | `demo_rate_limited` (429) | \"Too many demos from this network. Try again in N min.\" — N from `Retry-After` |\n * | `demo_capacity` (429) | \"The demo is full right now. Try again later.\" |\n * | `demo_disabled` (404), `demo_not_ready` (503) | \"The demo isn't available right now.\" |\n * | anything else | \"The demo could not be started. Please try again.\" |\n *\n * keksdose fell back to `/register` with a toast; registration is invitation-only now,\n * so the way on is the access mail.\n *\n * Put it in the app's `AuthLayout`. A live demo session never reaches it: the `/demo`\n * route sits in `RedirectIfAuthed` WITHOUT `allowDemo`, which continues the live demo at\n * its resume target instead of starting a second one. A cold start is the kit's\n * `ServerWakeNotice`'s, once the app's watcher counts this POST\n * (`watchReadsAnd(/\\/auth\\/demo-session\\b/)`).\n */\nexport function DemoStart<T>({\n start,\n onStarted,\n access,\n backHref = \"/\",\n noIndex = true,\n labels: labelsProp,\n landingLabels,\n className,\n ...rest\n}: DemoStartProps<T>) {\n const labels = useDemoLabels(labelsProp);\n const accessLink = useAccessAction(access, landingLabels);\n const [refusal, setRefusal] = useState<Refusal | null>(null);\n useNoIndex(noIndex);\n useOncePerKey<T>(ONCE, () => start(), (_key, result) => {\n if (result.ok) onStarted(result.value);\n else setRefusal(refusalOf(result.error));\n });\n\n return (\n <div {...rest} className={cn(\"flex flex-col items-center gap-4 py-4 text-center\", className)}>\n {/* One live region, there from the start, so the refusal that replaces the\n progress line is announced (a region inserted with its text often is not). */}\n <div role=\"status\" aria-live=\"polite\" className=\"w-full\">\n {refusal === null ? (\n <p className=\"flex items-center justify-center gap-2 text-sm text-secondary\">\n <Spinner label={null} className=\"size-5\" />\n {labels.starting}\n </p>\n ) : (\n <AlertBanner tone={refusal.reason === \"failed\" ? \"danger\" : \"warning\"} className=\"text-start\">\n {messageOf(refusal, labels)}\n </AlertBanner>\n )}\n </div>\n {refusal !== null && (\n <div className=\"flex flex-wrap justify-center gap-3\">\n <Button href={accessLink.href} variant=\"accent\">\n {accessLink.label}\n </Button>\n <Button href={backHref} variant=\"secondary\">\n {labels.backToStart}\n </Button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";AAiIU,SACE,KADF;AAjIV,SAAS,gBAAgB;AAGzB,SAAS,UAAU;AACnB,SAAS,mBAAmB;AAC5B,SAAS,cAAc;AACvB,SAAS,eAAe;AACxB,SAAS,kBAAkB;AAC3B,SAAS,eAAe,eAAe,yBAAyB;AAChE,SAAS,qBAAqB;AAC9B,SAAS,uBAAuB;AAGhC,SAAS,qBAAqB;AAiB9B,SAAS,UAAU,OAAyB;AAC1C,QAAM,OAAO,cAAc,KAAK;AAChC,MAAI,SAAS,gBAAiB,QAAO,EAAE,QAAQ,WAAW;AAC1D,MAAI,SAAS,mBAAmB,SAAS,iBAAkB,QAAO,EAAE,QAAQ,cAAc;AAC1F,MAAI,SAAS,uBAAwB,SAAS,UAAa,cAAc,KAAK,GAAI;AAChF,UAAM,UAAU,kBAAkB,KAAK;AACvC,WAAO,EAAE,QAAQ,eAAe,SAAS,UAAU,KAAK,KAAK,UAAU,EAAE,IAAI,OAAU;AAAA,EACzF;AACA,SAAO,EAAE,QAAQ,SAAS;AAC5B;AAEA,SAAS,UAAU,SAAkB,QAA4B;AAC/D,UAAQ,QAAQ,QAAQ;AAAA,IACtB,KAAK;AACH,aAAO,OAAO,YAAY,QAAQ,OAAO;AAAA,IAC3C,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,OAAO;AAAA,EAClB;AACF;AAGA,MAAM,OAAO;AAgDN,SAAS,UAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,UAAU;AAAA,EACV,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAsB;AACpB,QAAM,SAAS,cAAc,UAAU;AACvC,QAAM,aAAa,gBAAgB,QAAQ,aAAa;AACxD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAyB,IAAI;AAC3D,aAAW,OAAO;AAClB,gBAAiB,MAAM,MAAM,MAAM,GAAG,CAAC,MAAM,WAAW;AACtD,QAAI,OAAO,GAAI,WAAU,OAAO,KAAK;AAAA,QAChC,YAAW,UAAU,OAAO,KAAK,CAAC;AAAA,EACzC,CAAC;AAED,SACE,qBAAC,SAAK,GAAG,MAAM,WAAW,GAAG,qDAAqD,SAAS,GAGzF;AAAA,wBAAC,SAAI,MAAK,UAAS,aAAU,UAAS,WAAU,UAC7C,sBAAY,OACX,qBAAC,OAAE,WAAU,iEACX;AAAA,0BAAC,WAAQ,OAAO,MAAM,WAAU,UAAS;AAAA,MACxC,OAAO;AAAA,OACV,IAEA,oBAAC,eAAY,MAAM,QAAQ,WAAW,WAAW,WAAW,WAAW,WAAU,cAC9E,oBAAU,SAAS,MAAM,GAC5B,GAEJ;AAAA,IACC,YAAY,QACX,qBAAC,SAAI,WAAU,uCACb;AAAA,0BAAC,UAAO,MAAM,WAAW,MAAM,SAAQ,UACpC,qBAAW,OACd;AAAA,MACA,oBAAC,UAAO,MAAM,UAAU,SAAQ,aAC7B,iBAAO,aACV;AAAA,OACF;AAAA,KAEJ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/demo/demo-start.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ComponentPropsWithRef } from \"react\";\n\nimport { cn } from \"../lib/cn\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { Button } from \"../components/button\";\nimport { Spinner } from \"../components/spinner\";\nimport { useNoIndex } from \"../hooks/use-noindex\";\nimport { authErrorCode, isRateLimited, retryAfterSeconds } from \"../auth/auth-errors\";\nimport { useOncePerKey } from \"../auth/status-parts\";\nimport { useAccessAction } from \"../landing/access\";\nimport type { AccessChoice } from \"../landing/access\";\nimport type { LandingLabels } from \"../landing/landing-labels\";\nimport { useDemoLabels } from \"./demo-labels\";\nimport type { DemoLabels } from \"./demo-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\n\n/** Why a start was refused, in the page's terms (§5.2). */\ntype Refusal =\n | { reason: \"rateLimited\"; minutes?: number }\n | { reason: \"capacity\" }\n | { reason: \"unavailable\" }\n | { reason: \"failed\" };\n\n/**\n * The server's answer to `POST /auth/demo-session`, read as a refusal (§5.1, §6.2):\n * `demo_rate_limited` (429, with `Retry-After`), `demo_capacity` (429, without),\n * `demo_disabled` (404) and `demo_not_ready` (503) by their codes; a bare 429 as the\n * per-IP window too; anything else is a plain failure.\n */\nfunction refusalOf(error: unknown): Refusal {\n const code = authErrorCode(error);\n if (code === \"demo_capacity\") return { reason: \"capacity\" };\n if (code === \"demo_disabled\" || code === \"demo_not_ready\") return { reason: \"unavailable\" };\n if (code === \"demo_rate_limited\" || (code === undefined && isRateLimited(error))) {\n const seconds = retryAfterSeconds(error);\n return { reason: \"rateLimited\", minutes: seconds ? Math.ceil(seconds / 60) : undefined };\n }\n return { reason: \"failed\" };\n}\n\nfunction messageOf(refusal: Refusal, labels: DemoLabels): string {\n switch (refusal.reason) {\n case \"rateLimited\":\n return labels.rateLimited(refusal.minutes);\n case \"capacity\":\n return labels.capacity;\n case \"unavailable\":\n return labels.unavailable;\n case \"failed\":\n return labels.failed;\n }\n}\n\n/** The one start a mount makes. */\nconst ONCE = \"demo\";\n\nexport interface DemoStartProps<T> extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n /**\n * Create the demo: the app's `POST /auth/demo-session {locale}`. Resolve with what it\n * answered (the `TokenResponse`, `refresh_token: null`, `expires_at`); reject with the\n * client's error — the codes are read out of it. Called once per mount.\n */\n start: () => Promise<T>;\n /** The demo exists: store the session (and, for kastlan, `kastlan.demoSession`) and\n * replace to the app's home. The page keeps saying \"Starting the demo…\" meanwhile. */\n onStarted: (session: T) => void;\n /** \"Request access\" beside a refusal. */\n access: AccessChoice;\n /** \"Back to the start page\". Default `/`. */\n backHref?: string;\n /** `false` adds no `noindex` — for a preview. Default `true`: `/demo` is never indexed\n * (§4.3). */\n noIndex?: boolean;\n labels?: LabelOverride<DemoLabels>;\n /** The access words (`landing` namespace) for this instance. */\n landingLabels?: Partial<LandingLabels>;\n}\n\n/**\n * The `/demo` page's content (docs/landing-demo-harmonization.md §5.2): it starts the\n * demo ONCE — StrictMode's double mount included, keksdose's ref guard — and says\n * \"Starting the demo…\" until the app has the session and has moved on.\n *\n * A refusal says why, in the visitor's words, and offers \"Request access\" and \"Back to\n * the start page\":\n *\n * | code | says |\n * |---|---|\n * | `demo_rate_limited` (429) | \"Too many demos from this network. Try again in N min.\" — N from `Retry-After` |\n * | `demo_capacity` (429) | \"The demo is full right now. Try again later.\" |\n * | `demo_disabled` (404), `demo_not_ready` (503) | \"The demo isn't available right now.\" |\n * | anything else | \"The demo could not be started. Please try again.\" |\n *\n * keksdose fell back to `/register` with a toast; registration is invitation-only now,\n * so the way on is the access mail.\n *\n * Put it in the app's `AuthLayout`. A live demo session never reaches it: the `/demo`\n * route sits in `RedirectIfAuthed` WITHOUT `allowDemo`, which continues the live demo at\n * its resume target instead of starting a second one. A cold start is the kit's\n * `ServerWakeNotice`'s, once the app's watcher counts this POST\n * (`watchReadsAnd(/\\/auth\\/demo-session\\b/)`).\n */\nexport function DemoStart<T>({\n start,\n onStarted,\n access,\n backHref = \"/\",\n noIndex = true,\n labels: labelsProp,\n landingLabels,\n className,\n ...rest\n}: DemoStartProps<T>) {\n const labels = useDemoLabels(labelsProp);\n const accessLink = useAccessAction(access, landingLabels);\n const [refusal, setRefusal] = useState<Refusal | null>(null);\n useNoIndex(noIndex);\n useOncePerKey<T>(\n ONCE,\n () => start(),\n (_key, result) => {\n if (result.ok) onStarted(result.value);\n else setRefusal(refusalOf(result.error));\n },\n );\n\n return (\n <div {...rest} className={cn(\"flex flex-col items-center gap-4 py-4 text-center\", className)}>\n {/* One live region, there from the start, so the refusal that replaces the\n progress line is announced (a region inserted with its text often is not). */}\n <div role=\"status\" aria-live=\"polite\" className=\"w-full\">\n {refusal === null ? (\n <p className=\"flex items-center justify-center gap-2 text-sm text-secondary\">\n <Spinner label={null} className=\"size-5\" />\n {labels.starting}\n </p>\n ) : (\n <AlertBanner tone={refusal.reason === \"failed\" ? \"danger\" : \"warning\"} className=\"text-start\">\n {messageOf(refusal, labels)}\n </AlertBanner>\n )}\n </div>\n {refusal !== null && (\n <div className=\"flex flex-wrap justify-center gap-3\">\n <Button href={accessLink.href} variant=\"accent\">\n {accessLink.label}\n </Button>\n <Button href={backHref} variant=\"secondary\">\n {labels.backToStart}\n </Button>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";AAqIU,SACE,KADF;AArIV,SAAS,gBAAgB;AAGzB,SAAS,UAAU;AACnB,SAAS,mBAAmB;AAC5B,SAAS,cAAc;AACvB,SAAS,eAAe;AACxB,SAAS,kBAAkB;AAC3B,SAAS,eAAe,eAAe,yBAAyB;AAChE,SAAS,qBAAqB;AAC9B,SAAS,uBAAuB;AAGhC,SAAS,qBAAqB;AAiB9B,SAAS,UAAU,OAAyB;AAC1C,QAAM,OAAO,cAAc,KAAK;AAChC,MAAI,SAAS,gBAAiB,QAAO,EAAE,QAAQ,WAAW;AAC1D,MAAI,SAAS,mBAAmB,SAAS,iBAAkB,QAAO,EAAE,QAAQ,cAAc;AAC1F,MAAI,SAAS,uBAAwB,SAAS,UAAa,cAAc,KAAK,GAAI;AAChF,UAAM,UAAU,kBAAkB,KAAK;AACvC,WAAO,EAAE,QAAQ,eAAe,SAAS,UAAU,KAAK,KAAK,UAAU,EAAE,IAAI,OAAU;AAAA,EACzF;AACA,SAAO,EAAE,QAAQ,SAAS;AAC5B;AAEA,SAAS,UAAU,SAAkB,QAA4B;AAC/D,UAAQ,QAAQ,QAAQ;AAAA,IACtB,KAAK;AACH,aAAO,OAAO,YAAY,QAAQ,OAAO;AAAA,IAC3C,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,OAAO;AAAA,IAChB,KAAK;AACH,aAAO,OAAO;AAAA,EAClB;AACF;AAGA,MAAM,OAAO;AAgDN,SAAS,UAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,UAAU;AAAA,EACV,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAsB;AACpB,QAAM,SAAS,cAAc,UAAU;AACvC,QAAM,aAAa,gBAAgB,QAAQ,aAAa;AACxD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAyB,IAAI;AAC3D,aAAW,OAAO;AAClB;AAAA,IACE;AAAA,IACA,MAAM,MAAM;AAAA,IACZ,CAAC,MAAM,WAAW;AAChB,UAAI,OAAO,GAAI,WAAU,OAAO,KAAK;AAAA,UAChC,YAAW,UAAU,OAAO,KAAK,CAAC;AAAA,IACzC;AAAA,EACF;AAEA,SACE,qBAAC,SAAK,GAAG,MAAM,WAAW,GAAG,qDAAqD,SAAS,GAGzF;AAAA,wBAAC,SAAI,MAAK,UAAS,aAAU,UAAS,WAAU,UAC7C,sBAAY,OACX,qBAAC,OAAE,WAAU,iEACX;AAAA,0BAAC,WAAQ,OAAO,MAAM,WAAU,UAAS;AAAA,MACxC,OAAO;AAAA,OACV,IAEA,oBAAC,eAAY,MAAM,QAAQ,WAAW,WAAW,WAAW,WAAW,WAAU,cAC9E,oBAAU,SAAS,MAAM,GAC5B,GAEJ;AAAA,IACC,YAAY,QACX,qBAAC,SAAI,WAAU,uCACb;AAAA,0BAAC,UAAO,MAAM,WAAW,MAAM,SAAQ,UACpC,qBAAW,OACd;AAAA,MACA,oBAAC,UAAO,MAAM,UAAU,SAAQ,aAC7B,iBAAO,aACV;AAAA,OACF;AAAA,KAEJ;AAEJ;","names":[]}
@@ -47,7 +47,7 @@ const roleUtilities = {
47
47
  };
48
48
  }
49
49
  };
50
- const VERSION = true ? "0.51.4" : "0.0.0-source";
50
+ const VERSION = true ? "0.51.5" : "0.0.0-source";
51
51
  const recommended = {
52
52
  name: "@eifi1/ui-kit/recommended",
53
53
  // Not the tests, as check:tokens skips the kit's: a test names the old class to assert it
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/feedback/feedback-attachment-labels.ts"],"sourcesContent":["// The English words of ./feedback-attachment, in a module with no component code: the\n// English catalogue (src/i18n/defaults.ts) imports them without the component, so `en`\n// loads as one chunk like the other languages (0.34, KS U8). ./feedback-attachment\n// re-exports them.\n\nimport type { FeedbackAttachmentFieldLabels } from \"./feedback-attachment\";\n\n/**\n * English, in the feedback contract's wording since 0.27.0 (docs/feedback-harmonization.md\n * §4.2: keksdose's `feedback.*` keys are the canon, and the kit owns them now). Three\n * strings moved with it:\n *\n * - `attachmentAdd` \"Attach image\" → **\"Add attachment\"**: the field takes a PDF or a text\n * log as well wherever the app passes `FEEDBACK_ATTACHMENT_ACCEPT` (keksdose feedback\n * #87), and a button that says \"image\" over a picker that offers a PDF is wrong in one\n * of the two places.\n * - `attachmentPaste` → keksdose's longer line, which says WHY one would paste — a region\n * snip shows one corner rather than the whole page (Steering Design feedback #39).\n * - `attachmentLimit` for one: \"remove **it**\", as keksdose's `attachments_full_one`;\n * \"remove one\" of a single file read oddly.\n */\nexport const DEFAULT_FEEDBACK_ATTACHMENT_LABELS: FeedbackAttachmentFieldLabels = {\n attachmentAdd: \"Add attachment\",\n attachmentCapture: \"Capture screenshot\",\n attachmentPaste: \"…or paste a screenshot straight from the clipboard, so you can show one corner rather than the whole page.\",\n attachmentRemove: \"Remove attachment\",\n attachmentList: \"Attachments\",\n attachmentScreenshot: \"Screenshot\",\n attachmentRemoveFile: (name) => `Remove ${name}`,\n attachmentLimit: (max) =>\n max === 1\n ? \"Up to 1 attachment — remove it to add another.\"\n : `Up to ${max} attachments — remove one to add another.`,\n attachmentUploading: \"Uploading…\",\n attachmentUploadFailed: (name) => `“${name}” could not be uploaded`,\n};\n"],"mappings":";AAqBO,MAAM,qCAAoE;AAAA,EAC/E,eAAe;AAAA,EACf,mBAAmB;AAAA,EACnB,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,sBAAsB;AAAA,EACtB,sBAAsB,CAAC,SAAS,UAAU,IAAI;AAAA,EAC9C,iBAAiB,CAAC,QAChB,QAAQ,IACJ,wDACA,SAAS,GAAG;AAAA,EAClB,qBAAqB;AAAA,EACrB,wBAAwB,CAAC,SAAS,SAAI,IAAI;AAC5C;","names":[]}
1
+ {"version":3,"sources":["../../src/feedback/feedback-attachment-labels.ts"],"sourcesContent":["// The English words of ./feedback-attachment, in a module with no component code: the\n// English catalogue (src/i18n/defaults.ts) imports them without the component, so `en`\n// loads as one chunk like the other languages (0.34, KS U8). ./feedback-attachment\n// re-exports them.\n\nimport type { FeedbackAttachmentFieldLabels } from \"./feedback-attachment\";\n\n/**\n * English, in the feedback contract's wording since 0.27.0 (docs/feedback-harmonization.md\n * §4.2: keksdose's `feedback.*` keys are the canon, and the kit owns them now). Three\n * strings moved with it:\n *\n * - `attachmentAdd` \"Attach image\" → **\"Add attachment\"**: the field takes a PDF or a text\n * log as well wherever the app passes `FEEDBACK_ATTACHMENT_ACCEPT` (keksdose feedback\n * #87), and a button that says \"image\" over a picker that offers a PDF is wrong in one\n * of the two places.\n * - `attachmentPaste` → keksdose's longer line, which says WHY one would paste — a region\n * snip shows one corner rather than the whole page (Steering Design feedback #39).\n * - `attachmentLimit` for one: \"remove **it**\", as keksdose's `attachments_full_one`;\n * \"remove one\" of a single file read oddly.\n */\nexport const DEFAULT_FEEDBACK_ATTACHMENT_LABELS: FeedbackAttachmentFieldLabels = {\n attachmentAdd: \"Add attachment\",\n attachmentCapture: \"Capture screenshot\",\n attachmentPaste:\n \"…or paste a screenshot straight from the clipboard, so you can show one corner rather than the whole page.\",\n attachmentRemove: \"Remove attachment\",\n attachmentList: \"Attachments\",\n attachmentScreenshot: \"Screenshot\",\n attachmentRemoveFile: (name) => `Remove ${name}`,\n attachmentLimit: (max) =>\n max === 1\n ? \"Up to 1 attachment — remove it to add another.\"\n : `Up to ${max} attachments — remove one to add another.`,\n attachmentUploading: \"Uploading…\",\n attachmentUploadFailed: (name) => `“${name}” could not be uploaded`,\n};\n"],"mappings":";AAqBO,MAAM,qCAAoE;AAAA,EAC/E,eAAe;AAAA,EACf,mBAAmB;AAAA,EACnB,iBACE;AAAA,EACF,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,sBAAsB;AAAA,EACtB,sBAAsB,CAAC,SAAS,UAAU,IAAI;AAAA,EAC9C,iBAAiB,CAAC,QAChB,QAAQ,IACJ,wDACA,SAAS,GAAG;AAAA,EAClB,qBAAqB;AAAA,EACrB,wBAAwB,CAAC,SAAS,SAAI,IAAI;AAC5C;","names":[]}
@@ -76,14 +76,7 @@ function SingleField({
76
76
  children: [
77
77
  /* @__PURE__ */ jsx(Heading, { text: labelsProp?.attachment }),
78
78
  value ? /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
79
- value.type.startsWith("image/") && preview ? /* @__PURE__ */ jsx(
80
- "img",
81
- {
82
- src: preview,
83
- alt: value.name,
84
- className: "h-20 w-20 rounded border border-subtle object-cover"
85
- }
86
- ) : (
79
+ value.type.startsWith("image/") && preview ? /* @__PURE__ */ jsx("img", { src: preview, alt: value.name, className: "h-20 w-20 rounded border border-subtle object-cover" }) : (
87
80
  // Non-image attachments (PDF, text) can't preview as an <img>, so
88
81
  // show a neutral file tile with the name/size beside it instead.
89
82
  /* @__PURE__ */ jsx("div", { className: "flex h-20 w-20 items-center justify-center rounded border border-subtle text-placeholder", children: /* @__PURE__ */ jsx(FileText, { className: "size-8" }) })
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/feedback/feedback-attachment.tsx"],"sourcesContent":["import { cn } from \"../lib/cn\";\nimport { hasContent } from \"../lib/presence\";\nimport {\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n type ClipboardEvent as ReactClipboardEvent,\n type ReactNode,\n type RefObject,\n} from \"react\";\nimport { Camera, FileText, Image as ImageIcon, Paperclip, X } from \"lucide-react\";\nimport { Button } from \"../components/button\";\nimport { IconButton } from \"../components/icon-button\";\nimport { Spinner } from \"../components/spinner\";\nimport type { ButtonSize, ButtonVariant } from \"../components/button-look\";\nimport { DEFAULT_FILE_PICKER_LABELS, formatAccept } from \"../components/file-button\";\nimport { useCommitReason, type CommitScope } from \"../components/write-lock\";\nimport { useKitFileLabels, useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport type { FeedbackAttachmentLabels } from \"./feedback-dialog\";\nimport { DEFAULT_FEEDBACK_ATTACHMENT_LABELS } from \"./feedback-attachment-labels\";\nimport { DEFAULT_ATTACHMENT_ACCEPT, DEFAULT_MAX_ATTACHMENT_BYTES, DEFAULT_MAX_ATTACHMENTS } from \"./feedback-record\";\n\n// Defined in ./feedback-record since 0.34, which a top bar can import without this\n// field (MOD-1); still exported from here, where they always were.\nexport { DEFAULT_ATTACHMENT_ACCEPT, DEFAULT_MAX_ATTACHMENT_BYTES, DEFAULT_MAX_ATTACHMENTS };\n\n/**\n * The field's own strings — the `feedbackAttachment` namespace of `<UiKitProvider\n * labels>`. The keys are {@link FeedbackAttachmentLabels}' own, so the `labels` prop\n * (and a whole `FeedbackDialogLabels` handed down by the dialog) merges over them\n * one to one. The optional heading, `attachment`, stays a prop: a note editor puts\n * the buttons straight under its textarea and wants none.\n *\n * Before 0.7.0 the two optional keys fell back to hard-coded English, so a German app\n * that relied on its provider got \"Capture screenshot\" under a German form.\n *\n * The three keys added in 0.15.5 for `multiple` mode are optional, so a complete\n * `UiKitLabels` typed before it still compiles; they fall back to English. So is\n * `attachmentScreenshot` (0.16.0).\n */\nexport interface FeedbackAttachmentFieldLabels {\n attachmentAdd: string;\n attachmentCapture: string;\n attachmentPaste: string;\n attachmentRemove: string;\n /** `multiple` mode: the accessible name of the list of chosen files — and, since\n * 0.16.0, the heading `<FeedbackDialog attachments=\"multiple\">` puts over it. */\n attachmentList?: string;\n /** `multiple` mode (0.16.0): what the screenshot slot's chip is called — \"Screenshot\"\n * rather than the capture's file name (\"screenshot.webp\"), which says nothing a\n * reporter chose. The file name stays as the chip's second line. */\n attachmentScreenshot?: string;\n /** `multiple` mode: one chip's remove button — \"Remove photo.jpg\". Named per file,\n * because five buttons all called \"Remove attachment\" say nothing about which. */\n attachmentRemoveFile?: (name: string) => string;\n /** `multiple` mode: the line shown in place of the add buttons once `max` is reached. */\n attachmentLimit?: (max: number) => string;\n /** `refs` mode (0.22.0, keksdose K16): the second line of a chip whose upload is still\n * running, where an uploaded one shows its size — \"Uploading…\". Optional, like the\n * keys above, so a `UiKitLabels` typed before it still compiles. */\n attachmentUploading?: string;\n /** `refs` mode (0.23.0, keksdose G5a): {@link FeedbackAttachmentErrorInfo.message} for\n * an `\"upload\"` refusal — \"“huge.png” could not be uploaded\". The other refusals'\n * sentences are the `filePicker` namespace's, shared with FileButton. Optional, like\n * the keys above. */\n attachmentUploadFailed?: (name: string) => string;\n}\n\n// Defined in ./feedback-attachment-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_FEEDBACK_ATTACHMENT_LABELS };\n\n/** Why a file was turned away. `\"count\"` only ever comes from `multiple` (and `refs`)\n * mode: more files arrived at once than `max` left room for, and the surplus was\n * dropped. */\nexport type FeedbackAttachmentError = \"type\" | \"size\" | \"count\";\n\n/** `refs` mode's reasons: {@link FeedbackAttachmentError}, or `\"upload\"` — `onUpload`\n * rejected, and what it rejected with is `onError`'s second argument. A type of its\n * own so a host's exhaustive switch over the File modes' reasons stays exhaustive. */\nexport type FeedbackAttachmentRefsError = FeedbackAttachmentError | \"upload\";\n\n/**\n * What `onError` knows about a refusal beyond its kind (0.23.0, keksdose G5a): the file\n * it was about, the limit that refused it, and a sentence that says both.\n *\n * keksdose's support chat toasted \"File too large\" with no name — the field's `onError`\n * carried a kind and nothing else, so a refusal could not say WHICH of three pasted\n * screenshots went, nor what the ceiling was, and its own `AttachmentPicker` (a\n * FileButton, whose `onReject` hands over `{ file, message }`) said more than the kit's\n * field would have. This is that, for every mode.\n *\n * It arrives as an EXTRA argument, after the ones `onError` already had, so a handler\n * written for 0.22 — `(kind) => …`, or `refs` mode's `(kind, error) => …` — compiles\n * and behaves as before:\n *\n * - single and `multiple`: `onError(kind, info)`;\n * - `refs`: `onError(kind, error, info)` — the second place is `onUpload`'s rejection\n * since 0.22 (`undefined` for the other kinds, as it always was), so the info comes\n * third rather than changing what a 0.22 handler reads there. It also carries that\n * `error`, so one helper `(kind, info) => …` serves all three modes.\n *\n * An object rather than more positional arguments because what a host reaches for\n * differs by kind — the bytes for `\"size\"`, `max` for `\"count\"`, the server's answer for\n * `\"upload\"` — and a field added later must not shift anyone's arguments.\n */\nexport interface FeedbackAttachmentErrorInfo {\n /** The file refused — under the name the field would have given it (a pasted image\n * is already \"pasted.png\"). For `\"count\"`, the first of the surplus; for\n * `\"upload\"`, the file whose upload failed. */\n file: File;\n /** `\"count\"`: every file of the surplus (several can arrive in one pick). Otherwise\n * `[file]`. */\n files: File[];\n /**\n * One translated sentence naming the file and the limit — the ones FileButton's\n * `onReject` hands over, from the same `filePicker` namespace: \"“huge.png” is larger\n * than 10 MB\", \"Only image/png, image/jpeg files\", \"“c.png” was not added: at most 5\n * files\" (several at once: \"3 files were not added\"). `\"upload\"`: the field's own\n * `attachmentUploadFailed`, \"“huge.png” could not be uploaded\" — a host with a better\n * sentence from the server's answer uses `error`. Ready for a toast as it is.\n */\n message: string;\n /** The field's `maxBytes` — the ceiling a `\"size\"` refusal went over, in bytes. */\n maxBytes: number;\n /** The field's `accept` — the types a `\"type\"` refusal was not among. */\n accept: string[];\n /** The field's `max` — the ceiling a `\"count\"` refusal hit. Absent in single mode,\n * which holds one file by definition. */\n max?: number;\n /** `\"upload\"` only: what `onUpload` rejected with — `onError`'s second argument. */\n error?: unknown;\n}\n\n/**\n * One uploaded attachment in `refs` mode: what the host's upload answered with. `key`\n * identifies it (a storage key, an upload id) and is what a remove goes by; `name` is\n * what the chip says. `size` (bytes) and `type` (mime) are optional — given, the chip\n * shows the size and an image icon or a file icon accordingly.\n */\nexport interface FeedbackAttachmentRef {\n key: string;\n name: string;\n size?: number;\n type?: string;\n}\n\ninterface FeedbackAttachmentFieldBaseProps {\n /** Prop > `<UiKitProvider labels={{ feedbackAttachment }}>` > English. Optional\n * since 0.7.0; `attachment` (the heading) is only ever read from here. */\n labels?: LabelOverride<FeedbackAttachmentLabels> &\n Pick<Partial<FeedbackAttachmentFieldLabels>, \"attachmentUploading\" | \"attachmentUploadFailed\">;\n accept?: string[];\n maxBytes?: number;\n /**\n * Take no files for now (0.23.0, keksdose G5b): the add and capture buttons, the\n * hidden file input and every chip's remove button are natively disabled, and a paste\n * is let through untouched. The chips stay as they are — what is about to be sent\n * stays readable.\n *\n * For the moment a send is in flight. keksdose's support picker is `disabled` while\n * the message goes out, because the message carries the attachments AS THEY WERE at\n * the press: a chip removed mid-send has gone with it all the same, and one added\n * mid-send looks as if it had. The field has no drop target; the paste is its drop.\n *\n * This is a pause, not a lock that explains itself — for that, `refs` mode (the mode\n * that commits) takes `commit` and `disabledReason`.\n */\n disabled?: boolean;\n /** Snapshot the app view behind this and return it as a File. A \"Capture\n * screenshot\" button appears only when it is given. */\n onCaptureScreenshot?: () => Promise<File | null>;\n /** Listen for the paste on `document` rather than on this field's own\n * subtree. For a modal, which owns the whole page while it is up. */\n documentPaste?: boolean;\n /** Listen for the paste within this element's subtree rather than this\n * field's own — for a field standing *beside* the text box a paste is made\n * in, whose common parent is where the event bubbles to. Ignored when\n * `documentPaste` is set. */\n pasteFrom?: RefObject<HTMLElement | null>;\n /**\n * The add and capture buttons' look (0.24.0, keksdose) — Button's own `variant`.\n * Default `\"secondary\"`, the look they always had.\n *\n * keksdose's support chat puts this field in ChatComposer's `attachmentSlot`, in the\n * row with Send, where its own picker had been a `ghost` / `sm` FileButton: two\n * bordered full-size buttons under a chat box read as a second form, and the field\n * gave no way to say otherwise. Every mode, wherever the buttons appear; the chips'\n * remove buttons keep their size.\n */\n buttonVariant?: ButtonVariant;\n /** The add and capture buttons' size — Button's own `size`, with `buttonVariant`.\n * Default `\"md\"`. `\"sm\"` draws their icons at 14px, as a small button's are. */\n buttonSize?: ButtonSize;\n className?: string;\n}\n\n/**\n * The field's root, in every mode. `relative` for the `sr-only` file input (see the\n * single field). `min-w-0 max-w-full` (0.24.0, keksdose) because the field is often a\n * FLEX ITEM — ChatComposer's `attachmentSlot` row, FormActions' `start` — and a flex item\n * is never narrower than its content's min-content width unless told it may be. A chip\n * truncates its name only inside a box of bounded width, and a screenshot's name\n * (\"Screenshot 2026-10-03 at 07.45.12.png\") was the min-content: the field grew to it\n * and ran off a 390px phone composer (measured in Chromium: the field's right edge at\n * 592px, the page 614px wide). keksdose passed `className=\"min-w-0 max-w-full\"` at both\n * its callers to stop it; here once, for every host. In normal flow — a dialog, a note\n * editor — both are no-ops.\n *\n * The names and sizes under it are `truncate-until-large` (0.33, docs/text-size-harmonization.md\n * §10.17): \"…\" at Normal, and from Large up they wrap, breaking a long file name\n * anywhere, so the bound above still holds and nothing is cut.\n */\nconst FIELD_ROOT = \"relative min-w-0 max-w-full\";\n\n/** Paperclip and camera at the size the button's text is: 16px at `md`, 14px at `sm`. */\nconst buttonIcon = (size: ButtonSize) => (size === \"sm\" ? \"size-3.5\" : \"size-4\");\n\n/** One file: choosing a second means removing the first. The field's only mode until 0.15.5. */\nexport interface FeedbackAttachmentFieldSingleProps extends FeedbackAttachmentFieldBaseProps {\n multiple?: false;\n refs?: false;\n value: File | null;\n onChange: (file: File | null) => void;\n /** A file was refused. `info` (0.23.0) names it and the limit — see\n * {@link FeedbackAttachmentErrorInfo}. */\n onError?: (kind: \"type\" | \"size\", info: FeedbackAttachmentErrorInfo) => void;\n // Multiple-mode props, refused here: without `multiple` they would be silently ignored.\n max?: never;\n screenshot?: never;\n onScreenshotChange?: never;\n // `refs` mode's lock: a staged File is not a commit (see the refs props).\n commit?: never;\n disabledReason?: never;\n}\n\n/**\n * Several files (0.15.5, keksdose dev#578: \"pasted two photos, the second overwrote\n * the first\"). Every way in ADDS — a pick of several files, a paste, a capture — up to\n * `max`, and each file is a removable chip.\n */\nexport interface FeedbackAttachmentFieldMultipleProps extends FeedbackAttachmentFieldBaseProps {\n multiple: true;\n refs?: false;\n value: File[];\n onChange: (files: File[]) => void;\n /** How many files `value` may hold. Default {@link DEFAULT_MAX_ATTACHMENTS} (5). The\n * add buttons and the paste hint are offered only while there is room. */\n max?: number;\n /** Per file: a rejected one is reported and the others are still added. `\"count\"` is\n * once per pick, with the whole surplus in `info.files`. `info` (0.23.0): see\n * {@link FeedbackAttachmentErrorInfo}. */\n onError?: (kind: FeedbackAttachmentError, info: FeedbackAttachmentErrorInfo) => void;\n /**\n * A separate slot for THE screenshot, outside `value` and `max`. Given\n * `onScreenshotChange`, what `onCaptureScreenshot` returns lands here instead of in\n * `value`, shown as the first chip, and the capture button is offered only while the\n * slot is empty — a second snapshot of the same view is not more evidence. This is\n * how `<FeedbackDialog attachments=\"multiple\">` keeps the screenshot apart from the\n * photos; a form built from the parts can do the same.\n */\n screenshot?: File | null;\n onScreenshotChange?: (file: File | null) => void;\n // `refs` mode's lock: a staged File is not a commit (see the refs props).\n commit?: never;\n disabledReason?: never;\n}\n\n/**\n * Uploaded on pick (0.22.0, keksdose K16). keksdose's support chat uploads each file\n * the moment it is chosen and sends the message with the REFS the uploads answered —\n * a 10 MB screenshot is refused while the reporter is still typing, not when they\n * press Send — so its `AttachmentPicker` could not use this field, whose value is\n * `File`s held until the form is submitted. Here the value is\n * {@link FeedbackAttachmentRef}s: every way in (pick, paste, capture) validates the\n * file as the other modes do, then hands it to `onUpload` and shows a chip with a\n * spinner until it answers; the ref it resolves to joins `value` through `onChange`.\n * A rejection drops the chip and reports `onError(\"upload\", error)`. A chip is removed\n * BY KEY: `onChange` gets the list without it, and `onRemove` the key, for a host that\n * deletes the upload on the server too.\n *\n * Uploads still running count against `max`, so five picks in quick succession cannot\n * overshoot it; they are not in `value` until they land, so a send that reads `value`\n * sends only what has arrived — hold Send back while {@link onUploadingChange} says\n * an upload is running. Several files upload side by side and join in the order they\n * finish. The File modes are unchanged.\n */\nexport interface FeedbackAttachmentFieldRefsProps extends FeedbackAttachmentFieldBaseProps {\n refs: true;\n multiple?: never;\n value: FeedbackAttachmentRef[];\n onChange: (refs: FeedbackAttachmentRef[]) => void;\n /** Upload one file; resolve to its ref, or reject to refuse it. Called once per\n * accepted file, the moment it is chosen. */\n onUpload: (file: File) => Promise<FeedbackAttachmentRef>;\n /** A chip was removed — after `onChange` — with its ref's key. */\n onRemove?: (key: string) => void;\n /** Whether any upload is running: `true` when the first starts, `false` when the\n * last settles — for the host's Send, which should wait. */\n onUploadingChange?: (uploading: boolean) => void;\n /** How many refs `value` may hold, uploads in flight included. Default\n * {@link DEFAULT_MAX_ATTACHMENTS} (5). */\n max?: number;\n /** Per file. `\"upload\"` carries what `onUpload` rejected with as `error` (`undefined`\n * for the other kinds). `info` (0.23.0) comes third so a 0.22 handler's `error` is\n * still the second argument — see {@link FeedbackAttachmentErrorInfo}. */\n onError?: (kind: FeedbackAttachmentRefsError, error: unknown, info: FeedbackAttachmentErrorInfo) => void;\n /**\n * This field COMMITS (0.23.0): each file is uploaded the moment it is chosen, and a\n * remove may delete the upload (`onRemove`) — a save, unlike the File modes, which\n * only stage a file for a form sent later (and so stay open under a lock, like every\n * field). Under a locked {@link WriteLockProvider} it takes the `disabledReason` path\n * with the lock's reason, which wins over one of its own. No provider, or an unlocked\n * one: no effect.\n */\n commit?: CommitScope;\n /**\n * Why no file can be added or removed — the write lock's sentence, a quota, a thread\n * closed to replies. Unlike `disabled`, it SAYS so, the kit's commit-control way: the\n * buttons (add, capture, every chip's remove) stay focusable, `aria-disabled`, with\n * the reason in the kit Tooltip and their description; no pick, paste or remove gets\n * through; and the reason stands where the paste hint was, for a phone that never\n * hovers. The chips stay readable.\n */\n disabledReason?: ReactNode;\n screenshot?: never;\n onScreenshotChange?: never;\n}\n\nexport type FeedbackAttachmentFieldProps =\n | FeedbackAttachmentFieldSingleProps\n | FeedbackAttachmentFieldMultipleProps\n | FeedbackAttachmentFieldRefsProps;\n\n/**\n * Picking one picture: the file dialog, a capture of the app view, or a paste.\n * With `multiple`, picking several (see {@link FeedbackAttachmentFieldMultipleProps}).\n *\n * Lifted out of {@link FeedbackDialog} so the *reply* half of the feature can\n * have it too (Steering Design feedback #128). A report is a conversation — it\n * is filed, it is answered, and the reporter sends it back saying that is not\n * what they meant — and the screenshot showing what they mean is taken at\n * whichever of those points they looked. Only the first of them had a way to\n * attach one, so everything after it had to be described in words.\n *\n * **Three ways in, and they are three because no one of them covers the others.**\n * The file dialog needs a file on disk; `onCaptureScreenshot` snapshots the\n * *whole* app view; and the clipboard is where a region snip already is on every\n * platform (Steering Design feedback #39). Somebody who wanted to show one panel\n * had to save a crop and then find it again.\n *\n * `documentPaste` and `pasteFrom` decide where the paste is listened for, and\n * it is a real choice rather than a flag with a default. A modal traps focus,\n * so while it is up every paste in the page is meant for it — including one\n * made with nothing in particular focused, which never reaches a React\n * `onPaste` on a child: that is `documentPaste`. An editor **inline on a\n * page** is not that: the page around it has its own fields, so it listens\n * within its own subtree and a paste elsewhere stays where it was aimed. But\n * \"its own subtree\" has to include the text box the paste is actually made in,\n * and that box is this field's *sibling*, not its child — a paste in it bubbles\n * to their common parent and never through here. `pasteFrom` is that parent\n * (Steering Design feedback #140): the element whose subtree is listened to,\n * handed in by whoever renders both the box and this field side by side.\n */\nexport function FeedbackAttachmentField(props: FeedbackAttachmentFieldProps) {\n // One component per mode rather than one with branches: the modes hold different\n // state (one preview vs. one per chip vs. uploads in flight), and a hook order that\n // depends on a prop is a crash the day a caller flips it.\n if (props.refs) return <RefsField {...props} />;\n return props.multiple ? <MultipleField {...props} /> : <SingleField {...props} />;\n}\n\nfunction SingleField({\n value,\n onChange,\n labels: labelsProp,\n accept = DEFAULT_ATTACHMENT_ACCEPT,\n maxBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onError,\n onCaptureScreenshot,\n documentPaste = false,\n pasteFrom,\n disabled = false,\n buttonVariant = \"secondary\",\n buttonSize = \"md\",\n className,\n}: FeedbackAttachmentFieldSingleProps) {\n // The object URL is keyed to the file it was made for, so a stale one (from the\n // previous file, or after `value` is cleared) is never shown — no reset needed.\n const [previewFor, setPreviewFor] = useState<{ file: File; url: string } | null>(null);\n const preview = value && previewFor?.file === value ? previewFor.url : null;\n const fileInputRef = useRef<HTMLInputElement>(null);\n // `file.size` from `<UiKitProvider labels>`, formatted in its locale — see FileDropzone.\n const fileText = useKitFileLabels();\n const text = useKitLabels(\"feedbackAttachment\", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);\n const errorInfo = useErrorInfo(accept, maxBytes, undefined, text);\n\n // Object-URL preview lifecycle (create on change, revoke on cleanup).\n useEffect(() => {\n if (!value) return;\n const url = URL.createObjectURL(value);\n // The URL is an external resource that must be created and revoked in step\n // with the effect (a render-time `createObjectURL` would leak, and StrictMode's\n // remount would revoke a memoised one out from under the <img>).\n // eslint-disable-next-line react-hooks/set-state-in-effect -- publishing an external resource's handle\n setPreviewFor({ file: value, url });\n return () => URL.revokeObjectURL(url);\n }, [value]);\n\n const pick = (file: File | undefined | null) => {\n if (!file) return;\n const problem = rejection(file, accept, maxBytes);\n if (problem) {\n onError?.(problem, errorInfo(problem, [file]));\n return;\n }\n onChange(file);\n };\n\n const onPaste = usePaste(\n (images) => {\n // Clipboard images arrive named \"image.png\" at best and unnamed at worst,\n // and the name is what the inbox shows beside the thumbnail. A name that\n // says where it came from is more use than the browser's.\n const file = images[0];\n pick(new File([file], pastedName(file.type), { type: file.type }));\n },\n documentPaste,\n pasteFrom,\n disabled,\n );\n\n return (\n <div\n // `relative` for the same reason as FileDropzone: the `sr-only` file input below\n // is `position: absolute`, and without a positioned ancestor it is laid out\n // against the initial containing block — extending the DOCUMENT height to its own\n // offset and producing a phantom second scrollbar on any long page. And\n // `min-w-0 max-w-full`, so a long name truncates in a flex row: see FIELD_ROOT.\n className={cn(FIELD_ROOT, className)}\n onPaste={onPaste}\n >\n <Heading text={labelsProp?.attachment} />\n {value ? (\n <div className=\"flex items-start gap-2\">\n {value.type.startsWith(\"image/\") && preview ? (\n <img\n src={preview}\n alt={value.name}\n className=\"h-20 w-20 rounded border border-subtle object-cover\"\n />\n ) : (\n // Non-image attachments (PDF, text) can't preview as an <img>, so\n // show a neutral file tile with the name/size beside it instead.\n <div className=\"flex h-20 w-20 items-center justify-center rounded border border-subtle text-placeholder\">\n <FileText className=\"size-8\" />\n </div>\n )}\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate-until-large text-sm text-secondary\">{value.name}</div>\n <div className=\"text-xs text-muted\">\n {fileText.size(value.size)}\n </div>\n </div>\n <RemoveButton label={text.attachmentRemove} onClick={() => onChange(null)} disabled={disabled} />\n </div>\n ) : (\n <div className=\"space-y-1.5\">\n <div className=\"flex flex-wrap gap-2\">\n <Button\n type=\"button\"\n variant={buttonVariant}\n size={buttonSize}\n disabled={disabled}\n onClick={() => fileInputRef.current?.click()}\n >\n <Paperclip className={buttonIcon(buttonSize)} /> {text.attachmentAdd}\n </Button>\n {onCaptureScreenshot && (\n <CaptureButton\n capture={onCaptureScreenshot}\n onFile={pick}\n label={text.attachmentCapture}\n disabled={disabled}\n variant={buttonVariant}\n size={buttonSize}\n />\n )}\n </div>\n {/* Said out loud, because a gesture with no affordance is a gesture\n nobody finds. */}\n <p className=\"text-xs text-muted\">\n {text.attachmentPaste}\n </p>\n </div>\n )}\n <input\n ref={fileInputRef}\n type=\"file\"\n accept={accept.join(\",\")}\n className=\"sr-only\"\n // Out of the tab order and the accessibility tree, as in the other two modes:\n // the add button is the way in (round 0.34, the showcase's axe check — this one\n // was a second, nameless stop).\n tabIndex={-1}\n aria-hidden\n disabled={disabled}\n onChange={(e) => {\n pick(e.target.files?.[0]);\n e.target.value = \"\";\n }}\n />\n </div>\n );\n}\n\nfunction MultipleField({\n value,\n onChange,\n max = DEFAULT_MAX_ATTACHMENTS,\n screenshot = null,\n onScreenshotChange,\n labels: labelsProp,\n accept = DEFAULT_ATTACHMENT_ACCEPT,\n maxBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onError,\n onCaptureScreenshot,\n documentPaste = false,\n pasteFrom,\n disabled = false,\n buttonVariant = \"secondary\",\n buttonSize = \"md\",\n className,\n}: FeedbackAttachmentFieldMultipleProps) {\n const rootRef = useRef<HTMLDivElement>(null);\n const fileInputRef = useRef<HTMLInputElement>(null);\n const fileText = useKitFileLabels();\n const text = useKitLabels(\"feedbackAttachment\", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);\n const removeLabel = text.attachmentRemoveFile ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentRemoveFile!;\n const limitLabel = text.attachmentLimit ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentLimit!;\n const screenshotLabel = text.attachmentScreenshot ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentScreenshot!;\n const errorInfo = useErrorInfo(accept, maxBytes, max, text);\n\n // What an add or a remove builds on: the `value` prop, plus whatever this field has\n // already handed to `onChange` since the parent last rendered. The field is\n // controlled, so between an `onChange` and the re-render that brings it back, `value`\n // is stale — and two adds in that gap (two pastes, a paste and a pick, delivered by\n // native listeners whose updates React batches) each spread the SAME old list, and\n // the second dropped the first. Re-synced to the prop after every commit, so a\n // parent that refuses a change (or edits the list itself) is what wins.\n const latest = useRef(value);\n useLayoutEffect(() => {\n latest.current = value;\n });\n const commit = (next: File[]) => {\n latest.current = next;\n onChange(next);\n };\n\n const hasSlot = onScreenshotChange !== undefined;\n const room = Math.max(0, max - value.length);\n // The screenshot slot is its own thing: it neither counts against `max` nor is\n // closed by it, so a full set of photos still leaves the capture on offer.\n const canCapture = !!onCaptureScreenshot && (hasSlot ? !screenshot : room > 0);\n\n const add = (incoming: File[], pasted = false) => {\n const current = latest.current;\n const accepted: File[] = [];\n const taken = new Set([...current, ...(screenshot ? [screenshot] : [])].map((f) => f.name));\n for (const raw of incoming) {\n // Two pastes are two files called \"pasted.png\", and a backend that stores by\n // name keeps one of them — the very loss this mode exists to end. A picked\n // file keeps its own name: it is the user's, and they may look for it. Named\n // before the check, so a refusal names it as the single field's does.\n const file = pasted ? renamed(raw, uniqueName(pastedName(raw.type), taken)) : raw;\n const problem = rejection(file, accept, maxBytes);\n if (problem) {\n onError?.(problem, errorInfo(problem, [file]));\n continue;\n }\n taken.add(file.name);\n // The same File object twice is one file chosen twice, not two.\n if (!current.includes(file)) accepted.push(file);\n }\n if (accepted.length === 0) return;\n const space = Math.max(0, max - current.length);\n if (accepted.length > space) onError?.(\"count\", errorInfo(\"count\", accepted.slice(space)));\n const kept = accepted.slice(0, space);\n if (kept.length > 0) commit([...current, ...kept]);\n };\n\n const onPaste = usePaste((images) => add(images, true), documentPaste, pasteFrom, disabled);\n\n const capture = (file: File) => {\n if (!hasSlot) {\n add([file]);\n return;\n }\n const problem = rejection(file, accept, maxBytes);\n if (problem) onError?.(problem, errorInfo(problem, [file]));\n else onScreenshotChange(file);\n };\n\n // Removing a chip removes the button that had focus. Focus goes to the chip that\n // took its place — the next one — and, when it was the last, to the first action\n // button (there is room again, so \"Attach\" is back), else to the chip before it.\n // Applied once the parent has re-rendered with the shorter list: the field is\n // controlled, and focusing before that would land on the chip being removed.\n const pendingFocus = useRef<number | null>(null);\n useEffect(() => {\n const index = pendingFocus.current;\n if (index === null) return;\n pendingFocus.current = null;\n const root = rootRef.current;\n if (!root) return;\n const removes = root.querySelectorAll<HTMLElement>(\"[data-attachment-remove]\");\n const target =\n removes[index] ?? root.querySelector<HTMLElement>(\"[data-attachment-action]\") ?? removes[index - 1];\n target?.focus();\n }, [value, screenshot]);\n\n const chips: Array<{ file: File; screenshot: boolean; name: string; remove: () => void }> = [\n ...(hasSlot && screenshot\n ? [{ file: screenshot, screenshot: true, name: screenshotLabel, remove: () => onScreenshotChange(null) }]\n : []),\n ...value.map((file) => ({\n file,\n screenshot: false,\n name: file.name,\n remove: () => commit(latest.current.filter((f) => f !== file)),\n })),\n ];\n\n return (\n <div ref={rootRef} className={cn(FIELD_ROOT, \"space-y-2\", className)} onPaste={onPaste}>\n <Heading text={labelsProp?.attachment} className=\"mb-0\" />\n {chips.length > 0 && (\n // One chip per row, full width: wrapped chips of name-dependent widths made a\n // ragged block in a 430px dialog, and a column keeps every name and remove\n // button in the same place — the next chip's button is right under the last.\n <ul aria-label={text.attachmentList} className=\"flex flex-col gap-2\">\n {chips.map((chip, index) => (\n <li\n key={fileKey(chip.file)}\n className=\"flex min-w-0 items-center gap-2 rounded-md border border-subtle bg-surface p-1\"\n >\n <ChipPreview file={chip.file} screenshot={chip.screenshot} />\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate-until-large text-sm text-secondary\">{chip.name}</div>\n <div className=\"truncate-until-large text-xs text-muted\">\n {/* The screenshot's file name moves down here: still there for the\n reporter who wants to know what will be sent, not its title. */}\n {chip.screenshot ? `${chip.file.name} · ${fileText.size(chip.file.size)}` : fileText.size(chip.file.size)}\n </div>\n </div>\n <RemoveButton\n label={removeLabel(chip.name)}\n disabled={disabled}\n onClick={() => {\n pendingFocus.current = index;\n chip.remove();\n }}\n />\n </li>\n ))}\n </ul>\n )}\n {(room > 0 || canCapture) && (\n <div className=\"flex flex-wrap gap-2\">\n {room > 0 && (\n <Button\n type=\"button\"\n variant={buttonVariant}\n size={buttonSize}\n data-attachment-action=\"\"\n disabled={disabled}\n onClick={() => fileInputRef.current?.click()}\n >\n <Paperclip className={buttonIcon(buttonSize)} /> {text.attachmentAdd}\n </Button>\n )}\n {canCapture && onCaptureScreenshot && (\n <CaptureButton\n capture={onCaptureScreenshot}\n onFile={capture}\n label={text.attachmentCapture}\n disabled={disabled}\n variant={buttonVariant}\n size={buttonSize}\n data-attachment-action=\"\"\n />\n )}\n </div>\n )}\n <p className=\"text-xs text-muted\">\n {room > 0 ? text.attachmentPaste : limitLabel(max)}\n </p>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple={room > 1}\n accept={accept.join(\",\")}\n className=\"sr-only\"\n // Out of the tab order and the accessibility tree: the button above is\n // the way in, and a second, unlabelled \"Choose files\" stop is noise.\n tabIndex={-1}\n aria-hidden\n disabled={disabled}\n onChange={(e) => {\n add(Array.from(e.target.files ?? []));\n e.target.value = \"\";\n }}\n />\n </div>\n );\n}\n\n/** A file `refs` mode is uploading: its own id (a ref has no key yet), and the file. */\ninterface Upload {\n id: number;\n file: File;\n}\n\nlet nextUploadId = 0;\n\nfunction RefsField({\n value,\n onChange,\n onUpload,\n onRemove,\n onUploadingChange,\n max = DEFAULT_MAX_ATTACHMENTS,\n labels: labelsProp,\n accept = DEFAULT_ATTACHMENT_ACCEPT,\n maxBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onError,\n onCaptureScreenshot,\n documentPaste = false,\n pasteFrom,\n disabled = false,\n commit: commits,\n disabledReason: ownDisabledReason,\n buttonVariant = \"secondary\",\n buttonSize = \"md\",\n className,\n}: FeedbackAttachmentFieldRefsProps) {\n const rootRef = useRef<HTMLDivElement>(null);\n const fileInputRef = useRef<HTMLInputElement>(null);\n const fileText = useKitFileLabels();\n const text = useKitLabels(\"feedbackAttachment\", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);\n const removeLabel = text.attachmentRemoveFile ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentRemoveFile!;\n const limitLabel = text.attachmentLimit ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentLimit!;\n const uploadingLabel = text.attachmentUploading ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentUploading!;\n const errorInfo = useErrorInfo(accept, maxBytes, max, text);\n // The lock's reason (under `commit`) over the field's own; with either, the controls\n // take the focusable `aria-disabled` path and say why — Button's and IconButton's\n // `disabledReason`, handed down as is.\n const disabledReason = useCommitReason(commits, ownDisabledReason);\n const locked = hasContent(disabledReason);\n const reason = locked ? disabledReason : undefined;\n const inert = disabled || locked;\n\n // As in `multiple` mode: `value` plus whatever has been handed to `onChange` since\n // the parent last rendered — two uploads landing in one tick must both survive.\n const latest = useRef(value);\n useLayoutEffect(() => {\n latest.current = value;\n });\n const commit = (next: FeedbackAttachmentRef[]) => {\n latest.current = next;\n onChange(next);\n };\n\n const [uploads, setUploads] = useState<Upload[]>([]);\n // The uploads in flight, kept in a ref as well: `add` runs several times before a\n // render when files arrive together, and each must see the others — their count\n // against `max`, their names against a pasted image's.\n const inFlight = useRef<Upload[]>([]);\n // Unmounted (the dialog closed mid-upload): a late answer is not this field's to\n // commit, and setting state on it would be a no-op at best.\n const mounted = useRef(true);\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n const uploadingChange = useRef(onUploadingChange);\n useEffect(() => {\n uploadingChange.current = onUploadingChange;\n });\n\n const room = Math.max(0, max - value.length - uploads.length);\n\n const start = (file: File) => {\n const upload: Upload = { id: nextUploadId++, file };\n inFlight.current = [...inFlight.current, upload];\n if (inFlight.current.length === 1) uploadingChange.current?.(true);\n setUploads(inFlight.current);\n const settle = () => {\n inFlight.current = inFlight.current.filter((u) => u.id !== upload.id);\n if (!mounted.current) return;\n setUploads(inFlight.current);\n if (inFlight.current.length === 0) uploadingChange.current?.(false);\n };\n let answer: Promise<FeedbackAttachmentRef>;\n try {\n answer = Promise.resolve(onUpload(file));\n } catch (error) {\n answer = Promise.reject(error);\n }\n answer.then(\n (ref) => {\n settle();\n if (mounted.current) commit([...latest.current, ref]);\n },\n (error: unknown) => {\n settle();\n if (mounted.current) onError?.(\"upload\", error, errorInfo(\"upload\", [file], error));\n },\n );\n };\n\n const add = (incoming: File[], pasted = false) => {\n let space = Math.max(0, max - latest.current.length - inFlight.current.length);\n const taken = new Set([...latest.current.map((r) => r.name), ...inFlight.current.map((u) => u.file.name)]);\n const dropped: File[] = [];\n for (const raw of incoming) {\n // Pasted images are all \"image.png\" to the clipboard; as in `multiple` mode,\n // each gets a name of its own before it is checked and uploaded under it.\n const file = pasted ? renamed(raw, uniqueName(pastedName(raw.type), taken)) : raw;\n const problem = rejection(file, accept, maxBytes);\n if (problem) {\n // `undefined` in `error`'s place, as in 0.22: only an upload has one.\n onError?.(problem, undefined, errorInfo(problem, [file]));\n continue;\n }\n taken.add(file.name);\n if (space === 0) {\n dropped.push(file);\n continue;\n }\n space -= 1;\n start(file);\n }\n if (dropped.length > 0) onError?.(\"count\", undefined, errorInfo(\"count\", dropped));\n };\n\n const onPaste = usePaste((images) => add(images, true), documentPaste, pasteFrom, inert);\n\n // Focus after a remove, as in `multiple` mode: the next chip, else the first action.\n const pendingFocus = useRef<number | null>(null);\n useEffect(() => {\n const index = pendingFocus.current;\n if (index === null) return;\n pendingFocus.current = null;\n const root = rootRef.current;\n if (!root) return;\n const removes = root.querySelectorAll<HTMLElement>(\"[data-attachment-remove]\");\n const target =\n removes[index] ?? root.querySelector<HTMLElement>(\"[data-attachment-action]\") ?? removes[index - 1];\n target?.focus();\n }, [value]);\n\n const remove = (key: string, index: number) => {\n if (inert) return;\n pendingFocus.current = index;\n commit(latest.current.filter((r) => r.key !== key));\n onRemove?.(key);\n };\n\n const hasChips = value.length > 0 || uploads.length > 0;\n\n return (\n <div ref={rootRef} className={cn(FIELD_ROOT, \"space-y-2\", className)} onPaste={onPaste}>\n <Heading text={labelsProp?.attachment} className=\"mb-0\" />\n {hasChips && (\n <ul aria-label={text.attachmentList} className=\"flex flex-col gap-2\">\n {value.map((ref, index) => (\n <li\n key={`ref:${ref.key}`}\n className=\"flex min-w-0 items-center gap-2 rounded-md border border-subtle bg-surface p-1\"\n >\n <RefGlyph type={ref.type} />\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate-until-large text-sm text-secondary\">{ref.name}</div>\n {ref.size !== undefined && (\n <div className=\"truncate-until-large text-xs text-muted\">{fileText.size(ref.size)}</div>\n )}\n </div>\n <RemoveButton\n label={removeLabel(ref.name)}\n onClick={() => remove(ref.key, index)}\n disabled={disabled}\n reason={reason}\n />\n </li>\n ))}\n {uploads.map((upload) => (\n <li\n key={`upload:${upload.id}`}\n aria-busy=\"true\"\n data-attachment-uploading=\"\"\n className=\"flex min-w-0 items-center gap-2 rounded-md border border-dashed border-subtle bg-surface p-1\"\n >\n <span className=\"flex size-12 shrink-0 items-center justify-center rounded border border-subtle bg-surface-2 text-placeholder\">\n <Spinner label={null} className=\"size-5\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate-until-large text-sm text-secondary\">{upload.file.name}</div>\n <div className=\"truncate-until-large text-xs text-muted\">{uploadingLabel}</div>\n </div>\n </li>\n ))}\n </ul>\n )}\n {room > 0 && (\n <div className=\"flex flex-wrap gap-2\">\n <Button\n type=\"button\"\n variant={buttonVariant}\n size={buttonSize}\n data-attachment-action=\"\"\n disabled={disabled}\n disabledReason={reason}\n onClick={() => fileInputRef.current?.click()}\n >\n <Paperclip className={buttonIcon(buttonSize)} /> {text.attachmentAdd}\n </Button>\n {onCaptureScreenshot && (\n <CaptureButton\n capture={onCaptureScreenshot}\n onFile={(file) => add([file])}\n label={text.attachmentCapture}\n disabled={disabled}\n disabledReason={reason}\n variant={buttonVariant}\n size={buttonSize}\n data-attachment-action=\"\"\n />\n )}\n </div>\n )}\n {locked ? (\n // Where the paste hint was: a phone never hovers, so the Tooltip alone would\n // leave a touch user with buttons that do nothing and no word why.\n <div data-attachment-reason=\"\" className=\"text-xs text-muted\">\n {disabledReason}\n </div>\n ) : (\n <p className=\"text-xs text-muted\">{room > 0 ? text.attachmentPaste : limitLabel(max)}</p>\n )}\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple={room > 1}\n accept={accept.join(\",\")}\n className=\"sr-only\"\n tabIndex={-1}\n aria-hidden\n disabled={inert}\n onChange={(e) => {\n add(Array.from(e.target.files ?? []));\n e.target.value = \"\";\n }}\n />\n </div>\n );\n}\n\n/** A ref chip's picture. The bytes are on the server, not here, so there is nothing to\n * preview: an image glyph for an image `type`, a file glyph for anything else. */\nfunction RefGlyph({ type }: { type?: string }) {\n return (\n <span className=\"flex size-12 shrink-0 items-center justify-center rounded border border-subtle bg-surface-2 text-placeholder\">\n {type?.startsWith(\"image/\") ? <ImageIcon aria-hidden className=\"size-6\" /> : <FileText aria-hidden className=\"size-6\" />}\n </span>\n );\n}\n\nfunction Heading({ text, className }: { text?: string; className?: string }) {\n if (!text) return null;\n return (\n <div className={cn(\"mb-1 text-xs font-medium uppercase tracking-wide text-muted\", className)}>\n {text}\n </div>\n );\n}\n\nfunction CaptureButton({\n capture,\n onFile,\n label,\n disabled = false,\n disabledReason,\n variant,\n size,\n ...rest\n}: {\n capture: () => Promise<File | null>;\n onFile: (file: File) => void;\n label: string;\n disabled?: boolean;\n disabledReason?: ReactNode;\n variant: ButtonVariant;\n size: ButtonSize;\n \"data-attachment-action\"?: string;\n}) {\n const [capturing, setCapturing] = useState(false);\n return (\n <Button\n type=\"button\"\n variant={variant}\n size={size}\n disabled={capturing || disabled}\n disabledReason={disabledReason}\n {...rest}\n onClick={() => {\n if (capturing || disabled) return;\n setCapturing(true);\n void capture()\n .then((file) => {\n if (file) onFile(file);\n })\n .finally(() => setCapturing(false));\n }}\n >\n <Camera className={buttonIcon(size)} /> {capturing ? \"…\" : label}\n </Button>\n );\n}\n\n/**\n * A chip's (or the single file's) remove button: the kit's IconButton at the 28px the\n * hand-built one had, so `disabled` (0.23.0) dims and disables it the kit way and a\n * `reason` takes IconButton's `disabledReason` path — focusable, `aria-disabled`, the\n * reason in the Tooltip — rather than a third copy of either here.\n */\nfunction RemoveButton({\n label,\n onClick,\n disabled,\n reason,\n}: {\n label: string;\n onClick: () => void;\n disabled: boolean;\n reason?: ReactNode;\n}) {\n return (\n <IconButton\n type=\"button\"\n size=\"xs\"\n tone=\"muted\"\n data-attachment-remove=\"\"\n aria-label={label}\n disabled={disabled}\n disabledReason={reason}\n onClick={onClick}\n className=\"shrink-0\"\n >\n <X aria-hidden />\n </IconButton>\n );\n}\n\n\n/**\n * The {@link FeedbackAttachmentErrorInfo} builder of one field: its limits, and the\n * sentence for each refusal. The sentences are the `filePicker` namespace's — the ones\n * FileButton and FileDropzone say — so an app that translated those has these too, and\n * a refusal reads the same whichever of the kit's pickers made it. Only the upload\n * failure, which no other picker has, is this field's own (`attachmentUploadFailed`).\n */\nfunction useErrorInfo(\n accept: string[],\n maxBytes: number,\n max: number | undefined,\n text: FeedbackAttachmentFieldLabels,\n) {\n const picker = useKitLabels(\"filePicker\", DEFAULT_FILE_PICKER_LABELS);\n const fileText = useKitFileLabels();\n const uploadFailed = text.attachmentUploadFailed ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentUploadFailed!;\n return (kind: FeedbackAttachmentRefsError, files: File[], error?: unknown): FeedbackAttachmentErrorInfo => {\n const file = files[0];\n let message: string;\n if (kind === \"type\") {\n // FileButton's rule: name what IS accepted (\"Only image/png, image/jpeg files\"),\n // unless the app translated `rejectedType` but not yet `rejectedTypeOnly` — its\n // own sentence beats an English one.\n const acceptText = formatAccept(accept.join(\",\"));\n const useOnly =\n acceptText !== \"\" &&\n (picker.rejectedTypeOnly !== DEFAULT_FILE_PICKER_LABELS.rejectedTypeOnly ||\n picker.rejectedType === DEFAULT_FILE_PICKER_LABELS.rejectedType);\n message = useOnly ? picker.rejectedTypeOnly(acceptText, file.name) : picker.rejectedType(file.name);\n } else if (kind === \"size\") {\n message = picker.rejectedSize(file.name, fileText.size(maxBytes));\n } else if (kind === \"count\") {\n message = files.length === 1 ? picker.rejectedCount(file.name, max ?? 1) : picker.rejectedMany(files.length);\n } else {\n message = uploadFailed(file.name);\n }\n return {\n file,\n files,\n message,\n maxBytes,\n accept,\n ...(max !== undefined ? { max } : null),\n ...(kind === \"upload\" ? { error } : null),\n };\n };\n}\n\n/** A chip's picture: the image itself, or a file glyph for anything that is not one.\n * Decorative (`alt=\"\"`) — the name stands beside it. Its own component so the object\n * URL lives exactly as long as the chip: removing the chip unmounts it and revokes. */\nfunction ChipPreview({ file, screenshot }: { file: File; screenshot: boolean }) {\n const isImage = file.type.startsWith(\"image/\");\n const [previewFor, setPreviewFor] = useState<{ file: File; url: string } | null>(null);\n const url = previewFor?.file === file ? previewFor.url : null;\n useEffect(() => {\n if (!isImage) return;\n const next = URL.createObjectURL(file);\n // As in the single field: an external resource created and revoked with the effect.\n // eslint-disable-next-line react-hooks/set-state-in-effect -- publishing an external resource's handle\n setPreviewFor({ file, url: next });\n return () => URL.revokeObjectURL(next);\n }, [file, isImage]);\n\n return (\n <span className=\"relative flex size-12 shrink-0 items-center justify-center overflow-hidden rounded border border-subtle bg-surface-2 text-placeholder\">\n {isImage && url ? (\n <img src={url} alt=\"\" className=\"size-full object-cover\" />\n ) : (\n <FileText aria-hidden className=\"size-6\" />\n )}\n {screenshot && (\n // Marks the slot apart from the photos: it is why the capture button is gone.\n <span className=\"absolute bottom-0.5 end-0.5 rounded-sm bg-surface p-0.5 text-secondary\">\n <Camera aria-hidden className=\"size-3\" />\n </span>\n )}\n </span>\n );\n}\n\n/** A stable React key per File object — the name is not one (two pastes, two\n * \"pasted.png\" before renaming; the same photo picked from two folders). */\nconst fileKeys = new WeakMap<File, number>();\nlet nextFileKey = 0;\nfunction fileKey(file: File): number {\n let key = fileKeys.get(file);\n if (key === undefined) {\n key = nextFileKey++;\n fileKeys.set(file, key);\n }\n return key;\n}\n\nfunction rejection(file: File, accept: string[], maxBytes: number): \"type\" | \"size\" | null {\n if (!accept.includes(file.type)) return \"type\";\n if (file.size > maxBytes) return \"size\";\n return null;\n}\n\nfunction renamed(file: File, name: string): File {\n return new File([file], name, { type: file.type });\n}\n\n/** `pasted.png`, then `pasted-2.png`, `pasted-3.png`… — the first name not in `taken`. */\nfunction uniqueName(name: string, taken: Set<string>): string {\n if (!taken.has(name)) return name;\n const dot = name.lastIndexOf(\".\");\n const [stem, ext] = dot > 0 ? [name.slice(0, dot), name.slice(dot)] : [name, \"\"];\n let n = 2;\n while (taken.has(`${stem}-${n}${ext}`)) n++;\n return `${stem}-${n}${ext}`;\n}\n\n/**\n * The paste listener both modes share. `onImages` gets every image file on the\n * clipboard (the single field takes the first). It is registered once rather than per\n * render, so it reads the handler — which closes over props that change identity on\n * every render — out of a ref rather than out of its own dependency list. Returns the\n * `onPaste` for the field's root, or `undefined` when listening elsewhere.\n */\nfunction usePaste(\n onImages: (images: File[]) => void,\n documentPaste: boolean,\n pasteFrom: RefObject<HTMLElement | null> | undefined,\n /** `disabled` / locked (0.23.0): the paste goes where it was going, untouched — not\n * swallowed, so a text half still lands in the box it was made in. */\n off: boolean,\n) {\n // `null` while off, so the listeners below leave the event alone entirely.\n const handler = off ? null : onImages;\n const latest = useRef(handler);\n useEffect(() => {\n latest.current = handler;\n });\n\n // The element listened on, where it is not this field's own subtree: the\n // document for a modal, the given parent for an inline editor. Read inside\n // the effect rather than in render, because a ref's `current` is only set\n // once the parent has mounted — which it has by the time effects run.\n useEffect(() => {\n const target: EventTarget | null = documentPaste ? document : (pasteFrom?.current ?? null);\n if (!target) return;\n const onPaste = (event: Event) => {\n if (!latest.current) return;\n const clipboard = (event as ClipboardEvent).clipboardData;\n takeImages(clipboard, () => event.preventDefault(), latest.current);\n };\n target.addEventListener(\"paste\", onPaste);\n return () => target.removeEventListener(\"paste\", onPaste);\n }, [documentPaste, pasteFrom]);\n\n const listensElsewhere = documentPaste || pasteFrom !== undefined;\n return listensElsewhere\n ? undefined\n : (event: ReactClipboardEvent) => {\n if (!latest.current) return;\n takeImages(event.clipboardData, () => event.preventDefault(), latest.current);\n };\n}\n\nfunction takeImages(clipboard: DataTransfer | null, stop: () => void, onImages: (images: File[]) => void) {\n const images = Array.from(clipboard?.items ?? [])\n .filter((item) => item.kind === \"file\" && item.type.startsWith(\"image/\"))\n .map((item) => item.getAsFile())\n .filter((file): file is File => file !== null);\n if (images.length === 0) return;\n // Only once there IS an image: a paste of text into a field must stay a\n // paste of text, and a clipboard holding both is a copy whose text half is\n // what the field was focused for.\n stop();\n onImages(images);\n}\n\n/** What a pasted image is called once it is an attachment.\n *\n * The extension is read off the mime type rather than assumed to be `.png`:\n * Safari puts TIFF on the clipboard and a file called `pasted.png` that is not\n * a PNG is one the receiving end opens wrong. */\nexport function pastedName(type: string): string {\n const subtype = type.split(\"/\")[1] ?? \"png\";\n // `image/svg+xml` and friends carry a suffix that is not part of the\n // extension, and none of them are in the accepted list anyway.\n return `pasted.${subtype.split(\"+\")[0]}`;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/feedback/feedback-attachment.tsx"],"sourcesContent":["import { cn } from \"../lib/cn\";\nimport { hasContent } from \"../lib/presence\";\nimport {\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n type ClipboardEvent as ReactClipboardEvent,\n type ReactNode,\n type RefObject,\n} from \"react\";\nimport { Camera, FileText, Image as ImageIcon, Paperclip, X } from \"lucide-react\";\nimport { Button } from \"../components/button\";\nimport { IconButton } from \"../components/icon-button\";\nimport { Spinner } from \"../components/spinner\";\nimport type { ButtonSize, ButtonVariant } from \"../components/button-look\";\nimport { DEFAULT_FILE_PICKER_LABELS, formatAccept } from \"../components/file-button\";\nimport { useCommitReason, type CommitScope } from \"../components/write-lock\";\nimport { useKitFileLabels, useKitLabels, type LabelOverride } from \"../i18n/kit-labels\";\nimport type { FeedbackAttachmentLabels } from \"./feedback-dialog\";\nimport { DEFAULT_FEEDBACK_ATTACHMENT_LABELS } from \"./feedback-attachment-labels\";\nimport { DEFAULT_ATTACHMENT_ACCEPT, DEFAULT_MAX_ATTACHMENT_BYTES, DEFAULT_MAX_ATTACHMENTS } from \"./feedback-record\";\n\n// Defined in ./feedback-record since 0.34, which a top bar can import without this\n// field (MOD-1); still exported from here, where they always were.\nexport { DEFAULT_ATTACHMENT_ACCEPT, DEFAULT_MAX_ATTACHMENT_BYTES, DEFAULT_MAX_ATTACHMENTS };\n\n/**\n * The field's own strings — the `feedbackAttachment` namespace of `<UiKitProvider\n * labels>`. The keys are {@link FeedbackAttachmentLabels}' own, so the `labels` prop\n * (and a whole `FeedbackDialogLabels` handed down by the dialog) merges over them\n * one to one. The optional heading, `attachment`, stays a prop: a note editor puts\n * the buttons straight under its textarea and wants none.\n *\n * Before 0.7.0 the two optional keys fell back to hard-coded English, so a German app\n * that relied on its provider got \"Capture screenshot\" under a German form.\n *\n * The three keys added in 0.15.5 for `multiple` mode are optional, so a complete\n * `UiKitLabels` typed before it still compiles; they fall back to English. So is\n * `attachmentScreenshot` (0.16.0).\n */\nexport interface FeedbackAttachmentFieldLabels {\n attachmentAdd: string;\n attachmentCapture: string;\n attachmentPaste: string;\n attachmentRemove: string;\n /** `multiple` mode: the accessible name of the list of chosen files — and, since\n * 0.16.0, the heading `<FeedbackDialog attachments=\"multiple\">` puts over it. */\n attachmentList?: string;\n /** `multiple` mode (0.16.0): what the screenshot slot's chip is called — \"Screenshot\"\n * rather than the capture's file name (\"screenshot.webp\"), which says nothing a\n * reporter chose. The file name stays as the chip's second line. */\n attachmentScreenshot?: string;\n /** `multiple` mode: one chip's remove button — \"Remove photo.jpg\". Named per file,\n * because five buttons all called \"Remove attachment\" say nothing about which. */\n attachmentRemoveFile?: (name: string) => string;\n /** `multiple` mode: the line shown in place of the add buttons once `max` is reached. */\n attachmentLimit?: (max: number) => string;\n /** `refs` mode (0.22.0, keksdose K16): the second line of a chip whose upload is still\n * running, where an uploaded one shows its size — \"Uploading…\". Optional, like the\n * keys above, so a `UiKitLabels` typed before it still compiles. */\n attachmentUploading?: string;\n /** `refs` mode (0.23.0, keksdose G5a): {@link FeedbackAttachmentErrorInfo.message} for\n * an `\"upload\"` refusal — \"“huge.png” could not be uploaded\". The other refusals'\n * sentences are the `filePicker` namespace's, shared with FileButton. Optional, like\n * the keys above. */\n attachmentUploadFailed?: (name: string) => string;\n}\n\n// Defined in ./feedback-attachment-labels, which holds no component code (0.34, KS U8).\nexport { DEFAULT_FEEDBACK_ATTACHMENT_LABELS };\n\n/** Why a file was turned away. `\"count\"` only ever comes from `multiple` (and `refs`)\n * mode: more files arrived at once than `max` left room for, and the surplus was\n * dropped. */\nexport type FeedbackAttachmentError = \"type\" | \"size\" | \"count\";\n\n/** `refs` mode's reasons: {@link FeedbackAttachmentError}, or `\"upload\"` — `onUpload`\n * rejected, and what it rejected with is `onError`'s second argument. A type of its\n * own so a host's exhaustive switch over the File modes' reasons stays exhaustive. */\nexport type FeedbackAttachmentRefsError = FeedbackAttachmentError | \"upload\";\n\n/**\n * What `onError` knows about a refusal beyond its kind (0.23.0, keksdose G5a): the file\n * it was about, the limit that refused it, and a sentence that says both.\n *\n * keksdose's support chat toasted \"File too large\" with no name — the field's `onError`\n * carried a kind and nothing else, so a refusal could not say WHICH of three pasted\n * screenshots went, nor what the ceiling was, and its own `AttachmentPicker` (a\n * FileButton, whose `onReject` hands over `{ file, message }`) said more than the kit's\n * field would have. This is that, for every mode.\n *\n * It arrives as an EXTRA argument, after the ones `onError` already had, so a handler\n * written for 0.22 — `(kind) => …`, or `refs` mode's `(kind, error) => …` — compiles\n * and behaves as before:\n *\n * - single and `multiple`: `onError(kind, info)`;\n * - `refs`: `onError(kind, error, info)` — the second place is `onUpload`'s rejection\n * since 0.22 (`undefined` for the other kinds, as it always was), so the info comes\n * third rather than changing what a 0.22 handler reads there. It also carries that\n * `error`, so one helper `(kind, info) => …` serves all three modes.\n *\n * An object rather than more positional arguments because what a host reaches for\n * differs by kind — the bytes for `\"size\"`, `max` for `\"count\"`, the server's answer for\n * `\"upload\"` — and a field added later must not shift anyone's arguments.\n */\nexport interface FeedbackAttachmentErrorInfo {\n /** The file refused — under the name the field would have given it (a pasted image\n * is already \"pasted.png\"). For `\"count\"`, the first of the surplus; for\n * `\"upload\"`, the file whose upload failed. */\n file: File;\n /** `\"count\"`: every file of the surplus (several can arrive in one pick). Otherwise\n * `[file]`. */\n files: File[];\n /**\n * One translated sentence naming the file and the limit — the ones FileButton's\n * `onReject` hands over, from the same `filePicker` namespace: \"“huge.png” is larger\n * than 10 MB\", \"Only image/png, image/jpeg files\", \"“c.png” was not added: at most 5\n * files\" (several at once: \"3 files were not added\"). `\"upload\"`: the field's own\n * `attachmentUploadFailed`, \"“huge.png” could not be uploaded\" — a host with a better\n * sentence from the server's answer uses `error`. Ready for a toast as it is.\n */\n message: string;\n /** The field's `maxBytes` — the ceiling a `\"size\"` refusal went over, in bytes. */\n maxBytes: number;\n /** The field's `accept` — the types a `\"type\"` refusal was not among. */\n accept: string[];\n /** The field's `max` — the ceiling a `\"count\"` refusal hit. Absent in single mode,\n * which holds one file by definition. */\n max?: number;\n /** `\"upload\"` only: what `onUpload` rejected with — `onError`'s second argument. */\n error?: unknown;\n}\n\n/**\n * One uploaded attachment in `refs` mode: what the host's upload answered with. `key`\n * identifies it (a storage key, an upload id) and is what a remove goes by; `name` is\n * what the chip says. `size` (bytes) and `type` (mime) are optional — given, the chip\n * shows the size and an image icon or a file icon accordingly.\n */\nexport interface FeedbackAttachmentRef {\n key: string;\n name: string;\n size?: number;\n type?: string;\n}\n\ninterface FeedbackAttachmentFieldBaseProps {\n /** Prop > `<UiKitProvider labels={{ feedbackAttachment }}>` > English. Optional\n * since 0.7.0; `attachment` (the heading) is only ever read from here. */\n labels?: LabelOverride<FeedbackAttachmentLabels> &\n Pick<Partial<FeedbackAttachmentFieldLabels>, \"attachmentUploading\" | \"attachmentUploadFailed\">;\n accept?: string[];\n maxBytes?: number;\n /**\n * Take no files for now (0.23.0, keksdose G5b): the add and capture buttons, the\n * hidden file input and every chip's remove button are natively disabled, and a paste\n * is let through untouched. The chips stay as they are — what is about to be sent\n * stays readable.\n *\n * For the moment a send is in flight. keksdose's support picker is `disabled` while\n * the message goes out, because the message carries the attachments AS THEY WERE at\n * the press: a chip removed mid-send has gone with it all the same, and one added\n * mid-send looks as if it had. The field has no drop target; the paste is its drop.\n *\n * This is a pause, not a lock that explains itself — for that, `refs` mode (the mode\n * that commits) takes `commit` and `disabledReason`.\n */\n disabled?: boolean;\n /** Snapshot the app view behind this and return it as a File. A \"Capture\n * screenshot\" button appears only when it is given. */\n onCaptureScreenshot?: () => Promise<File | null>;\n /** Listen for the paste on `document` rather than on this field's own\n * subtree. For a modal, which owns the whole page while it is up. */\n documentPaste?: boolean;\n /** Listen for the paste within this element's subtree rather than this\n * field's own — for a field standing *beside* the text box a paste is made\n * in, whose common parent is where the event bubbles to. Ignored when\n * `documentPaste` is set. */\n pasteFrom?: RefObject<HTMLElement | null>;\n /**\n * The add and capture buttons' look (0.24.0, keksdose) — Button's own `variant`.\n * Default `\"secondary\"`, the look they always had.\n *\n * keksdose's support chat puts this field in ChatComposer's `attachmentSlot`, in the\n * row with Send, where its own picker had been a `ghost` / `sm` FileButton: two\n * bordered full-size buttons under a chat box read as a second form, and the field\n * gave no way to say otherwise. Every mode, wherever the buttons appear; the chips'\n * remove buttons keep their size.\n */\n buttonVariant?: ButtonVariant;\n /** The add and capture buttons' size — Button's own `size`, with `buttonVariant`.\n * Default `\"md\"`. `\"sm\"` draws their icons at 14px, as a small button's are. */\n buttonSize?: ButtonSize;\n className?: string;\n}\n\n/**\n * The field's root, in every mode. `relative` for the `sr-only` file input (see the\n * single field). `min-w-0 max-w-full` (0.24.0, keksdose) because the field is often a\n * FLEX ITEM — ChatComposer's `attachmentSlot` row, FormActions' `start` — and a flex item\n * is never narrower than its content's min-content width unless told it may be. A chip\n * truncates its name only inside a box of bounded width, and a screenshot's name\n * (\"Screenshot 2026-10-03 at 07.45.12.png\") was the min-content: the field grew to it\n * and ran off a 390px phone composer (measured in Chromium: the field's right edge at\n * 592px, the page 614px wide). keksdose passed `className=\"min-w-0 max-w-full\"` at both\n * its callers to stop it; here once, for every host. In normal flow — a dialog, a note\n * editor — both are no-ops.\n *\n * The names and sizes under it are `truncate-until-large` (0.33, docs/text-size-harmonization.md\n * §10.17): \"…\" at Normal, and from Large up they wrap, breaking a long file name\n * anywhere, so the bound above still holds and nothing is cut.\n */\nconst FIELD_ROOT = \"relative min-w-0 max-w-full\";\n\n/** Paperclip and camera at the size the button's text is: 16px at `md`, 14px at `sm`. */\nconst buttonIcon = (size: ButtonSize) => (size === \"sm\" ? \"size-3.5\" : \"size-4\");\n\n/** One file: choosing a second means removing the first. The field's only mode until 0.15.5. */\nexport interface FeedbackAttachmentFieldSingleProps extends FeedbackAttachmentFieldBaseProps {\n multiple?: false;\n refs?: false;\n value: File | null;\n onChange: (file: File | null) => void;\n /** A file was refused. `info` (0.23.0) names it and the limit — see\n * {@link FeedbackAttachmentErrorInfo}. */\n onError?: (kind: \"type\" | \"size\", info: FeedbackAttachmentErrorInfo) => void;\n // Multiple-mode props, refused here: without `multiple` they would be silently ignored.\n max?: never;\n screenshot?: never;\n onScreenshotChange?: never;\n // `refs` mode's lock: a staged File is not a commit (see the refs props).\n commit?: never;\n disabledReason?: never;\n}\n\n/**\n * Several files (0.15.5, keksdose dev#578: \"pasted two photos, the second overwrote\n * the first\"). Every way in ADDS — a pick of several files, a paste, a capture — up to\n * `max`, and each file is a removable chip.\n */\nexport interface FeedbackAttachmentFieldMultipleProps extends FeedbackAttachmentFieldBaseProps {\n multiple: true;\n refs?: false;\n value: File[];\n onChange: (files: File[]) => void;\n /** How many files `value` may hold. Default {@link DEFAULT_MAX_ATTACHMENTS} (5). The\n * add buttons and the paste hint are offered only while there is room. */\n max?: number;\n /** Per file: a rejected one is reported and the others are still added. `\"count\"` is\n * once per pick, with the whole surplus in `info.files`. `info` (0.23.0): see\n * {@link FeedbackAttachmentErrorInfo}. */\n onError?: (kind: FeedbackAttachmentError, info: FeedbackAttachmentErrorInfo) => void;\n /**\n * A separate slot for THE screenshot, outside `value` and `max`. Given\n * `onScreenshotChange`, what `onCaptureScreenshot` returns lands here instead of in\n * `value`, shown as the first chip, and the capture button is offered only while the\n * slot is empty — a second snapshot of the same view is not more evidence. This is\n * how `<FeedbackDialog attachments=\"multiple\">` keeps the screenshot apart from the\n * photos; a form built from the parts can do the same.\n */\n screenshot?: File | null;\n onScreenshotChange?: (file: File | null) => void;\n // `refs` mode's lock: a staged File is not a commit (see the refs props).\n commit?: never;\n disabledReason?: never;\n}\n\n/**\n * Uploaded on pick (0.22.0, keksdose K16). keksdose's support chat uploads each file\n * the moment it is chosen and sends the message with the REFS the uploads answered —\n * a 10 MB screenshot is refused while the reporter is still typing, not when they\n * press Send — so its `AttachmentPicker` could not use this field, whose value is\n * `File`s held until the form is submitted. Here the value is\n * {@link FeedbackAttachmentRef}s: every way in (pick, paste, capture) validates the\n * file as the other modes do, then hands it to `onUpload` and shows a chip with a\n * spinner until it answers; the ref it resolves to joins `value` through `onChange`.\n * A rejection drops the chip and reports `onError(\"upload\", error)`. A chip is removed\n * BY KEY: `onChange` gets the list without it, and `onRemove` the key, for a host that\n * deletes the upload on the server too.\n *\n * Uploads still running count against `max`, so five picks in quick succession cannot\n * overshoot it; they are not in `value` until they land, so a send that reads `value`\n * sends only what has arrived — hold Send back while {@link onUploadingChange} says\n * an upload is running. Several files upload side by side and join in the order they\n * finish. The File modes are unchanged.\n */\nexport interface FeedbackAttachmentFieldRefsProps extends FeedbackAttachmentFieldBaseProps {\n refs: true;\n multiple?: never;\n value: FeedbackAttachmentRef[];\n onChange: (refs: FeedbackAttachmentRef[]) => void;\n /** Upload one file; resolve to its ref, or reject to refuse it. Called once per\n * accepted file, the moment it is chosen. */\n onUpload: (file: File) => Promise<FeedbackAttachmentRef>;\n /** A chip was removed — after `onChange` — with its ref's key. */\n onRemove?: (key: string) => void;\n /** Whether any upload is running: `true` when the first starts, `false` when the\n * last settles — for the host's Send, which should wait. */\n onUploadingChange?: (uploading: boolean) => void;\n /** How many refs `value` may hold, uploads in flight included. Default\n * {@link DEFAULT_MAX_ATTACHMENTS} (5). */\n max?: number;\n /** Per file. `\"upload\"` carries what `onUpload` rejected with as `error` (`undefined`\n * for the other kinds). `info` (0.23.0) comes third so a 0.22 handler's `error` is\n * still the second argument — see {@link FeedbackAttachmentErrorInfo}. */\n onError?: (kind: FeedbackAttachmentRefsError, error: unknown, info: FeedbackAttachmentErrorInfo) => void;\n /**\n * This field COMMITS (0.23.0): each file is uploaded the moment it is chosen, and a\n * remove may delete the upload (`onRemove`) — a save, unlike the File modes, which\n * only stage a file for a form sent later (and so stay open under a lock, like every\n * field). Under a locked {@link WriteLockProvider} it takes the `disabledReason` path\n * with the lock's reason, which wins over one of its own. No provider, or an unlocked\n * one: no effect.\n */\n commit?: CommitScope;\n /**\n * Why no file can be added or removed — the write lock's sentence, a quota, a thread\n * closed to replies. Unlike `disabled`, it SAYS so, the kit's commit-control way: the\n * buttons (add, capture, every chip's remove) stay focusable, `aria-disabled`, with\n * the reason in the kit Tooltip and their description; no pick, paste or remove gets\n * through; and the reason stands where the paste hint was, for a phone that never\n * hovers. The chips stay readable.\n */\n disabledReason?: ReactNode;\n screenshot?: never;\n onScreenshotChange?: never;\n}\n\nexport type FeedbackAttachmentFieldProps =\n FeedbackAttachmentFieldSingleProps | FeedbackAttachmentFieldMultipleProps | FeedbackAttachmentFieldRefsProps;\n\n/**\n * Picking one picture: the file dialog, a capture of the app view, or a paste.\n * With `multiple`, picking several (see {@link FeedbackAttachmentFieldMultipleProps}).\n *\n * Lifted out of {@link FeedbackDialog} so the *reply* half of the feature can\n * have it too (Steering Design feedback #128). A report is a conversation — it\n * is filed, it is answered, and the reporter sends it back saying that is not\n * what they meant — and the screenshot showing what they mean is taken at\n * whichever of those points they looked. Only the first of them had a way to\n * attach one, so everything after it had to be described in words.\n *\n * **Three ways in, and they are three because no one of them covers the others.**\n * The file dialog needs a file on disk; `onCaptureScreenshot` snapshots the\n * *whole* app view; and the clipboard is where a region snip already is on every\n * platform (Steering Design feedback #39). Somebody who wanted to show one panel\n * had to save a crop and then find it again.\n *\n * `documentPaste` and `pasteFrom` decide where the paste is listened for, and\n * it is a real choice rather than a flag with a default. A modal traps focus,\n * so while it is up every paste in the page is meant for it — including one\n * made with nothing in particular focused, which never reaches a React\n * `onPaste` on a child: that is `documentPaste`. An editor **inline on a\n * page** is not that: the page around it has its own fields, so it listens\n * within its own subtree and a paste elsewhere stays where it was aimed. But\n * \"its own subtree\" has to include the text box the paste is actually made in,\n * and that box is this field's *sibling*, not its child — a paste in it bubbles\n * to their common parent and never through here. `pasteFrom` is that parent\n * (Steering Design feedback #140): the element whose subtree is listened to,\n * handed in by whoever renders both the box and this field side by side.\n */\nexport function FeedbackAttachmentField(props: FeedbackAttachmentFieldProps) {\n // One component per mode rather than one with branches: the modes hold different\n // state (one preview vs. one per chip vs. uploads in flight), and a hook order that\n // depends on a prop is a crash the day a caller flips it.\n if (props.refs) return <RefsField {...props} />;\n return props.multiple ? <MultipleField {...props} /> : <SingleField {...props} />;\n}\n\nfunction SingleField({\n value,\n onChange,\n labels: labelsProp,\n accept = DEFAULT_ATTACHMENT_ACCEPT,\n maxBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onError,\n onCaptureScreenshot,\n documentPaste = false,\n pasteFrom,\n disabled = false,\n buttonVariant = \"secondary\",\n buttonSize = \"md\",\n className,\n}: FeedbackAttachmentFieldSingleProps) {\n // The object URL is keyed to the file it was made for, so a stale one (from the\n // previous file, or after `value` is cleared) is never shown — no reset needed.\n const [previewFor, setPreviewFor] = useState<{ file: File; url: string } | null>(null);\n const preview = value && previewFor?.file === value ? previewFor.url : null;\n const fileInputRef = useRef<HTMLInputElement>(null);\n // `file.size` from `<UiKitProvider labels>`, formatted in its locale — see FileDropzone.\n const fileText = useKitFileLabels();\n const text = useKitLabels(\"feedbackAttachment\", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);\n const errorInfo = useErrorInfo(accept, maxBytes, undefined, text);\n\n // Object-URL preview lifecycle (create on change, revoke on cleanup).\n useEffect(() => {\n if (!value) return;\n const url = URL.createObjectURL(value);\n // The URL is an external resource that must be created and revoked in step\n // with the effect (a render-time `createObjectURL` would leak, and StrictMode's\n // remount would revoke a memoised one out from under the <img>).\n // eslint-disable-next-line react-hooks/set-state-in-effect -- publishing an external resource's handle\n setPreviewFor({ file: value, url });\n return () => URL.revokeObjectURL(url);\n }, [value]);\n\n const pick = (file: File | undefined | null) => {\n if (!file) return;\n const problem = rejection(file, accept, maxBytes);\n if (problem) {\n onError?.(problem, errorInfo(problem, [file]));\n return;\n }\n onChange(file);\n };\n\n const onPaste = usePaste(\n (images) => {\n // Clipboard images arrive named \"image.png\" at best and unnamed at worst,\n // and the name is what the inbox shows beside the thumbnail. A name that\n // says where it came from is more use than the browser's.\n const file = images[0];\n pick(new File([file], pastedName(file.type), { type: file.type }));\n },\n documentPaste,\n pasteFrom,\n disabled,\n );\n\n return (\n <div\n // `relative` for the same reason as FileDropzone: the `sr-only` file input below\n // is `position: absolute`, and without a positioned ancestor it is laid out\n // against the initial containing block — extending the DOCUMENT height to its own\n // offset and producing a phantom second scrollbar on any long page. And\n // `min-w-0 max-w-full`, so a long name truncates in a flex row: see FIELD_ROOT.\n className={cn(FIELD_ROOT, className)}\n onPaste={onPaste}\n >\n <Heading text={labelsProp?.attachment} />\n {value ? (\n <div className=\"flex items-start gap-2\">\n {value.type.startsWith(\"image/\") && preview ? (\n <img src={preview} alt={value.name} className=\"h-20 w-20 rounded border border-subtle object-cover\" />\n ) : (\n // Non-image attachments (PDF, text) can't preview as an <img>, so\n // show a neutral file tile with the name/size beside it instead.\n <div className=\"flex h-20 w-20 items-center justify-center rounded border border-subtle text-placeholder\">\n <FileText className=\"size-8\" />\n </div>\n )}\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate-until-large text-sm text-secondary\">{value.name}</div>\n <div className=\"text-xs text-muted\">{fileText.size(value.size)}</div>\n </div>\n <RemoveButton label={text.attachmentRemove} onClick={() => onChange(null)} disabled={disabled} />\n </div>\n ) : (\n <div className=\"space-y-1.5\">\n <div className=\"flex flex-wrap gap-2\">\n <Button\n type=\"button\"\n variant={buttonVariant}\n size={buttonSize}\n disabled={disabled}\n onClick={() => fileInputRef.current?.click()}\n >\n <Paperclip className={buttonIcon(buttonSize)} /> {text.attachmentAdd}\n </Button>\n {onCaptureScreenshot && (\n <CaptureButton\n capture={onCaptureScreenshot}\n onFile={pick}\n label={text.attachmentCapture}\n disabled={disabled}\n variant={buttonVariant}\n size={buttonSize}\n />\n )}\n </div>\n {/* Said out loud, because a gesture with no affordance is a gesture\n nobody finds. */}\n <p className=\"text-xs text-muted\">{text.attachmentPaste}</p>\n </div>\n )}\n <input\n ref={fileInputRef}\n type=\"file\"\n accept={accept.join(\",\")}\n className=\"sr-only\"\n // Out of the tab order and the accessibility tree, as in the other two modes:\n // the add button is the way in (round 0.34, the showcase's axe check — this one\n // was a second, nameless stop).\n tabIndex={-1}\n aria-hidden\n disabled={disabled}\n onChange={(e) => {\n pick(e.target.files?.[0]);\n e.target.value = \"\";\n }}\n />\n </div>\n );\n}\n\nfunction MultipleField({\n value,\n onChange,\n max = DEFAULT_MAX_ATTACHMENTS,\n screenshot = null,\n onScreenshotChange,\n labels: labelsProp,\n accept = DEFAULT_ATTACHMENT_ACCEPT,\n maxBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onError,\n onCaptureScreenshot,\n documentPaste = false,\n pasteFrom,\n disabled = false,\n buttonVariant = \"secondary\",\n buttonSize = \"md\",\n className,\n}: FeedbackAttachmentFieldMultipleProps) {\n const rootRef = useRef<HTMLDivElement>(null);\n const fileInputRef = useRef<HTMLInputElement>(null);\n const fileText = useKitFileLabels();\n const text = useKitLabels(\"feedbackAttachment\", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);\n const removeLabel = text.attachmentRemoveFile ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentRemoveFile!;\n const limitLabel = text.attachmentLimit ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentLimit!;\n const screenshotLabel = text.attachmentScreenshot ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentScreenshot!;\n const errorInfo = useErrorInfo(accept, maxBytes, max, text);\n\n // What an add or a remove builds on: the `value` prop, plus whatever this field has\n // already handed to `onChange` since the parent last rendered. The field is\n // controlled, so between an `onChange` and the re-render that brings it back, `value`\n // is stale — and two adds in that gap (two pastes, a paste and a pick, delivered by\n // native listeners whose updates React batches) each spread the SAME old list, and\n // the second dropped the first. Re-synced to the prop after every commit, so a\n // parent that refuses a change (or edits the list itself) is what wins.\n const latest = useRef(value);\n useLayoutEffect(() => {\n latest.current = value;\n });\n const commit = (next: File[]) => {\n latest.current = next;\n onChange(next);\n };\n\n const hasSlot = onScreenshotChange !== undefined;\n const room = Math.max(0, max - value.length);\n // The screenshot slot is its own thing: it neither counts against `max` nor is\n // closed by it, so a full set of photos still leaves the capture on offer.\n const canCapture = !!onCaptureScreenshot && (hasSlot ? !screenshot : room > 0);\n\n const add = (incoming: File[], pasted = false) => {\n const current = latest.current;\n const accepted: File[] = [];\n const taken = new Set([...current, ...(screenshot ? [screenshot] : [])].map((f) => f.name));\n for (const raw of incoming) {\n // Two pastes are two files called \"pasted.png\", and a backend that stores by\n // name keeps one of them — the very loss this mode exists to end. A picked\n // file keeps its own name: it is the user's, and they may look for it. Named\n // before the check, so a refusal names it as the single field's does.\n const file = pasted ? renamed(raw, uniqueName(pastedName(raw.type), taken)) : raw;\n const problem = rejection(file, accept, maxBytes);\n if (problem) {\n onError?.(problem, errorInfo(problem, [file]));\n continue;\n }\n taken.add(file.name);\n // The same File object twice is one file chosen twice, not two.\n if (!current.includes(file)) accepted.push(file);\n }\n if (accepted.length === 0) return;\n const space = Math.max(0, max - current.length);\n if (accepted.length > space) onError?.(\"count\", errorInfo(\"count\", accepted.slice(space)));\n const kept = accepted.slice(0, space);\n if (kept.length > 0) commit([...current, ...kept]);\n };\n\n const onPaste = usePaste((images) => add(images, true), documentPaste, pasteFrom, disabled);\n\n const capture = (file: File) => {\n if (!hasSlot) {\n add([file]);\n return;\n }\n const problem = rejection(file, accept, maxBytes);\n if (problem) onError?.(problem, errorInfo(problem, [file]));\n else onScreenshotChange(file);\n };\n\n // Removing a chip removes the button that had focus. Focus goes to the chip that\n // took its place — the next one — and, when it was the last, to the first action\n // button (there is room again, so \"Attach\" is back), else to the chip before it.\n // Applied once the parent has re-rendered with the shorter list: the field is\n // controlled, and focusing before that would land on the chip being removed.\n const pendingFocus = useRef<number | null>(null);\n useEffect(() => {\n const index = pendingFocus.current;\n if (index === null) return;\n pendingFocus.current = null;\n const root = rootRef.current;\n if (!root) return;\n const removes = root.querySelectorAll<HTMLElement>(\"[data-attachment-remove]\");\n const target = removes[index] ?? root.querySelector<HTMLElement>(\"[data-attachment-action]\") ?? removes[index - 1];\n target?.focus();\n }, [value, screenshot]);\n\n const chips: Array<{ file: File; screenshot: boolean; name: string; remove: () => void }> = [\n ...(hasSlot && screenshot\n ? [{ file: screenshot, screenshot: true, name: screenshotLabel, remove: () => onScreenshotChange(null) }]\n : []),\n ...value.map((file) => ({\n file,\n screenshot: false,\n name: file.name,\n remove: () => commit(latest.current.filter((f) => f !== file)),\n })),\n ];\n\n return (\n <div ref={rootRef} className={cn(FIELD_ROOT, \"space-y-2\", className)} onPaste={onPaste}>\n <Heading text={labelsProp?.attachment} className=\"mb-0\" />\n {chips.length > 0 && (\n // One chip per row, full width: wrapped chips of name-dependent widths made a\n // ragged block in a 430px dialog, and a column keeps every name and remove\n // button in the same place — the next chip's button is right under the last.\n <ul aria-label={text.attachmentList} className=\"flex flex-col gap-2\">\n {chips.map((chip, index) => (\n <li\n key={fileKey(chip.file)}\n className=\"flex min-w-0 items-center gap-2 rounded-md border border-subtle bg-surface p-1\"\n >\n <ChipPreview file={chip.file} screenshot={chip.screenshot} />\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate-until-large text-sm text-secondary\">{chip.name}</div>\n <div className=\"truncate-until-large text-xs text-muted\">\n {/* The screenshot's file name moves down here: still there for the\n reporter who wants to know what will be sent, not its title. */}\n {chip.screenshot\n ? `${chip.file.name} · ${fileText.size(chip.file.size)}`\n : fileText.size(chip.file.size)}\n </div>\n </div>\n <RemoveButton\n label={removeLabel(chip.name)}\n disabled={disabled}\n onClick={() => {\n pendingFocus.current = index;\n chip.remove();\n }}\n />\n </li>\n ))}\n </ul>\n )}\n {(room > 0 || canCapture) && (\n <div className=\"flex flex-wrap gap-2\">\n {room > 0 && (\n <Button\n type=\"button\"\n variant={buttonVariant}\n size={buttonSize}\n data-attachment-action=\"\"\n disabled={disabled}\n onClick={() => fileInputRef.current?.click()}\n >\n <Paperclip className={buttonIcon(buttonSize)} /> {text.attachmentAdd}\n </Button>\n )}\n {canCapture && onCaptureScreenshot && (\n <CaptureButton\n capture={onCaptureScreenshot}\n onFile={capture}\n label={text.attachmentCapture}\n disabled={disabled}\n variant={buttonVariant}\n size={buttonSize}\n data-attachment-action=\"\"\n />\n )}\n </div>\n )}\n <p className=\"text-xs text-muted\">{room > 0 ? text.attachmentPaste : limitLabel(max)}</p>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple={room > 1}\n accept={accept.join(\",\")}\n className=\"sr-only\"\n // Out of the tab order and the accessibility tree: the button above is\n // the way in, and a second, unlabelled \"Choose files\" stop is noise.\n tabIndex={-1}\n aria-hidden\n disabled={disabled}\n onChange={(e) => {\n add(Array.from(e.target.files ?? []));\n e.target.value = \"\";\n }}\n />\n </div>\n );\n}\n\n/** A file `refs` mode is uploading: its own id (a ref has no key yet), and the file. */\ninterface Upload {\n id: number;\n file: File;\n}\n\nlet nextUploadId = 0;\n\nfunction RefsField({\n value,\n onChange,\n onUpload,\n onRemove,\n onUploadingChange,\n max = DEFAULT_MAX_ATTACHMENTS,\n labels: labelsProp,\n accept = DEFAULT_ATTACHMENT_ACCEPT,\n maxBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onError,\n onCaptureScreenshot,\n documentPaste = false,\n pasteFrom,\n disabled = false,\n commit: commits,\n disabledReason: ownDisabledReason,\n buttonVariant = \"secondary\",\n buttonSize = \"md\",\n className,\n}: FeedbackAttachmentFieldRefsProps) {\n const rootRef = useRef<HTMLDivElement>(null);\n const fileInputRef = useRef<HTMLInputElement>(null);\n const fileText = useKitFileLabels();\n const text = useKitLabels(\"feedbackAttachment\", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);\n const removeLabel = text.attachmentRemoveFile ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentRemoveFile!;\n const limitLabel = text.attachmentLimit ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentLimit!;\n const uploadingLabel = text.attachmentUploading ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentUploading!;\n const errorInfo = useErrorInfo(accept, maxBytes, max, text);\n // The lock's reason (under `commit`) over the field's own; with either, the controls\n // take the focusable `aria-disabled` path and say why — Button's and IconButton's\n // `disabledReason`, handed down as is.\n const disabledReason = useCommitReason(commits, ownDisabledReason);\n const locked = hasContent(disabledReason);\n const reason = locked ? disabledReason : undefined;\n const inert = disabled || locked;\n\n // As in `multiple` mode: `value` plus whatever has been handed to `onChange` since\n // the parent last rendered — two uploads landing in one tick must both survive.\n const latest = useRef(value);\n useLayoutEffect(() => {\n latest.current = value;\n });\n const commit = (next: FeedbackAttachmentRef[]) => {\n latest.current = next;\n onChange(next);\n };\n\n const [uploads, setUploads] = useState<Upload[]>([]);\n // The uploads in flight, kept in a ref as well: `add` runs several times before a\n // render when files arrive together, and each must see the others — their count\n // against `max`, their names against a pasted image's.\n const inFlight = useRef<Upload[]>([]);\n // Unmounted (the dialog closed mid-upload): a late answer is not this field's to\n // commit, and setting state on it would be a no-op at best.\n const mounted = useRef(true);\n useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n const uploadingChange = useRef(onUploadingChange);\n useEffect(() => {\n uploadingChange.current = onUploadingChange;\n });\n\n const room = Math.max(0, max - value.length - uploads.length);\n\n const start = (file: File) => {\n const upload: Upload = { id: nextUploadId++, file };\n inFlight.current = [...inFlight.current, upload];\n if (inFlight.current.length === 1) uploadingChange.current?.(true);\n setUploads(inFlight.current);\n const settle = () => {\n inFlight.current = inFlight.current.filter((u) => u.id !== upload.id);\n if (!mounted.current) return;\n setUploads(inFlight.current);\n if (inFlight.current.length === 0) uploadingChange.current?.(false);\n };\n let answer: Promise<FeedbackAttachmentRef>;\n try {\n answer = Promise.resolve(onUpload(file));\n } catch (error) {\n answer = Promise.reject(error);\n }\n answer.then(\n (ref) => {\n settle();\n if (mounted.current) commit([...latest.current, ref]);\n },\n (error: unknown) => {\n settle();\n if (mounted.current) onError?.(\"upload\", error, errorInfo(\"upload\", [file], error));\n },\n );\n };\n\n const add = (incoming: File[], pasted = false) => {\n let space = Math.max(0, max - latest.current.length - inFlight.current.length);\n const taken = new Set([...latest.current.map((r) => r.name), ...inFlight.current.map((u) => u.file.name)]);\n const dropped: File[] = [];\n for (const raw of incoming) {\n // Pasted images are all \"image.png\" to the clipboard; as in `multiple` mode,\n // each gets a name of its own before it is checked and uploaded under it.\n const file = pasted ? renamed(raw, uniqueName(pastedName(raw.type), taken)) : raw;\n const problem = rejection(file, accept, maxBytes);\n if (problem) {\n // `undefined` in `error`'s place, as in 0.22: only an upload has one.\n onError?.(problem, undefined, errorInfo(problem, [file]));\n continue;\n }\n taken.add(file.name);\n if (space === 0) {\n dropped.push(file);\n continue;\n }\n space -= 1;\n start(file);\n }\n if (dropped.length > 0) onError?.(\"count\", undefined, errorInfo(\"count\", dropped));\n };\n\n const onPaste = usePaste((images) => add(images, true), documentPaste, pasteFrom, inert);\n\n // Focus after a remove, as in `multiple` mode: the next chip, else the first action.\n const pendingFocus = useRef<number | null>(null);\n useEffect(() => {\n const index = pendingFocus.current;\n if (index === null) return;\n pendingFocus.current = null;\n const root = rootRef.current;\n if (!root) return;\n const removes = root.querySelectorAll<HTMLElement>(\"[data-attachment-remove]\");\n const target = removes[index] ?? root.querySelector<HTMLElement>(\"[data-attachment-action]\") ?? removes[index - 1];\n target?.focus();\n }, [value]);\n\n const remove = (key: string, index: number) => {\n if (inert) return;\n pendingFocus.current = index;\n commit(latest.current.filter((r) => r.key !== key));\n onRemove?.(key);\n };\n\n const hasChips = value.length > 0 || uploads.length > 0;\n\n return (\n <div ref={rootRef} className={cn(FIELD_ROOT, \"space-y-2\", className)} onPaste={onPaste}>\n <Heading text={labelsProp?.attachment} className=\"mb-0\" />\n {hasChips && (\n <ul aria-label={text.attachmentList} className=\"flex flex-col gap-2\">\n {value.map((ref, index) => (\n <li\n key={`ref:${ref.key}`}\n className=\"flex min-w-0 items-center gap-2 rounded-md border border-subtle bg-surface p-1\"\n >\n <RefGlyph type={ref.type} />\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate-until-large text-sm text-secondary\">{ref.name}</div>\n {ref.size !== undefined && (\n <div className=\"truncate-until-large text-xs text-muted\">{fileText.size(ref.size)}</div>\n )}\n </div>\n <RemoveButton\n label={removeLabel(ref.name)}\n onClick={() => remove(ref.key, index)}\n disabled={disabled}\n reason={reason}\n />\n </li>\n ))}\n {uploads.map((upload) => (\n <li\n key={`upload:${upload.id}`}\n aria-busy=\"true\"\n data-attachment-uploading=\"\"\n className=\"flex min-w-0 items-center gap-2 rounded-md border border-dashed border-subtle bg-surface p-1\"\n >\n <span className=\"flex size-12 shrink-0 items-center justify-center rounded border border-subtle bg-surface-2 text-placeholder\">\n <Spinner label={null} className=\"size-5\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"truncate-until-large text-sm text-secondary\">{upload.file.name}</div>\n <div className=\"truncate-until-large text-xs text-muted\">{uploadingLabel}</div>\n </div>\n </li>\n ))}\n </ul>\n )}\n {room > 0 && (\n <div className=\"flex flex-wrap gap-2\">\n <Button\n type=\"button\"\n variant={buttonVariant}\n size={buttonSize}\n data-attachment-action=\"\"\n disabled={disabled}\n disabledReason={reason}\n onClick={() => fileInputRef.current?.click()}\n >\n <Paperclip className={buttonIcon(buttonSize)} /> {text.attachmentAdd}\n </Button>\n {onCaptureScreenshot && (\n <CaptureButton\n capture={onCaptureScreenshot}\n onFile={(file) => add([file])}\n label={text.attachmentCapture}\n disabled={disabled}\n disabledReason={reason}\n variant={buttonVariant}\n size={buttonSize}\n data-attachment-action=\"\"\n />\n )}\n </div>\n )}\n {locked ? (\n // Where the paste hint was: a phone never hovers, so the Tooltip alone would\n // leave a touch user with buttons that do nothing and no word why.\n <div data-attachment-reason=\"\" className=\"text-xs text-muted\">\n {disabledReason}\n </div>\n ) : (\n <p className=\"text-xs text-muted\">{room > 0 ? text.attachmentPaste : limitLabel(max)}</p>\n )}\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple={room > 1}\n accept={accept.join(\",\")}\n className=\"sr-only\"\n tabIndex={-1}\n aria-hidden\n disabled={inert}\n onChange={(e) => {\n add(Array.from(e.target.files ?? []));\n e.target.value = \"\";\n }}\n />\n </div>\n );\n}\n\n/** A ref chip's picture. The bytes are on the server, not here, so there is nothing to\n * preview: an image glyph for an image `type`, a file glyph for anything else. */\nfunction RefGlyph({ type }: { type?: string }) {\n return (\n <span className=\"flex size-12 shrink-0 items-center justify-center rounded border border-subtle bg-surface-2 text-placeholder\">\n {type?.startsWith(\"image/\") ? (\n <ImageIcon aria-hidden className=\"size-6\" />\n ) : (\n <FileText aria-hidden className=\"size-6\" />\n )}\n </span>\n );\n}\n\nfunction Heading({ text, className }: { text?: string; className?: string }) {\n if (!text) return null;\n return <div className={cn(\"mb-1 text-xs font-medium uppercase tracking-wide text-muted\", className)}>{text}</div>;\n}\n\nfunction CaptureButton({\n capture,\n onFile,\n label,\n disabled = false,\n disabledReason,\n variant,\n size,\n ...rest\n}: {\n capture: () => Promise<File | null>;\n onFile: (file: File) => void;\n label: string;\n disabled?: boolean;\n disabledReason?: ReactNode;\n variant: ButtonVariant;\n size: ButtonSize;\n \"data-attachment-action\"?: string;\n}) {\n const [capturing, setCapturing] = useState(false);\n return (\n <Button\n type=\"button\"\n variant={variant}\n size={size}\n disabled={capturing || disabled}\n disabledReason={disabledReason}\n {...rest}\n onClick={() => {\n if (capturing || disabled) return;\n setCapturing(true);\n void capture()\n .then((file) => {\n if (file) onFile(file);\n })\n .finally(() => setCapturing(false));\n }}\n >\n <Camera className={buttonIcon(size)} /> {capturing ? \"…\" : label}\n </Button>\n );\n}\n\n/**\n * A chip's (or the single file's) remove button: the kit's IconButton at the 28px the\n * hand-built one had, so `disabled` (0.23.0) dims and disables it the kit way and a\n * `reason` takes IconButton's `disabledReason` path — focusable, `aria-disabled`, the\n * reason in the Tooltip — rather than a third copy of either here.\n */\nfunction RemoveButton({\n label,\n onClick,\n disabled,\n reason,\n}: {\n label: string;\n onClick: () => void;\n disabled: boolean;\n reason?: ReactNode;\n}) {\n return (\n <IconButton\n type=\"button\"\n size=\"xs\"\n tone=\"muted\"\n data-attachment-remove=\"\"\n aria-label={label}\n disabled={disabled}\n disabledReason={reason}\n onClick={onClick}\n className=\"shrink-0\"\n >\n <X aria-hidden />\n </IconButton>\n );\n}\n\n/**\n * The {@link FeedbackAttachmentErrorInfo} builder of one field: its limits, and the\n * sentence for each refusal. The sentences are the `filePicker` namespace's — the ones\n * FileButton and FileDropzone say — so an app that translated those has these too, and\n * a refusal reads the same whichever of the kit's pickers made it. Only the upload\n * failure, which no other picker has, is this field's own (`attachmentUploadFailed`).\n */\nfunction useErrorInfo(\n accept: string[],\n maxBytes: number,\n max: number | undefined,\n text: FeedbackAttachmentFieldLabels,\n) {\n const picker = useKitLabels(\"filePicker\", DEFAULT_FILE_PICKER_LABELS);\n const fileText = useKitFileLabels();\n const uploadFailed = text.attachmentUploadFailed ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentUploadFailed!;\n return (kind: FeedbackAttachmentRefsError, files: File[], error?: unknown): FeedbackAttachmentErrorInfo => {\n const file = files[0];\n let message: string;\n if (kind === \"type\") {\n // FileButton's rule: name what IS accepted (\"Only image/png, image/jpeg files\"),\n // unless the app translated `rejectedType` but not yet `rejectedTypeOnly` — its\n // own sentence beats an English one.\n const acceptText = formatAccept(accept.join(\",\"));\n const useOnly =\n acceptText !== \"\" &&\n (picker.rejectedTypeOnly !== DEFAULT_FILE_PICKER_LABELS.rejectedTypeOnly ||\n picker.rejectedType === DEFAULT_FILE_PICKER_LABELS.rejectedType);\n message = useOnly ? picker.rejectedTypeOnly(acceptText, file.name) : picker.rejectedType(file.name);\n } else if (kind === \"size\") {\n message = picker.rejectedSize(file.name, fileText.size(maxBytes));\n } else if (kind === \"count\") {\n message = files.length === 1 ? picker.rejectedCount(file.name, max ?? 1) : picker.rejectedMany(files.length);\n } else {\n message = uploadFailed(file.name);\n }\n return {\n file,\n files,\n message,\n maxBytes,\n accept,\n ...(max !== undefined ? { max } : null),\n ...(kind === \"upload\" ? { error } : null),\n };\n };\n}\n\n/** A chip's picture: the image itself, or a file glyph for anything that is not one.\n * Decorative (`alt=\"\"`) — the name stands beside it. Its own component so the object\n * URL lives exactly as long as the chip: removing the chip unmounts it and revokes. */\nfunction ChipPreview({ file, screenshot }: { file: File; screenshot: boolean }) {\n const isImage = file.type.startsWith(\"image/\");\n const [previewFor, setPreviewFor] = useState<{ file: File; url: string } | null>(null);\n const url = previewFor?.file === file ? previewFor.url : null;\n useEffect(() => {\n if (!isImage) return;\n const next = URL.createObjectURL(file);\n // As in the single field: an external resource created and revoked with the effect.\n // eslint-disable-next-line react-hooks/set-state-in-effect -- publishing an external resource's handle\n setPreviewFor({ file, url: next });\n return () => URL.revokeObjectURL(next);\n }, [file, isImage]);\n\n return (\n <span className=\"relative flex size-12 shrink-0 items-center justify-center overflow-hidden rounded border border-subtle bg-surface-2 text-placeholder\">\n {isImage && url ? (\n <img src={url} alt=\"\" className=\"size-full object-cover\" />\n ) : (\n <FileText aria-hidden className=\"size-6\" />\n )}\n {screenshot && (\n // Marks the slot apart from the photos: it is why the capture button is gone.\n <span className=\"absolute bottom-0.5 end-0.5 rounded-sm bg-surface p-0.5 text-secondary\">\n <Camera aria-hidden className=\"size-3\" />\n </span>\n )}\n </span>\n );\n}\n\n/** A stable React key per File object — the name is not one (two pastes, two\n * \"pasted.png\" before renaming; the same photo picked from two folders). */\nconst fileKeys = new WeakMap<File, number>();\nlet nextFileKey = 0;\nfunction fileKey(file: File): number {\n let key = fileKeys.get(file);\n if (key === undefined) {\n key = nextFileKey++;\n fileKeys.set(file, key);\n }\n return key;\n}\n\nfunction rejection(file: File, accept: string[], maxBytes: number): \"type\" | \"size\" | null {\n if (!accept.includes(file.type)) return \"type\";\n if (file.size > maxBytes) return \"size\";\n return null;\n}\n\nfunction renamed(file: File, name: string): File {\n return new File([file], name, { type: file.type });\n}\n\n/** `pasted.png`, then `pasted-2.png`, `pasted-3.png`… — the first name not in `taken`. */\nfunction uniqueName(name: string, taken: Set<string>): string {\n if (!taken.has(name)) return name;\n const dot = name.lastIndexOf(\".\");\n const [stem, ext] = dot > 0 ? [name.slice(0, dot), name.slice(dot)] : [name, \"\"];\n let n = 2;\n while (taken.has(`${stem}-${n}${ext}`)) n++;\n return `${stem}-${n}${ext}`;\n}\n\n/**\n * The paste listener both modes share. `onImages` gets every image file on the\n * clipboard (the single field takes the first). It is registered once rather than per\n * render, so it reads the handler — which closes over props that change identity on\n * every render — out of a ref rather than out of its own dependency list. Returns the\n * `onPaste` for the field's root, or `undefined` when listening elsewhere.\n */\nfunction usePaste(\n onImages: (images: File[]) => void,\n documentPaste: boolean,\n pasteFrom: RefObject<HTMLElement | null> | undefined,\n /** `disabled` / locked (0.23.0): the paste goes where it was going, untouched — not\n * swallowed, so a text half still lands in the box it was made in. */\n off: boolean,\n) {\n // `null` while off, so the listeners below leave the event alone entirely.\n const handler = off ? null : onImages;\n const latest = useRef(handler);\n useEffect(() => {\n latest.current = handler;\n });\n\n // The element listened on, where it is not this field's own subtree: the\n // document for a modal, the given parent for an inline editor. Read inside\n // the effect rather than in render, because a ref's `current` is only set\n // once the parent has mounted — which it has by the time effects run.\n useEffect(() => {\n const target: EventTarget | null = documentPaste ? document : (pasteFrom?.current ?? null);\n if (!target) return;\n const onPaste = (event: Event) => {\n if (!latest.current) return;\n const clipboard = (event as ClipboardEvent).clipboardData;\n takeImages(clipboard, () => event.preventDefault(), latest.current);\n };\n target.addEventListener(\"paste\", onPaste);\n return () => target.removeEventListener(\"paste\", onPaste);\n }, [documentPaste, pasteFrom]);\n\n const listensElsewhere = documentPaste || pasteFrom !== undefined;\n return listensElsewhere\n ? undefined\n : (event: ReactClipboardEvent) => {\n if (!latest.current) return;\n takeImages(event.clipboardData, () => event.preventDefault(), latest.current);\n };\n}\n\nfunction takeImages(clipboard: DataTransfer | null, stop: () => void, onImages: (images: File[]) => void) {\n const images = Array.from(clipboard?.items ?? [])\n .filter((item) => item.kind === \"file\" && item.type.startsWith(\"image/\"))\n .map((item) => item.getAsFile())\n .filter((file): file is File => file !== null);\n if (images.length === 0) return;\n // Only once there IS an image: a paste of text into a field must stay a\n // paste of text, and a clipboard holding both is a copy whose text half is\n // what the field was focused for.\n stop();\n onImages(images);\n}\n\n/** What a pasted image is called once it is an attachment.\n *\n * The extension is read off the mime type rather than assumed to be `.png`:\n * Safari puts TIFF on the clipboard and a file called `pasted.png` that is not\n * a PNG is one the receiving end opens wrong. */\nexport function pastedName(type: string): string {\n const subtype = type.split(\"/\")[1] ?? \"png\";\n // `image/svg+xml` and friends carry a suffix that is not part of the\n // extension, and none of them are in the accepted list anyway.\n return `pasted.${subtype.split(\"+\")[0]}`;\n}\n"],"mappings":"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","names":[]}
@@ -66,4 +66,4 @@ export interface CaptureAppScreenshotOptions {
66
66
  * Call it from the "Capture screenshot" button's own click and nowhere else (keksdose
67
67
  * checked: running it at dialog OPEN was suspected and was never the cause of the wait).
68
68
  */
69
- export declare function captureAppScreenshot({ rootId }?: CaptureAppScreenshotOptions): Promise<File | null>;
69
+ export declare function captureAppScreenshot({ rootId, }?: CaptureAppScreenshotOptions): Promise<File | null>;
@@ -165,7 +165,9 @@ function inViewport(node) {
165
165
  if (rect.width === 0 && rect.height === 0) return true;
166
166
  return !(rect.bottom <= 0 || rect.right <= 0 || rect.top >= window.innerHeight || rect.left >= window.innerWidth);
167
167
  }
168
- async function captureAppScreenshot({ rootId = "root" } = {}) {
168
+ async function captureAppScreenshot({
169
+ rootId = "root"
170
+ } = {}) {
169
171
  let domToBlob;
170
172
  try {
171
173
  ({ domToBlob } = await import("modern-screenshot"));