@eifi1/ui-kit 0.31.1 → 0.32.1

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 (565) hide show
  1. package/README.md +51 -28
  2. package/dist/account/delete-account-setting.d.ts +8 -3
  3. package/dist/admin/admin-action-confirm.d.ts +8 -3
  4. package/dist/admin/admin-action-log.d.ts +1 -1
  5. package/dist/admin/admin-action-log.js +4 -2
  6. package/dist/admin/admin-action-log.js.map +1 -1
  7. package/dist/admin/invitations-panel.d.ts +8 -3
  8. package/dist/admin/invitations-panel.js +29 -26
  9. package/dist/admin/invitations-panel.js.map +1 -1
  10. package/dist/admin/reviewer-scope-editor.d.ts +8 -3
  11. package/dist/admin/role-select.d.ts +10 -5
  12. package/dist/admin/transfer-ownership-dialog.d.ts +8 -3
  13. package/dist/admin/user-roster.d.ts +10 -5
  14. package/dist/admin/user-roster.js +8 -47
  15. package/dist/admin/user-roster.js.map +1 -1
  16. package/dist/auth/auth-errors.d.ts +43 -8
  17. package/dist/auth/auth-errors.js +24 -1
  18. package/dist/auth/auth-errors.js.map +1 -1
  19. package/dist/auth/register-form.d.ts +8 -3
  20. package/dist/billing/billing-banner.d.ts +184 -0
  21. package/dist/billing/billing-banner.js +98 -0
  22. package/dist/billing/billing-banner.js.map +1 -0
  23. package/dist/billing/billing-labels.d.ts +100 -0
  24. package/dist/billing/billing-labels.js +76 -0
  25. package/dist/billing/billing-labels.js.map +1 -0
  26. package/dist/billing/billing-lock.d.ts +158 -0
  27. package/dist/billing/billing-lock.js +17 -0
  28. package/dist/billing/billing-lock.js.map +1 -0
  29. package/dist/billing/billing-standing.d.ts +149 -0
  30. package/dist/billing/billing-standing.js +28 -0
  31. package/dist/billing/billing-standing.js.map +1 -0
  32. package/dist/billing/plan-card.d.ts +248 -0
  33. package/dist/billing/plan-card.js +248 -0
  34. package/dist/billing/plan-card.js.map +1 -0
  35. package/dist/billing/plan-limit-notice.d.ts +156 -0
  36. package/dist/billing/plan-limit-notice.js +76 -0
  37. package/dist/billing/plan-limit-notice.js.map +1 -0
  38. package/dist/billing/plan-limit.d.ts +34 -0
  39. package/dist/billing/plan-limit.js +20 -0
  40. package/dist/billing/plan-limit.js.map +1 -0
  41. package/dist/billing/plan-price.d.ts +61 -0
  42. package/dist/billing/plan-price.js +45 -0
  43. package/dist/billing/plan-price.js.map +1 -0
  44. package/dist/billing/subscription-actions.d.ts +133 -0
  45. package/dist/billing/subscription-actions.js +28 -0
  46. package/dist/billing/subscription-actions.js.map +1 -0
  47. package/dist/billing/subscription-status-chip.d.ts +149 -0
  48. package/dist/billing/subscription-status-chip.js +48 -0
  49. package/dist/billing/subscription-status-chip.js.map +1 -0
  50. package/dist/chart.d.ts +9 -3
  51. package/dist/chart.js.map +1 -1
  52. package/dist/components/account-chips.d.ts +16 -4
  53. package/dist/components/account-chips.js +21 -3
  54. package/dist/components/account-chips.js.map +1 -1
  55. package/dist/components/alert-banner.d.ts +6 -1
  56. package/dist/components/alert-banner.js +5 -4
  57. package/dist/components/alert-banner.js.map +1 -1
  58. package/dist/components/amount-input.d.ts +8 -3
  59. package/dist/components/amount-input.js +5 -5
  60. package/dist/components/amount-input.js.map +1 -1
  61. package/dist/components/appearance-labels.d.ts +38 -0
  62. package/dist/components/appearance-labels.js +19 -0
  63. package/dist/components/appearance-labels.js.map +1 -0
  64. package/dist/components/autocomplete.js +2 -2
  65. package/dist/components/autocomplete.js.map +1 -1
  66. package/dist/components/breadcrumbs.js +3 -2
  67. package/dist/components/breadcrumbs.js.map +1 -1
  68. package/dist/components/bulk-action-bar.d.ts +2 -2
  69. package/dist/components/bulk-action-bar.js +7 -12
  70. package/dist/components/bulk-action-bar.js.map +1 -1
  71. package/dist/components/button-group.d.ts +8 -3
  72. package/dist/components/button-group.js +7 -1
  73. package/dist/components/button-group.js.map +1 -1
  74. package/dist/components/calculator.d.ts +8 -3
  75. package/dist/components/calculator.js +4 -3
  76. package/dist/components/calculator.js.map +1 -1
  77. package/dist/components/calendar-heatmap.js +5 -5
  78. package/dist/components/calendar-heatmap.js.map +1 -1
  79. package/dist/components/chart-height.d.ts +23 -0
  80. package/dist/components/chart-height.js +15 -0
  81. package/dist/components/chart-height.js.map +1 -0
  82. package/dist/components/chart-zoom.js +6 -1
  83. package/dist/components/chart-zoom.js.map +1 -1
  84. package/dist/components/checkbox-group.js +2 -2
  85. package/dist/components/checkbox-group.js.map +1 -1
  86. package/dist/components/checkbox.d.ts +10 -0
  87. package/dist/components/checkbox.js +25 -10
  88. package/dist/components/checkbox.js.map +1 -1
  89. package/dist/components/chip.d.ts +19 -3
  90. package/dist/components/chip.js +54 -23
  91. package/dist/components/chip.js.map +1 -1
  92. package/dist/components/choice-card.d.ts +1 -1
  93. package/dist/components/choice-card.js +9 -8
  94. package/dist/components/choice-card.js.map +1 -1
  95. package/dist/components/column-mapper.js +1 -1
  96. package/dist/components/column-mapper.js.map +1 -1
  97. package/dist/components/combobox-core.js +8 -7
  98. package/dist/components/combobox-core.js.map +1 -1
  99. package/dist/components/combobox.js +8 -8
  100. package/dist/components/combobox.js.map +1 -1
  101. package/dist/components/confirm-dialog.d.ts +8 -3
  102. package/dist/components/copy-button.d.ts +10 -5
  103. package/dist/components/country-select.js +1 -1
  104. package/dist/components/country-select.js.map +1 -1
  105. package/dist/components/currency-select.js +3 -3
  106. package/dist/components/currency-select.js.map +1 -1
  107. package/dist/components/danger-confirm.d.ts +8 -3
  108. package/dist/components/data-table-cells.d.ts +1 -1
  109. package/dist/components/data-table-filter-popover.d.ts +1 -1
  110. package/dist/components/data-table-filter-popover.js +4 -1
  111. package/dist/components/data-table-filter-popover.js.map +1 -1
  112. package/dist/components/data-table-filters.d.ts +1 -1
  113. package/dist/components/data-table-labels.d.ts +1 -1
  114. package/dist/components/data-table-pagination.d.ts +3 -2
  115. package/dist/components/data-table-pagination.js +15 -6
  116. package/dist/components/data-table-pagination.js.map +1 -1
  117. package/dist/components/data-table.d.ts +1 -1
  118. package/dist/components/data-table.js +123 -52
  119. package/dist/components/data-table.js.map +1 -1
  120. package/dist/components/date-picker.d.ts +1 -1
  121. package/dist/components/date-picker.js +18 -6
  122. package/dist/components/date-picker.js.map +1 -1
  123. package/dist/components/description-list.js +2 -2
  124. package/dist/components/description-list.js.map +1 -1
  125. package/dist/components/dialog-frame.js +2 -1
  126. package/dist/components/dialog-frame.js.map +1 -1
  127. package/dist/components/disclosure.js +3 -2
  128. package/dist/components/disclosure.js.map +1 -1
  129. package/dist/components/dropdown.d.ts +1 -1
  130. package/dist/components/dropdown.js +7 -1
  131. package/dist/components/dropdown.js.map +1 -1
  132. package/dist/components/facing-pair.d.ts +19 -6
  133. package/dist/components/facing-pair.js +3 -2
  134. package/dist/components/facing-pair.js.map +1 -1
  135. package/dist/components/field-parts.d.ts +102 -6
  136. package/dist/components/field-parts.js +81 -6
  137. package/dist/components/field-parts.js.map +1 -1
  138. package/dist/components/field-sync.d.ts +4 -1
  139. package/dist/components/field-sync.js +12 -2
  140. package/dist/components/field-sync.js.map +1 -1
  141. package/dist/components/field.js +2 -2
  142. package/dist/components/field.js.map +1 -1
  143. package/dist/components/file-button.d.ts +8 -3
  144. package/dist/components/file-dropzone.d.ts +8 -3
  145. package/dist/components/floating-panel.js +7 -4
  146. package/dist/components/floating-panel.js.map +1 -1
  147. package/dist/components/focus-ring.d.ts +12 -0
  148. package/dist/components/focus-ring.js +8 -0
  149. package/dist/components/focus-ring.js.map +1 -0
  150. package/dist/components/form-actions.d.ts +8 -3
  151. package/dist/components/form-actions.js +33 -19
  152. package/dist/components/form-actions.js.map +1 -1
  153. package/dist/components/full-bleed-dialog.d.ts +11 -1
  154. package/dist/components/full-bleed-dialog.js +3 -1
  155. package/dist/components/full-bleed-dialog.js.map +1 -1
  156. package/dist/components/iban-input.d.ts +8 -3
  157. package/dist/components/image-grid.js +1 -1
  158. package/dist/components/image-grid.js.map +1 -1
  159. package/dist/components/inline-edit-field.js +10 -4
  160. package/dist/components/inline-edit-field.js.map +1 -1
  161. package/dist/components/language-select.d.ts +8 -3
  162. package/dist/components/legal-page.d.ts +26 -5
  163. package/dist/components/legal-page.js +3 -1
  164. package/dist/components/legal-page.js.map +1 -1
  165. package/dist/components/legal.d.ts +8 -3
  166. package/dist/components/legal.js +2 -1
  167. package/dist/components/legal.js.map +1 -1
  168. package/dist/components/lightbox.js +24 -2
  169. package/dist/components/lightbox.js.map +1 -1
  170. package/dist/components/line-items.js +2 -1
  171. package/dist/components/line-items.js.map +1 -1
  172. package/dist/components/list.d.ts +5 -0
  173. package/dist/components/list.js +13 -9
  174. package/dist/components/list.js.map +1 -1
  175. package/dist/components/measured-grid.d.ts +3 -1
  176. package/dist/components/measured-grid.js +38 -20
  177. package/dist/components/measured-grid.js.map +1 -1
  178. package/dist/components/menu-item.js +2 -1
  179. package/dist/components/menu-item.js.map +1 -1
  180. package/dist/components/mini-calendar.js +8 -4
  181. package/dist/components/mini-calendar.js.map +1 -1
  182. package/dist/components/money-field.d.ts +8 -3
  183. package/dist/components/month-picker.js +50 -39
  184. package/dist/components/month-picker.js.map +1 -1
  185. package/dist/components/nav-pills.js +1 -1
  186. package/dist/components/nav-pills.js.map +1 -1
  187. package/dist/components/number-field.d.ts +8 -3
  188. package/dist/components/number-input.d.ts +11 -4
  189. package/dist/components/number-input.js +8 -6
  190. package/dist/components/number-input.js.map +1 -1
  191. package/dist/components/numpad-sheet.d.ts +8 -3
  192. package/dist/components/numpad-sheet.js +2 -1
  193. package/dist/components/numpad-sheet.js.map +1 -1
  194. package/dist/components/one-time-code-input.js +2 -2
  195. package/dist/components/one-time-code-input.js.map +1 -1
  196. package/dist/components/page-contents.js +2 -2
  197. package/dist/components/page-contents.js.map +1 -1
  198. package/dist/components/phone-input.d.ts +8 -3
  199. package/dist/components/phone-input.js +4 -4
  200. package/dist/components/phone-input.js.map +1 -1
  201. package/dist/components/pie-chart.d.ts +8 -2
  202. package/dist/components/pie-chart.js +11 -7
  203. package/dist/components/pie-chart.js.map +1 -1
  204. package/dist/components/popover.d.ts +9 -1
  205. package/dist/components/popover.js +8 -4
  206. package/dist/components/popover.js.map +1 -1
  207. package/dist/components/row-actions.d.ts +100 -0
  208. package/dist/components/row-actions.js +231 -0
  209. package/dist/components/row-actions.js.map +1 -0
  210. package/dist/components/scroll-area.js +1 -1
  211. package/dist/components/scroll-area.js.map +1 -1
  212. package/dist/components/search-field.js +3 -2
  213. package/dist/components/search-field.js.map +1 -1
  214. package/dist/components/series-chart-budget.d.ts +30 -7
  215. package/dist/components/series-chart-budget.js +12 -4
  216. package/dist/components/series-chart-budget.js.map +1 -1
  217. package/dist/components/series-chart-ticks.d.ts +7 -1
  218. package/dist/components/series-chart-ticks.js +6 -0
  219. package/dist/components/series-chart-ticks.js.map +1 -1
  220. package/dist/components/series-chart.d.ts +58 -28
  221. package/dist/components/series-chart.js +42 -32
  222. package/dist/components/series-chart.js.map +1 -1
  223. package/dist/components/settings-fields.d.ts +57 -8
  224. package/dist/components/settings-fields.js +39 -1
  225. package/dist/components/settings-fields.js.map +1 -1
  226. package/dist/components/share-card.d.ts +11 -6
  227. package/dist/components/share-card.js +8 -2
  228. package/dist/components/share-card.js.map +1 -1
  229. package/dist/components/sign-chip.d.ts +4 -0
  230. package/dist/components/sign-chip.js +17 -4
  231. package/dist/components/sign-chip.js.map +1 -1
  232. package/dist/components/signature-pad.d.ts +3 -1
  233. package/dist/components/signature-pad.js +8 -5
  234. package/dist/components/signature-pad.js.map +1 -1
  235. package/dist/components/slider.js +7 -6
  236. package/dist/components/slider.js.map +1 -1
  237. package/dist/components/sparkline.d.ts +5 -1
  238. package/dist/components/sparkline.js +4 -2
  239. package/dist/components/sparkline.js.map +1 -1
  240. package/dist/components/stat-tile.d.ts +17 -1
  241. package/dist/components/stat-tile.js +48 -20
  242. package/dist/components/stat-tile.js.map +1 -1
  243. package/dist/components/status-dot.d.ts +4 -0
  244. package/dist/components/status-dot.js +9 -2
  245. package/dist/components/status-dot.js.map +1 -1
  246. package/dist/components/swipeable-row.js +10 -2
  247. package/dist/components/swipeable-row.js.map +1 -1
  248. package/dist/components/switch.d.ts +21 -7
  249. package/dist/components/switch.js +24 -11
  250. package/dist/components/switch.js.map +1 -1
  251. package/dist/components/table.js +2 -2
  252. package/dist/components/table.js.map +1 -1
  253. package/dist/components/text-link.d.ts +8 -3
  254. package/dist/components/text-link.js +1 -1
  255. package/dist/components/text-link.js.map +1 -1
  256. package/dist/components/text.d.ts +7 -2
  257. package/dist/components/text.js +5 -5
  258. package/dist/components/text.js.map +1 -1
  259. package/dist/components/tile-radio.js +2 -1
  260. package/dist/components/tile-radio.js.map +1 -1
  261. package/dist/components/time-input.d.ts +8 -3
  262. package/dist/components/toast.d.ts +18 -2
  263. package/dist/components/toast.js +8 -5
  264. package/dist/components/toast.js.map +1 -1
  265. package/dist/components/toggle-group.d.ts +25 -3
  266. package/dist/components/toggle-group.js +40 -11
  267. package/dist/components/toggle-group.js.map +1 -1
  268. package/dist/components/toggle-legend.d.ts +10 -1
  269. package/dist/components/toggle-legend.js +21 -7
  270. package/dist/components/toggle-legend.js.map +1 -1
  271. package/dist/components/translation-review-editor.js +1 -1
  272. package/dist/components/translation-review-editor.js.map +1 -1
  273. package/dist/components/translation-review-parts.d.ts +4 -0
  274. package/dist/components/translation-review.d.ts +4 -0
  275. package/dist/components/translation-review.js +3 -3
  276. package/dist/components/translation-review.js.map +1 -1
  277. package/dist/components/tree-view.js +3 -2
  278. package/dist/components/tree-view.js.map +1 -1
  279. package/dist/components/treemap.d.ts +5 -2
  280. package/dist/components/treemap.js +33 -16
  281. package/dist/components/treemap.js.map +1 -1
  282. package/dist/components/ui.d.ts +8 -3
  283. package/dist/components/ui.js +207 -119
  284. package/dist/components/ui.js.map +1 -1
  285. package/dist/components/use-table-state.d.ts +1 -1
  286. package/dist/components/user-avatar.d.ts +5 -1
  287. package/dist/components/user-avatar.js +1 -1
  288. package/dist/components/user-avatar.js.map +1 -1
  289. package/dist/components/wizard-stepper.js +20 -14
  290. package/dist/components/wizard-stepper.js.map +1 -1
  291. package/dist/{data-table-labels-DUELeXpa.d.ts → data-table-labels-kpAAVi6g.d.ts} +26 -2
  292. package/dist/data-table.d.ts +1 -1
  293. package/dist/{delete-account-setting-CpIhzxxZ.d.ts → delete-account-setting-FLyJf-kZ.d.ts} +642 -60
  294. package/dist/demo/demo-banner.js +1 -0
  295. package/dist/demo/demo-banner.js.map +1 -1
  296. package/dist/feedback/feedback-attachment.d.ts +8 -3
  297. package/dist/feedback/feedback-context.d.ts +8 -3
  298. package/dist/feedback/feedback-crash.d.ts +8 -3
  299. package/dist/feedback/feedback-dialog.d.ts +8 -3
  300. package/dist/feedback/feedback-dialog.js +3 -3
  301. package/dist/feedback/feedback-dialog.js.map +1 -1
  302. package/dist/feedback/feedback-inbox.d.ts +8 -3
  303. package/dist/feedback/feedback-inbox.js +2 -2
  304. package/dist/feedback/feedback-inbox.js.map +1 -1
  305. package/dist/feedback/feedback-labels.d.ts +8 -3
  306. package/dist/feedback/feedback-menu.d.ts +9 -4
  307. package/dist/feedback/feedback-record.d.ts +8 -3
  308. package/dist/feedback/feedback-row-detail.d.ts +8 -3
  309. package/dist/feedback/feedback-status-undo.d.ts +8 -3
  310. package/dist/feedback/feedback-submit.d.ts +8 -3
  311. package/dist/feedback/feedback-swipe.d.ts +8 -3
  312. package/dist/feedback/feedback-table.d.ts +8 -3
  313. package/dist/feedback/feedback-thread.d.ts +8 -3
  314. package/dist/feedback/feedback-thread.js +3 -3
  315. package/dist/feedback/feedback-thread.js.map +1 -1
  316. package/dist/feedback.d.ts +8 -3
  317. package/dist/hooks/use-anchored-panel.d.ts +32 -3
  318. package/dist/hooks/use-anchored-panel.js +21 -3
  319. package/dist/hooks/use-anchored-panel.js.map +1 -1
  320. package/dist/hooks/use-breakpoint.d.ts +88 -0
  321. package/dist/hooks/use-breakpoint.js +50 -0
  322. package/dist/hooks/use-breakpoint.js.map +1 -0
  323. package/dist/hooks/use-file-drop.d.ts +8 -3
  324. package/dist/hooks/use-large-text.d.ts +29 -0
  325. package/dist/hooks/use-large-text.js +20 -0
  326. package/dist/hooks/use-large-text.js.map +1 -0
  327. package/dist/hooks/use-windowed-rows.d.ts +52 -8
  328. package/dist/hooks/use-windowed-rows.js +94 -5
  329. package/dist/hooks/use-windowed-rows.js.map +1 -1
  330. package/dist/i18n/defaults.d.ts +8 -3
  331. package/dist/i18n/defaults.js +8 -0
  332. package/dist/i18n/defaults.js.map +1 -1
  333. package/dist/i18n/german.d.ts +8 -3
  334. package/dist/i18n/german.js +94 -3
  335. package/dist/i18n/german.js.map +1 -1
  336. package/dist/i18n/kit-labels.d.ts +8 -3
  337. package/dist/i18n/kit-labels.js.map +1 -1
  338. package/dist/i18n/languages.d.ts +10 -5
  339. package/dist/i18n/locales/de-CH.d.ts +8 -3
  340. package/dist/i18n/locales/en.d.ts +8 -3
  341. package/dist/i18n/locales/es.d.ts +8 -3
  342. package/dist/i18n/locales/es.js +89 -3
  343. package/dist/i18n/locales/es.js.map +1 -1
  344. package/dist/i18n/locales/fr.d.ts +8 -3
  345. package/dist/i18n/locales/fr.js +91 -3
  346. package/dist/i18n/locales/fr.js.map +1 -1
  347. package/dist/i18n/locales/hu.d.ts +8 -3
  348. package/dist/i18n/locales/hu.js +91 -3
  349. package/dist/i18n/locales/hu.js.map +1 -1
  350. package/dist/i18n/locales/it.d.ts +8 -3
  351. package/dist/i18n/locales/it.js +92 -3
  352. package/dist/i18n/locales/it.js.map +1 -1
  353. package/dist/i18n/locales/zh.d.ts +8 -3
  354. package/dist/i18n/locales/zh.js +89 -3
  355. package/dist/i18n/locales/zh.js.map +1 -1
  356. package/dist/i18n/review.d.ts +8 -3
  357. package/dist/i18n/review.js +15 -1
  358. package/dist/i18n/review.js.map +1 -1
  359. package/dist/index.d.ts +27 -11
  360. package/dist/index.js +77 -4
  361. package/dist/index.js.map +1 -1
  362. package/dist/landing/landing-actions.d.ts +8 -3
  363. package/dist/landing/landing-sections.d.ts +8 -3
  364. package/dist/landing/landing-sections.js +7 -3
  365. package/dist/landing/landing-sections.js.map +1 -1
  366. package/dist/landing/public-header.d.ts +9 -4
  367. package/dist/landing/public-header.js +3 -3
  368. package/dist/landing/public-header.js.map +1 -1
  369. package/dist/landing/routing.d.ts +8 -3
  370. package/dist/lib/cn.js +3 -2
  371. package/dist/lib/cn.js.map +1 -1
  372. package/dist/rhf/fields.d.ts +8 -3
  373. package/dist/rhf/fields.js +1 -1
  374. package/dist/rhf/fields.js.map +1 -1
  375. package/dist/rhf/form.d.ts +8 -3
  376. package/dist/rhf/form.js +2 -2
  377. package/dist/rhf/form.js.map +1 -1
  378. package/dist/rhf.d.ts +8 -3
  379. package/dist/search/command-palette.d.ts +1 -1
  380. package/dist/search/command-palette.js +9 -8
  381. package/dist/search/command-palette.js.map +1 -1
  382. package/dist/settings/settings-layout.js +2 -1
  383. package/dist/settings/settings-layout.js.map +1 -1
  384. package/dist/settings/settings-section.d.ts +8 -3
  385. package/dist/settings/use-account-appearance.d.ts +92 -0
  386. package/dist/settings/use-account-appearance.js +63 -0
  387. package/dist/settings/use-account-appearance.js.map +1 -0
  388. package/dist/settings/use-settings-route.d.ts +1 -1
  389. package/dist/settings/use-settings-route.js +2 -3
  390. package/dist/settings/use-settings-route.js.map +1 -1
  391. package/dist/shell/app-shell.d.ts +12 -171
  392. package/dist/shell/app-shell.js +144 -28
  393. package/dist/shell/app-shell.js.map +1 -1
  394. package/dist/shell/auth-layout.js +1 -1
  395. package/dist/shell/auth-layout.js.map +1 -1
  396. package/dist/shell/option-switcher-menu.js +1 -1
  397. package/dist/shell/option-switcher-menu.js.map +1 -1
  398. package/dist/shell/top-bar-brand.d.ts +8 -3
  399. package/dist/shell/top-bar-brand.js +3 -1
  400. package/dist/shell/top-bar-brand.js.map +1 -1
  401. package/dist/shell/topbar-action-menu.d.ts +4 -0
  402. package/dist/shell/topbar-action-menu.js +3 -3
  403. package/dist/shell/topbar-action-menu.js.map +1 -1
  404. package/dist/shell/topbar-controls.js +4 -4
  405. package/dist/shell/topbar-controls.js.map +1 -1
  406. package/dist/shell.d.ts +9 -5
  407. package/dist/theme/contrast-tokens.d.ts +41 -0
  408. package/dist/theme/contrast-tokens.js +59 -0
  409. package/dist/theme/contrast-tokens.js.map +1 -0
  410. package/dist/theme/contrast.d.ts +110 -0
  411. package/dist/theme/contrast.js +128 -0
  412. package/dist/theme/contrast.js.map +1 -0
  413. package/dist/theme/palette-presets.d.ts +12 -1
  414. package/dist/theme/palette-presets.js +9 -4
  415. package/dist/theme/palette-presets.js.map +1 -1
  416. package/dist/theme/palette-store.js.map +1 -1
  417. package/dist/theme/text-size.d.ts +173 -0
  418. package/dist/theme/text-size.js +121 -0
  419. package/dist/theme/text-size.js.map +1 -0
  420. package/dist/wizard/stepper-nav.d.ts +8 -3
  421. package/dist/wizard/wizard-summary.js +1 -1
  422. package/dist/wizard/wizard-summary.js.map +1 -1
  423. package/dist/wizard.d.ts +8 -3
  424. package/package.json +1 -1
  425. package/src/admin/admin-action-log.tsx +6 -1
  426. package/src/admin/invitations-panel.tsx +36 -29
  427. package/src/admin/user-roster.tsx +17 -72
  428. package/src/auth/auth-errors.ts +68 -7
  429. package/src/billing/billing-banner.tsx +205 -0
  430. package/src/billing/billing-labels.ts +240 -0
  431. package/src/billing/billing-lock.ts +75 -0
  432. package/src/billing/billing-standing.ts +72 -0
  433. package/src/billing/plan-card.tsx +458 -0
  434. package/src/billing/plan-limit-notice.tsx +145 -0
  435. package/src/billing/plan-limit.ts +53 -0
  436. package/src/billing/plan-price.ts +101 -0
  437. package/src/billing/subscription-actions.tsx +76 -0
  438. package/src/billing/subscription-status-chip.tsx +95 -0
  439. package/src/chart.ts +2 -0
  440. package/src/components/account-chips.tsx +36 -10
  441. package/src/components/alert-banner.tsx +16 -7
  442. package/src/components/amount-input.tsx +5 -5
  443. package/src/components/appearance-labels.ts +47 -0
  444. package/src/components/autocomplete.tsx +2 -2
  445. package/src/components/breadcrumbs.tsx +3 -3
  446. package/src/components/bulk-action-bar.tsx +11 -14
  447. package/src/components/button-group.tsx +21 -0
  448. package/src/components/calculator.tsx +4 -3
  449. package/src/components/calendar-heatmap.tsx +5 -5
  450. package/src/components/chart-height.ts +30 -0
  451. package/src/components/chart-zoom.tsx +6 -1
  452. package/src/components/checkbox-group.tsx +2 -2
  453. package/src/components/checkbox.tsx +62 -20
  454. package/src/components/chip.tsx +139 -30
  455. package/src/components/choice-card.tsx +16 -11
  456. package/src/components/column-mapper.tsx +1 -1
  457. package/src/components/combobox-core.tsx +8 -7
  458. package/src/components/combobox.tsx +8 -8
  459. package/src/components/country-select.tsx +1 -1
  460. package/src/components/currency-select.tsx +3 -3
  461. package/src/components/data-table-filter-popover.tsx +7 -1
  462. package/src/components/data-table-pagination.tsx +60 -31
  463. package/src/components/data-table.tsx +164 -43
  464. package/src/components/date-picker.tsx +44 -10
  465. package/src/components/description-list.tsx +2 -2
  466. package/src/components/dialog-frame.tsx +2 -1
  467. package/src/components/disclosure.tsx +3 -2
  468. package/src/components/dropdown.tsx +10 -2
  469. package/src/components/facing-pair.ts +12 -5
  470. package/src/components/field-parts.tsx +190 -7
  471. package/src/components/field-sync.tsx +17 -3
  472. package/src/components/field.tsx +2 -2
  473. package/src/components/floating-panel.tsx +7 -4
  474. package/src/components/focus-ring.ts +12 -0
  475. package/src/components/form-actions.tsx +55 -18
  476. package/src/components/full-bleed-dialog.tsx +18 -4
  477. package/src/components/image-grid.tsx +1 -1
  478. package/src/components/inline-edit-field.tsx +26 -4
  479. package/src/components/legal-page.tsx +28 -2
  480. package/src/components/legal.tsx +19 -1
  481. package/src/components/lightbox.tsx +24 -2
  482. package/src/components/line-items.tsx +7 -1
  483. package/src/components/list.tsx +36 -10
  484. package/src/components/measured-grid.tsx +49 -24
  485. package/src/components/menu-item.tsx +2 -2
  486. package/src/components/mini-calendar.tsx +8 -4
  487. package/src/components/month-picker.tsx +15 -6
  488. package/src/components/nav-pills.tsx +1 -1
  489. package/src/components/number-input.tsx +14 -7
  490. package/src/components/numpad-sheet.tsx +2 -2
  491. package/src/components/one-time-code-input.tsx +2 -2
  492. package/src/components/page-contents.tsx +2 -2
  493. package/src/components/phone-input.tsx +9 -4
  494. package/src/components/pie-chart.tsx +19 -9
  495. package/src/components/popover.tsx +22 -5
  496. package/src/components/row-actions.tsx +450 -0
  497. package/src/components/scroll-area.tsx +1 -1
  498. package/src/components/search-field.tsx +4 -2
  499. package/src/components/series-chart-budget.ts +40 -8
  500. package/src/components/series-chart-ticks.ts +10 -0
  501. package/src/components/series-chart.tsx +123 -72
  502. package/src/components/settings-fields.tsx +89 -5
  503. package/src/components/share-card.tsx +31 -2
  504. package/src/components/sign-chip.tsx +28 -4
  505. package/src/components/signature-pad.tsx +13 -7
  506. package/src/components/slider.tsx +22 -9
  507. package/src/components/sparkline.tsx +11 -3
  508. package/src/components/stat-tile.tsx +70 -14
  509. package/src/components/status-dot.tsx +25 -2
  510. package/src/components/swipeable-row.tsx +10 -2
  511. package/src/components/switch.tsx +57 -23
  512. package/src/components/table.tsx +2 -2
  513. package/src/components/text-link.tsx +1 -1
  514. package/src/components/text.tsx +11 -6
  515. package/src/components/tile-radio.tsx +4 -1
  516. package/src/components/toast.tsx +27 -7
  517. package/src/components/toggle-group.tsx +115 -14
  518. package/src/components/toggle-legend.tsx +53 -8
  519. package/src/components/translation-review-editor.tsx +1 -1
  520. package/src/components/translation-review.tsx +11 -5
  521. package/src/components/tree-view.tsx +3 -2
  522. package/src/components/treemap.tsx +27 -10
  523. package/src/components/ui.tsx +353 -153
  524. package/src/components/user-avatar.tsx +1 -1
  525. package/src/components/wizard-stepper.tsx +5 -1
  526. package/src/demo/demo-banner.tsx +5 -0
  527. package/src/feedback/feedback-dialog.tsx +3 -3
  528. package/src/feedback/feedback-inbox.tsx +2 -2
  529. package/src/feedback/feedback-thread.tsx +3 -3
  530. package/src/hooks/use-anchored-panel.ts +67 -5
  531. package/src/hooks/use-breakpoint.ts +132 -0
  532. package/src/hooks/use-large-text.ts +41 -0
  533. package/src/hooks/use-windowed-rows.ts +168 -12
  534. package/src/i18n/defaults.ts +8 -0
  535. package/src/i18n/german.ts +107 -0
  536. package/src/i18n/kit-labels.tsx +12 -0
  537. package/src/i18n/locales/es.ts +98 -0
  538. package/src/i18n/locales/fr.ts +102 -0
  539. package/src/i18n/locales/hu.ts +100 -0
  540. package/src/i18n/locales/it.ts +103 -0
  541. package/src/i18n/locales/zh.ts +94 -0
  542. package/src/i18n/review.ts +14 -0
  543. package/src/index.ts +76 -6
  544. package/src/landing/landing-sections.tsx +7 -3
  545. package/src/landing/public-header.tsx +4 -4
  546. package/src/lib/cn.ts +13 -2
  547. package/src/rhf/fields.tsx +1 -1
  548. package/src/rhf/form.tsx +2 -2
  549. package/src/search/command-palette.tsx +10 -9
  550. package/src/settings/settings-layout.tsx +2 -1
  551. package/src/settings/use-account-appearance.ts +148 -0
  552. package/src/settings/use-settings-route.ts +3 -4
  553. package/src/shell/app-shell.tsx +278 -54
  554. package/src/shell/auth-layout.tsx +1 -1
  555. package/src/shell/option-switcher-menu.tsx +1 -1
  556. package/src/shell/top-bar-brand.tsx +3 -1
  557. package/src/shell/topbar-action-menu.tsx +3 -3
  558. package/src/shell/topbar-controls.tsx +4 -4
  559. package/src/theme/contrast-tokens.ts +112 -0
  560. package/src/theme/contrast.ts +225 -0
  561. package/src/theme/palette-presets.ts +24 -4
  562. package/src/theme/palette-store.ts +6 -1
  563. package/src/theme/text-size.ts +283 -0
  564. package/src/wizard/wizard-summary.tsx +4 -2
  565. package/tokens.css +158 -8
@@ -75,6 +75,7 @@ function DemoBanner({
75
75
  size: "2xs",
76
76
  variant: "ghost",
77
77
  label: labels.details,
78
+ labelVisible: false,
78
79
  "aria-expanded": !collapsed,
79
80
  onClick: toggle,
80
81
  children: collapsed ? /* @__PURE__ */ jsx(ChevronDown, {}) : /* @__PURE__ */ jsx(ChevronUp, {})
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/demo/demo-banner.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ComponentPropsWithoutRef } from \"react\";\nimport { ChevronDown, ChevronUp, Timer } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { Button, IconButton } from \"../components/ui\";\nimport { useAccessAction } from \"../landing/access\";\nimport type { AccessChoice } from \"../landing/access\";\nimport { useLandingLabels } from \"../landing/landing-labels\";\nimport type { LandingLabels } from \"../landing/landing-labels\";\nimport { useDemoCountdown } from \"./demo-countdown\";\nimport type { DemoCountdown, DemoExpiry } from \"./demo-countdown\";\nimport { useDemoLabels } from \"./demo-labels\";\nimport type { DemoLabels } from \"./demo-labels\";\nimport type { DemoModel } from \"./demo-session\";\n\n/** The default `sessionStorage` key of the collapsed state. */\nconst COLLAPSE_KEY = \"kit.demoBanner.collapsed\";\n\n/** `sessionStorage`, guarded like `lib/safe-storage`'s `localStorage`: blocked storage\n * reads as \"not set\" and writes nothing. */\nfunction readSession(key: string): string | null {\n if (typeof window === \"undefined\") return null;\n try {\n return window.sessionStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\nfunction writeSession(key: string, value: string | null): void {\n if (typeof window === \"undefined\") return;\n try {\n if (value === null) window.sessionStorage.removeItem(key);\n else window.sessionStorage.setItem(key, value);\n } catch {\n // storage blocked: the banner just opens again next time\n }\n}\n\n/** \"Demo · 23 h 12 min left\", \"Demo · 50 min left\", the end, or plain \"Demo\". */\nfunction timeText(countdown: DemoCountdown, labels: DemoLabels): string {\n if (countdown.minutesLeft === null) return labels.badge;\n if (countdown.ended) return labels.endedTitle;\n return countdown.lastHour\n ? labels.minutesLeft(countdown.minutes)\n : labels.hoursLeft(countdown.hours, countdown.minutes);\n}\n\nexport interface DemoBannerProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\" | \"onClick\"> {\n /** The demo's end: `expires_at` from the `TokenResponse`, or `demo_expires_at` from\n * `/auth/me` (§5.3). */\n expiresAt: DemoExpiry | null | undefined;\n /** Which line the banner says about the data (§2.3). */\n model: DemoModel;\n /** \"Request access\". */\n access: AccessChoice;\n /** \"Sign in\" — let through for a demo session by `RedirectIfAuthed allowDemo`. Default\n * `/login`. */\n signInHref?: string;\n /** The countdown reached zero: clear the session and replace to `/demo/ended` (§5.5). */\n onEnded?: () => void;\n /** Where the collapsed state is kept for the tab's session. Default\n * `kit.demoBanner.collapsed`. */\n collapseKey?: string;\n /** The clock, for tests and previews. Default `Date.now`. Keep it stable. */\n now?: () => number;\n labels?: Partial<DemoLabels>;\n /** The access words (`landing` namespace) for this instance. */\n landingLabels?: Partial<LandingLabels>;\n}\n\n/**\n * The demo's strip under the top bar, on every page of a demo session\n * (docs/landing-demo-harmonization.md §5.4) — a kit `AlertBanner` strip:\n *\n * - the time left, \"Demo · 23 h 12 min left\" — minutes only in the last hour, and the\n * warning tone in the last ten minutes (`useDemoCountdown`);\n * - one line on the model: R \"You're looking at sample data. Changes aren't possible.\",\n * S \"Your own work is deleted when the demo ends.\";\n * - \"Request access\" (the mail) and \"Sign in\".\n *\n * It collapses to the countdown alone, for the tab's session (`sessionStorage`) — never\n * hidden for good: it is a live state, like keksdose's preview banner, which a demo user\n * gets instead of. At zero it calls `onEnded`.\n *\n * The countdown is no live region: a sentence read out every minute would drown the\n * page. A screen-reader user meets it at the top of the page, and the end page says\n * the rest.\n */\nexport function DemoBanner({\n expiresAt,\n model,\n access,\n signInHref = \"/login\",\n onEnded,\n collapseKey = COLLAPSE_KEY,\n now,\n labels: labelsProp,\n landingLabels,\n className,\n ...rest\n}: DemoBannerProps) {\n const labels = useDemoLabels(labelsProp);\n const landing = useLandingLabels(landingLabels);\n const accessLink = useAccessAction(access, landingLabels);\n const countdown = useDemoCountdown(expiresAt, { now, onEnded });\n const [collapsed, setCollapsed] = useState(() => readSession(collapseKey) === \"1\");\n\n const toggle = () => {\n const next = !collapsed;\n setCollapsed(next);\n writeSession(collapseKey, next ? \"1\" : null);\n };\n\n return (\n <AlertBanner\n {...rest}\n variant=\"strip\"\n size=\"sm\"\n tone={countdown.warning || countdown.ended ? \"warning\" : \"info\"}\n icon={<Timer />}\n data-demo-model={model}\n className={cn(\"sm:text-sm\", className)}\n action={\n <>\n {!collapsed && (\n <>\n <Button href={accessLink.href} variant=\"brand\" size=\"sm\">\n {accessLink.label}\n </Button>\n <Button href={signInHref} variant=\"secondary\" size=\"sm\">\n {landing.signIn}\n </Button>\n </>\n )}\n <IconButton\n size=\"2xs\"\n variant=\"ghost\"\n label={labels.details}\n aria-expanded={!collapsed}\n onClick={toggle}\n >\n {collapsed ? <ChevronDown /> : <ChevronUp />}\n </IconButton>\n </>\n }\n >\n <span className=\"font-semibold\">{timeText(countdown, labels)}</span>\n {!collapsed && (\n <>\n {\" \"}\n <span>{model === \"read-only\" ? labels.readOnly : labels.sandbox}</span>\n </>\n )}\n </AlertBanner>\n );\n}\n"],"mappings":";AA0HY,SAMA,UANA,KAMA,YANA;AA1HZ,SAAS,gBAAgB;AAEzB,SAAS,aAAa,WAAW,aAAa;AAE9C,SAAS,UAAU;AACnB,SAAS,mBAAmB;AAC5B,SAAS,QAAQ,kBAAkB;AACnC,SAAS,uBAAuB;AAEhC,SAAS,wBAAwB;AAEjC,SAAS,wBAAwB;AAEjC,SAAS,qBAAqB;AAK9B,MAAM,eAAe;AAIrB,SAAS,YAAY,KAA4B;AAC/C,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,MAAI;AACF,WAAO,OAAO,eAAe,QAAQ,GAAG;AAAA,EAC1C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,aAAa,KAAa,OAA4B;AAC7D,MAAI,OAAO,WAAW,YAAa;AACnC,MAAI;AACF,QAAI,UAAU,KAAM,QAAO,eAAe,WAAW,GAAG;AAAA,QACnD,QAAO,eAAe,QAAQ,KAAK,KAAK;AAAA,EAC/C,QAAQ;AAAA,EAER;AACF;AAGA,SAAS,SAAS,WAA0B,QAA4B;AACtE,MAAI,UAAU,gBAAgB,KAAM,QAAO,OAAO;AAClD,MAAI,UAAU,MAAO,QAAO,OAAO;AACnC,SAAO,UAAU,WACb,OAAO,YAAY,UAAU,OAAO,IACpC,OAAO,UAAU,UAAU,OAAO,UAAU,OAAO;AACzD;AA2CO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,QAAM,SAAS,cAAc,UAAU;AACvC,QAAM,UAAU,iBAAiB,aAAa;AAC9C,QAAM,aAAa,gBAAgB,QAAQ,aAAa;AACxD,QAAM,YAAY,iBAAiB,WAAW,EAAE,KAAK,QAAQ,CAAC;AAC9D,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,MAAM,YAAY,WAAW,MAAM,GAAG;AAEjF,QAAM,SAAS,MAAM;AACnB,UAAM,OAAO,CAAC;AACd,iBAAa,IAAI;AACjB,iBAAa,aAAa,OAAO,MAAM,IAAI;AAAA,EAC7C;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,MAAM,UAAU,WAAW,UAAU,QAAQ,YAAY;AAAA,MACzD,MAAM,oBAAC,SAAM;AAAA,MACb,mBAAiB;AAAA,MACjB,WAAW,GAAG,cAAc,SAAS;AAAA,MACrC,QACE,iCACG;AAAA,SAAC,aACA,iCACE;AAAA,8BAAC,UAAO,MAAM,WAAW,MAAM,SAAQ,SAAQ,MAAK,MACjD,qBAAW,OACd;AAAA,UACA,oBAAC,UAAO,MAAM,YAAY,SAAQ,aAAY,MAAK,MAChD,kBAAQ,QACX;AAAA,WACF;AAAA,QAEF;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,OAAO,OAAO;AAAA,YACd,iBAAe,CAAC;AAAA,YAChB,SAAS;AAAA,YAER,sBAAY,oBAAC,eAAY,IAAK,oBAAC,aAAU;AAAA;AAAA,QAC5C;AAAA,SACF;AAAA,MAGF;AAAA,4BAAC,UAAK,WAAU,iBAAiB,mBAAS,WAAW,MAAM,GAAE;AAAA,QAC5D,CAAC,aACA,iCACG;AAAA;AAAA,UACD,oBAAC,UAAM,oBAAU,cAAc,OAAO,WAAW,OAAO,SAAQ;AAAA,WAClE;AAAA;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/demo/demo-banner.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ComponentPropsWithoutRef } from \"react\";\nimport { ChevronDown, ChevronUp, Timer } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { Button, IconButton } from \"../components/ui\";\nimport { useAccessAction } from \"../landing/access\";\nimport type { AccessChoice } from \"../landing/access\";\nimport { useLandingLabels } from \"../landing/landing-labels\";\nimport type { LandingLabels } from \"../landing/landing-labels\";\nimport { useDemoCountdown } from \"./demo-countdown\";\nimport type { DemoCountdown, DemoExpiry } from \"./demo-countdown\";\nimport { useDemoLabels } from \"./demo-labels\";\nimport type { DemoLabels } from \"./demo-labels\";\nimport type { DemoModel } from \"./demo-session\";\n\n/** The default `sessionStorage` key of the collapsed state. */\nconst COLLAPSE_KEY = \"kit.demoBanner.collapsed\";\n\n/** `sessionStorage`, guarded like `lib/safe-storage`'s `localStorage`: blocked storage\n * reads as \"not set\" and writes nothing. */\nfunction readSession(key: string): string | null {\n if (typeof window === \"undefined\") return null;\n try {\n return window.sessionStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\nfunction writeSession(key: string, value: string | null): void {\n if (typeof window === \"undefined\") return;\n try {\n if (value === null) window.sessionStorage.removeItem(key);\n else window.sessionStorage.setItem(key, value);\n } catch {\n // storage blocked: the banner just opens again next time\n }\n}\n\n/** \"Demo · 23 h 12 min left\", \"Demo · 50 min left\", the end, or plain \"Demo\". */\nfunction timeText(countdown: DemoCountdown, labels: DemoLabels): string {\n if (countdown.minutesLeft === null) return labels.badge;\n if (countdown.ended) return labels.endedTitle;\n return countdown.lastHour\n ? labels.minutesLeft(countdown.minutes)\n : labels.hoursLeft(countdown.hours, countdown.minutes);\n}\n\nexport interface DemoBannerProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\" | \"onClick\"> {\n /** The demo's end: `expires_at` from the `TokenResponse`, or `demo_expires_at` from\n * `/auth/me` (§5.3). */\n expiresAt: DemoExpiry | null | undefined;\n /** Which line the banner says about the data (§2.3). */\n model: DemoModel;\n /** \"Request access\". */\n access: AccessChoice;\n /** \"Sign in\" — let through for a demo session by `RedirectIfAuthed allowDemo`. Default\n * `/login`. */\n signInHref?: string;\n /** The countdown reached zero: clear the session and replace to `/demo/ended` (§5.5). */\n onEnded?: () => void;\n /** Where the collapsed state is kept for the tab's session. Default\n * `kit.demoBanner.collapsed`. */\n collapseKey?: string;\n /** The clock, for tests and previews. Default `Date.now`. Keep it stable. */\n now?: () => number;\n labels?: Partial<DemoLabels>;\n /** The access words (`landing` namespace) for this instance. */\n landingLabels?: Partial<LandingLabels>;\n}\n\n/**\n * The demo's strip under the top bar, on every page of a demo session\n * (docs/landing-demo-harmonization.md §5.4) — a kit `AlertBanner` strip:\n *\n * - the time left, \"Demo · 23 h 12 min left\" — minutes only in the last hour, and the\n * warning tone in the last ten minutes (`useDemoCountdown`);\n * - one line on the model: R \"You're looking at sample data. Changes aren't possible.\",\n * S \"Your own work is deleted when the demo ends.\";\n * - \"Request access\" (the mail) and \"Sign in\".\n *\n * It collapses to the countdown alone, for the tab's session (`sessionStorage`) — never\n * hidden for good: it is a live state, like keksdose's preview banner, which a demo user\n * gets instead of. At zero it calls `onEnded`.\n *\n * The countdown is no live region: a sentence read out every minute would drown the\n * page. A screen-reader user meets it at the top of the page, and the end page says\n * the rest.\n */\nexport function DemoBanner({\n expiresAt,\n model,\n access,\n signInHref = \"/login\",\n onEnded,\n collapseKey = COLLAPSE_KEY,\n now,\n labels: labelsProp,\n landingLabels,\n className,\n ...rest\n}: DemoBannerProps) {\n const labels = useDemoLabels(labelsProp);\n const landing = useLandingLabels(landingLabels);\n const accessLink = useAccessAction(access, landingLabels);\n const countdown = useDemoCountdown(expiresAt, { now, onEnded });\n const [collapsed, setCollapsed] = useState(() => readSession(collapseKey) === \"1\");\n\n const toggle = () => {\n const next = !collapsed;\n setCollapsed(next);\n writeSession(collapseKey, next ? \"1\" : null);\n };\n\n return (\n <AlertBanner\n {...rest}\n variant=\"strip\"\n size=\"sm\"\n tone={countdown.warning || countdown.ended ? \"warning\" : \"info\"}\n icon={<Timer />}\n data-demo-model={model}\n className={cn(\"sm:text-sm\", className)}\n action={\n <>\n {!collapsed && (\n <>\n <Button href={accessLink.href} variant=\"brand\" size=\"sm\">\n {accessLink.label}\n </Button>\n <Button href={signInHref} variant=\"secondary\" size=\"sm\">\n {landing.signIn}\n </Button>\n </>\n )}\n <IconButton\n size=\"2xs\"\n variant=\"ghost\"\n label={labels.details}\n // The icon alone at every text size (docs/text-size-harmonization.md §10.8):\n // a strip's one disclosure chevron beside its two links, whose state the\n // chevron and `aria-expanded` say; \"Details\" as text at Large would push the\n // links onto another line of a strip that is meant to stay one.\n labelVisible={false}\n aria-expanded={!collapsed}\n onClick={toggle}\n >\n {collapsed ? <ChevronDown /> : <ChevronUp />}\n </IconButton>\n </>\n }\n >\n <span className=\"font-semibold\">{timeText(countdown, labels)}</span>\n {!collapsed && (\n <>\n {\" \"}\n <span>{model === \"read-only\" ? labels.readOnly : labels.sandbox}</span>\n </>\n )}\n </AlertBanner>\n );\n}\n"],"mappings":";AA0HY,SAMA,UANA,KAMA,YANA;AA1HZ,SAAS,gBAAgB;AAEzB,SAAS,aAAa,WAAW,aAAa;AAE9C,SAAS,UAAU;AACnB,SAAS,mBAAmB;AAC5B,SAAS,QAAQ,kBAAkB;AACnC,SAAS,uBAAuB;AAEhC,SAAS,wBAAwB;AAEjC,SAAS,wBAAwB;AAEjC,SAAS,qBAAqB;AAK9B,MAAM,eAAe;AAIrB,SAAS,YAAY,KAA4B;AAC/C,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,MAAI;AACF,WAAO,OAAO,eAAe,QAAQ,GAAG;AAAA,EAC1C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,aAAa,KAAa,OAA4B;AAC7D,MAAI,OAAO,WAAW,YAAa;AACnC,MAAI;AACF,QAAI,UAAU,KAAM,QAAO,eAAe,WAAW,GAAG;AAAA,QACnD,QAAO,eAAe,QAAQ,KAAK,KAAK;AAAA,EAC/C,QAAQ;AAAA,EAER;AACF;AAGA,SAAS,SAAS,WAA0B,QAA4B;AACtE,MAAI,UAAU,gBAAgB,KAAM,QAAO,OAAO;AAClD,MAAI,UAAU,MAAO,QAAO,OAAO;AACnC,SAAO,UAAU,WACb,OAAO,YAAY,UAAU,OAAO,IACpC,OAAO,UAAU,UAAU,OAAO,UAAU,OAAO;AACzD;AA2CO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb;AAAA,EACA,cAAc;AAAA,EACd;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAoB;AAClB,QAAM,SAAS,cAAc,UAAU;AACvC,QAAM,UAAU,iBAAiB,aAAa;AAC9C,QAAM,aAAa,gBAAgB,QAAQ,aAAa;AACxD,QAAM,YAAY,iBAAiB,WAAW,EAAE,KAAK,QAAQ,CAAC;AAC9D,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,MAAM,YAAY,WAAW,MAAM,GAAG;AAEjF,QAAM,SAAS,MAAM;AACnB,UAAM,OAAO,CAAC;AACd,iBAAa,IAAI;AACjB,iBAAa,aAAa,OAAO,MAAM,IAAI;AAAA,EAC7C;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,MAAM,UAAU,WAAW,UAAU,QAAQ,YAAY;AAAA,MACzD,MAAM,oBAAC,SAAM;AAAA,MACb,mBAAiB;AAAA,MACjB,WAAW,GAAG,cAAc,SAAS;AAAA,MACrC,QACE,iCACG;AAAA,SAAC,aACA,iCACE;AAAA,8BAAC,UAAO,MAAM,WAAW,MAAM,SAAQ,SAAQ,MAAK,MACjD,qBAAW,OACd;AAAA,UACA,oBAAC,UAAO,MAAM,YAAY,SAAQ,aAAY,MAAK,MAChD,kBAAQ,QACX;AAAA,WACF;AAAA,QAEF;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,OAAO,OAAO;AAAA,YAKd,cAAc;AAAA,YACd,iBAAe,CAAC;AAAA,YAChB,SAAS;AAAA,YAER,sBAAY,oBAAC,eAAY,IAAK,oBAAC,aAAU;AAAA;AAAA,QAC5C;AAAA,SACF;AAAA,MAGF;AAAA,4BAAC,UAAK,WAAU,iBAAiB,mBAAS,WAAW,MAAM,GAAE;AAAA,QAC5D,CAAC,aACA,iCACG;AAAA;AAAA,UACD,oBAAC,UAAM,oBAAU,cAAc,OAAO,WAAW,OAAO,SAAQ;AAAA,WAClE;AAAA;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
@@ -1,16 +1,18 @@
1
1
  import 'react';
2
- export { D as DEFAULT_ATTACHMENT_ACCEPT, e as DEFAULT_FEEDBACK_ATTACHMENT_LABELS, p as DEFAULT_MAX_ATTACHMENTS, q as DEFAULT_MAX_ATTACHMENT_BYTES, A as FeedbackAttachmentError, B as FeedbackAttachmentErrorInfo, E as FeedbackAttachmentField, G as FeedbackAttachmentFieldLabels, H as FeedbackAttachmentFieldMultipleProps, I as FeedbackAttachmentFieldProps, J as FeedbackAttachmentFieldRefsProps, K as FeedbackAttachmentFieldSingleProps, M as FeedbackAttachmentRef, N as FeedbackAttachmentRefsError, be as pastedName } from '../delete-account-setting-CpIhzxxZ.js';
2
+ export { D as DEFAULT_ATTACHMENT_ACCEPT, e as DEFAULT_FEEDBACK_ATTACHMENT_LABELS, p as DEFAULT_MAX_ATTACHMENTS, q as DEFAULT_MAX_ATTACHMENT_BYTES, A as FeedbackAttachmentError, B as FeedbackAttachmentErrorInfo, E as FeedbackAttachmentField, G as FeedbackAttachmentFieldLabels, H as FeedbackAttachmentFieldMultipleProps, I as FeedbackAttachmentFieldProps, J as FeedbackAttachmentFieldRefsProps, K as FeedbackAttachmentFieldSingleProps, M as FeedbackAttachmentRef, N as FeedbackAttachmentRefsError, be as pastedName } from '../delete-account-setting-FLyJf-kZ.js';
3
3
  import '../components/tooltip.js';
4
4
  import '../hooks/use-anchored-rect.js';
5
5
  import '../lib/clipping.js';
6
- import '../data-table-labels-DUELeXpa.js';
6
+ import '../data-table-labels-kpAAVi6g.js';
7
7
  import '../components/data-table-sort.js';
8
8
  import '../components/swipeable-row.js';
9
9
  import '../components/mini-calendar.js';
10
10
  import '../components/calendar-heatmap.js';
11
11
  import '../components/popover.js';
12
+ import '../hooks/use-breakpoint.js';
12
13
  import '../components/chip.js';
13
14
  import 'lucide-react';
15
+ import '../components/field-parts.js';
14
16
  import '../components/field-sync.js';
15
17
  import '../wizard/types.js';
16
18
  import '../tour/tour.js';
@@ -79,6 +81,9 @@ import '../admin/admin-parts.js';
79
81
  import '../admin/admin-action-log.js';
80
82
  import '../settings/settings-labels.js';
81
83
  import '../demo/demo-labels.js';
84
+ import '../components/appearance-labels.js';
85
+ import '../theme/contrast.js';
86
+ import '../theme/text-size.js';
82
87
  import '../components/reauth-dialog.js';
83
88
  import '../components/server-wake.js';
84
89
  import '../lib/server-wake.js';
@@ -92,4 +97,4 @@ import '../components/sign-chip.js';
92
97
  import '../components/column-mapper.js';
93
98
  import '../lib/column-mapping.js';
94
99
  import '../lib/table-text.js';
95
- import '../components/field-parts.js';
100
+ import '../components/focus-ring.js';
@@ -1,16 +1,18 @@
1
1
  import 'react';
2
- export { h as DEFAULT_FEEDBACK_CONTEXT_LABELS, a0 as FeedbackContextBox, a1 as FeedbackContextBoxProps, a2 as FeedbackContextInput, a3 as FeedbackContextLabels, aM as FeedbackSubmitter, b2 as feedbackContext } from '../delete-account-setting-CpIhzxxZ.js';
2
+ export { h as DEFAULT_FEEDBACK_CONTEXT_LABELS, a0 as FeedbackContextBox, a1 as FeedbackContextBoxProps, a2 as FeedbackContextInput, a3 as FeedbackContextLabels, aM as FeedbackSubmitter, b2 as feedbackContext } from '../delete-account-setting-FLyJf-kZ.js';
3
3
  import '../components/tooltip.js';
4
4
  import '../hooks/use-anchored-rect.js';
5
5
  import '../lib/clipping.js';
6
- import '../data-table-labels-DUELeXpa.js';
6
+ import '../data-table-labels-kpAAVi6g.js';
7
7
  import '../components/data-table-sort.js';
8
8
  import '../components/swipeable-row.js';
9
9
  import '../components/mini-calendar.js';
10
10
  import '../components/calendar-heatmap.js';
11
11
  import '../components/popover.js';
12
+ import '../hooks/use-breakpoint.js';
12
13
  import '../components/chip.js';
13
14
  import 'lucide-react';
15
+ import '../components/field-parts.js';
14
16
  import '../components/field-sync.js';
15
17
  import '../wizard/types.js';
16
18
  import '../tour/tour.js';
@@ -79,6 +81,9 @@ import '../admin/admin-parts.js';
79
81
  import '../admin/admin-action-log.js';
80
82
  import '../settings/settings-labels.js';
81
83
  import '../demo/demo-labels.js';
84
+ import '../components/appearance-labels.js';
85
+ import '../theme/contrast.js';
86
+ import '../theme/text-size.js';
82
87
  import '../components/reauth-dialog.js';
83
88
  import '../components/server-wake.js';
84
89
  import '../lib/server-wake.js';
@@ -92,4 +97,4 @@ import '../components/sign-chip.js';
92
97
  import '../components/column-mapper.js';
93
98
  import '../lib/column-mapping.js';
94
99
  import '../lib/table-text.js';
95
- import '../components/field-parts.js';
100
+ import '../components/focus-ring.js';
@@ -1,17 +1,19 @@
1
1
  import { CrashReport } from '../components/error-boundary.js';
2
- import { ah as FeedbackEnvironment } from '../delete-account-setting-CpIhzxxZ.js';
2
+ import { ah as FeedbackEnvironment } from '../delete-account-setting-FLyJf-kZ.js';
3
3
  import 'react';
4
4
  import '../components/tooltip.js';
5
5
  import '../hooks/use-anchored-rect.js';
6
6
  import '../lib/clipping.js';
7
- import '../data-table-labels-DUELeXpa.js';
7
+ import '../data-table-labels-kpAAVi6g.js';
8
8
  import '../components/data-table-sort.js';
9
9
  import '../components/swipeable-row.js';
10
10
  import '../components/mini-calendar.js';
11
11
  import '../components/calendar-heatmap.js';
12
12
  import '../components/popover.js';
13
+ import '../hooks/use-breakpoint.js';
13
14
  import '../components/chip.js';
14
15
  import 'lucide-react';
16
+ import '../components/field-parts.js';
15
17
  import '../components/field-sync.js';
16
18
  import '../wizard/types.js';
17
19
  import '../tour/tour.js';
@@ -79,6 +81,9 @@ import '../admin/admin-parts.js';
79
81
  import '../admin/admin-action-log.js';
80
82
  import '../settings/settings-labels.js';
81
83
  import '../demo/demo-labels.js';
84
+ import '../components/appearance-labels.js';
85
+ import '../theme/contrast.js';
86
+ import '../theme/text-size.js';
82
87
  import '../components/reauth-dialog.js';
83
88
  import '../components/server-wake.js';
84
89
  import '../lib/server-wake.js';
@@ -92,7 +97,7 @@ import '../components/sign-chip.js';
92
97
  import '../components/column-mapper.js';
93
98
  import '../lib/column-mapping.js';
94
99
  import '../lib/table-text.js';
95
- import '../components/field-parts.js';
100
+ import '../components/focus-ring.js';
96
101
 
97
102
  /**
98
103
  * `POST /feedback/crash`'s body — `CrashReportCreate`, §3.6 (kk `schemas/feedback.py`),
@@ -1,16 +1,18 @@
1
1
  import 'react';
2
- export { j as DEFAULT_FEEDBACK_DIALOG_LABELS, L as FeedbackAttachmentLabels, T as FeedbackCategoryOption, a9 as FeedbackDialog, aa as FeedbackDialogLabels, ab as FeedbackDialogMultipleProps, ac as FeedbackDialogProps, ad as FeedbackDialogSingleProps, ae as FeedbackDialogTextLabels, an as FeedbackMultipleSubmission, aK as FeedbackSubmission, aL as FeedbackSubmitHint } from '../delete-account-setting-CpIhzxxZ.js';
2
+ export { j as DEFAULT_FEEDBACK_DIALOG_LABELS, L as FeedbackAttachmentLabels, T as FeedbackCategoryOption, a9 as FeedbackDialog, aa as FeedbackDialogLabels, ab as FeedbackDialogMultipleProps, ac as FeedbackDialogProps, ad as FeedbackDialogSingleProps, ae as FeedbackDialogTextLabels, an as FeedbackMultipleSubmission, aK as FeedbackSubmission, aL as FeedbackSubmitHint } from '../delete-account-setting-FLyJf-kZ.js';
3
3
  import '../components/tooltip.js';
4
4
  import '../hooks/use-anchored-rect.js';
5
5
  import '../lib/clipping.js';
6
- import '../data-table-labels-DUELeXpa.js';
6
+ import '../data-table-labels-kpAAVi6g.js';
7
7
  import '../components/data-table-sort.js';
8
8
  import '../components/swipeable-row.js';
9
9
  import '../components/mini-calendar.js';
10
10
  import '../components/calendar-heatmap.js';
11
11
  import '../components/popover.js';
12
+ import '../hooks/use-breakpoint.js';
12
13
  import '../components/chip.js';
13
14
  import 'lucide-react';
15
+ import '../components/field-parts.js';
14
16
  import '../components/field-sync.js';
15
17
  import '../wizard/types.js';
16
18
  import '../tour/tour.js';
@@ -79,6 +81,9 @@ import '../admin/admin-parts.js';
79
81
  import '../admin/admin-action-log.js';
80
82
  import '../settings/settings-labels.js';
81
83
  import '../demo/demo-labels.js';
84
+ import '../components/appearance-labels.js';
85
+ import '../theme/contrast.js';
86
+ import '../theme/text-size.js';
82
87
  import '../components/reauth-dialog.js';
83
88
  import '../components/server-wake.js';
84
89
  import '../lib/server-wake.js';
@@ -92,4 +97,4 @@ import '../components/sign-chip.js';
92
97
  import '../components/column-mapper.js';
93
98
  import '../lib/column-mapping.js';
94
99
  import '../lib/table-text.js';
95
- import '../components/field-parts.js';
100
+ import '../components/focus-ring.js';
@@ -1,10 +1,10 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
- import { Button, Input, PHONE_QUERY, Select, Textarea } from "../components/ui.js";
4
+ import { Button, Input, Select, Textarea } from "../components/ui.js";
5
5
  import { Modal } from "../components/modal.js";
6
6
  import { isApplePlatform } from "../hooks/use-hotkey.js";
7
- import { useMediaQuery } from "../hooks/use-media-query.js";
7
+ import { usePhoneLayout } from "../hooks/use-breakpoint.js";
8
8
  import { useKitLabels } from "../i18n/kit-labels.js";
9
9
  import {
10
10
  DEFAULT_ATTACHMENT_ACCEPT,
@@ -48,7 +48,7 @@ function FeedbackDialog(props) {
48
48
  const [attachment, setAttachment] = useState(null);
49
49
  const [screenshot, setScreenshot] = useState(null);
50
50
  const [files, setFiles] = useState([]);
51
- const isMobile = useMediaQuery(PHONE_QUERY, false);
51
+ const isMobile = usePhoneLayout();
52
52
  const labels = useKitLabels("feedbackDialog", DEFAULT_FEEDBACK_DIALOG_LABELS, labelsProp);
53
53
  const attachmentText = useKitLabels("feedbackAttachment", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);
54
54
  const listHeading = labelsProp?.attachmentList ?? labels.attachments ?? attachmentText.attachmentList ?? DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentList;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/feedback/feedback-dialog.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Button, Input, PHONE_QUERY, Select, Textarea } from \"../components/ui\";\nimport type { ButtonSize, ButtonVariant } from \"../components/ui\";\nimport { Modal } from \"../components/modal\";\nimport { isApplePlatform } from \"../hooks/use-hotkey\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport {\n DEFAULT_ATTACHMENT_ACCEPT,\n DEFAULT_FEEDBACK_ATTACHMENT_LABELS,\n DEFAULT_MAX_ATTACHMENT_BYTES,\n FeedbackAttachmentField,\n type FeedbackAttachmentError,\n type FeedbackAttachmentErrorInfo,\n} from \"./feedback-attachment\";\n\nexport interface FeedbackCategoryOption {\n value: string;\n label: string;\n}\n\nexport interface FeedbackAttachmentLabels {\n /** The heading over the field. Optional: a note editor puts the buttons\n * straight under its textarea, where a second heading is noise. */\n attachment?: string;\n attachmentAdd: string;\n /** Label for the \"capture screenshot\" button. Optional — falls back to the provider's\n * `feedbackAttachment.attachmentCapture`, then English. */\n attachmentCapture?: string;\n /** The line under the attachment buttons saying a screenshot can be pasted\n * straight in. Optional — falls back to the provider's `feedbackAttachment`, then English. */\n attachmentPaste?: string;\n attachmentRemove: string;\n /** `multiple` mode (0.15.5): the chip list's accessible name — and, in\n * `<FeedbackDialog attachments=\"multiple\">`, the heading over it (0.16.0). Optional,\n * as are the keys below — they fall back to the provider's `feedbackAttachment`,\n * then English. */\n attachmentList?: string;\n /** `multiple` mode (0.16.0): the screenshot slot's chip title, in place of its file name. */\n attachmentScreenshot?: string;\n /** `multiple` mode: one chip's remove button, given the file's name. */\n attachmentRemoveFile?: (name: string) => string;\n /** `multiple` mode: the line shown once `max` files are attached. */\n attachmentLimit?: (max: number) => string;\n /** `refs` mode (0.22.0): the second line of a chip whose upload is still running. */\n attachmentUploading?: string;\n}\n\nexport interface FeedbackDialogLabels extends FeedbackAttachmentLabels {\n title: string;\n category: string;\n subject: string;\n body: string;\n attachment: string;\n /** See {@link FeedbackDialogTextLabels.submitHint}. */\n submitHint: FeedbackSubmitHint;\n cancel: string;\n save: string;\n /** @deprecated since 0.16.0 — use `attachmentList`, the one key for the heading and\n * the chip list it names. Still honoured: set, it wins over the provider's\n * `attachmentList` (but not over an `attachmentList` passed beside it). */\n attachments?: string;\n}\n\n/** The submit hint: a string, or the line for the platform (`apple`: Mac, iPhone, iPad). */\nexport type FeedbackSubmitHint = string | ((apple: boolean) => string);\n\n/**\n * The dialog's own strings — the `feedbackDialog` namespace of `<UiKitProvider\n * labels>` (0.12.0). The attachment keys are not repeated here: they are the\n * `feedbackAttachment` namespace's, which the field inside already reads.\n *\n * Before, every key was required at the call site, so each app restated ten strings\n * wherever it mounted the dialog (kastlan feedback-button.tsx built the object inline).\n */\nexport interface FeedbackDialogTextLabels {\n title: string;\n category: string;\n subject: string;\n body: string;\n /** The body's label when `requireBody={false}`: \"What happened? (optional)\".\n * Optional, so a complete `UiKitLabels` typed before 0.14.2 still compiles. */\n bodyOptional?: string;\n /** The heading over the attachment buttons. */\n attachment: string;\n /**\n * The line beside the buttons naming the shortcut. Since 0.27.0 a function of the\n * platform, like `form.submitShortcut`: `apple` is true on a Mac, iPhone or iPad, where\n * the key is Cmd — \"⌘ Enter to send\" there, keksdose's \"Ctrl+Enter to send\" elsewhere\n * (§7.12). A plain string is still taken and shown everywhere, as before 0.27.0.\n */\n submitHint: FeedbackSubmitHint;\n cancel: string;\n save: string;\n /** @deprecated since 0.16.0 — use `feedbackAttachment.attachmentList`. It and this\n * key were two names for one heading (\"Attachments\" over the chips that list names\n * \"Attachments\"), and a catalogue that translated one left the other English. Still\n * honoured when set: it wins over `feedbackAttachment.attachmentList`. */\n attachments?: string;\n}\n\n/**\n * English. The feedback contract's wording (docs/feedback-harmonization.md §4.2, 0.27.0):\n * \"Send\", not keksdose's \"Save\", and the body asks \"What happened?\" (Marcel's two calls);\n * the rest is keksdose's. Since 0.27.0 `attachment`, the single-mode heading, is keksdose's\n * `feedback.attachment` **\"Attachment\"** (it said \"Screenshot\" over a button that says \"Add\n * attachment\" and a picker that may offer a PDF), and `submitHint` is keksdose's\n * **\"Ctrl+Enter to send\"** — \"⌘ Enter to send\" on Apple platforms, where the old\n * \"Ctrl/⌘ + Enter\" made every reader parse both. The `attachments=\"multiple\"` heading is\n * `feedbackAttachment.attachmentList` (\"Attachments\") and did not change.\n */\nexport const DEFAULT_FEEDBACK_DIALOG_LABELS: FeedbackDialogTextLabels = {\n title: \"Send feedback\",\n category: \"Category\",\n subject: \"Subject\",\n body: \"What happened?\",\n bodyOptional: \"What happened? (optional)\",\n attachment: \"Attachment\",\n // No `attachments`: the multiple-mode heading is `feedbackAttachment.attachmentList`,\n // and a default here would shadow a provider that translated only that one.\n // The modifier as `form.submitShortcut` writes it: \"⌘ Enter\" on Apple, \"Ctrl+Enter\" elsewhere.\n submitHint: (apple) => (apple ? \"⌘ Enter to send\" : \"Ctrl+Enter to send\"),\n cancel: \"Cancel\",\n save: \"Send\",\n};\n\nexport interface FeedbackSubmission {\n title: string;\n body: string;\n category: string;\n attachment: File | null;\n}\n\n/**\n * What `onSubmit` gets under `attachments=\"multiple\"` (0.15.5): the screenshot and\n * the picked/pasted files apart, because an app files them differently — keksdose\n * attaches the screenshot to the report and the photos as its evidence list. No\n * `attachment`: there is no one file it could name.\n */\nexport interface FeedbackMultipleSubmission {\n title: string;\n body: string;\n category: string;\n /** What `onCaptureScreenshot` returned, or `null` — at most one. */\n screenshot: File | null;\n /** Picked and pasted files, in the order they were added; at most `maxAttachments`. */\n attachments: File[];\n}\n\ninterface FeedbackDialogBaseProps {\n open: boolean;\n onClose: () => void;\n categories: FeedbackCategoryOption[];\n category: string;\n onCategoryChange: (value: string) => void;\n /** Prop > `<UiKitProvider labels={{ feedbackDialog, feedbackAttachment }}>` >\n * English. Optional since 0.12.0; a whole `FeedbackDialogLabels` still fits. */\n labels?: Partial<FeedbackDialogLabels>;\n submitting?: boolean;\n contextSlot?: ReactNode;\n attachmentAccept?: string[];\n maxAttachmentBytes?: number;\n /**\n * Optional: capture a screenshot of the underlying app view and return it as a\n * File. When provided, a \"Capture screenshot\" button is shown next to \"Add\n * attachment\"; the returned file is fed through the same validation + preview.\n */\n onCaptureScreenshot?: () => Promise<File | null>;\n /**\n * `false`: a report with only a subject can be sent, and the body's label says it is\n * optional (`bodyOptional`, unless `labels.body` is passed). Default `true`, the\n * dialog's behaviour until 0.14.2 (keksdose K1: its backend takes title-only reports).\n */\n requireBody?: boolean;\n /**\n * The attachment field's add and capture buttons: its `buttonVariant` (0.25.0),\n * Button's own `variant`. Default `\"secondary\"`, the look they always had.\n *\n * The field took `buttonVariant` / `buttonSize` in 0.24.0 (keksdose's support chat),\n * but the dialog builds its field itself and passed neither, so a host that wanted\n * quieter buttons here — a dialog that already ends in a row of Cancel / Send — had\n * no way to say so short of rebuilding the dialog. Named after the dialog's other\n * attachment options (`attachmentAccept`, `onAttachmentError`) and handed through\n * unchanged, in both `attachments` modes.\n */\n attachmentButtonVariant?: ButtonVariant;\n /** The attachment field's `buttonSize` (0.25.0) — with {@link attachmentButtonVariant}.\n * Default `\"md\"`; `\"sm\"` draws the paperclip and camera at 14px. */\n attachmentButtonSize?: ButtonSize;\n}\n\n/** One attachment — a screenshot, a picked or a pasted image; a second replaces\n * nothing, it has to wait for the first to be removed. The default. */\nexport interface FeedbackDialogSingleProps extends FeedbackDialogBaseProps {\n attachments?: \"single\";\n onSubmit: (data: FeedbackSubmission) => void | Promise<void>;\n /** `info` (0.23.0) names the refused file and the limit — see\n * {@link FeedbackAttachmentErrorInfo}. */\n onAttachmentError?: (kind: \"type\" | \"size\", info: FeedbackAttachmentErrorInfo) => void;\n /** Multiple mode only — refused here rather than silently ignored. */\n maxAttachments?: never;\n}\n\n/**\n * One screenshot plus up to `maxAttachments` picked or pasted files (0.15.5, keksdose\n * dev#578: \"pasted two photos, the second overwrote the first\"). The capture button is\n * offered while there is no screenshot yet; the add button while there is room.\n */\nexport interface FeedbackDialogMultipleProps extends FeedbackDialogBaseProps {\n attachments: \"multiple\";\n /** Default `DEFAULT_MAX_ATTACHMENTS` (5). The screenshot does not count. */\n maxAttachments?: number;\n onSubmit: (data: FeedbackMultipleSubmission) => void | Promise<void>;\n /** `\"count\"`: more files arrived at once than there was room for; the surplus was\n * dropped. `info` (0.23.0) names the refused file(s) and the limit. */\n onAttachmentError?: (kind: FeedbackAttachmentError, info: FeedbackAttachmentErrorInfo) => void;\n}\n\nexport type FeedbackDialogProps = FeedbackDialogSingleProps | FeedbackDialogMultipleProps;\n\n/**\n * The generic feedback form dialog: category + subject + body + an optional image\n * attachment, with Ctrl/Cmd+Enter to submit. Domain-free — the app supplies the\n * category options, labels and an `onSubmit` that talks to its own backend, plus\n * an optional `contextSlot` for app-specific context (user, current URL, …).\n *\n * **A screenshot can be pasted straight in.** Ctrl/Cmd+V anywhere in the dialog\n * takes an image off the clipboard and makes it the attachment, through the same\n * validation and the same preview as the file picker. It is the gesture the two\n * ways in did not cover: `onCaptureScreenshot` snapshots the *whole* app view,\n * and the file picker needs a file — so somebody who wanted to show one panel,\n * or one region of one, had to save a crop to disk first and then find it again\n * (Steering Design feedback #39). The clipboard is where a region snip already\n * is on every platform.\n *\n * **`attachments=\"multiple\"`** (0.15.5) takes several: every paste and pick adds a\n * chip, and `onSubmit` gets `{ screenshot, attachments }` ({@link\n * FeedbackMultipleSubmission}) instead of `attachment`. Opt-in, so a caller typed\n * against the one-file submission keeps compiling and behaving as before.\n */\nexport function FeedbackDialog(props: FeedbackDialogProps) {\n const {\n open,\n onClose,\n categories,\n category,\n onCategoryChange,\n labels: labelsProp,\n submitting = false,\n contextSlot,\n attachmentAccept = DEFAULT_ATTACHMENT_ACCEPT,\n maxAttachmentBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onCaptureScreenshot,\n requireBody = true,\n attachmentButtonVariant,\n attachmentButtonSize,\n } = props;\n const [title, setTitle] = useState(\"\");\n const [body, setBody] = useState(\"\");\n const [attachment, setAttachment] = useState<File | null>(null);\n // `attachments=\"multiple\"` only: the capture, apart from the picked/pasted files.\n const [screenshot, setScreenshot] = useState<File | null>(null);\n const [files, setFiles] = useState<File[]>([]);\n const isMobile = useMediaQuery(PHONE_QUERY, false);\n const labels = useKitLabels(\"feedbackDialog\", DEFAULT_FEEDBACK_DIALOG_LABELS, labelsProp);\n const attachmentText = useKitLabels(\"feedbackAttachment\", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);\n // Under `multiple` the heading is `attachmentList` — a screenshot AND photos — which\n // also names the chip list beneath it: one string, so the two cannot disagree. The\n // deprecated `feedbackDialog.attachments` (prop or provider) still wins over the\n // provider's `attachmentList`, as it did before 0.16.0; an `attachmentList` prop wins\n // over both.\n const listHeading =\n labelsProp?.attachmentList ??\n labels.attachments ??\n attachmentText.attachmentList ??\n DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentList!;\n // The attachment field resolves its own keys from `feedbackAttachment`; it gets the\n // prop's (so a caller's `attachmentAdd` still wins) plus the resolved heading.\n const attachmentLabels =\n props.attachments === \"multiple\"\n ? { ...labelsProp, attachment: listHeading, attachmentList: listHeading }\n : { ...labelsProp, attachment: labels.attachment };\n\n // Reset the form whenever the dialog is (re)opened — during render, on the\n // closed-to-open transition, so the old draft never paints for a frame.\n const [wasOpen, setWasOpen] = useState(open);\n if (open !== wasOpen) {\n setWasOpen(open);\n if (open) {\n setTitle(\"\");\n setBody(\"\");\n setAttachment(null);\n setScreenshot(null);\n setFiles([]);\n }\n }\n\n // Trimmed: a subject or body of spaces is empty (keksdose's API answers a blank title\n // with a 422, and a blank report is no report).\n const canSubmit = !!title.trim() && (!requireBody || !!body.trim()) && !submitting;\n const trySubmit = () => {\n if (!canSubmit) return;\n if (props.attachments === \"multiple\") {\n void props.onSubmit({ title, body, category, screenshot, attachments: files });\n } else {\n void props.onSubmit({ title, body, category, attachment });\n }\n };\n\n if (!open) return null;\n\n const categoryField = (\n <Select label={labels.category} value={category} onChange={(e) => onCategoryChange(e.target.value)}>\n {categories.map((c) => (\n <option key={c.value} value={c.value}>\n {c.label}\n </option>\n ))}\n </Select>\n );\n // On a phone the subject leads as a heading rather than as the second of two\n // identical boxes under a select (Keksdose feedback #179). `variant=\"display\"`\n // is a no-op above the phone breakpoint, so the desktop dialog is unchanged.\n const subjectField = (\n <Input label={labels.subject} value={title} onChange={(e) => setTitle(e.target.value)} variant=\"display\" />\n );\n const bodyField = (\n <Textarea\n rows={5}\n label={requireBody ? labels.body : (labelsProp?.body ?? labels.bodyOptional ?? labels.body)}\n value={body}\n onChange={(e) => setBody(e.target.value)}\n />\n );\n const fieldProps = {\n labels: attachmentLabels,\n accept: attachmentAccept,\n maxBytes: maxAttachmentBytes,\n onCaptureScreenshot,\n // Left undefined they are the field's own defaults (secondary / md).\n buttonVariant: attachmentButtonVariant,\n buttonSize: attachmentButtonSize,\n // On `document`, not on the panel: the Modal focuses its own panel on open\n // and traps Tab inside it, so while this dialog is up every paste in the\n // page is meant for it — including the one made with nothing in\n // particular focused, which never reaches a React `onPaste` on a child.\n documentPaste: open,\n };\n const attachmentField =\n props.attachments === \"multiple\" ? (\n <FeedbackAttachmentField\n {...fieldProps}\n multiple\n value={files}\n onChange={setFiles}\n max={props.maxAttachments}\n // The capture goes to its own slot, so it is offered once and never crowds\n // out a photo; a paste is a photo (a region snip is chosen evidence, not \"the\n // screenshot\").\n screenshot={screenshot}\n onScreenshotChange={setScreenshot}\n onError={props.onAttachmentError}\n />\n ) : (\n <FeedbackAttachmentField\n {...fieldProps}\n value={attachment}\n onChange={setAttachment}\n onError={props.onAttachmentError}\n />\n );\n\n return (\n <Modal\n onClose={onClose}\n // The one dialog whose subject is the page behind it (Keksdose dev#460: \"Make\n // the feedback dialog draggable so I can see behind it if it blocks\n // something\") — you are describing what is under it while you type.\n draggable\n onKeyDown={(e) => {\n if ((e.ctrlKey || e.metaKey) && e.key === \"Enter\") {\n e.preventDefault();\n trySubmit();\n }\n }}\n >\n {/* Also the drag handle: pressing anywhere on the panel's own chrome moves it,\n and the heading is the strip a user reaches for. */}\n <h2 className=\"mb-3 text-lg font-semibold\">{labels.title}</h2>\n <div className=\"space-y-3\">\n {isMobile ? (\n // Phone shape, the same reasoning as the transaction editor's (#177):\n // the desktop order is the DATA MODEL's order — category, then subject,\n // then body — but nobody opens this dialog to pick a category. What they\n // came to do is say the thing, so that leads; the classification and the\n // evidence follow, a tier down.\n <>\n {subjectField}\n {bodyField}\n <div className=\"space-y-3 border-t border-[var(--border)] pt-3\">\n {categoryField}\n {attachmentField}\n {contextSlot}\n </div>\n </>\n ) : (\n <>\n {categoryField}\n {subjectField}\n {bodyField}\n {attachmentField}\n {contextSlot}\n </>\n )}\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"text-xs text-[var(--text-placeholder)]\">\n {typeof labels.submitHint === \"function\" ? labels.submitHint(isApplePlatform()) : labels.submitHint}\n </div>\n <div className=\"flex gap-2\">\n <Button variant=\"ghost\" onClick={onClose}>\n {labels.cancel}\n </Button>\n <Button onClick={trySubmit} disabled={!canSubmit}>\n {labels.save}\n </Button>\n </div>\n </div>\n </div>\n </Modal>\n );\n}\n"],"mappings":";AA2TQ,SAkFE,UAlFF,KAqFI,YArFJ;AA3TR,SAAS,gBAAgB;AAEzB,SAAS,QAAQ,OAAO,aAAa,QAAQ,gBAAgB;AAE7D,SAAS,aAAa;AACtB,SAAS,uBAAuB;AAChC,SAAS,qBAAqB;AAC9B,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAiGA,MAAM,iCAA2D;AAAA,EACtE,OAAO;AAAA,EACP,UAAU;AAAA,EACV,SAAS;AAAA,EACT,MAAM;AAAA,EACN,cAAc;AAAA,EACd,YAAY;AAAA;AAAA;AAAA;AAAA,EAIZ,YAAY,CAAC,UAAW,QAAQ,yBAAoB;AAAA,EACpD,QAAQ;AAAA,EACR,MAAM;AACR;AAoHO,SAAS,eAAe,OAA4B;AACzD,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,IACR,aAAa;AAAA,IACb;AAAA,IACA,mBAAmB;AAAA,IACnB,qBAAqB;AAAA,IACrB;AAAA,IACA,cAAc;AAAA,IACd;AAAA,IACA;AAAA,EACF,IAAI;AACJ,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AACrC,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,EAAE;AACnC,QAAM,CAAC,YAAY,aAAa,IAAI,SAAsB,IAAI;AAE9D,QAAM,CAAC,YAAY,aAAa,IAAI,SAAsB,IAAI;AAC9D,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAiB,CAAC,CAAC;AAC7C,QAAM,WAAW,cAAc,aAAa,KAAK;AACjD,QAAM,SAAS,aAAa,kBAAkB,gCAAgC,UAAU;AACxF,QAAM,iBAAiB,aAAa,sBAAsB,oCAAoC,UAAU;AAMxG,QAAM,cACJ,YAAY,kBACZ,OAAO,eACP,eAAe,kBACf,mCAAmC;AAGrC,QAAM,mBACJ,MAAM,gBAAgB,aAClB,EAAE,GAAG,YAAY,YAAY,aAAa,gBAAgB,YAAY,IACtE,EAAE,GAAG,YAAY,YAAY,OAAO,WAAW;AAIrD,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,IAAI;AAC3C,MAAI,SAAS,SAAS;AACpB,eAAW,IAAI;AACf,QAAI,MAAM;AACR,eAAS,EAAE;AACX,cAAQ,EAAE;AACV,oBAAc,IAAI;AAClB,oBAAc,IAAI;AAClB,eAAS,CAAC,CAAC;AAAA,IACb;AAAA,EACF;AAIA,QAAM,YAAY,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC,eAAe,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC;AACxE,QAAM,YAAY,MAAM;AACtB,QAAI,CAAC,UAAW;AAChB,QAAI,MAAM,gBAAgB,YAAY;AACpC,WAAK,MAAM,SAAS,EAAE,OAAO,MAAM,UAAU,YAAY,aAAa,MAAM,CAAC;AAAA,IAC/E,OAAO;AACL,WAAK,MAAM,SAAS,EAAE,OAAO,MAAM,UAAU,WAAW,CAAC;AAAA,IAC3D;AAAA,EACF;AAEA,MAAI,CAAC,KAAM,QAAO;AAElB,QAAM,gBACJ,oBAAC,UAAO,OAAO,OAAO,UAAU,OAAO,UAAU,UAAU,CAAC,MAAM,iBAAiB,EAAE,OAAO,KAAK,GAC9F,qBAAW,IAAI,CAAC,MACf,oBAAC,YAAqB,OAAO,EAAE,OAC5B,YAAE,SADQ,EAAE,KAEf,CACD,GACH;AAKF,QAAM,eACJ,oBAAC,SAAM,OAAO,OAAO,SAAS,OAAO,OAAO,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK,GAAG,SAAQ,WAAU;AAE3G,QAAM,YACJ;AAAA,IAAC;AAAA;AAAA,MACC,MAAM;AAAA,MACN,OAAO,cAAc,OAAO,OAAQ,YAAY,QAAQ,OAAO,gBAAgB,OAAO;AAAA,MACtF,OAAO;AAAA,MACP,UAAU,CAAC,MAAM,QAAQ,EAAE,OAAO,KAAK;AAAA;AAAA,EACzC;AAEF,QAAM,aAAa;AAAA,IACjB,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,UAAU;AAAA,IACV;AAAA;AAAA,IAEA,eAAe;AAAA,IACf,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA,IAKZ,eAAe;AAAA,EACjB;AACA,QAAM,kBACJ,MAAM,gBAAgB,aACpB;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,UAAQ;AAAA,MACR,OAAO;AAAA,MACP,UAAU;AAAA,MACV,KAAK,MAAM;AAAA,MAIX;AAAA,MACA,oBAAoB;AAAA,MACpB,SAAS,MAAM;AAAA;AAAA,EACjB,IAEA;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,OAAO;AAAA,MACP,UAAU;AAAA,MACV,SAAS,MAAM;AAAA;AAAA,EACjB;AAGJ,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MAIA,WAAS;AAAA,MACT,WAAW,CAAC,MAAM;AAChB,aAAK,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,SAAS;AACjD,YAAE,eAAe;AACjB,oBAAU;AAAA,QACZ;AAAA,MACF;AAAA,MAIA;AAAA,4BAAC,QAAG,WAAU,8BAA8B,iBAAO,OAAM;AAAA,QACzD,qBAAC,SAAI,WAAU,aACZ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YAMC,iCACG;AAAA;AAAA,cACA;AAAA,cACD,qBAAC,SAAI,WAAU,kDACZ;AAAA;AAAA,gBACA;AAAA,gBACA;AAAA,iBACH;AAAA,eACF;AAAA,cAEA,iCACG;AAAA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,aACH;AAAA,UAEF,qBAAC,SAAI,WAAU,2CACb;AAAA,gCAAC,SAAI,WAAU,0CACZ,iBAAO,OAAO,eAAe,aAAa,OAAO,WAAW,gBAAgB,CAAC,IAAI,OAAO,YAC3F;AAAA,YACA,qBAAC,SAAI,WAAU,cACb;AAAA,kCAAC,UAAO,SAAQ,SAAQ,SAAS,SAC9B,iBAAO,QACV;AAAA,cACA,oBAAC,UAAO,SAAS,WAAW,UAAU,CAAC,WACpC,iBAAO,MACV;AAAA,eACF;AAAA,aACF;AAAA,WACF;AAAA;AAAA;AAAA,EACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/feedback/feedback-dialog.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Button, Input, Select, Textarea } from \"../components/ui\";\nimport type { ButtonSize, ButtonVariant } from \"../components/ui\";\nimport { Modal } from \"../components/modal\";\nimport { isApplePlatform } from \"../hooks/use-hotkey\";\nimport { usePhoneLayout } from \"../hooks/use-breakpoint\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport {\n DEFAULT_ATTACHMENT_ACCEPT,\n DEFAULT_FEEDBACK_ATTACHMENT_LABELS,\n DEFAULT_MAX_ATTACHMENT_BYTES,\n FeedbackAttachmentField,\n type FeedbackAttachmentError,\n type FeedbackAttachmentErrorInfo,\n} from \"./feedback-attachment\";\n\nexport interface FeedbackCategoryOption {\n value: string;\n label: string;\n}\n\nexport interface FeedbackAttachmentLabels {\n /** The heading over the field. Optional: a note editor puts the buttons\n * straight under its textarea, where a second heading is noise. */\n attachment?: string;\n attachmentAdd: string;\n /** Label for the \"capture screenshot\" button. Optional — falls back to the provider's\n * `feedbackAttachment.attachmentCapture`, then English. */\n attachmentCapture?: string;\n /** The line under the attachment buttons saying a screenshot can be pasted\n * straight in. Optional — falls back to the provider's `feedbackAttachment`, then English. */\n attachmentPaste?: string;\n attachmentRemove: string;\n /** `multiple` mode (0.15.5): the chip list's accessible name — and, in\n * `<FeedbackDialog attachments=\"multiple\">`, the heading over it (0.16.0). Optional,\n * as are the keys below — they fall back to the provider's `feedbackAttachment`,\n * then English. */\n attachmentList?: string;\n /** `multiple` mode (0.16.0): the screenshot slot's chip title, in place of its file name. */\n attachmentScreenshot?: string;\n /** `multiple` mode: one chip's remove button, given the file's name. */\n attachmentRemoveFile?: (name: string) => string;\n /** `multiple` mode: the line shown once `max` files are attached. */\n attachmentLimit?: (max: number) => string;\n /** `refs` mode (0.22.0): the second line of a chip whose upload is still running. */\n attachmentUploading?: string;\n}\n\nexport interface FeedbackDialogLabels extends FeedbackAttachmentLabels {\n title: string;\n category: string;\n subject: string;\n body: string;\n attachment: string;\n /** See {@link FeedbackDialogTextLabels.submitHint}. */\n submitHint: FeedbackSubmitHint;\n cancel: string;\n save: string;\n /** @deprecated since 0.16.0 — use `attachmentList`, the one key for the heading and\n * the chip list it names. Still honoured: set, it wins over the provider's\n * `attachmentList` (but not over an `attachmentList` passed beside it). */\n attachments?: string;\n}\n\n/** The submit hint: a string, or the line for the platform (`apple`: Mac, iPhone, iPad). */\nexport type FeedbackSubmitHint = string | ((apple: boolean) => string);\n\n/**\n * The dialog's own strings — the `feedbackDialog` namespace of `<UiKitProvider\n * labels>` (0.12.0). The attachment keys are not repeated here: they are the\n * `feedbackAttachment` namespace's, which the field inside already reads.\n *\n * Before, every key was required at the call site, so each app restated ten strings\n * wherever it mounted the dialog (kastlan feedback-button.tsx built the object inline).\n */\nexport interface FeedbackDialogTextLabels {\n title: string;\n category: string;\n subject: string;\n body: string;\n /** The body's label when `requireBody={false}`: \"What happened? (optional)\".\n * Optional, so a complete `UiKitLabels` typed before 0.14.2 still compiles. */\n bodyOptional?: string;\n /** The heading over the attachment buttons. */\n attachment: string;\n /**\n * The line beside the buttons naming the shortcut. Since 0.27.0 a function of the\n * platform, like `form.submitShortcut`: `apple` is true on a Mac, iPhone or iPad, where\n * the key is Cmd — \"⌘ Enter to send\" there, keksdose's \"Ctrl+Enter to send\" elsewhere\n * (§7.12). A plain string is still taken and shown everywhere, as before 0.27.0.\n */\n submitHint: FeedbackSubmitHint;\n cancel: string;\n save: string;\n /** @deprecated since 0.16.0 — use `feedbackAttachment.attachmentList`. It and this\n * key were two names for one heading (\"Attachments\" over the chips that list names\n * \"Attachments\"), and a catalogue that translated one left the other English. Still\n * honoured when set: it wins over `feedbackAttachment.attachmentList`. */\n attachments?: string;\n}\n\n/**\n * English. The feedback contract's wording (docs/feedback-harmonization.md §4.2, 0.27.0):\n * \"Send\", not keksdose's \"Save\", and the body asks \"What happened?\" (Marcel's two calls);\n * the rest is keksdose's. Since 0.27.0 `attachment`, the single-mode heading, is keksdose's\n * `feedback.attachment` **\"Attachment\"** (it said \"Screenshot\" over a button that says \"Add\n * attachment\" and a picker that may offer a PDF), and `submitHint` is keksdose's\n * **\"Ctrl+Enter to send\"** — \"⌘ Enter to send\" on Apple platforms, where the old\n * \"Ctrl/⌘ + Enter\" made every reader parse both. The `attachments=\"multiple\"` heading is\n * `feedbackAttachment.attachmentList` (\"Attachments\") and did not change.\n */\nexport const DEFAULT_FEEDBACK_DIALOG_LABELS: FeedbackDialogTextLabels = {\n title: \"Send feedback\",\n category: \"Category\",\n subject: \"Subject\",\n body: \"What happened?\",\n bodyOptional: \"What happened? (optional)\",\n attachment: \"Attachment\",\n // No `attachments`: the multiple-mode heading is `feedbackAttachment.attachmentList`,\n // and a default here would shadow a provider that translated only that one.\n // The modifier as `form.submitShortcut` writes it: \"⌘ Enter\" on Apple, \"Ctrl+Enter\" elsewhere.\n submitHint: (apple) => (apple ? \"⌘ Enter to send\" : \"Ctrl+Enter to send\"),\n cancel: \"Cancel\",\n save: \"Send\",\n};\n\nexport interface FeedbackSubmission {\n title: string;\n body: string;\n category: string;\n attachment: File | null;\n}\n\n/**\n * What `onSubmit` gets under `attachments=\"multiple\"` (0.15.5): the screenshot and\n * the picked/pasted files apart, because an app files them differently — keksdose\n * attaches the screenshot to the report and the photos as its evidence list. No\n * `attachment`: there is no one file it could name.\n */\nexport interface FeedbackMultipleSubmission {\n title: string;\n body: string;\n category: string;\n /** What `onCaptureScreenshot` returned, or `null` — at most one. */\n screenshot: File | null;\n /** Picked and pasted files, in the order they were added; at most `maxAttachments`. */\n attachments: File[];\n}\n\ninterface FeedbackDialogBaseProps {\n open: boolean;\n onClose: () => void;\n categories: FeedbackCategoryOption[];\n category: string;\n onCategoryChange: (value: string) => void;\n /** Prop > `<UiKitProvider labels={{ feedbackDialog, feedbackAttachment }}>` >\n * English. Optional since 0.12.0; a whole `FeedbackDialogLabels` still fits. */\n labels?: Partial<FeedbackDialogLabels>;\n submitting?: boolean;\n contextSlot?: ReactNode;\n attachmentAccept?: string[];\n maxAttachmentBytes?: number;\n /**\n * Optional: capture a screenshot of the underlying app view and return it as a\n * File. When provided, a \"Capture screenshot\" button is shown next to \"Add\n * attachment\"; the returned file is fed through the same validation + preview.\n */\n onCaptureScreenshot?: () => Promise<File | null>;\n /**\n * `false`: a report with only a subject can be sent, and the body's label says it is\n * optional (`bodyOptional`, unless `labels.body` is passed). Default `true`, the\n * dialog's behaviour until 0.14.2 (keksdose K1: its backend takes title-only reports).\n */\n requireBody?: boolean;\n /**\n * The attachment field's add and capture buttons: its `buttonVariant` (0.25.0),\n * Button's own `variant`. Default `\"secondary\"`, the look they always had.\n *\n * The field took `buttonVariant` / `buttonSize` in 0.24.0 (keksdose's support chat),\n * but the dialog builds its field itself and passed neither, so a host that wanted\n * quieter buttons here — a dialog that already ends in a row of Cancel / Send — had\n * no way to say so short of rebuilding the dialog. Named after the dialog's other\n * attachment options (`attachmentAccept`, `onAttachmentError`) and handed through\n * unchanged, in both `attachments` modes.\n */\n attachmentButtonVariant?: ButtonVariant;\n /** The attachment field's `buttonSize` (0.25.0) — with {@link attachmentButtonVariant}.\n * Default `\"md\"`; `\"sm\"` draws the paperclip and camera at 14px. */\n attachmentButtonSize?: ButtonSize;\n}\n\n/** One attachment — a screenshot, a picked or a pasted image; a second replaces\n * nothing, it has to wait for the first to be removed. The default. */\nexport interface FeedbackDialogSingleProps extends FeedbackDialogBaseProps {\n attachments?: \"single\";\n onSubmit: (data: FeedbackSubmission) => void | Promise<void>;\n /** `info` (0.23.0) names the refused file and the limit — see\n * {@link FeedbackAttachmentErrorInfo}. */\n onAttachmentError?: (kind: \"type\" | \"size\", info: FeedbackAttachmentErrorInfo) => void;\n /** Multiple mode only — refused here rather than silently ignored. */\n maxAttachments?: never;\n}\n\n/**\n * One screenshot plus up to `maxAttachments` picked or pasted files (0.15.5, keksdose\n * dev#578: \"pasted two photos, the second overwrote the first\"). The capture button is\n * offered while there is no screenshot yet; the add button while there is room.\n */\nexport interface FeedbackDialogMultipleProps extends FeedbackDialogBaseProps {\n attachments: \"multiple\";\n /** Default `DEFAULT_MAX_ATTACHMENTS` (5). The screenshot does not count. */\n maxAttachments?: number;\n onSubmit: (data: FeedbackMultipleSubmission) => void | Promise<void>;\n /** `\"count\"`: more files arrived at once than there was room for; the surplus was\n * dropped. `info` (0.23.0) names the refused file(s) and the limit. */\n onAttachmentError?: (kind: FeedbackAttachmentError, info: FeedbackAttachmentErrorInfo) => void;\n}\n\nexport type FeedbackDialogProps = FeedbackDialogSingleProps | FeedbackDialogMultipleProps;\n\n/**\n * The generic feedback form dialog: category + subject + body + an optional image\n * attachment, with Ctrl/Cmd+Enter to submit. Domain-free — the app supplies the\n * category options, labels and an `onSubmit` that talks to its own backend, plus\n * an optional `contextSlot` for app-specific context (user, current URL, …).\n *\n * **A screenshot can be pasted straight in.** Ctrl/Cmd+V anywhere in the dialog\n * takes an image off the clipboard and makes it the attachment, through the same\n * validation and the same preview as the file picker. It is the gesture the two\n * ways in did not cover: `onCaptureScreenshot` snapshots the *whole* app view,\n * and the file picker needs a file — so somebody who wanted to show one panel,\n * or one region of one, had to save a crop to disk first and then find it again\n * (Steering Design feedback #39). The clipboard is where a region snip already\n * is on every platform.\n *\n * **`attachments=\"multiple\"`** (0.15.5) takes several: every paste and pick adds a\n * chip, and `onSubmit` gets `{ screenshot, attachments }` ({@link\n * FeedbackMultipleSubmission}) instead of `attachment`. Opt-in, so a caller typed\n * against the one-file submission keeps compiling and behaving as before.\n */\nexport function FeedbackDialog(props: FeedbackDialogProps) {\n const {\n open,\n onClose,\n categories,\n category,\n onCategoryChange,\n labels: labelsProp,\n submitting = false,\n contextSlot,\n attachmentAccept = DEFAULT_ATTACHMENT_ACCEPT,\n maxAttachmentBytes = DEFAULT_MAX_ATTACHMENT_BYTES,\n onCaptureScreenshot,\n requireBody = true,\n attachmentButtonVariant,\n attachmentButtonSize,\n } = props;\n const [title, setTitle] = useState(\"\");\n const [body, setBody] = useState(\"\");\n const [attachment, setAttachment] = useState<File | null>(null);\n // `attachments=\"multiple\"` only: the capture, apart from the picked/pasted files.\n const [screenshot, setScreenshot] = useState<File | null>(null);\n const [files, setFiles] = useState<File[]>([]);\n const isMobile = usePhoneLayout();\n const labels = useKitLabels(\"feedbackDialog\", DEFAULT_FEEDBACK_DIALOG_LABELS, labelsProp);\n const attachmentText = useKitLabels(\"feedbackAttachment\", DEFAULT_FEEDBACK_ATTACHMENT_LABELS, labelsProp);\n // Under `multiple` the heading is `attachmentList` — a screenshot AND photos — which\n // also names the chip list beneath it: one string, so the two cannot disagree. The\n // deprecated `feedbackDialog.attachments` (prop or provider) still wins over the\n // provider's `attachmentList`, as it did before 0.16.0; an `attachmentList` prop wins\n // over both.\n const listHeading =\n labelsProp?.attachmentList ??\n labels.attachments ??\n attachmentText.attachmentList ??\n DEFAULT_FEEDBACK_ATTACHMENT_LABELS.attachmentList!;\n // The attachment field resolves its own keys from `feedbackAttachment`; it gets the\n // prop's (so a caller's `attachmentAdd` still wins) plus the resolved heading.\n const attachmentLabels =\n props.attachments === \"multiple\"\n ? { ...labelsProp, attachment: listHeading, attachmentList: listHeading }\n : { ...labelsProp, attachment: labels.attachment };\n\n // Reset the form whenever the dialog is (re)opened — during render, on the\n // closed-to-open transition, so the old draft never paints for a frame.\n const [wasOpen, setWasOpen] = useState(open);\n if (open !== wasOpen) {\n setWasOpen(open);\n if (open) {\n setTitle(\"\");\n setBody(\"\");\n setAttachment(null);\n setScreenshot(null);\n setFiles([]);\n }\n }\n\n // Trimmed: a subject or body of spaces is empty (keksdose's API answers a blank title\n // with a 422, and a blank report is no report).\n const canSubmit = !!title.trim() && (!requireBody || !!body.trim()) && !submitting;\n const trySubmit = () => {\n if (!canSubmit) return;\n if (props.attachments === \"multiple\") {\n void props.onSubmit({ title, body, category, screenshot, attachments: files });\n } else {\n void props.onSubmit({ title, body, category, attachment });\n }\n };\n\n if (!open) return null;\n\n const categoryField = (\n <Select label={labels.category} value={category} onChange={(e) => onCategoryChange(e.target.value)}>\n {categories.map((c) => (\n <option key={c.value} value={c.value}>\n {c.label}\n </option>\n ))}\n </Select>\n );\n // On a phone the subject leads as a heading rather than as the second of two\n // identical boxes under a select (Keksdose feedback #179). `variant=\"display\"`\n // is a no-op above the phone breakpoint, so the desktop dialog is unchanged.\n const subjectField = (\n <Input label={labels.subject} value={title} onChange={(e) => setTitle(e.target.value)} variant=\"display\" />\n );\n const bodyField = (\n <Textarea\n rows={5}\n label={requireBody ? labels.body : (labelsProp?.body ?? labels.bodyOptional ?? labels.body)}\n value={body}\n onChange={(e) => setBody(e.target.value)}\n />\n );\n const fieldProps = {\n labels: attachmentLabels,\n accept: attachmentAccept,\n maxBytes: maxAttachmentBytes,\n onCaptureScreenshot,\n // Left undefined they are the field's own defaults (secondary / md).\n buttonVariant: attachmentButtonVariant,\n buttonSize: attachmentButtonSize,\n // On `document`, not on the panel: the Modal focuses its own panel on open\n // and traps Tab inside it, so while this dialog is up every paste in the\n // page is meant for it — including the one made with nothing in\n // particular focused, which never reaches a React `onPaste` on a child.\n documentPaste: open,\n };\n const attachmentField =\n props.attachments === \"multiple\" ? (\n <FeedbackAttachmentField\n {...fieldProps}\n multiple\n value={files}\n onChange={setFiles}\n max={props.maxAttachments}\n // The capture goes to its own slot, so it is offered once and never crowds\n // out a photo; a paste is a photo (a region snip is chosen evidence, not \"the\n // screenshot\").\n screenshot={screenshot}\n onScreenshotChange={setScreenshot}\n onError={props.onAttachmentError}\n />\n ) : (\n <FeedbackAttachmentField\n {...fieldProps}\n value={attachment}\n onChange={setAttachment}\n onError={props.onAttachmentError}\n />\n );\n\n return (\n <Modal\n onClose={onClose}\n // The one dialog whose subject is the page behind it (Keksdose dev#460: \"Make\n // the feedback dialog draggable so I can see behind it if it blocks\n // something\") — you are describing what is under it while you type.\n draggable\n onKeyDown={(e) => {\n if ((e.ctrlKey || e.metaKey) && e.key === \"Enter\") {\n e.preventDefault();\n trySubmit();\n }\n }}\n >\n {/* Also the drag handle: pressing anywhere on the panel's own chrome moves it,\n and the heading is the strip a user reaches for. */}\n <h2 className=\"mb-3 text-lg font-semibold\">{labels.title}</h2>\n <div className=\"space-y-3\">\n {isMobile ? (\n // Phone shape, the same reasoning as the transaction editor's (#177):\n // the desktop order is the DATA MODEL's order — category, then subject,\n // then body — but nobody opens this dialog to pick a category. What they\n // came to do is say the thing, so that leads; the classification and the\n // evidence follow, a tier down.\n <>\n {subjectField}\n {bodyField}\n <div className=\"space-y-3 border-t border-[var(--border)] pt-3\">\n {categoryField}\n {attachmentField}\n {contextSlot}\n </div>\n </>\n ) : (\n <>\n {categoryField}\n {subjectField}\n {bodyField}\n {attachmentField}\n {contextSlot}\n </>\n )}\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"text-xs text-[var(--text-placeholder)]\">\n {typeof labels.submitHint === \"function\" ? labels.submitHint(isApplePlatform()) : labels.submitHint}\n </div>\n <div className=\"flex gap-2\">\n <Button variant=\"ghost\" onClick={onClose}>\n {labels.cancel}\n </Button>\n <Button onClick={trySubmit} disabled={!canSubmit}>\n {labels.save}\n </Button>\n </div>\n </div>\n </div>\n </Modal>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,16 +1,18 @@
1
1
  import 'react';
2
2
  import 'lucide-react';
3
- export { t as FEEDBACK_CATEGORY_META, u as FEEDBACK_CATEGORY_ORDER, x as FEEDBACK_STATUS_META, y as FEEDBACK_STATUS_ORDER, Q as FeedbackCategory, R as FeedbackCategoryBadge, a4 as FeedbackDetail, a7 as FeedbackDetailSection, ao as FeedbackNoteAttachment, ap as FeedbackNoteEditor, ar as FeedbackProse, az as FeedbackStatus, aA as FeedbackStatusBadge, aG as FeedbackStatusTransitions, b0 as feedbackCategoryRank, bd as nextFeedbackStatus, bh as selectableFeedbackStatuses, bs as visibleFeedbackStatuses } from '../delete-account-setting-CpIhzxxZ.js';
3
+ export { t as FEEDBACK_CATEGORY_META, u as FEEDBACK_CATEGORY_ORDER, x as FEEDBACK_STATUS_META, y as FEEDBACK_STATUS_ORDER, Q as FeedbackCategory, R as FeedbackCategoryBadge, a4 as FeedbackDetail, a7 as FeedbackDetailSection, ao as FeedbackNoteAttachment, ap as FeedbackNoteEditor, ar as FeedbackProse, az as FeedbackStatus, aA as FeedbackStatusBadge, aG as FeedbackStatusTransitions, b0 as feedbackCategoryRank, bd as nextFeedbackStatus, bh as selectableFeedbackStatuses, bs as visibleFeedbackStatuses } from '../delete-account-setting-FLyJf-kZ.js';
4
4
  import '../components/tooltip.js';
5
5
  import '../hooks/use-anchored-rect.js';
6
6
  import '../lib/clipping.js';
7
- import '../data-table-labels-DUELeXpa.js';
7
+ import '../data-table-labels-kpAAVi6g.js';
8
8
  import '../components/data-table-sort.js';
9
9
  import '../components/swipeable-row.js';
10
10
  import '../components/mini-calendar.js';
11
11
  import '../components/calendar-heatmap.js';
12
12
  import '../components/popover.js';
13
+ import '../hooks/use-breakpoint.js';
13
14
  import '../components/chip.js';
15
+ import '../components/field-parts.js';
14
16
  import '../components/field-sync.js';
15
17
  import '../wizard/types.js';
16
18
  import '../tour/tour.js';
@@ -79,6 +81,9 @@ import '../admin/admin-parts.js';
79
81
  import '../admin/admin-action-log.js';
80
82
  import '../settings/settings-labels.js';
81
83
  import '../demo/demo-labels.js';
84
+ import '../components/appearance-labels.js';
85
+ import '../theme/contrast.js';
86
+ import '../theme/text-size.js';
82
87
  import '../components/reauth-dialog.js';
83
88
  import '../components/server-wake.js';
84
89
  import '../lib/server-wake.js';
@@ -92,4 +97,4 @@ import '../components/sign-chip.js';
92
97
  import '../components/column-mapper.js';
93
98
  import '../lib/column-mapping.js';
94
99
  import '../lib/table-text.js';
95
- import '../components/field-parts.js';
100
+ import '../components/focus-ring.js';
@@ -153,7 +153,7 @@ function FeedbackCategoryBadge({
153
153
  {
154
154
  className: cn(
155
155
  "inline-flex items-center gap-1 rounded font-medium",
156
- compact ? "px-1.5 py-0.5 text-[11px]" : "px-2 py-0.5 text-xs",
156
+ compact ? "px-1.5 py-0.5 text-caption" : "px-2 py-0.5 text-xs",
157
157
  meta.badgeBg,
158
158
  meta.badgeText,
159
159
  className
@@ -178,7 +178,7 @@ function FeedbackStatusBadge({
178
178
  "span",
179
179
  {
180
180
  className: cn(
181
- "inline-flex items-center gap-1 whitespace-nowrap rounded px-1.5 py-0.5 text-[11px] font-medium",
181
+ "inline-flex items-center gap-1 whitespace-nowrap rounded px-1.5 py-0.5 text-caption font-medium",
182
182
  meta.activeBg,
183
183
  meta.activeText,
184
184
  className
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/feedback/feedback-inbox.tsx"],"sourcesContent":["import { useId, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport {\n Ban,\n Bug,\n CheckCircle2,\n ClipboardCheck,\n CloudUpload,\n Eye,\n HelpCircle,\n Inbox,\n Lightbulb,\n MoreHorizontal,\n OctagonAlert,\n PauseCircle,\n Wrench,\n} from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { Button, Textarea } from \"../components/ui\";\nimport type { ButtonSize, ButtonVariant } from \"../components/ui\";\nimport { Tooltip } from \"../components/tooltip\";\nimport { useCommitReason, useWriteLock } from \"../components/write-lock\";\nimport { FeedbackAttachmentField, type FeedbackAttachmentErrorInfo } from \"./feedback-attachment\";\nimport type { FeedbackAttachmentLabels } from \"./feedback-dialog\";\nimport { useFeedbackCategoryLabels, useFeedbackStatusLabels } from \"./feedback-labels\";\n\n/**\n * The feedback **inbox**, as the parts two apps were each writing separately.\n *\n * `feedback-dialog.tsx` next door is the other half — the form somebody files a\n * report with — and the split between them is the same one: this owns the\n * *vocabulary*, the *policy* about how a report moves, and the *look*; the app\n * owns the data, the API and every string.\n *\n * It exists because both apps had grown their own copy. Keksdose's is the one\n * this is lifted from, comments and all, because it is the one that had been\n * argued with users for a year — the glyph per status, the two sizes of the\n * status control, what a row offers from where it currently stands. Steering\n * Design's was thinner in every one of those places and read as a different\n * product for no reason anyone chose.\n *\n * **The report itself lives next door (0.27.0).** This file used to say there\n * was deliberately no `Feedback` type here: the apps stored a report differently\n * (a context JSON column and an object store in one, a `page_path` and the bytes\n * in the row in another). Marcel's feedback round (2026-10-04,\n * docs/feedback-harmonization.md §5) gave all three apps one contract, so the\n * shape is now shared — `FeedbackRecord` / `FeedbackContext` in\n * feedback-record.ts, with the constants and body helpers. The components HERE\n * still take the values they draw rather than a row, which keeps them usable\n * from any surface; the detail panel stays a *shell* the row detail fills.\n *\n * **The words are the kit's too (0.27.0).** The status and category names are\n * the `feedbackStatus` / `feedbackCategory` namespaces (feedback-labels.ts,\n * keksdose's wording), read by default by the badges and the transitions below;\n * a `label` the caller passes still wins.\n */\n\n/** The eight states a report can be in.\n *\n * Shared value for value across the apps on purpose: it is what lets one habit,\n * and one agent prompt, work on any repo. Five are a chain and three sit off\n * it — see {@link visibleFeedbackStatuses} for what that buys. `READY` joined in 0.31\n * (feedback contract §8.2): OPEN is \"filed, not yet triaged\", READY \"released for\n * implementation\", and nobody works an OPEN row. */\nexport type FeedbackStatus =\n | \"OPEN\"\n | \"READY\"\n | \"IN_PROGRESS\"\n | \"IN_EVALUATION\"\n | \"NEEDS_LIVE_TEST\"\n | \"POSTPONED\"\n | \"DONE\"\n | \"WONT_DO\";\n\n/** What a report is about. `CRASH` is filed by an error boundary and never\n * chosen, which is exactly why it is loud below and absent from every picker. */\nexport type FeedbackCategory = \"CRASH\" | \"BUG\" | \"IDEA\" | \"QUESTION\" | \"OTHER\";\n\n/**\n * A glyph and a tone per status.\n *\n * Colour alone is not a label — two of these sit a hue apart, and a consumer who\n * moves `--brand` can park it beside any of the others — so every status carries\n * its own shape as well. Declaration order is chain order, and\n * {@link FEEDBACK_STATUS_ORDER} is derived from it rather than restated, because\n * a restated list cannot be checked for exhaustiveness: an eighth status would\n * break the build here and leave a literal eight long (as READY did in 0.31).\n */\nexport const FEEDBACK_STATUS_META: Record<\n FeedbackStatus,\n { icon: LucideIcon; activeBg: string; activeText: string }\n> = {\n OPEN: {\n icon: Inbox,\n activeBg: \"bg-[var(--bg-active)]\",\n activeText: \"text-[var(--text-primary)]\",\n },\n // Triaged and released for implementation (feedback contract §8.2, keksdose live\n // #396): OPEN is \"filed, nobody has looked yet\", READY is \"go\". The brand tint at full\n // strength, so it reads as the go-ahead beside OPEN's neutral fill and\n // NEEDS_LIVE_TEST's muted brand.\n READY: {\n icon: ClipboardCheck,\n activeBg: \"bg-[var(--brand-bg)]\",\n activeText: \"text-[var(--brand)]\",\n },\n IN_PROGRESS: {\n icon: Wrench,\n activeBg: \"bg-[var(--warning-bg)]\",\n activeText: \"text-[var(--warning)]\",\n },\n // Where solved work is parked, waiting on the person who reported it. Its own\n // colour, because \"somebody has to check this\" is a state you want to find by\n // scanning rather than by reading.\n IN_EVALUATION: {\n icon: Eye,\n activeBg: \"bg-[var(--info-bg)]\",\n activeText: \"text-[var(--info)]\",\n },\n // Resolved but only checkable on a deployed build — the SOFT brand tint, so it\n // reads as \"waiting on something\" rather than as a done or a refusal, and it\n // follows whatever the consumer's brand is instead of pinning an indigo island\n // beside it.\n NEEDS_LIVE_TEST: {\n icon: CloudUpload,\n activeBg: \"bg-[var(--brand-bg)]\",\n activeText: \"text-[var(--brand-muted)]\",\n },\n // Parked on purpose. Muted, not `--danger`: it is not a refusal.\n POSTPONED: {\n icon: PauseCircle,\n activeBg: \"bg-[var(--bg-active)]\",\n activeText: \"text-[var(--text-secondary)]\",\n },\n // The one status with nowhere to move to: the vocabulary has a brand, an info, a\n // warning and a danger, and nothing that means \"succeeded\". Deliberately NOT\n // borrowed from a family that means something else — `--status-synced` is a\n // FIELD's trip to the database rather than an outcome, and tokens.css says so in\n // as many words. Green beside WONT_DO's red is the classic CVD-unsafe pair, which\n // is why both carry a glyph and neither leans on colour alone.\n DONE: {\n icon: CheckCircle2,\n activeBg: \"bg-[var(--success-bg)]\",\n activeText: \"text-[var(--success)]\",\n },\n WONT_DO: {\n icon: Ban,\n activeBg: \"bg-[var(--danger-bg)]\",\n activeText: \"text-[var(--danger)]\",\n },\n};\n\n/** Chain order, then the three off it. The order a picker offers, and the rank a\n * status column sorts by — so a list sorted by status reads as a queue rather\n * than as an alphabet. */\nexport const FEEDBACK_STATUS_ORDER = Object.keys(FEEDBACK_STATUS_META) as FeedbackStatus[];\n\n/** States that sit OFF the linear chain: reachable from anywhere, leading\n * nowhere by themselves. `WONT_DO` has always worked this way; `POSTPONED`\n * (\"not now\") and `NEEDS_LIVE_TEST` (\"resolved, but only provable on a deployed\n * build\") are the same shape — a row can be parked or handed to the next deploy\n * from any point, and comes back to the chain when somebody picks a real state\n * for it.\n *\n * Kept out of the forward chain deliberately: putting them in it would mean an\n * \"advance\" gesture could park an item, and every row would have to pass\n * through them to reach `DONE`. */\nconst OFF_CHAIN: FeedbackStatus[] = [\"NEEDS_LIVE_TEST\", \"POSTPONED\", \"WONT_DO\"];\n\nconst FORWARD_CHAIN: FeedbackStatus[] = [\"OPEN\", \"READY\", \"IN_PROGRESS\", \"IN_EVALUATION\", \"DONE\"];\n\n/** The next status along the chain, or null at the end (or off it, as `WONT_DO`\n * is). What a swipe-to-advance gesture commits. */\nexport function nextFeedbackStatus(current: FeedbackStatus): FeedbackStatus | null {\n const at = FORWARD_CHAIN.indexOf(current);\n return at !== -1 && at < FORWARD_CHAIN.length - 1 ? FORWARD_CHAIN[at + 1] : null;\n}\n\n/**\n * The steps that make sense from where a row currently stands.\n *\n * For the compact control in a table cell: it is a glanceable triage affordance,\n * and a row of eight icons in a cell is noise. One step forward, one step back\n * (so an item can be sent back for rework), and any of the three off-chain\n * verdicts — which is what makes those usable as verdicts at all.\n *\n * The two parking states are not dead ends: each offers the step that resumes\n * work and the ones that close it, but deliberately **not each other** —\n * \"postponed\" and \"waiting on a deploy\" are different answers to different\n * questions, and a row moving between them directly is a re-triage, which starts\n * by picking the work back up.\n */\nexport function visibleFeedbackStatuses(current: FeedbackStatus): FeedbackStatus[] {\n const set = new Set<FeedbackStatus>([current]);\n const at = FORWARD_CHAIN.indexOf(current);\n if (at !== -1) {\n if (at < FORWARD_CHAIN.length - 1) set.add(FORWARD_CHAIN[at + 1]);\n if (at > 0) set.add(FORWARD_CHAIN[at - 1]);\n for (const off of OFF_CHAIN) set.add(off);\n } else if (current !== \"WONT_DO\") {\n set.add(\"IN_PROGRESS\");\n set.add(\"DONE\");\n set.add(\"WONT_DO\");\n }\n // WONT_DO falls through with nothing added: it stays terminal, reopened by a\n // note from its author rather than by a status pill.\n return FEEDBACK_STATUS_ORDER.filter((status) => set.has(status));\n}\n\n/**\n * Every status, in chain order — what an EXPANDED row offers.\n *\n * {@link visibleFeedbackStatuses} narrows the choice to the steps that make\n * sense from where a row is, which is right for a table cell. Once the row is\n * open in front of you, that same narrowing turns every non-adjacent move into a\n * walk: OPEN to DONE meant three round trips, and WONT_DO could not be left at\n * all except through a rework note.\n *\n * Takes a `current` it does not use, so the two policies read as one pair at the\n * call sites and a future rule (\"terminal rows still cannot jump to X\") has a\n * place to live.\n */\nexport function selectableFeedbackStatuses(_current: FeedbackStatus): FeedbackStatus[] {\n return [...FEEDBACK_STATUS_ORDER];\n}\n\n/**\n * Per-category badge treatment, the counterpart to the status meta above.\n *\n * A category cell that is one grey pill holding the raw enum makes a CRASH —\n * filed automatically, by somebody staring at a broken page right now — read\n * exactly like a QUESTION, and sit unnoticed in the queue. `--danger` plus its own\n * glyph is what makes that impossible; the hand-filed categories stay\n * deliberately quiet so that the loud one means something.\n *\n * Declaration order is triage order, CRASH first.\n */\nexport const FEEDBACK_CATEGORY_META: Record<\n FeedbackCategory,\n { icon: LucideIcon; badgeBg: string; badgeText: string }\n> = {\n CRASH: {\n icon: OctagonAlert,\n badgeBg: \"bg-[var(--danger-bg)]\",\n badgeText: \"text-[var(--danger)]\",\n },\n BUG: {\n icon: Bug,\n badgeBg: \"bg-[var(--warning-bg)]\",\n badgeText: \"text-[var(--warning)]\",\n },\n IDEA: {\n icon: Lightbulb,\n badgeBg: \"bg-[var(--bg-surface-2)]\",\n badgeText: \"text-[var(--text-secondary)]\",\n },\n QUESTION: {\n icon: HelpCircle,\n badgeBg: \"bg-[var(--bg-surface-2)]\",\n badgeText: \"text-[var(--text-secondary)]\",\n },\n OTHER: {\n icon: MoreHorizontal,\n badgeBg: \"bg-[var(--bg-surface-2)]\",\n badgeText: \"text-[var(--text-secondary)]\",\n },\n};\n\n/** Triage order, CRASH first — also a category column's sort key and the option\n * order in its filter. Sorting by the raw enum is alphabetical, which parks\n * CRASH between BUG and IDEA. */\nexport const FEEDBACK_CATEGORY_ORDER = Object.keys(FEEDBACK_CATEGORY_META) as FeedbackCategory[];\n\n/** A category's rank for a column sort. Anything this build has never heard of —\n * an older client against a newer API — sorts LAST rather than to `indexOf`'s\n * −1, which would rank it above CRASH. */\nexport function feedbackCategoryRank(category: FeedbackCategory): number {\n const at = FEEDBACK_CATEGORY_ORDER.indexOf(category);\n return at === -1 ? FEEDBACK_CATEGORY_ORDER.length : at;\n}\n\n/**\n * The category as a badge.\n *\n * Falls back to OTHER's treatment for a category this build has never heard of,\n * so an older client against a newer API degrades to a readable neutral pill\n * instead of throwing on `meta.icon` — a crash report must not be able to cause\n * one. The *label* gets the same guard: by default the `feedbackCategory`\n * namespace's word (0.27.0), and for a value it has no word for, what OTHER is\n * called.\n */\nexport function FeedbackCategoryBadge({\n category,\n label,\n compact = false,\n className,\n}: {\n category: FeedbackCategory;\n /** Over the `feedbackCategory` label (0.27.0: optional — before, every app passed\n * its own translation). */\n label?: ReactNode;\n compact?: boolean;\n className?: string;\n}) {\n const names = useFeedbackCategoryLabels();\n // Annotated `| undefined` because the Record's index signature promises a hit\n // for every FeedbackCategory, and at runtime the API can hand us one that is not.\n const known: (typeof FEEDBACK_CATEGORY_META)[\"OTHER\"] | undefined = FEEDBACK_CATEGORY_META[category];\n const meta = known ?? FEEDBACK_CATEGORY_META.OTHER;\n const Icon = meta.icon;\n return (\n <span\n className={cn(\n \"inline-flex items-center gap-1 rounded font-medium\",\n compact ? \"px-1.5 py-0.5 text-[11px]\" : \"px-2 py-0.5 text-xs\",\n meta.badgeBg,\n meta.badgeText,\n className,\n )}\n >\n <Icon className={compact ? \"size-3\" : \"size-3.5\"} aria-hidden />\n {label ?? (known ? names[category] : names.OTHER)}\n </span>\n );\n}\n\n/** Where a report stands, as a badge — for the places that only *report* the\n * status rather than offering to change it. Named by the `feedbackStatus`\n * namespace unless `label` says otherwise (0.27.0); a status this build has never\n * heard of shows OPEN's look and its own raw value, never OPEN's name. */\nexport function FeedbackStatusBadge({\n status,\n label,\n className,\n}: {\n status: FeedbackStatus;\n /** Over the `feedbackStatus` label (0.27.0: optional). */\n label?: ReactNode;\n className?: string;\n}) {\n const names = useFeedbackStatusLabels();\n const meta = FEEDBACK_STATUS_META[status] ?? FEEDBACK_STATUS_META.OPEN;\n const name: string | undefined = names[status];\n const Icon = meta.icon;\n return (\n <span\n className={cn(\n \"inline-flex items-center gap-1 whitespace-nowrap rounded px-1.5 py-0.5 text-[11px] font-medium\",\n meta.activeBg,\n meta.activeText,\n className,\n )}\n >\n <Icon className=\"size-3\" aria-hidden />\n {label ?? name ?? status}\n </span>\n );\n}\n\n/**\n * A row of statuses to switch a report to — either a table column's icon row or\n * an expanded panel's labelled pills.\n *\n * One component for both, because everything except the class strings is the\n * same decision: which one is current, and when the buttons are disabled. Two\n * copies had already drifted — only the icon variant carried `aria-label` and\n * `aria-pressed`, so the pill row was unlabelled for a screen reader.\n *\n * WHICH statuses to offer is the caller's, not this component's:\n * {@link visibleFeedbackStatuses} for a compact column,\n * {@link selectableFeedbackStatuses} for an expanded row. Passing the list keeps\n * that policy readable at the two call sites instead of hiding it behind\n * `variant`.\n */\nexport function FeedbackStatusTransitions({\n status,\n statuses,\n canEdit,\n onPick,\n variant,\n label,\n commit,\n disabledReason: ownDisabledReason,\n className,\n}: {\n status: FeedbackStatus;\n statuses: FeedbackStatus[];\n canEdit: boolean;\n onPick: (status: FeedbackStatus) => void;\n variant: \"icon\" | \"pill\";\n /** What each status is called. Default (0.27.0): the `feedbackStatus` namespace —\n * before, every app passed its own translation. */\n label?: (status: FeedbackStatus) => string;\n /**\n * Every button here SAVES the moment it is pressed (0.27.0) — opt in and, under a\n * locked {@link WriteLockProvider}, the row stays as it is and says why: each status\n * the row could move to is `aria-disabled` but focusable, the click does nothing, and\n * the lock's reason is in the kit Tooltip and the button's description — the\n * `disabledReason` path `Button` takes. Off by default: a feedback inbox is not what\n * every app's lock is about (keksdose's shell lock is a read-only demo BUDGET, and a\n * report is not budget data).\n */\n commit?: boolean;\n /** Why the statuses cannot be changed right now, the same way (a lock's own reason\n * wins over it). Only read while `canEdit`: a read-only row is a picture of the\n * workflow, not a refused action. */\n disabledReason?: ReactNode;\n className?: string;\n}) {\n const names = useFeedbackStatusLabels();\n const nameOf = label ?? ((value: FeedbackStatus) => names[value]);\n const reason = useCommitReason(commit, ownDisabledReason);\n const locked = canEdit && reason !== undefined && reason !== null && reason !== false && reason !== \"\";\n const reasonId = useId();\n return (\n // Not a control: it only stops clicks on the buttons inside from bubbling to\n // the row. The buttons are the keyboard path, and Enter/Space on them fires\n // their own click, which lands here the same way.\n // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- propagation guard around real buttons\n <div\n className={cn(variant === \"icon\" ? \"flex items-center gap-0.5\" : \"flex flex-wrap gap-1.5\", className)}\n // A status control inside a clickable row must not also open the row.\n onClick={(event) => event.stopPropagation()}\n >\n {locked && (\n <span id={reasonId} hidden>\n {reason}\n </span>\n )}\n {statuses.map((value) => {\n const meta = FEEDBACK_STATUS_META[value];\n const Icon = meta.icon;\n const active = status === value;\n const name = nameOf(value);\n // Locked: focusable and described, never firing (see `commit`). The current\n // status stays natively disabled — it was never something to press.\n const refused = locked && !active;\n const button = (\n <button\n key={value}\n type=\"button\"\n disabled={refused ? undefined : !canEdit || active}\n aria-disabled={refused || undefined}\n aria-describedby={refused ? reasonId : undefined}\n onClick={() => {\n if (!refused) onPick(value);\n }}\n aria-label={name}\n aria-pressed={active}\n className={cn(\n \"transition-colors\",\n variant === \"icon\"\n ? cn(\n \"flex size-7 items-center justify-center rounded\",\n active\n ? cn(meta.activeBg, meta.activeText)\n : \"text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]\",\n )\n : cn(\n \"inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs font-medium\",\n active\n ? cn(meta.activeBg, meta.activeText, \"border-transparent\")\n : \"border-[var(--border)] bg-transparent text-[var(--text-primary)] hover:bg-[var(--bg-surface-2)]\",\n ),\n // Both stay unclickable, but only the read-only case is DIMMED: the\n // current status is the one thing in the row that has to be legible\n // at a glance, and among eight pills a faded active one reads as\n // \"unavailable\" rather than as \"this is where the row stands\".\n (!canEdit || active) && \"cursor-default\",\n !canEdit && \"opacity-60\",\n refused && \"cursor-not-allowed opacity-50\",\n )}\n >\n <Icon className={variant === \"icon\" ? \"size-4\" : \"size-3.5\"} aria-hidden />\n {variant === \"pill\" && name}\n </button>\n );\n // Locked, the bubble says WHY in both variants; the icon's name is still its\n // `aria-label`, and the reason its description — the hidden copy above, which\n // stays put, rather than the bubble (as on `Button`: the FRAGMENT keeps Tooltip\n // from adding the bubble as a second, coming-and-going description).\n if (refused) {\n return (\n <Tooltip key={value} label={reason} side=\"top\" portal={variant === \"icon\" || undefined}>\n <>{button}</>\n </Tooltip>\n );\n }\n // The icon row has no visible label, so it needs the tooltip; the pills\n // carry theirs inline.\n //\n // ⚠️ ABOVE, and portalled (Keksdose live #339). `side=\"bottom\"` put the bubble\n // straight over the NEXT ROW's icons — in a queue you triage by running down it\n // with the pointer, that is the row you are about to reach, and it was covered\n // every time you paused on one. Above covers the row you have just left instead.\n //\n // `portal` is not cosmetic here. An absolutely-positioned bubble is clipped by\n // any ancestor that scrolls — this cell lives inside the DataTable's own\n // scroller — and it cannot flip: `placeTooltip` is what moves the bubble below\n // the icon for the FIRST row, where there is no room above it, instead of\n // painting it off the top of the list.\n return variant === \"icon\" ? (\n <Tooltip key={value} label={name} side=\"top\" portal>\n {button}\n </Tooltip>\n ) : (\n button\n );\n })}\n </div>\n );\n}\n\n/**\n * A textarea and its two buttons — for an outcome, for a note sent back with a\n * report, for a body being corrected.\n *\n * One component because all three are the same gesture: a draft that is not\n * committed until it is saved. Ctrl/⌘+Enter submits, which is the same shortcut\n * the compose dialog next door uses, so the habit carries across the feature.\n *\n * ## `initial` seeds the draft; it does not own it (changed 2026-09-22)\n *\n * This used to re-seed from `initial` in an effect, and that effect threw away\n * whatever was in the box — **and the picture attached to it** — every time the\n * prop arrived with a different value. `initial` is normally the saved note, so\n * the list refreshing under the editor, or the owner re-deriving the same string,\n * was enough: a triager halfway through a reply, with a screenshot picked out,\n * lost both and got the saved text back with no way to undo it.\n *\n * A draft belongs to the thing being edited, so switching to a different note is a\n * different editor — say so with a `key`:\n *\n * ```tsx\n * <FeedbackNoteEditor key={note.id} initial={note.body} … />\n * ```\n *\n * Where the id is not in hand at the call site, pass {@link resetKey} instead and\n * the editor re-seeds when THAT changes. Either way the decision is the caller's,\n * which is the point: the editor cannot tell a new subject from a new render.\n */\nexport function FeedbackNoteEditor({\n initial,\n pending,\n onSave,\n onCancel,\n saveLabel,\n cancelLabel,\n placeholder,\n rows = 3,\n attachment,\n resetKey,\n required = false,\n commit,\n}: {\n initial: string;\n pending: boolean;\n onSave: (value: string, attachment?: File | null) => void;\n onCancel: () => void;\n saveLabel: ReactNode;\n cancelLabel: ReactNode;\n /** A line above the field saying what to write, not an in-field placeholder:\n * a hint that disappears the moment somebody starts typing is a hint that is\n * gone exactly when it is being followed. */\n placeholder?: ReactNode;\n rows?: number;\n /** Offer a picture with the note (Steering Design feedback #128). Omitted,\n * the editor is exactly the text box it always was — which is what the\n * *outcome* editor beside it wants, since an outcome is the answer rather\n * than the evidence. */\n attachment?: FeedbackNoteAttachment;\n /** Change this to say \"the editor is now editing something ELSE\", and the draft\n * and its attachment are dropped and re-seeded from `initial`. For call sites\n * that cannot put a `key` on the editor (see the note above). Leave it out and\n * the draft is never thrown away behind the user's back. */\n resetKey?: string | number;\n /**\n * A blank note cannot be saved (0.27.0): Save stays disabled — and Ctrl/⌘+Enter does\n * nothing — until the trimmed text is non-empty, and the box is `aria-required`. For\n * the rework note, which the feedback contract requires (§3.4: a file alone cannot\n * be sent; keksdose `feedback-page.tsx:577` dropped the click silently instead, so a\n * press on Send with an empty box looked like a lost send). Default `false`: an\n * outcome or a description may be cleared on purpose.\n */\n required?: boolean;\n /** Save COMMITS (0.27.0): under a locked `WriteLockProvider` it is disabled the\n * `disabledReason` way, with the lock's reason, and Ctrl/⌘+Enter saves nothing. The\n * text box stays editable — nothing in it reaches the server until the save. */\n commit?: boolean;\n}) {\n const [draft, setDraft] = useState(initial);\n const [file, setFile] = useState<File | null>(null);\n // Adjusted during render rather than in an effect: an effect would paint the old\n // draft first and then replace it, and — far worse — it is a second definition of\n // when a draft dies that the caller cannot see. React re-runs this component\n // immediately, before anything is committed to the screen.\n const [seededFor, setSeededFor] = useState(resetKey);\n if (resetKey !== seededFor) {\n setSeededFor(resetKey);\n setDraft(initial);\n setFile(null);\n }\n // The editor's own root, which is where a paste made in the text box bubbles\n // to: the attachment field below is the box's sibling, so a paste in the box\n // never passes through the field's own subtree. It listens here instead\n // (Steering Design feedback #140).\n const root = useRef<HTMLDivElement>(null);\n const hintId = useId();\n const lock = useWriteLock();\n const blank = required && !draft.trim();\n // The shortcut obeys what the button shows: no save while one is in flight, while the\n // note a `required` editor needs is missing, or while a `commit` save is locked.\n const canSubmit = !pending && !blank && !(commit && lock.locked);\n const submit = () => onSave(draft, file);\n return (\n // Not a control: a delegated Ctrl/Cmd+Enter shortcut for the textarea inside,\n // which is what holds focus. The save button is the non-shortcut path.\n // eslint-disable-next-line jsx-a11y/no-static-element-interactions -- delegated keyboard shortcut for the focused field\n <div\n ref={root}\n className=\"space-y-2\"\n onKeyDown={(event) => {\n if ((event.ctrlKey || event.metaKey) && event.key === \"Enter\" && canSubmit) {\n event.preventDefault();\n submit();\n }\n }}\n >\n {/* The line names the box (0.27.0): it is the only text that says what to write\n in it, and an unnamed textarea is read out as just \"edit text\". */}\n {placeholder && (\n <p id={hintId} className=\"text-xs text-[var(--text-muted)]\">\n {placeholder}\n </p>\n )}\n <Textarea\n rows={rows}\n value={draft}\n onChange={(event) => setDraft(event.target.value)}\n aria-labelledby={placeholder ? hintId : undefined}\n aria-required={required || undefined}\n />\n {attachment && (\n <FeedbackAttachmentField\n value={file}\n onChange={setFile}\n // A pause while the save is in flight (the field's G5b `disabled`): the note\n // goes out with the file AS IT WAS at the press, so a chip removed or added\n // mid-send would be a lie about what was sent.\n disabled={pending}\n labels={attachment.labels}\n accept={attachment.accept}\n maxBytes={attachment.maxBytes}\n onError={attachment.onError}\n onCaptureScreenshot={attachment.onCaptureScreenshot}\n buttonVariant={attachment.buttonVariant}\n buttonSize={attachment.buttonSize}\n // Within THIS editor's subtree, not on `document`: the editor sits\n // inline on a page that has other fields, so a paste made in one of\n // them is meant for that one. The textarea above is where the caret\n // already is, and it is the field's sibling — so the field is told\n // to listen on their common parent, where the paste bubbles to.\n pasteFrom={root}\n />\n )}\n <div className=\"flex justify-end gap-2\">\n <Button variant=\"ghost\" onClick={onCancel}>\n {cancelLabel}\n </Button>\n <Button variant=\"brand\" disabled={pending || blank} commit={commit} onClick={submit}>\n {saveLabel}\n </Button>\n </div>\n </div>\n );\n}\n\n/** What {@link FeedbackNoteEditor} needs in order to offer a picture with the\n * note: the same things {@link FeedbackAttachmentField} takes, so the reply path is\n * held to the app's own limits — and drawn in its own look — rather than the\n * defaults. */\nexport interface FeedbackNoteAttachment {\n /** Over the field's own `feedbackAttachment` namespace. Optional since 0.27.0 — the\n * field has read the provider's words since 0.7.0, so an app with a catalogue has\n * nothing to restate. */\n labels?: Partial<FeedbackAttachmentLabels>;\n accept?: string[];\n maxBytes?: number;\n /** `info` (0.23.0) names the refused file and the limit. */\n onError?: (kind: \"type\" | \"size\", info: FeedbackAttachmentErrorInfo) => void;\n onCaptureScreenshot?: () => Promise<File | null>;\n /**\n * The add and capture buttons' look (0.25.0) — the field's `buttonVariant` (0.24.0),\n * Button's own `variant`. Default `\"secondary\"`, as before.\n *\n * The note editor sits inline in a triage panel, and its buttons end in a ghost\n * Cancel and a brand Save: two bordered full-size attachment buttons above them read\n * as a second form. The field could already be told otherwise; the editor builds it\n * itself and had nowhere to say so.\n */\n buttonVariant?: ButtonVariant;\n /** The add and capture buttons' size (0.25.0) — the field's `buttonSize`. Default\n * `\"md\"`; `\"sm\"` draws their icons at 14px. */\n buttonSize?: ButtonSize;\n}\n\n/**\n * One labelled block of an opened report.\n *\n * The detail panel is a stack of these, and it is a stack rather than a\n * component with fixed fields on purpose: the two apps keep a report's context\n * and its screenshot in genuinely different places, and a shell that insisted on\n * both would force one of them to invent a shape it does not have. What is\n * shared is what a section *looks* like — and that is all that made the two\n * panels read as different products.\n */\nexport function FeedbackDetailSection({\n title,\n action,\n children,\n}: {\n title: ReactNode;\n /** Something on the title's own line — an edit button, a resolved-at date. */\n action?: ReactNode;\n children: ReactNode;\n}) {\n return (\n <div>\n <div className=\"mb-1 flex items-center justify-between gap-2\">\n <div className=\"text-xs font-medium uppercase tracking-wide text-[var(--text-muted)]\">\n {title}\n </div>\n {action}\n </div>\n {children}\n </div>\n );\n}\n\n/** The stack a detail panel is. Here so the spacing between sections is decided\n * once rather than by whichever app was written second. */\nexport function FeedbackDetail({ children, className }: { children: ReactNode; className?: string }) {\n return <div className={cn(\"space-y-3 text-sm\", className)}>{children}</div>;\n}\n\n/** Prose inside a section — a body, an outcome, a note.\n *\n * `whitespace-pre-wrap` is the whole component: a report is written in\n * paragraphs, and a note appended to it later is separated by blank lines that\n * carry the entire \"this arrived after the answer\" reading. */\nexport function FeedbackProse({ children, empty }: { children?: string; empty?: ReactNode }) {\n if (!children) return <p className=\"text-[var(--text-placeholder)]\">{empty ?? \"—\"}</p>;\n return (\n <p className=\"whitespace-pre-wrap text-[var(--text-secondary)]\">{children}</p>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/feedback/feedback-inbox.tsx"],"sourcesContent":["import { useId, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport {\n Ban,\n Bug,\n CheckCircle2,\n ClipboardCheck,\n CloudUpload,\n Eye,\n HelpCircle,\n Inbox,\n Lightbulb,\n MoreHorizontal,\n OctagonAlert,\n PauseCircle,\n Wrench,\n} from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { Button, Textarea } from \"../components/ui\";\nimport type { ButtonSize, ButtonVariant } from \"../components/ui\";\nimport { Tooltip } from \"../components/tooltip\";\nimport { useCommitReason, useWriteLock } from \"../components/write-lock\";\nimport { FeedbackAttachmentField, type FeedbackAttachmentErrorInfo } from \"./feedback-attachment\";\nimport type { FeedbackAttachmentLabels } from \"./feedback-dialog\";\nimport { useFeedbackCategoryLabels, useFeedbackStatusLabels } from \"./feedback-labels\";\n\n/**\n * The feedback **inbox**, as the parts two apps were each writing separately.\n *\n * `feedback-dialog.tsx` next door is the other half — the form somebody files a\n * report with — and the split between them is the same one: this owns the\n * *vocabulary*, the *policy* about how a report moves, and the *look*; the app\n * owns the data, the API and every string.\n *\n * It exists because both apps had grown their own copy. Keksdose's is the one\n * this is lifted from, comments and all, because it is the one that had been\n * argued with users for a year — the glyph per status, the two sizes of the\n * status control, what a row offers from where it currently stands. Steering\n * Design's was thinner in every one of those places and read as a different\n * product for no reason anyone chose.\n *\n * **The report itself lives next door (0.27.0).** This file used to say there\n * was deliberately no `Feedback` type here: the apps stored a report differently\n * (a context JSON column and an object store in one, a `page_path` and the bytes\n * in the row in another). Marcel's feedback round (2026-10-04,\n * docs/feedback-harmonization.md §5) gave all three apps one contract, so the\n * shape is now shared — `FeedbackRecord` / `FeedbackContext` in\n * feedback-record.ts, with the constants and body helpers. The components HERE\n * still take the values they draw rather than a row, which keeps them usable\n * from any surface; the detail panel stays a *shell* the row detail fills.\n *\n * **The words are the kit's too (0.27.0).** The status and category names are\n * the `feedbackStatus` / `feedbackCategory` namespaces (feedback-labels.ts,\n * keksdose's wording), read by default by the badges and the transitions below;\n * a `label` the caller passes still wins.\n */\n\n/** The eight states a report can be in.\n *\n * Shared value for value across the apps on purpose: it is what lets one habit,\n * and one agent prompt, work on any repo. Five are a chain and three sit off\n * it — see {@link visibleFeedbackStatuses} for what that buys. `READY` joined in 0.31\n * (feedback contract §8.2): OPEN is \"filed, not yet triaged\", READY \"released for\n * implementation\", and nobody works an OPEN row. */\nexport type FeedbackStatus =\n | \"OPEN\"\n | \"READY\"\n | \"IN_PROGRESS\"\n | \"IN_EVALUATION\"\n | \"NEEDS_LIVE_TEST\"\n | \"POSTPONED\"\n | \"DONE\"\n | \"WONT_DO\";\n\n/** What a report is about. `CRASH` is filed by an error boundary and never\n * chosen, which is exactly why it is loud below and absent from every picker. */\nexport type FeedbackCategory = \"CRASH\" | \"BUG\" | \"IDEA\" | \"QUESTION\" | \"OTHER\";\n\n/**\n * A glyph and a tone per status.\n *\n * Colour alone is not a label — two of these sit a hue apart, and a consumer who\n * moves `--brand` can park it beside any of the others — so every status carries\n * its own shape as well. Declaration order is chain order, and\n * {@link FEEDBACK_STATUS_ORDER} is derived from it rather than restated, because\n * a restated list cannot be checked for exhaustiveness: an eighth status would\n * break the build here and leave a literal eight long (as READY did in 0.31).\n */\nexport const FEEDBACK_STATUS_META: Record<\n FeedbackStatus,\n { icon: LucideIcon; activeBg: string; activeText: string }\n> = {\n OPEN: {\n icon: Inbox,\n activeBg: \"bg-[var(--bg-active)]\",\n activeText: \"text-[var(--text-primary)]\",\n },\n // Triaged and released for implementation (feedback contract §8.2, keksdose live\n // #396): OPEN is \"filed, nobody has looked yet\", READY is \"go\". The brand tint at full\n // strength, so it reads as the go-ahead beside OPEN's neutral fill and\n // NEEDS_LIVE_TEST's muted brand.\n READY: {\n icon: ClipboardCheck,\n activeBg: \"bg-[var(--brand-bg)]\",\n activeText: \"text-[var(--brand)]\",\n },\n IN_PROGRESS: {\n icon: Wrench,\n activeBg: \"bg-[var(--warning-bg)]\",\n activeText: \"text-[var(--warning)]\",\n },\n // Where solved work is parked, waiting on the person who reported it. Its own\n // colour, because \"somebody has to check this\" is a state you want to find by\n // scanning rather than by reading.\n IN_EVALUATION: {\n icon: Eye,\n activeBg: \"bg-[var(--info-bg)]\",\n activeText: \"text-[var(--info)]\",\n },\n // Resolved but only checkable on a deployed build — the SOFT brand tint, so it\n // reads as \"waiting on something\" rather than as a done or a refusal, and it\n // follows whatever the consumer's brand is instead of pinning an indigo island\n // beside it.\n NEEDS_LIVE_TEST: {\n icon: CloudUpload,\n activeBg: \"bg-[var(--brand-bg)]\",\n activeText: \"text-[var(--brand-muted)]\",\n },\n // Parked on purpose. Muted, not `--danger`: it is not a refusal.\n POSTPONED: {\n icon: PauseCircle,\n activeBg: \"bg-[var(--bg-active)]\",\n activeText: \"text-[var(--text-secondary)]\",\n },\n // The one status with nowhere to move to: the vocabulary has a brand, an info, a\n // warning and a danger, and nothing that means \"succeeded\". Deliberately NOT\n // borrowed from a family that means something else — `--status-synced` is a\n // FIELD's trip to the database rather than an outcome, and tokens.css says so in\n // as many words. Green beside WONT_DO's red is the classic CVD-unsafe pair, which\n // is why both carry a glyph and neither leans on colour alone.\n DONE: {\n icon: CheckCircle2,\n activeBg: \"bg-[var(--success-bg)]\",\n activeText: \"text-[var(--success)]\",\n },\n WONT_DO: {\n icon: Ban,\n activeBg: \"bg-[var(--danger-bg)]\",\n activeText: \"text-[var(--danger)]\",\n },\n};\n\n/** Chain order, then the three off it. The order a picker offers, and the rank a\n * status column sorts by — so a list sorted by status reads as a queue rather\n * than as an alphabet. */\nexport const FEEDBACK_STATUS_ORDER = Object.keys(FEEDBACK_STATUS_META) as FeedbackStatus[];\n\n/** States that sit OFF the linear chain: reachable from anywhere, leading\n * nowhere by themselves. `WONT_DO` has always worked this way; `POSTPONED`\n * (\"not now\") and `NEEDS_LIVE_TEST` (\"resolved, but only provable on a deployed\n * build\") are the same shape — a row can be parked or handed to the next deploy\n * from any point, and comes back to the chain when somebody picks a real state\n * for it.\n *\n * Kept out of the forward chain deliberately: putting them in it would mean an\n * \"advance\" gesture could park an item, and every row would have to pass\n * through them to reach `DONE`. */\nconst OFF_CHAIN: FeedbackStatus[] = [\"NEEDS_LIVE_TEST\", \"POSTPONED\", \"WONT_DO\"];\n\nconst FORWARD_CHAIN: FeedbackStatus[] = [\"OPEN\", \"READY\", \"IN_PROGRESS\", \"IN_EVALUATION\", \"DONE\"];\n\n/** The next status along the chain, or null at the end (or off it, as `WONT_DO`\n * is). What a swipe-to-advance gesture commits. */\nexport function nextFeedbackStatus(current: FeedbackStatus): FeedbackStatus | null {\n const at = FORWARD_CHAIN.indexOf(current);\n return at !== -1 && at < FORWARD_CHAIN.length - 1 ? FORWARD_CHAIN[at + 1] : null;\n}\n\n/**\n * The steps that make sense from where a row currently stands.\n *\n * For the compact control in a table cell: it is a glanceable triage affordance,\n * and a row of eight icons in a cell is noise. One step forward, one step back\n * (so an item can be sent back for rework), and any of the three off-chain\n * verdicts — which is what makes those usable as verdicts at all.\n *\n * The two parking states are not dead ends: each offers the step that resumes\n * work and the ones that close it, but deliberately **not each other** —\n * \"postponed\" and \"waiting on a deploy\" are different answers to different\n * questions, and a row moving between them directly is a re-triage, which starts\n * by picking the work back up.\n */\nexport function visibleFeedbackStatuses(current: FeedbackStatus): FeedbackStatus[] {\n const set = new Set<FeedbackStatus>([current]);\n const at = FORWARD_CHAIN.indexOf(current);\n if (at !== -1) {\n if (at < FORWARD_CHAIN.length - 1) set.add(FORWARD_CHAIN[at + 1]);\n if (at > 0) set.add(FORWARD_CHAIN[at - 1]);\n for (const off of OFF_CHAIN) set.add(off);\n } else if (current !== \"WONT_DO\") {\n set.add(\"IN_PROGRESS\");\n set.add(\"DONE\");\n set.add(\"WONT_DO\");\n }\n // WONT_DO falls through with nothing added: it stays terminal, reopened by a\n // note from its author rather than by a status pill.\n return FEEDBACK_STATUS_ORDER.filter((status) => set.has(status));\n}\n\n/**\n * Every status, in chain order — what an EXPANDED row offers.\n *\n * {@link visibleFeedbackStatuses} narrows the choice to the steps that make\n * sense from where a row is, which is right for a table cell. Once the row is\n * open in front of you, that same narrowing turns every non-adjacent move into a\n * walk: OPEN to DONE meant three round trips, and WONT_DO could not be left at\n * all except through a rework note.\n *\n * Takes a `current` it does not use, so the two policies read as one pair at the\n * call sites and a future rule (\"terminal rows still cannot jump to X\") has a\n * place to live.\n */\nexport function selectableFeedbackStatuses(_current: FeedbackStatus): FeedbackStatus[] {\n return [...FEEDBACK_STATUS_ORDER];\n}\n\n/**\n * Per-category badge treatment, the counterpart to the status meta above.\n *\n * A category cell that is one grey pill holding the raw enum makes a CRASH —\n * filed automatically, by somebody staring at a broken page right now — read\n * exactly like a QUESTION, and sit unnoticed in the queue. `--danger` plus its own\n * glyph is what makes that impossible; the hand-filed categories stay\n * deliberately quiet so that the loud one means something.\n *\n * Declaration order is triage order, CRASH first.\n */\nexport const FEEDBACK_CATEGORY_META: Record<\n FeedbackCategory,\n { icon: LucideIcon; badgeBg: string; badgeText: string }\n> = {\n CRASH: {\n icon: OctagonAlert,\n badgeBg: \"bg-[var(--danger-bg)]\",\n badgeText: \"text-[var(--danger)]\",\n },\n BUG: {\n icon: Bug,\n badgeBg: \"bg-[var(--warning-bg)]\",\n badgeText: \"text-[var(--warning)]\",\n },\n IDEA: {\n icon: Lightbulb,\n badgeBg: \"bg-[var(--bg-surface-2)]\",\n badgeText: \"text-[var(--text-secondary)]\",\n },\n QUESTION: {\n icon: HelpCircle,\n badgeBg: \"bg-[var(--bg-surface-2)]\",\n badgeText: \"text-[var(--text-secondary)]\",\n },\n OTHER: {\n icon: MoreHorizontal,\n badgeBg: \"bg-[var(--bg-surface-2)]\",\n badgeText: \"text-[var(--text-secondary)]\",\n },\n};\n\n/** Triage order, CRASH first — also a category column's sort key and the option\n * order in its filter. Sorting by the raw enum is alphabetical, which parks\n * CRASH between BUG and IDEA. */\nexport const FEEDBACK_CATEGORY_ORDER = Object.keys(FEEDBACK_CATEGORY_META) as FeedbackCategory[];\n\n/** A category's rank for a column sort. Anything this build has never heard of —\n * an older client against a newer API — sorts LAST rather than to `indexOf`'s\n * −1, which would rank it above CRASH. */\nexport function feedbackCategoryRank(category: FeedbackCategory): number {\n const at = FEEDBACK_CATEGORY_ORDER.indexOf(category);\n return at === -1 ? FEEDBACK_CATEGORY_ORDER.length : at;\n}\n\n/**\n * The category as a badge.\n *\n * Falls back to OTHER's treatment for a category this build has never heard of,\n * so an older client against a newer API degrades to a readable neutral pill\n * instead of throwing on `meta.icon` — a crash report must not be able to cause\n * one. The *label* gets the same guard: by default the `feedbackCategory`\n * namespace's word (0.27.0), and for a value it has no word for, what OTHER is\n * called.\n */\nexport function FeedbackCategoryBadge({\n category,\n label,\n compact = false,\n className,\n}: {\n category: FeedbackCategory;\n /** Over the `feedbackCategory` label (0.27.0: optional — before, every app passed\n * its own translation). */\n label?: ReactNode;\n compact?: boolean;\n className?: string;\n}) {\n const names = useFeedbackCategoryLabels();\n // Annotated `| undefined` because the Record's index signature promises a hit\n // for every FeedbackCategory, and at runtime the API can hand us one that is not.\n const known: (typeof FEEDBACK_CATEGORY_META)[\"OTHER\"] | undefined = FEEDBACK_CATEGORY_META[category];\n const meta = known ?? FEEDBACK_CATEGORY_META.OTHER;\n const Icon = meta.icon;\n return (\n <span\n className={cn(\n \"inline-flex items-center gap-1 rounded font-medium\",\n compact ? \"px-1.5 py-0.5 text-caption\" : \"px-2 py-0.5 text-xs\",\n meta.badgeBg,\n meta.badgeText,\n className,\n )}\n >\n <Icon className={compact ? \"size-3\" : \"size-3.5\"} aria-hidden />\n {label ?? (known ? names[category] : names.OTHER)}\n </span>\n );\n}\n\n/** Where a report stands, as a badge — for the places that only *report* the\n * status rather than offering to change it. Named by the `feedbackStatus`\n * namespace unless `label` says otherwise (0.27.0); a status this build has never\n * heard of shows OPEN's look and its own raw value, never OPEN's name. */\nexport function FeedbackStatusBadge({\n status,\n label,\n className,\n}: {\n status: FeedbackStatus;\n /** Over the `feedbackStatus` label (0.27.0: optional). */\n label?: ReactNode;\n className?: string;\n}) {\n const names = useFeedbackStatusLabels();\n const meta = FEEDBACK_STATUS_META[status] ?? FEEDBACK_STATUS_META.OPEN;\n const name: string | undefined = names[status];\n const Icon = meta.icon;\n return (\n <span\n className={cn(\n \"inline-flex items-center gap-1 whitespace-nowrap rounded px-1.5 py-0.5 text-caption font-medium\",\n meta.activeBg,\n meta.activeText,\n className,\n )}\n >\n <Icon className=\"size-3\" aria-hidden />\n {label ?? name ?? status}\n </span>\n );\n}\n\n/**\n * A row of statuses to switch a report to — either a table column's icon row or\n * an expanded panel's labelled pills.\n *\n * One component for both, because everything except the class strings is the\n * same decision: which one is current, and when the buttons are disabled. Two\n * copies had already drifted — only the icon variant carried `aria-label` and\n * `aria-pressed`, so the pill row was unlabelled for a screen reader.\n *\n * WHICH statuses to offer is the caller's, not this component's:\n * {@link visibleFeedbackStatuses} for a compact column,\n * {@link selectableFeedbackStatuses} for an expanded row. Passing the list keeps\n * that policy readable at the two call sites instead of hiding it behind\n * `variant`.\n */\nexport function FeedbackStatusTransitions({\n status,\n statuses,\n canEdit,\n onPick,\n variant,\n label,\n commit,\n disabledReason: ownDisabledReason,\n className,\n}: {\n status: FeedbackStatus;\n statuses: FeedbackStatus[];\n canEdit: boolean;\n onPick: (status: FeedbackStatus) => void;\n variant: \"icon\" | \"pill\";\n /** What each status is called. Default (0.27.0): the `feedbackStatus` namespace —\n * before, every app passed its own translation. */\n label?: (status: FeedbackStatus) => string;\n /**\n * Every button here SAVES the moment it is pressed (0.27.0) — opt in and, under a\n * locked {@link WriteLockProvider}, the row stays as it is and says why: each status\n * the row could move to is `aria-disabled` but focusable, the click does nothing, and\n * the lock's reason is in the kit Tooltip and the button's description — the\n * `disabledReason` path `Button` takes. Off by default: a feedback inbox is not what\n * every app's lock is about (keksdose's shell lock is a read-only demo BUDGET, and a\n * report is not budget data).\n */\n commit?: boolean;\n /** Why the statuses cannot be changed right now, the same way (a lock's own reason\n * wins over it). Only read while `canEdit`: a read-only row is a picture of the\n * workflow, not a refused action. */\n disabledReason?: ReactNode;\n className?: string;\n}) {\n const names = useFeedbackStatusLabels();\n const nameOf = label ?? ((value: FeedbackStatus) => names[value]);\n const reason = useCommitReason(commit, ownDisabledReason);\n const locked = canEdit && reason !== undefined && reason !== null && reason !== false && reason !== \"\";\n const reasonId = useId();\n return (\n // Not a control: it only stops clicks on the buttons inside from bubbling to\n // the row. The buttons are the keyboard path, and Enter/Space on them fires\n // their own click, which lands here the same way.\n // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- propagation guard around real buttons\n <div\n className={cn(variant === \"icon\" ? \"flex items-center gap-0.5\" : \"flex flex-wrap gap-1.5\", className)}\n // A status control inside a clickable row must not also open the row.\n onClick={(event) => event.stopPropagation()}\n >\n {locked && (\n <span id={reasonId} hidden>\n {reason}\n </span>\n )}\n {statuses.map((value) => {\n const meta = FEEDBACK_STATUS_META[value];\n const Icon = meta.icon;\n const active = status === value;\n const name = nameOf(value);\n // Locked: focusable and described, never firing (see `commit`). The current\n // status stays natively disabled — it was never something to press.\n const refused = locked && !active;\n const button = (\n <button\n key={value}\n type=\"button\"\n disabled={refused ? undefined : !canEdit || active}\n aria-disabled={refused || undefined}\n aria-describedby={refused ? reasonId : undefined}\n onClick={() => {\n if (!refused) onPick(value);\n }}\n aria-label={name}\n aria-pressed={active}\n className={cn(\n \"transition-colors\",\n variant === \"icon\"\n ? cn(\n \"flex size-7 items-center justify-center rounded\",\n active\n ? cn(meta.activeBg, meta.activeText)\n : \"text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]\",\n )\n : cn(\n \"inline-flex items-center gap-1.5 rounded-md border px-2 py-1 text-xs font-medium\",\n active\n ? cn(meta.activeBg, meta.activeText, \"border-transparent\")\n : \"border-[var(--border)] bg-transparent text-[var(--text-primary)] hover:bg-[var(--bg-surface-2)]\",\n ),\n // Both stay unclickable, but only the read-only case is DIMMED: the\n // current status is the one thing in the row that has to be legible\n // at a glance, and among eight pills a faded active one reads as\n // \"unavailable\" rather than as \"this is where the row stands\".\n (!canEdit || active) && \"cursor-default\",\n !canEdit && \"opacity-60\",\n refused && \"cursor-not-allowed opacity-50\",\n )}\n >\n <Icon className={variant === \"icon\" ? \"size-4\" : \"size-3.5\"} aria-hidden />\n {variant === \"pill\" && name}\n </button>\n );\n // Locked, the bubble says WHY in both variants; the icon's name is still its\n // `aria-label`, and the reason its description — the hidden copy above, which\n // stays put, rather than the bubble (as on `Button`: the FRAGMENT keeps Tooltip\n // from adding the bubble as a second, coming-and-going description).\n if (refused) {\n return (\n <Tooltip key={value} label={reason} side=\"top\" portal={variant === \"icon\" || undefined}>\n <>{button}</>\n </Tooltip>\n );\n }\n // The icon row has no visible label, so it needs the tooltip; the pills\n // carry theirs inline.\n //\n // ⚠️ ABOVE, and portalled (Keksdose live #339). `side=\"bottom\"` put the bubble\n // straight over the NEXT ROW's icons — in a queue you triage by running down it\n // with the pointer, that is the row you are about to reach, and it was covered\n // every time you paused on one. Above covers the row you have just left instead.\n //\n // `portal` is not cosmetic here. An absolutely-positioned bubble is clipped by\n // any ancestor that scrolls — this cell lives inside the DataTable's own\n // scroller — and it cannot flip: `placeTooltip` is what moves the bubble below\n // the icon for the FIRST row, where there is no room above it, instead of\n // painting it off the top of the list.\n return variant === \"icon\" ? (\n <Tooltip key={value} label={name} side=\"top\" portal>\n {button}\n </Tooltip>\n ) : (\n button\n );\n })}\n </div>\n );\n}\n\n/**\n * A textarea and its two buttons — for an outcome, for a note sent back with a\n * report, for a body being corrected.\n *\n * One component because all three are the same gesture: a draft that is not\n * committed until it is saved. Ctrl/⌘+Enter submits, which is the same shortcut\n * the compose dialog next door uses, so the habit carries across the feature.\n *\n * ## `initial` seeds the draft; it does not own it (changed 2026-09-22)\n *\n * This used to re-seed from `initial` in an effect, and that effect threw away\n * whatever was in the box — **and the picture attached to it** — every time the\n * prop arrived with a different value. `initial` is normally the saved note, so\n * the list refreshing under the editor, or the owner re-deriving the same string,\n * was enough: a triager halfway through a reply, with a screenshot picked out,\n * lost both and got the saved text back with no way to undo it.\n *\n * A draft belongs to the thing being edited, so switching to a different note is a\n * different editor — say so with a `key`:\n *\n * ```tsx\n * <FeedbackNoteEditor key={note.id} initial={note.body} … />\n * ```\n *\n * Where the id is not in hand at the call site, pass {@link resetKey} instead and\n * the editor re-seeds when THAT changes. Either way the decision is the caller's,\n * which is the point: the editor cannot tell a new subject from a new render.\n */\nexport function FeedbackNoteEditor({\n initial,\n pending,\n onSave,\n onCancel,\n saveLabel,\n cancelLabel,\n placeholder,\n rows = 3,\n attachment,\n resetKey,\n required = false,\n commit,\n}: {\n initial: string;\n pending: boolean;\n onSave: (value: string, attachment?: File | null) => void;\n onCancel: () => void;\n saveLabel: ReactNode;\n cancelLabel: ReactNode;\n /** A line above the field saying what to write, not an in-field placeholder:\n * a hint that disappears the moment somebody starts typing is a hint that is\n * gone exactly when it is being followed. */\n placeholder?: ReactNode;\n rows?: number;\n /** Offer a picture with the note (Steering Design feedback #128). Omitted,\n * the editor is exactly the text box it always was — which is what the\n * *outcome* editor beside it wants, since an outcome is the answer rather\n * than the evidence. */\n attachment?: FeedbackNoteAttachment;\n /** Change this to say \"the editor is now editing something ELSE\", and the draft\n * and its attachment are dropped and re-seeded from `initial`. For call sites\n * that cannot put a `key` on the editor (see the note above). Leave it out and\n * the draft is never thrown away behind the user's back. */\n resetKey?: string | number;\n /**\n * A blank note cannot be saved (0.27.0): Save stays disabled — and Ctrl/⌘+Enter does\n * nothing — until the trimmed text is non-empty, and the box is `aria-required`. For\n * the rework note, which the feedback contract requires (§3.4: a file alone cannot\n * be sent; keksdose `feedback-page.tsx:577` dropped the click silently instead, so a\n * press on Send with an empty box looked like a lost send). Default `false`: an\n * outcome or a description may be cleared on purpose.\n */\n required?: boolean;\n /** Save COMMITS (0.27.0): under a locked `WriteLockProvider` it is disabled the\n * `disabledReason` way, with the lock's reason, and Ctrl/⌘+Enter saves nothing. The\n * text box stays editable — nothing in it reaches the server until the save. */\n commit?: boolean;\n}) {\n const [draft, setDraft] = useState(initial);\n const [file, setFile] = useState<File | null>(null);\n // Adjusted during render rather than in an effect: an effect would paint the old\n // draft first and then replace it, and — far worse — it is a second definition of\n // when a draft dies that the caller cannot see. React re-runs this component\n // immediately, before anything is committed to the screen.\n const [seededFor, setSeededFor] = useState(resetKey);\n if (resetKey !== seededFor) {\n setSeededFor(resetKey);\n setDraft(initial);\n setFile(null);\n }\n // The editor's own root, which is where a paste made in the text box bubbles\n // to: the attachment field below is the box's sibling, so a paste in the box\n // never passes through the field's own subtree. It listens here instead\n // (Steering Design feedback #140).\n const root = useRef<HTMLDivElement>(null);\n const hintId = useId();\n const lock = useWriteLock();\n const blank = required && !draft.trim();\n // The shortcut obeys what the button shows: no save while one is in flight, while the\n // note a `required` editor needs is missing, or while a `commit` save is locked.\n const canSubmit = !pending && !blank && !(commit && lock.locked);\n const submit = () => onSave(draft, file);\n return (\n // Not a control: a delegated Ctrl/Cmd+Enter shortcut for the textarea inside,\n // which is what holds focus. The save button is the non-shortcut path.\n // eslint-disable-next-line jsx-a11y/no-static-element-interactions -- delegated keyboard shortcut for the focused field\n <div\n ref={root}\n className=\"space-y-2\"\n onKeyDown={(event) => {\n if ((event.ctrlKey || event.metaKey) && event.key === \"Enter\" && canSubmit) {\n event.preventDefault();\n submit();\n }\n }}\n >\n {/* The line names the box (0.27.0): it is the only text that says what to write\n in it, and an unnamed textarea is read out as just \"edit text\". */}\n {placeholder && (\n <p id={hintId} className=\"text-xs text-[var(--text-muted)]\">\n {placeholder}\n </p>\n )}\n <Textarea\n rows={rows}\n value={draft}\n onChange={(event) => setDraft(event.target.value)}\n aria-labelledby={placeholder ? hintId : undefined}\n aria-required={required || undefined}\n />\n {attachment && (\n <FeedbackAttachmentField\n value={file}\n onChange={setFile}\n // A pause while the save is in flight (the field's G5b `disabled`): the note\n // goes out with the file AS IT WAS at the press, so a chip removed or added\n // mid-send would be a lie about what was sent.\n disabled={pending}\n labels={attachment.labels}\n accept={attachment.accept}\n maxBytes={attachment.maxBytes}\n onError={attachment.onError}\n onCaptureScreenshot={attachment.onCaptureScreenshot}\n buttonVariant={attachment.buttonVariant}\n buttonSize={attachment.buttonSize}\n // Within THIS editor's subtree, not on `document`: the editor sits\n // inline on a page that has other fields, so a paste made in one of\n // them is meant for that one. The textarea above is where the caret\n // already is, and it is the field's sibling — so the field is told\n // to listen on their common parent, where the paste bubbles to.\n pasteFrom={root}\n />\n )}\n <div className=\"flex justify-end gap-2\">\n <Button variant=\"ghost\" onClick={onCancel}>\n {cancelLabel}\n </Button>\n <Button variant=\"brand\" disabled={pending || blank} commit={commit} onClick={submit}>\n {saveLabel}\n </Button>\n </div>\n </div>\n );\n}\n\n/** What {@link FeedbackNoteEditor} needs in order to offer a picture with the\n * note: the same things {@link FeedbackAttachmentField} takes, so the reply path is\n * held to the app's own limits — and drawn in its own look — rather than the\n * defaults. */\nexport interface FeedbackNoteAttachment {\n /** Over the field's own `feedbackAttachment` namespace. Optional since 0.27.0 — the\n * field has read the provider's words since 0.7.0, so an app with a catalogue has\n * nothing to restate. */\n labels?: Partial<FeedbackAttachmentLabels>;\n accept?: string[];\n maxBytes?: number;\n /** `info` (0.23.0) names the refused file and the limit. */\n onError?: (kind: \"type\" | \"size\", info: FeedbackAttachmentErrorInfo) => void;\n onCaptureScreenshot?: () => Promise<File | null>;\n /**\n * The add and capture buttons' look (0.25.0) — the field's `buttonVariant` (0.24.0),\n * Button's own `variant`. Default `\"secondary\"`, as before.\n *\n * The note editor sits inline in a triage panel, and its buttons end in a ghost\n * Cancel and a brand Save: two bordered full-size attachment buttons above them read\n * as a second form. The field could already be told otherwise; the editor builds it\n * itself and had nowhere to say so.\n */\n buttonVariant?: ButtonVariant;\n /** The add and capture buttons' size (0.25.0) — the field's `buttonSize`. Default\n * `\"md\"`; `\"sm\"` draws their icons at 14px. */\n buttonSize?: ButtonSize;\n}\n\n/**\n * One labelled block of an opened report.\n *\n * The detail panel is a stack of these, and it is a stack rather than a\n * component with fixed fields on purpose: the two apps keep a report's context\n * and its screenshot in genuinely different places, and a shell that insisted on\n * both would force one of them to invent a shape it does not have. What is\n * shared is what a section *looks* like — and that is all that made the two\n * panels read as different products.\n */\nexport function FeedbackDetailSection({\n title,\n action,\n children,\n}: {\n title: ReactNode;\n /** Something on the title's own line — an edit button, a resolved-at date. */\n action?: ReactNode;\n children: ReactNode;\n}) {\n return (\n <div>\n <div className=\"mb-1 flex items-center justify-between gap-2\">\n <div className=\"text-xs font-medium uppercase tracking-wide text-[var(--text-muted)]\">\n {title}\n </div>\n {action}\n </div>\n {children}\n </div>\n );\n}\n\n/** The stack a detail panel is. Here so the spacing between sections is decided\n * once rather than by whichever app was written second. */\nexport function FeedbackDetail({ children, className }: { children: ReactNode; className?: string }) {\n return <div className={cn(\"space-y-3 text-sm\", className)}>{children}</div>;\n}\n\n/** Prose inside a section — a body, an outcome, a note.\n *\n * `whitespace-pre-wrap` is the whole component: a report is written in\n * paragraphs, and a note appended to it later is separated by blank lines that\n * carry the entire \"this arrived after the answer\" reading. */\nexport function FeedbackProse({ children, empty }: { children?: string; empty?: ReactNode }) {\n if (!children) return <p className=\"text-[var(--text-placeholder)]\">{empty ?? \"—\"}</p>;\n return (\n <p className=\"whitespace-pre-wrap text-[var(--text-secondary)]\">{children}</p>\n );\n}\n"],"mappings":"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","names":[]}