@eifi1/ui-kit 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (382) hide show
  1. package/README.md +66 -37
  2. package/dist/chart.d.ts +3 -1
  3. package/dist/chart.js +2 -0
  4. package/dist/chart.js.map +1 -1
  5. package/dist/components/account-settings-labels.d.ts +105 -0
  6. package/dist/components/account-settings-labels.js +81 -0
  7. package/dist/components/account-settings-labels.js.map +1 -0
  8. package/dist/components/account-settings.d.ts +36 -52
  9. package/dist/components/account-settings.js +44 -8
  10. package/dist/components/account-settings.js.map +1 -1
  11. package/dist/components/alert-banner.d.ts +26 -1
  12. package/dist/components/alert-banner.js +10 -7
  13. package/dist/components/alert-banner.js.map +1 -1
  14. package/dist/components/amount-input.d.ts +26 -4
  15. package/dist/components/amount-input.js +5 -2
  16. package/dist/components/amount-input.js.map +1 -1
  17. package/dist/components/authed-image.d.ts +68 -0
  18. package/dist/components/authed-image.js +117 -0
  19. package/dist/components/authed-image.js.map +1 -0
  20. package/dist/components/breadcrumbs.d.ts +2 -1
  21. package/dist/components/breadcrumbs.js +5 -2
  22. package/dist/components/breadcrumbs.js.map +1 -1
  23. package/dist/components/bulk-action-bar.d.ts +51 -5
  24. package/dist/components/bulk-action-bar.js +88 -41
  25. package/dist/components/bulk-action-bar.js.map +1 -1
  26. package/dist/components/button-group.d.ts +107 -3
  27. package/dist/components/button-group.js +60 -5
  28. package/dist/components/button-group.js.map +1 -1
  29. package/dist/components/calculator.d.ts +17 -4
  30. package/dist/components/calendar-heatmap.d.ts +162 -0
  31. package/dist/components/calendar-heatmap.js +336 -0
  32. package/dist/components/calendar-heatmap.js.map +1 -0
  33. package/dist/components/chip.d.ts +2 -1
  34. package/dist/components/chip.js +6 -3
  35. package/dist/components/chip.js.map +1 -1
  36. package/dist/components/choice-card.d.ts +66 -2
  37. package/dist/components/choice-card.js +108 -1
  38. package/dist/components/choice-card.js.map +1 -1
  39. package/dist/components/combobox-core.js +2 -0
  40. package/dist/components/combobox-core.js.map +1 -1
  41. package/dist/components/combobox.d.ts +2 -2
  42. package/dist/components/combobox.js +31 -9
  43. package/dist/components/combobox.js.map +1 -1
  44. package/dist/components/copy-button.d.ts +52 -70
  45. package/dist/components/copy-button.js +1 -1
  46. package/dist/components/copy-button.js.map +1 -1
  47. package/dist/components/data-table-cells.d.ts +63 -0
  48. package/dist/components/data-table-cells.js +64 -0
  49. package/dist/components/data-table-cells.js.map +1 -0
  50. package/dist/components/data-table-filter-popover.d.ts +1 -1
  51. package/dist/components/data-table-filters.d.ts +1 -1
  52. package/dist/components/data-table-filters.js +31 -1
  53. package/dist/components/data-table-filters.js.map +1 -1
  54. package/dist/components/data-table-labels.d.ts +13 -0
  55. package/dist/components/data-table-labels.js +8 -1
  56. package/dist/components/data-table-labels.js.map +1 -1
  57. package/dist/components/data-table.d.ts +1 -1
  58. package/dist/components/data-table.js +104 -12
  59. package/dist/components/data-table.js.map +1 -1
  60. package/dist/components/date-picker.d.ts +3 -1
  61. package/dist/components/date-picker.js +15 -2
  62. package/dist/components/date-picker.js.map +1 -1
  63. package/dist/components/description-list.d.ts +23 -3
  64. package/dist/components/description-list.js +23 -3
  65. package/dist/components/description-list.js.map +1 -1
  66. package/dist/components/dialog-frame.d.ts +1 -0
  67. package/dist/components/entity-combobox.d.ts +1 -1
  68. package/dist/components/entity-combobox.js +18 -3
  69. package/dist/components/entity-combobox.js.map +1 -1
  70. package/dist/components/error-boundary.d.ts +92 -0
  71. package/dist/components/error-boundary.js +111 -0
  72. package/dist/components/error-boundary.js.map +1 -0
  73. package/dist/components/field.d.ts +69 -0
  74. package/dist/components/field.js +75 -0
  75. package/dist/components/field.js.map +1 -0
  76. package/dist/components/file-button.d.ts +51 -208
  77. package/dist/components/file-dropzone.d.ts +50 -2
  78. package/dist/components/floating-panel.d.ts +156 -4
  79. package/dist/components/floating-panel.js +193 -29
  80. package/dist/components/floating-panel.js.map +1 -1
  81. package/dist/components/form-actions.d.ts +53 -0
  82. package/dist/components/form-actions.js +98 -0
  83. package/dist/components/form-actions.js.map +1 -0
  84. package/dist/components/image-grid.d.ts +67 -0
  85. package/dist/components/image-grid.js +112 -0
  86. package/dist/components/image-grid.js.map +1 -0
  87. package/dist/components/lightbox.d.ts +104 -0
  88. package/dist/components/lightbox.js +229 -0
  89. package/dist/components/lightbox.js.map +1 -0
  90. package/dist/components/line-items.d.ts +114 -0
  91. package/dist/components/line-items.js +238 -0
  92. package/dist/components/line-items.js.map +1 -0
  93. package/dist/components/list.d.ts +37 -3
  94. package/dist/components/list.js +39 -14
  95. package/dist/components/list.js.map +1 -1
  96. package/dist/components/loading-state.d.ts +24 -0
  97. package/dist/components/loading-state.js +32 -0
  98. package/dist/components/loading-state.js.map +1 -0
  99. package/dist/components/menu-item.d.ts +13 -2
  100. package/dist/components/menu-item.js +11 -2
  101. package/dist/components/menu-item.js.map +1 -1
  102. package/dist/components/mini-calendar.d.ts +82 -5
  103. package/dist/components/mini-calendar.js +212 -64
  104. package/dist/components/mini-calendar.js.map +1 -1
  105. package/dist/components/modal.d.ts +20 -1
  106. package/dist/components/modal.js +26 -2
  107. package/dist/components/modal.js.map +1 -1
  108. package/dist/components/month-picker.d.ts +21 -1
  109. package/dist/components/month-picker.js +139 -26
  110. package/dist/components/month-picker.js.map +1 -1
  111. package/dist/components/multi-entity-combobox.d.ts +1 -1
  112. package/dist/components/multi-entity-combobox.js +19 -3
  113. package/dist/components/multi-entity-combobox.js.map +1 -1
  114. package/dist/components/nav-pills.d.ts +67 -0
  115. package/dist/components/nav-pills.js +69 -0
  116. package/dist/components/nav-pills.js.map +1 -0
  117. package/dist/components/number-field.d.ts +26 -5
  118. package/dist/components/number-field.js +5 -7
  119. package/dist/components/number-field.js.map +1 -1
  120. package/dist/components/number-input.d.ts +27 -5
  121. package/dist/components/number-input.js +7 -1
  122. package/dist/components/number-input.js.map +1 -1
  123. package/dist/components/numpad-sheet.d.ts +17 -4
  124. package/dist/components/passkeys-setting.d.ts +67 -0
  125. package/dist/components/passkeys-setting.js +214 -0
  126. package/dist/components/passkeys-setting.js.map +1 -0
  127. package/dist/components/picker-sheet.js +2 -1
  128. package/dist/components/picker-sheet.js.map +1 -1
  129. package/dist/components/pie-chart-labels.d.ts +20 -0
  130. package/dist/components/pie-chart-labels.js +12 -0
  131. package/dist/components/pie-chart-labels.js.map +1 -0
  132. package/dist/components/pie-chart.d.ts +101 -0
  133. package/dist/components/pie-chart.js +283 -0
  134. package/dist/components/pie-chart.js.map +1 -0
  135. package/dist/components/progress-bar.d.ts +73 -3
  136. package/dist/components/progress-bar.js +110 -8
  137. package/dist/components/progress-bar.js.map +1 -1
  138. package/dist/components/qr-code.d.ts +34 -0
  139. package/dist/components/qr-code.js +57 -0
  140. package/dist/components/qr-code.js.map +1 -0
  141. package/dist/components/search-field.d.ts +15 -3
  142. package/dist/components/search-field.js +5 -1
  143. package/dist/components/search-field.js.map +1 -1
  144. package/dist/components/series-chart.d.ts +16 -0
  145. package/dist/components/series-chart.js +4 -1
  146. package/dist/components/series-chart.js.map +1 -1
  147. package/dist/components/settings-fields.d.ts +50 -1
  148. package/dist/components/signed-amount.d.ts +121 -0
  149. package/dist/components/signed-amount.js +152 -0
  150. package/dist/components/signed-amount.js.map +1 -0
  151. package/dist/components/skeleton.d.ts +10 -3
  152. package/dist/components/skeleton.js +12 -2
  153. package/dist/components/skeleton.js.map +1 -1
  154. package/dist/components/stat-tile.d.ts +2 -1
  155. package/dist/components/stat-tile.js +5 -2
  156. package/dist/components/stat-tile.js.map +1 -1
  157. package/dist/components/status-dot.d.ts +6 -2
  158. package/dist/components/status-dot.js +6 -1
  159. package/dist/components/status-dot.js.map +1 -1
  160. package/dist/components/table.d.ts +77 -11
  161. package/dist/components/table.js +85 -17
  162. package/dist/components/table.js.map +1 -1
  163. package/dist/components/text-link.d.ts +176 -0
  164. package/dist/components/text-link.js +109 -0
  165. package/dist/components/text-link.js.map +1 -0
  166. package/dist/components/text.d.ts +8 -1
  167. package/dist/components/text.js +1 -0
  168. package/dist/components/text.js.map +1 -1
  169. package/dist/components/time-input.d.ts +50 -1
  170. package/dist/components/toggle-group.d.ts +28 -0
  171. package/dist/components/toggle-group.js +58 -9
  172. package/dist/components/toggle-group.js.map +1 -1
  173. package/dist/components/toggle-legend.d.ts +36 -3
  174. package/dist/components/toggle-legend.js +49 -0
  175. package/dist/components/toggle-legend.js.map +1 -1
  176. package/dist/components/tooltip.d.ts +11 -3
  177. package/dist/components/tooltip.js +2 -0
  178. package/dist/components/tooltip.js.map +1 -1
  179. package/dist/components/ui.d.ts +52 -729
  180. package/dist/components/ui.js +296 -27
  181. package/dist/components/ui.js.map +1 -1
  182. package/dist/components/use-table-state.d.ts +3 -52
  183. package/dist/components/use-table-state.js +128 -30
  184. package/dist/components/use-table-state.js.map +1 -1
  185. package/dist/components/user-avatar.d.ts +25 -3
  186. package/dist/components/user-avatar.js +32 -3
  187. package/dist/components/user-avatar.js.map +1 -1
  188. package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
  189. package/dist/data-table.d.ts +2 -1
  190. package/dist/data-table.js +5 -1
  191. package/dist/data-table.js.map +1 -1
  192. package/dist/feedback/feedback-dialog.d.ts +26 -3
  193. package/dist/feedback/feedback-dialog.js +16 -2
  194. package/dist/feedback/feedback-dialog.js.map +1 -1
  195. package/dist/feedback/feedback-thread.d.ts +133 -0
  196. package/dist/feedback/feedback-thread.js +237 -0
  197. package/dist/feedback/feedback-thread.js.map +1 -0
  198. package/dist/feedback.d.ts +2 -1
  199. package/dist/feedback.js +1 -0
  200. package/dist/feedback.js.map +1 -1
  201. package/dist/hooks/use-authed-src.d.ts +71 -0
  202. package/dist/hooks/use-authed-src.js +63 -0
  203. package/dist/hooks/use-authed-src.js.map +1 -0
  204. package/dist/hooks/use-file-drop.d.ts +50 -2
  205. package/dist/hooks/use-hotkey.d.ts +59 -0
  206. package/dist/hooks/use-hotkey.js +105 -0
  207. package/dist/hooks/use-hotkey.js.map +1 -0
  208. package/dist/hooks/use-search-param-state.d.ts +83 -0
  209. package/dist/hooks/use-search-param-state.js +94 -0
  210. package/dist/hooks/use-search-param-state.js.map +1 -0
  211. package/dist/i18n/defaults.d.ts +17 -4
  212. package/dist/i18n/defaults.js +33 -1
  213. package/dist/i18n/defaults.js.map +1 -1
  214. package/dist/i18n/kit-labels.d.ts +47 -310
  215. package/dist/i18n/kit-labels.js +20 -5
  216. package/dist/i18n/kit-labels.js.map +1 -1
  217. package/dist/i18n/locales/de-CH-informal.d.ts +17 -4
  218. package/dist/i18n/locales/de-CH.d.ts +17 -4
  219. package/dist/i18n/locales/de-informal.d.ts +17 -4
  220. package/dist/i18n/locales/de.d.ts +17 -4
  221. package/dist/i18n/locales/de.js +165 -4
  222. package/dist/i18n/locales/de.js.map +1 -1
  223. package/dist/i18n/locales/es.d.ts +17 -4
  224. package/dist/i18n/locales/es.js +162 -4
  225. package/dist/i18n/locales/es.js.map +1 -1
  226. package/dist/i18n/locales/fr.d.ts +17 -4
  227. package/dist/i18n/locales/fr.js +162 -4
  228. package/dist/i18n/locales/fr.js.map +1 -1
  229. package/dist/i18n/locales/hu.d.ts +17 -4
  230. package/dist/i18n/locales/hu.js +162 -4
  231. package/dist/i18n/locales/hu.js.map +1 -1
  232. package/dist/i18n/locales/it.d.ts +17 -4
  233. package/dist/i18n/locales/it.js +162 -4
  234. package/dist/i18n/locales/it.js.map +1 -1
  235. package/dist/i18n/locales/zh.d.ts +17 -4
  236. package/dist/i18n/locales/zh.js +162 -4
  237. package/dist/i18n/locales/zh.js.map +1 -1
  238. package/dist/index.d.ts +45 -21
  239. package/dist/index.js +43 -1
  240. package/dist/index.js.map +1 -1
  241. package/dist/kit-labels-v3biUF1L.d.ts +1645 -0
  242. package/dist/lib/clipping.d.ts +21 -5
  243. package/dist/lib/clipping.js +3 -0
  244. package/dist/lib/clipping.js.map +1 -1
  245. package/dist/lib/dates.d.ts +17 -1
  246. package/dist/lib/dates.js +19 -0
  247. package/dist/lib/dates.js.map +1 -1
  248. package/dist/lib/format.d.ts +144 -0
  249. package/dist/lib/format.js +149 -0
  250. package/dist/lib/format.js.map +1 -0
  251. package/dist/lib/qr-encode.d.ts +44 -0
  252. package/dist/lib/qr-encode.js +338 -0
  253. package/dist/lib/qr-encode.js.map +1 -0
  254. package/dist/rhf/fields.d.ts +241 -0
  255. package/dist/rhf/fields.js +601 -0
  256. package/dist/rhf/fields.js.map +1 -0
  257. package/dist/rhf/form.d.ts +50 -1
  258. package/dist/rhf/line-items.d.ts +31 -0
  259. package/dist/rhf/line-items.js +59 -0
  260. package/dist/rhf/line-items.js.map +1 -0
  261. package/dist/rhf/use-rhf-wizard-step.d.ts +22 -0
  262. package/dist/rhf/use-rhf-wizard-step.js +38 -0
  263. package/dist/rhf/use-rhf-wizard-step.js.map +1 -0
  264. package/dist/rhf.d.ts +63 -1
  265. package/dist/rhf.js +3 -0
  266. package/dist/rhf.js.map +1 -1
  267. package/dist/shell/app-shell.d.ts +99 -2
  268. package/dist/shell/app-shell.js +58 -3
  269. package/dist/shell/app-shell.js.map +1 -1
  270. package/dist/shell/auth-layout.d.ts +74 -0
  271. package/dist/shell/auth-layout.js +97 -0
  272. package/dist/shell/auth-layout.js.map +1 -0
  273. package/dist/shell/top-bar-brand.d.ts +81 -0
  274. package/dist/shell/top-bar-brand.js +47 -0
  275. package/dist/shell/top-bar-brand.js.map +1 -0
  276. package/dist/shell/topbar-action-menu.d.ts +84 -3
  277. package/dist/shell/topbar-action-menu.js +115 -27
  278. package/dist/shell/topbar-action-menu.js.map +1 -1
  279. package/dist/shell.d.ts +58 -2
  280. package/dist/shell.js +2 -0
  281. package/dist/shell.js.map +1 -1
  282. package/dist/wizard/stepper-nav.d.ts +49 -1
  283. package/dist/wizard/wizard-context.d.ts +30 -1
  284. package/dist/wizard/wizard-context.js +23 -2
  285. package/dist/wizard/wizard-context.js.map +1 -1
  286. package/dist/wizard/wizard-step.d.ts +20 -5
  287. package/dist/wizard/wizard-step.js +16 -3
  288. package/dist/wizard/wizard-step.js.map +1 -1
  289. package/dist/wizard.d.ts +51 -3
  290. package/package.json +1 -1
  291. package/src/chart.ts +4 -0
  292. package/src/components/account-settings-labels.ts +190 -0
  293. package/src/components/account-settings.tsx +110 -63
  294. package/src/components/alert-banner.tsx +36 -7
  295. package/src/components/amount-input.tsx +13 -1
  296. package/src/components/authed-image.tsx +190 -0
  297. package/src/components/breadcrumbs.tsx +8 -2
  298. package/src/components/bulk-action-bar.tsx +163 -57
  299. package/src/components/button-group.tsx +129 -4
  300. package/src/components/calendar-heatmap.tsx +599 -0
  301. package/src/components/chip.tsx +8 -4
  302. package/src/components/choice-card.tsx +208 -1
  303. package/src/components/combobox-core.tsx +5 -0
  304. package/src/components/combobox.tsx +31 -9
  305. package/src/components/copy-button.tsx +3 -2
  306. package/src/components/data-table-cells.tsx +119 -0
  307. package/src/components/data-table-filters.ts +77 -0
  308. package/src/components/data-table-labels.ts +23 -0
  309. package/src/components/data-table.tsx +231 -13
  310. package/src/components/date-picker.tsx +19 -3
  311. package/src/components/description-list.tsx +52 -3
  312. package/src/components/entity-combobox.tsx +18 -3
  313. package/src/components/error-boundary.tsx +216 -0
  314. package/src/components/field.tsx +188 -0
  315. package/src/components/floating-panel.tsx +380 -16
  316. package/src/components/form-actions.tsx +191 -0
  317. package/src/components/image-grid.tsx +181 -0
  318. package/src/components/lightbox.tsx +374 -0
  319. package/src/components/line-items.tsx +385 -0
  320. package/src/components/list.tsx +93 -18
  321. package/src/components/loading-state.tsx +47 -0
  322. package/src/components/menu-item.tsx +29 -3
  323. package/src/components/mini-calendar.tsx +317 -57
  324. package/src/components/modal.tsx +61 -3
  325. package/src/components/month-picker.tsx +181 -26
  326. package/src/components/multi-entity-combobox.tsx +19 -3
  327. package/src/components/nav-pills.tsx +152 -0
  328. package/src/components/number-field.tsx +15 -10
  329. package/src/components/number-input.tsx +16 -1
  330. package/src/components/passkeys-setting.tsx +331 -0
  331. package/src/components/picker-sheet.tsx +7 -1
  332. package/src/components/pie-chart-labels.ts +32 -0
  333. package/src/components/pie-chart.tsx +479 -0
  334. package/src/components/progress-bar.tsx +241 -13
  335. package/src/components/qr-code.tsx +83 -0
  336. package/src/components/search-field.tsx +20 -4
  337. package/src/components/series-chart.tsx +21 -0
  338. package/src/components/signed-amount.tsx +286 -0
  339. package/src/components/skeleton.tsx +21 -3
  340. package/src/components/stat-tile.tsx +8 -2
  341. package/src/components/status-dot.tsx +10 -2
  342. package/src/components/table.tsx +186 -23
  343. package/src/components/text-link.tsx +248 -0
  344. package/src/components/text.tsx +9 -1
  345. package/src/components/toggle-group.tsx +106 -13
  346. package/src/components/toggle-legend.tsx +87 -2
  347. package/src/components/tooltip.tsx +17 -3
  348. package/src/components/ui.tsx +572 -44
  349. package/src/components/use-table-state.ts +265 -33
  350. package/src/components/user-avatar.tsx +57 -3
  351. package/src/data-table.ts +10 -0
  352. package/src/feedback/feedback-dialog.tsx +42 -3
  353. package/src/feedback/feedback-thread.tsx +434 -0
  354. package/src/feedback.ts +1 -0
  355. package/src/hooks/use-authed-src.ts +161 -0
  356. package/src/hooks/use-hotkey.ts +172 -0
  357. package/src/hooks/use-search-param-state.ts +187 -0
  358. package/src/i18n/defaults.ts +32 -0
  359. package/src/i18n/kit-labels.tsx +104 -8
  360. package/src/i18n/locales/de.ts +162 -0
  361. package/src/i18n/locales/es.ts +159 -0
  362. package/src/i18n/locales/fr.ts +162 -0
  363. package/src/i18n/locales/hu.ts +160 -0
  364. package/src/i18n/locales/it.ts +160 -0
  365. package/src/i18n/locales/zh.ts +159 -0
  366. package/src/index.ts +64 -0
  367. package/src/lib/clipping.ts +22 -4
  368. package/src/lib/dates.ts +37 -0
  369. package/src/lib/format.ts +325 -0
  370. package/src/lib/qr-encode.ts +420 -0
  371. package/src/rhf/fields.tsx +1054 -0
  372. package/src/rhf/line-items.tsx +130 -0
  373. package/src/rhf/use-rhf-wizard-step.ts +113 -0
  374. package/src/rhf.ts +8 -0
  375. package/src/shell/app-shell.tsx +147 -2
  376. package/src/shell/auth-layout.tsx +190 -0
  377. package/src/shell/top-bar-brand.tsx +76 -0
  378. package/src/shell/topbar-action-menu.tsx +260 -49
  379. package/src/shell.ts +2 -0
  380. package/src/wizard/wizard-context.tsx +50 -1
  381. package/src/wizard/wizard-step.tsx +43 -6
  382. package/tokens.css +12 -0
@@ -0,0 +1,133 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { FeedbackNoteAttachment } from './feedback-inbox.js';
4
+ import { FeedbackAttachmentLabels } from './feedback-dialog.js';
5
+ import 'lucide-react';
6
+
7
+ /**
8
+ * The conversation under a report: who said what, when, with what attached — and the
9
+ * box to answer in.
10
+ *
11
+ * Both apps had written it by hand. kastlan's feedback page grew a comment list of
12
+ * bordered boxes with `slate-*` literals (feedback-page.tsx, "keksdose left this
13
+ * unbuilt"), and keksdose built the same thing a second time for support
14
+ * (support-thread.tsx) with sides, bubbles and a composer that clears only what it
15
+ * sent. This is that second one, generalised: the kit owns the look and the
16
+ * behaviour, the app owns the data, the upload and every word it does not share.
17
+ *
18
+ * **Own vs other** is the caller's per message (`own`), not a comparison the kit makes
19
+ * against a user id: whose message sits at the END side depends on who is reading,
20
+ * and only the app knows that (keksdose's support thread labelled a member's question
21
+ * "Du" in the admin panel the first time round, by reading the wrong field).
22
+ *
23
+ * **Redaction.** A thread is somebody's words about their own data — exactly what a
24
+ * host's demo / screen-share mode blurs. Bodies, authors and attachments carry
25
+ * `data-private` (the attribute `Tooltip redact` and `StatTile sensitive` use) unless
26
+ * `redact={false}`.
27
+ *
28
+ * **RTL.** Sides are logical — `justify-end` is the reading end, and the bubble's
29
+ * tucked corner is `rounded-ee` / `rounded-es` — and each body is `dir="auto"`, so a
30
+ * Hebrew reply in an English thread (or the reverse) lays out as its own language.
31
+ */
32
+ /** The `feedbackThread` namespace of `<UiKitProvider labels>`. */
33
+ interface FeedbackThreadLabels {
34
+ /** Accessible name of the message list. */
35
+ thread: string;
36
+ empty: string;
37
+ loading: string;
38
+ /** Author line of an `own` message that has no `author` of its own. */
39
+ you: string;
40
+ /** The badge beside a `staff` author. */
41
+ staff: string;
42
+ /** Accessible name of a message's attachment list. */
43
+ attachments: string;
44
+ }
45
+ declare const DEFAULT_FEEDBACK_THREAD_LABELS: FeedbackThreadLabels;
46
+ /** The `feedbackComposer` namespace of `<UiKitProvider labels>`. The attach buttons'
47
+ * words are `feedbackAttachment`'s, shared with the dialog and the note editor. */
48
+ interface FeedbackComposerLabels {
49
+ /** Accessible name of the text box. */
50
+ field: string;
51
+ placeholder: string;
52
+ send: string;
53
+ /** The shortcut line, given the platform's modifier ("Ctrl" or "⌘"). */
54
+ sendHint: (modifier: string) => string;
55
+ }
56
+ declare const DEFAULT_FEEDBACK_COMPOSER_LABELS: FeedbackComposerLabels;
57
+ interface FeedbackThreadAttachment {
58
+ id?: string | number;
59
+ name: string;
60
+ /** Where to open it. Left out, the attachment is listed by name only. */
61
+ url?: string;
62
+ /** MIME type; an `image/*` with a `url` is shown as a thumbnail. */
63
+ type?: string;
64
+ /** Bytes, shown beside a non-image. */
65
+ size?: number;
66
+ }
67
+ interface FeedbackThreadMessage {
68
+ id: string | number;
69
+ /** Who wrote it. Left out on an `own` message, it reads "You". */
70
+ author?: ReactNode;
71
+ /** A `Date`, an ISO string or epoch ms. */
72
+ createdAt: Date | string | number;
73
+ body: string;
74
+ attachments?: readonly FeedbackThreadAttachment[];
75
+ /** Written by whoever is reading: sits at the end side, in the brand tint. */
76
+ own?: boolean;
77
+ /** Written by the team rather than a user: gets the "Staff" badge. */
78
+ staff?: boolean;
79
+ }
80
+ interface FeedbackThreadProps {
81
+ messages: readonly FeedbackThreadMessage[];
82
+ loading?: boolean;
83
+ /** Replaces the default "No comments yet". */
84
+ empty?: ReactNode;
85
+ /**
86
+ * Draw one attachment yourself — for files behind auth, which a plain `<img src>`
87
+ * cannot fetch (kastlan's `AuthedImage`), or a lightbox. The default is a thumbnail
88
+ * link for an image with a `url`, a file link otherwise.
89
+ */
90
+ renderAttachment?: (attachment: FeedbackThreadAttachment, message: FeedbackThreadMessage) => ReactNode;
91
+ /** Tag bodies, authors and attachments `data-private`. Default true. */
92
+ redact?: boolean;
93
+ /** "Now" for the relative times — pin it in tests and stories. Default: the clock. */
94
+ now?: Date | number;
95
+ /** Overrides the provider's locale for the times. */
96
+ locale?: string;
97
+ labels?: Partial<FeedbackThreadLabels>;
98
+ className?: string;
99
+ }
100
+ declare function FeedbackThread({ messages, loading, empty, renderAttachment, redact, now, locale: localeProp, labels: labelsProp, className, }: FeedbackThreadProps): react.JSX.Element;
101
+ /** What the composer's attach control takes — {@link FeedbackNoteAttachment} with the
102
+ * labels optional, since the field reads `feedbackAttachment` from the provider. */
103
+ type FeedbackComposerAttachment = Omit<FeedbackNoteAttachment, "labels"> & {
104
+ labels?: Partial<FeedbackAttachmentLabels>;
105
+ };
106
+ interface FeedbackComposerProps {
107
+ /**
108
+ * Send the comment. Return the promise: the box clears when it RESOLVES, and only of
109
+ * what was sent — text typed while it was in flight stays. A rejection keeps
110
+ * everything for a retry (surface the error yourself; the composer stays quiet).
111
+ */
112
+ onSend: (body: string, attachment: File | null) => void | Promise<unknown>;
113
+ /** The send is in flight: a spinner on the button and no second send. */
114
+ pending?: boolean;
115
+ /** Offer one picture with the comment (file, paste or `onCaptureScreenshot`).
116
+ * `true` for the defaults. Left out, the composer is text only. */
117
+ attachment?: boolean | FeedbackComposerAttachment;
118
+ /** Rendered INSTEAD of the composer — the thread is closed, or read-only for this
119
+ * reader. */
120
+ disabledReason?: ReactNode;
121
+ rows?: number;
122
+ labels?: Partial<FeedbackComposerLabels>;
123
+ className?: string;
124
+ }
125
+ /**
126
+ * The answer box under a {@link FeedbackThread}: a textarea, an optional picture,
127
+ * and Send — Ctrl/⌘+Enter sends, the shortcut the report dialog and the note editor
128
+ * use, so the habit carries across the feature. Plain Enter is a new line: feedback is
129
+ * written in paragraphs, unlike a chat.
130
+ */
131
+ declare function FeedbackComposer({ onSend, pending, attachment, disabledReason, rows, labels: labelsProp, className, }: FeedbackComposerProps): react.JSX.Element;
132
+
133
+ export { DEFAULT_FEEDBACK_COMPOSER_LABELS, DEFAULT_FEEDBACK_THREAD_LABELS, FeedbackComposer, type FeedbackComposerAttachment, type FeedbackComposerLabels, type FeedbackComposerProps, FeedbackThread, type FeedbackThreadAttachment, type FeedbackThreadLabels, type FeedbackThreadMessage, type FeedbackThreadProps };
@@ -0,0 +1,237 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState, useSyncExternalStore } from "react";
4
+ import { Paperclip, Send } from "lucide-react";
5
+ import { cn } from "../lib/cn.js";
6
+ import { Button, EmptyState, Spinner, Textarea } from "../components/ui.js";
7
+ import { Skeleton } from "../components/skeleton.js";
8
+ import { useKitFileLabels, useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
9
+ import { FeedbackAttachmentField } from "./feedback-attachment.js";
10
+ const DEFAULT_FEEDBACK_THREAD_LABELS = {
11
+ thread: "Comments",
12
+ empty: "No comments yet",
13
+ loading: "Loading comments\u2026",
14
+ you: "You",
15
+ staff: "Staff",
16
+ attachments: "Attachments"
17
+ };
18
+ const DEFAULT_FEEDBACK_COMPOSER_LABELS = {
19
+ field: "Write a comment",
20
+ placeholder: "Write a comment\u2026",
21
+ send: "Send",
22
+ sendHint: (modifier) => `${modifier} + Enter to send`
23
+ };
24
+ function FeedbackThread({
25
+ messages,
26
+ loading = false,
27
+ empty,
28
+ renderAttachment,
29
+ redact = true,
30
+ now,
31
+ locale: localeProp,
32
+ labels: labelsProp,
33
+ className
34
+ }) {
35
+ const labels = useKitLabels("feedbackThread", DEFAULT_FEEDBACK_THREAD_LABELS, labelsProp);
36
+ const locale = useKitLocale(localeProp);
37
+ const file = useKitFileLabels();
38
+ const priv = redact ? "" : void 0;
39
+ const clock = useMinuteClock(now === void 0);
40
+ const reference = now === void 0 ? clock : +now;
41
+ if (loading && messages.length === 0) {
42
+ return /* @__PURE__ */ jsx(Skeleton, { lines: 3, label: labels.loading, className });
43
+ }
44
+ if (messages.length === 0) {
45
+ return /* @__PURE__ */ jsx(
46
+ EmptyState,
47
+ {
48
+ variant: "inline",
49
+ size: "sm",
50
+ title: empty ?? labels.empty,
51
+ className: cn("justify-start", className)
52
+ }
53
+ );
54
+ }
55
+ return /* @__PURE__ */ jsx("ol", { "aria-label": labels.thread, "aria-busy": loading || void 0, className: cn("space-y-3", className), children: messages.map((message) => {
56
+ const date = toDate(message.createdAt);
57
+ const author = message.author ?? (message.own ? labels.you : null);
58
+ const attachments = message.attachments ?? [];
59
+ return /* @__PURE__ */ jsx(
60
+ "li",
61
+ {
62
+ className: cn("flex", message.own ? "justify-end" : "justify-start"),
63
+ "data-own": message.own ? "" : void 0,
64
+ children: /* @__PURE__ */ jsxs(
65
+ "article",
66
+ {
67
+ className: cn(
68
+ "max-w-[85%] min-w-0 rounded-lg px-3 py-2 text-sm",
69
+ message.own ? "rounded-ee-sm bg-[var(--brand-bg)]" : "rounded-es-sm bg-[var(--bg-surface-2)]"
70
+ ),
71
+ children: [
72
+ /* @__PURE__ */ jsxs("header", { className: "mb-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[11px] text-[var(--text-muted)]", children: [
73
+ author != null && /* @__PURE__ */ jsx("span", { "data-private": priv, className: "font-medium text-[var(--text-secondary)]", children: author }),
74
+ message.staff && /* @__PURE__ */ jsx("span", { className: "rounded border border-[var(--border)] bg-[var(--bg-surface)] px-1 text-[10px] font-medium uppercase tracking-wide text-[var(--text-secondary)]", children: labels.staff }),
75
+ date && /* @__PURE__ */ jsx(
76
+ "time",
77
+ {
78
+ dateTime: date.toISOString(),
79
+ title: new Intl.DateTimeFormat(locale, { dateStyle: "full", timeStyle: "short" }).format(date),
80
+ className: "tabular-nums",
81
+ children: formatRelativeTime(date, reference, locale)
82
+ }
83
+ )
84
+ ] }),
85
+ message.body && /* @__PURE__ */ jsx(
86
+ "p",
87
+ {
88
+ "data-private": priv,
89
+ dir: "auto",
90
+ className: "whitespace-pre-wrap break-words text-start text-[var(--text-primary)]",
91
+ children: message.body
92
+ }
93
+ ),
94
+ attachments.length > 0 && /* @__PURE__ */ jsx("ul", { "aria-label": labels.attachments, "data-private": priv, className: "mt-2 flex flex-wrap gap-2", children: attachments.map((attachment, index) => /* @__PURE__ */ jsx("li", { className: "min-w-0", children: renderAttachment ? renderAttachment(attachment, message) : /* @__PURE__ */ jsx(DefaultAttachment, { attachment, size: file.size }) }, attachment.id ?? `${index}-${attachment.name}`)) })
95
+ ]
96
+ }
97
+ )
98
+ },
99
+ message.id
100
+ );
101
+ }) });
102
+ }
103
+ function DefaultAttachment({
104
+ attachment,
105
+ size
106
+ }) {
107
+ const { name, url, type } = attachment;
108
+ if (url && type?.startsWith("image/")) {
109
+ return /* @__PURE__ */ jsx("a", { href: url, target: "_blank", rel: "noopener noreferrer", className: "block", children: /* @__PURE__ */ jsx(
110
+ "img",
111
+ {
112
+ src: url,
113
+ alt: name,
114
+ loading: "lazy",
115
+ className: "h-20 w-20 rounded border border-[var(--border)] object-cover"
116
+ }
117
+ ) });
118
+ }
119
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
120
+ /* @__PURE__ */ jsx(Paperclip, { className: "size-3.5 shrink-0", "aria-hidden": true }),
121
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: name }),
122
+ attachment.size !== void 0 && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[var(--text-muted)]", children: size(attachment.size) })
123
+ ] });
124
+ const chip = "inline-flex max-w-full items-center gap-1 rounded border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1 text-xs text-[var(--text-secondary)]";
125
+ return url ? /* @__PURE__ */ jsx("a", { href: url, target: "_blank", rel: "noopener noreferrer", className: cn(chip, "hover:bg-[var(--bg-hover)]"), children: content }) : /* @__PURE__ */ jsx("span", { className: chip, children: content });
126
+ }
127
+ function useMinuteClock(active) {
128
+ const [time, setTime] = useState(() => Date.now());
129
+ useEffect(() => {
130
+ if (!active) return;
131
+ const id = setInterval(() => setTime(Date.now()), 6e4);
132
+ return () => clearInterval(id);
133
+ }, [active]);
134
+ return time;
135
+ }
136
+ function toDate(value) {
137
+ const date = value instanceof Date ? value : new Date(value);
138
+ return Number.isNaN(date.getTime()) ? null : date;
139
+ }
140
+ const MINUTE = 6e4;
141
+ const HOUR = 60 * MINUTE;
142
+ const DAY = 24 * HOUR;
143
+ function formatRelativeTime(date, now, locale) {
144
+ const diff = date.getTime() - now;
145
+ const abs = Math.abs(diff);
146
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
147
+ if (abs < 45e3) return rtf.format(0, "second");
148
+ if (abs < 45 * MINUTE) return rtf.format(Math.round(diff / MINUTE), "minute");
149
+ if (abs < 22 * HOUR) return rtf.format(Math.round(diff / HOUR), "hour");
150
+ if (abs < 7 * DAY) return rtf.format(Math.round(diff / DAY), "day");
151
+ return new Intl.DateTimeFormat(locale, { dateStyle: "medium" }).format(date);
152
+ }
153
+ const noSubscribe = () => () => {
154
+ };
155
+ const isApple = () => typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/i.test(navigator.platform || navigator.userAgent);
156
+ function FeedbackComposer({
157
+ onSend,
158
+ pending = false,
159
+ attachment,
160
+ disabledReason,
161
+ rows = 3,
162
+ labels: labelsProp,
163
+ className
164
+ }) {
165
+ const labels = useKitLabels("feedbackComposer", DEFAULT_FEEDBACK_COMPOSER_LABELS, labelsProp);
166
+ const [draft, setDraft] = useState("");
167
+ const [file, setFile] = useState(null);
168
+ const root = useRef(null);
169
+ const modifier = useSyncExternalStore(
170
+ noSubscribe,
171
+ () => isApple() ? "\u2318" : "Ctrl",
172
+ () => "Ctrl"
173
+ );
174
+ if (disabledReason != null && disabledReason !== false) {
175
+ return /* @__PURE__ */ jsx("p", { className: cn("text-xs text-[var(--text-muted)]", className), children: disabledReason });
176
+ }
177
+ const canSend = draft.trim().length > 0 && !pending;
178
+ const send = () => {
179
+ if (!canSend) return;
180
+ const sentDraft = draft;
181
+ const sentFile = file;
182
+ Promise.resolve(onSend(draft.trim(), file)).then(() => {
183
+ setDraft((current) => current === sentDraft ? "" : current);
184
+ setFile((current) => current === sentFile ? null : current);
185
+ }).catch(() => {
186
+ });
187
+ };
188
+ const config = attachment === true ? {} : attachment || null;
189
+ return /* @__PURE__ */ jsxs("div", { ref: root, className: cn("space-y-2", className), children: [
190
+ /* @__PURE__ */ jsx(
191
+ Textarea,
192
+ {
193
+ rows,
194
+ dir: "auto",
195
+ "aria-label": labels.field,
196
+ placeholder: labels.placeholder,
197
+ value: draft,
198
+ onChange: (event) => setDraft(event.target.value),
199
+ onKeyDown: (event) => {
200
+ if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
201
+ event.preventDefault();
202
+ send();
203
+ }
204
+ }
205
+ }
206
+ ),
207
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-end justify-between gap-2", children: [
208
+ config ? /* @__PURE__ */ jsx(
209
+ FeedbackAttachmentField,
210
+ {
211
+ value: file,
212
+ onChange: setFile,
213
+ labels: config.labels,
214
+ accept: config.accept,
215
+ maxBytes: config.maxBytes,
216
+ onError: config.onError,
217
+ onCaptureScreenshot: config.onCaptureScreenshot,
218
+ pasteFrom: root
219
+ }
220
+ ) : /* @__PURE__ */ jsx("span", {}),
221
+ /* @__PURE__ */ jsxs("div", { className: "ms-auto flex items-center gap-2", children: [
222
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-[var(--text-placeholder)]", children: labels.sendHint(modifier) }),
223
+ /* @__PURE__ */ jsxs(Button, { onClick: send, disabled: !canSend, "aria-busy": pending || void 0, children: [
224
+ pending ? /* @__PURE__ */ jsx(Spinner, { label: null, className: "size-4" }) : /* @__PURE__ */ jsx(Send, { className: "size-4 rtl:-scale-x-100", "aria-hidden": true }),
225
+ labels.send
226
+ ] })
227
+ ] })
228
+ ] })
229
+ ] });
230
+ }
231
+ export {
232
+ DEFAULT_FEEDBACK_COMPOSER_LABELS,
233
+ DEFAULT_FEEDBACK_THREAD_LABELS,
234
+ FeedbackComposer,
235
+ FeedbackThread
236
+ };
237
+ //# sourceMappingURL=feedback-thread.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/feedback/feedback-thread.tsx"],"sourcesContent":["import { useEffect, useRef, useState, useSyncExternalStore } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Paperclip, Send } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { Button, EmptyState, Spinner, Textarea } from \"../components/ui\";\nimport { Skeleton } from \"../components/skeleton\";\nimport { useKitFileLabels, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport { FeedbackAttachmentField } from \"./feedback-attachment\";\nimport type { FeedbackNoteAttachment } from \"./feedback-inbox\";\nimport type { FeedbackAttachmentLabels } from \"./feedback-dialog\";\n\n/**\n * The conversation under a report: who said what, when, with what attached — and the\n * box to answer in.\n *\n * Both apps had written it by hand. kastlan's feedback page grew a comment list of\n * bordered boxes with `slate-*` literals (feedback-page.tsx, \"keksdose left this\n * unbuilt\"), and keksdose built the same thing a second time for support\n * (support-thread.tsx) with sides, bubbles and a composer that clears only what it\n * sent. This is that second one, generalised: the kit owns the look and the\n * behaviour, the app owns the data, the upload and every word it does not share.\n *\n * **Own vs other** is the caller's per message (`own`), not a comparison the kit makes\n * against a user id: whose message sits at the END side depends on who is reading,\n * and only the app knows that (keksdose's support thread labelled a member's question\n * \"Du\" in the admin panel the first time round, by reading the wrong field).\n *\n * **Redaction.** A thread is somebody's words about their own data — exactly what a\n * host's demo / screen-share mode blurs. Bodies, authors and attachments carry\n * `data-private` (the attribute `Tooltip redact` and `StatTile sensitive` use) unless\n * `redact={false}`.\n *\n * **RTL.** Sides are logical — `justify-end` is the reading end, and the bubble's\n * tucked corner is `rounded-ee` / `rounded-es` — and each body is `dir=\"auto\"`, so a\n * Hebrew reply in an English thread (or the reverse) lays out as its own language.\n */\n\n/* ── Labels ───────────────────────────────────────────────────────────────── */\n\n/** The `feedbackThread` namespace of `<UiKitProvider labels>`. */\nexport interface FeedbackThreadLabels {\n /** Accessible name of the message list. */\n thread: string;\n empty: string;\n loading: string;\n /** Author line of an `own` message that has no `author` of its own. */\n you: string;\n /** The badge beside a `staff` author. */\n staff: string;\n /** Accessible name of a message's attachment list. */\n attachments: string;\n}\n\nexport const DEFAULT_FEEDBACK_THREAD_LABELS: FeedbackThreadLabels = {\n thread: \"Comments\",\n empty: \"No comments yet\",\n loading: \"Loading comments…\",\n you: \"You\",\n staff: \"Staff\",\n attachments: \"Attachments\",\n};\n\n/** The `feedbackComposer` namespace of `<UiKitProvider labels>`. The attach buttons'\n * words are `feedbackAttachment`'s, shared with the dialog and the note editor. */\nexport interface FeedbackComposerLabels {\n /** Accessible name of the text box. */\n field: string;\n placeholder: string;\n send: string;\n /** The shortcut line, given the platform's modifier (\"Ctrl\" or \"⌘\"). */\n sendHint: (modifier: string) => string;\n}\n\nexport const DEFAULT_FEEDBACK_COMPOSER_LABELS: FeedbackComposerLabels = {\n field: \"Write a comment\",\n placeholder: \"Write a comment…\",\n send: \"Send\",\n sendHint: (modifier) => `${modifier} + Enter to send`,\n};\n\n/* ── Thread ───────────────────────────────────────────────────────────────── */\n\nexport interface FeedbackThreadAttachment {\n id?: string | number;\n name: string;\n /** Where to open it. Left out, the attachment is listed by name only. */\n url?: string;\n /** MIME type; an `image/*` with a `url` is shown as a thumbnail. */\n type?: string;\n /** Bytes, shown beside a non-image. */\n size?: number;\n}\n\nexport interface FeedbackThreadMessage {\n id: string | number;\n /** Who wrote it. Left out on an `own` message, it reads \"You\". */\n author?: ReactNode;\n /** A `Date`, an ISO string or epoch ms. */\n createdAt: Date | string | number;\n body: string;\n attachments?: readonly FeedbackThreadAttachment[];\n /** Written by whoever is reading: sits at the end side, in the brand tint. */\n own?: boolean;\n /** Written by the team rather than a user: gets the \"Staff\" badge. */\n staff?: boolean;\n}\n\nexport interface FeedbackThreadProps {\n messages: readonly FeedbackThreadMessage[];\n loading?: boolean;\n /** Replaces the default \"No comments yet\". */\n empty?: ReactNode;\n /**\n * Draw one attachment yourself — for files behind auth, which a plain `<img src>`\n * cannot fetch (kastlan's `AuthedImage`), or a lightbox. The default is a thumbnail\n * link for an image with a `url`, a file link otherwise.\n */\n renderAttachment?: (attachment: FeedbackThreadAttachment, message: FeedbackThreadMessage) => ReactNode;\n /** Tag bodies, authors and attachments `data-private`. Default true. */\n redact?: boolean;\n /** \"Now\" for the relative times — pin it in tests and stories. Default: the clock. */\n now?: Date | number;\n /** Overrides the provider's locale for the times. */\n locale?: string;\n labels?: Partial<FeedbackThreadLabels>;\n className?: string;\n}\n\nexport function FeedbackThread({\n messages,\n loading = false,\n empty,\n renderAttachment,\n redact = true,\n now,\n locale: localeProp,\n labels: labelsProp,\n className,\n}: FeedbackThreadProps) {\n const labels = useKitLabels(\"feedbackThread\", DEFAULT_FEEDBACK_THREAD_LABELS, labelsProp);\n const locale = useKitLocale(localeProp);\n const file = useKitFileLabels();\n const priv = redact ? \"\" : undefined;\n const clock = useMinuteClock(now === undefined);\n const reference = now === undefined ? clock : +now;\n\n if (loading && messages.length === 0) {\n return <Skeleton lines={3} label={labels.loading} className={className} />;\n }\n if (messages.length === 0) {\n return (\n <EmptyState\n variant=\"inline\"\n size=\"sm\"\n title={empty ?? labels.empty}\n className={cn(\"justify-start\", className)}\n />\n );\n }\n\n return (\n <ol aria-label={labels.thread} aria-busy={loading || undefined} className={cn(\"space-y-3\", className)}>\n {messages.map((message) => {\n const date = toDate(message.createdAt);\n const author = message.author ?? (message.own ? labels.you : null);\n const attachments = message.attachments ?? [];\n return (\n <li\n key={message.id}\n className={cn(\"flex\", message.own ? \"justify-end\" : \"justify-start\")}\n data-own={message.own ? \"\" : undefined}\n >\n <article\n className={cn(\n \"max-w-[85%] min-w-0 rounded-lg px-3 py-2 text-sm\",\n message.own\n ? \"rounded-ee-sm bg-[var(--brand-bg)]\"\n : \"rounded-es-sm bg-[var(--bg-surface-2)]\",\n )}\n >\n <header className=\"mb-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[11px] text-[var(--text-muted)]\">\n {author != null && (\n <span data-private={priv} className=\"font-medium text-[var(--text-secondary)]\">\n {author}\n </span>\n )}\n {message.staff && (\n <span className=\"rounded border border-[var(--border)] bg-[var(--bg-surface)] px-1 text-[10px] font-medium uppercase tracking-wide text-[var(--text-secondary)]\">\n {labels.staff}\n </span>\n )}\n {date && (\n <time\n dateTime={date.toISOString()}\n title={new Intl.DateTimeFormat(locale, { dateStyle: \"full\", timeStyle: \"short\" }).format(date)}\n className=\"tabular-nums\"\n >\n {formatRelativeTime(date, reference, locale)}\n </time>\n )}\n </header>\n {message.body && (\n <p\n data-private={priv}\n dir=\"auto\"\n className=\"whitespace-pre-wrap break-words text-start text-[var(--text-primary)]\"\n >\n {message.body}\n </p>\n )}\n {attachments.length > 0 && (\n <ul aria-label={labels.attachments} data-private={priv} className=\"mt-2 flex flex-wrap gap-2\">\n {attachments.map((attachment, index) => (\n <li key={attachment.id ?? `${index}-${attachment.name}`} className=\"min-w-0\">\n {renderAttachment ? (\n renderAttachment(attachment, message)\n ) : (\n <DefaultAttachment attachment={attachment} size={file.size} />\n )}\n </li>\n ))}\n </ul>\n )}\n </article>\n </li>\n );\n })}\n </ol>\n );\n}\n\nfunction DefaultAttachment({\n attachment,\n size,\n}: {\n attachment: FeedbackThreadAttachment;\n size: (bytes: number) => string;\n}) {\n const { name, url, type } = attachment;\n if (url && type?.startsWith(\"image/\")) {\n return (\n <a href={url} target=\"_blank\" rel=\"noopener noreferrer\" className=\"block\">\n <img\n src={url}\n alt={name}\n loading=\"lazy\"\n className=\"h-20 w-20 rounded border border-[var(--border)] object-cover\"\n />\n </a>\n );\n }\n const content = (\n <>\n <Paperclip className=\"size-3.5 shrink-0\" aria-hidden />\n <span className=\"truncate\">{name}</span>\n {attachment.size !== undefined && (\n <span className=\"shrink-0 text-[var(--text-muted)]\">{size(attachment.size)}</span>\n )}\n </>\n );\n const chip =\n \"inline-flex max-w-full items-center gap-1 rounded border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1 text-xs text-[var(--text-secondary)]\";\n return url ? (\n <a href={url} target=\"_blank\" rel=\"noopener noreferrer\" className={cn(chip, \"hover:bg-[var(--bg-hover)]\")}>\n {content}\n </a>\n ) : (\n <span className={chip}>{content}</span>\n );\n}\n\n/** The time, refreshed once a minute while `active`, so \"2 minutes ago\" does not stay\n * \"just now\" on a page left open. Read in state rather than in render, which must be\n * pure. */\nfunction useMinuteClock(active: boolean): number {\n const [time, setTime] = useState(() => Date.now());\n useEffect(() => {\n if (!active) return;\n const id = setInterval(() => setTime(Date.now()), 60_000);\n return () => clearInterval(id);\n }, [active]);\n return time;\n}\n\nfunction toDate(value: Date | string | number): Date | null {\n const date = value instanceof Date ? value : new Date(value);\n return Number.isNaN(date.getTime()) ? null : date;\n}\n\nconst MINUTE = 60_000;\nconst HOUR = 60 * MINUTE;\nconst DAY = 24 * HOUR;\n\n/**\n * \"just now\", \"5 minutes ago\", \"yesterday\", \"3 days ago\" — `Intl.RelativeTimeFormat`\n * in `locale`, so the grammar is the language's. Past a week it is a date: \"12 days\n * ago\" makes the reader do arithmetic that the date already did. The full date and\n * time are on the `<time>`'s title.\n */\nfunction formatRelativeTime(date: Date, now: number, locale: string | undefined): string {\n const diff = date.getTime() - now;\n const abs = Math.abs(diff);\n const rtf = new Intl.RelativeTimeFormat(locale, { numeric: \"auto\" });\n if (abs < 45_000) return rtf.format(0, \"second\");\n if (abs < 45 * MINUTE) return rtf.format(Math.round(diff / MINUTE), \"minute\");\n if (abs < 22 * HOUR) return rtf.format(Math.round(diff / HOUR), \"hour\");\n if (abs < 7 * DAY) return rtf.format(Math.round(diff / DAY), \"day\");\n return new Intl.DateTimeFormat(locale, { dateStyle: \"medium\" }).format(date);\n}\n\n/* ── Composer ─────────────────────────────────────────────────────────────── */\n\n/** What the composer's attach control takes — {@link FeedbackNoteAttachment} with the\n * labels optional, since the field reads `feedbackAttachment` from the provider. */\nexport type FeedbackComposerAttachment = Omit<FeedbackNoteAttachment, \"labels\"> & {\n labels?: Partial<FeedbackAttachmentLabels>;\n};\n\nexport interface FeedbackComposerProps {\n /**\n * Send the comment. Return the promise: the box clears when it RESOLVES, and only of\n * what was sent — text typed while it was in flight stays. A rejection keeps\n * everything for a retry (surface the error yourself; the composer stays quiet).\n */\n onSend: (body: string, attachment: File | null) => void | Promise<unknown>;\n /** The send is in flight: a spinner on the button and no second send. */\n pending?: boolean;\n /** Offer one picture with the comment (file, paste or `onCaptureScreenshot`).\n * `true` for the defaults. Left out, the composer is text only. */\n attachment?: boolean | FeedbackComposerAttachment;\n /** Rendered INSTEAD of the composer — the thread is closed, or read-only for this\n * reader. */\n disabledReason?: ReactNode;\n rows?: number;\n labels?: Partial<FeedbackComposerLabels>;\n className?: string;\n}\n\nconst noSubscribe = () => () => {};\nconst isApple = () =>\n typeof navigator !== \"undefined\" && /Mac|iPhone|iPad|iPod/i.test(navigator.platform || navigator.userAgent);\n\n/**\n * The answer box under a {@link FeedbackThread}: a textarea, an optional picture,\n * and Send — Ctrl/⌘+Enter sends, the shortcut the report dialog and the note editor\n * use, so the habit carries across the feature. Plain Enter is a new line: feedback is\n * written in paragraphs, unlike a chat.\n */\nexport function FeedbackComposer({\n onSend,\n pending = false,\n attachment,\n disabledReason,\n rows = 3,\n labels: labelsProp,\n className,\n}: FeedbackComposerProps) {\n const labels = useKitLabels(\"feedbackComposer\", DEFAULT_FEEDBACK_COMPOSER_LABELS, labelsProp);\n const [draft, setDraft] = useState(\"\");\n const [file, setFile] = useState<File | null>(null);\n const root = useRef<HTMLDivElement>(null);\n // The server snapshot is \"Ctrl\"; the client corrects it after hydration.\n const modifier = useSyncExternalStore(\n noSubscribe,\n () => (isApple() ? \"⌘\" : \"Ctrl\"),\n () => \"Ctrl\",\n );\n\n if (disabledReason != null && disabledReason !== false) {\n return <p className={cn(\"text-xs text-[var(--text-muted)]\", className)}>{disabledReason}</p>;\n }\n\n const canSend = draft.trim().length > 0 && !pending;\n const send = () => {\n if (!canSend) return;\n const sentDraft = draft;\n const sentFile = file;\n Promise.resolve(onSend(draft.trim(), file))\n .then(() => {\n setDraft((current) => (current === sentDraft ? \"\" : current));\n setFile((current) => (current === sentFile ? null : current));\n })\n .catch(() => {});\n };\n\n const config = attachment === true ? {} : attachment || null;\n\n return (\n <div ref={root} className={cn(\"space-y-2\", className)}>\n <Textarea\n rows={rows}\n dir=\"auto\"\n aria-label={labels.field}\n placeholder={labels.placeholder}\n value={draft}\n onChange={(event) => setDraft(event.target.value)}\n onKeyDown={(event) => {\n if ((event.ctrlKey || event.metaKey) && event.key === \"Enter\") {\n event.preventDefault();\n send();\n }\n }}\n />\n <div className=\"flex flex-wrap items-end justify-between gap-2\">\n {config ? (\n <FeedbackAttachmentField\n value={file}\n onChange={setFile}\n labels={config.labels}\n accept={config.accept}\n maxBytes={config.maxBytes}\n onError={config.onError}\n onCaptureScreenshot={config.onCaptureScreenshot}\n // The box is this field's sibling; a paste in it bubbles to `root`.\n pasteFrom={root}\n />\n ) : (\n <span />\n )}\n <div className=\"ms-auto flex items-center gap-2\">\n <span className=\"text-xs text-[var(--text-placeholder)]\">{labels.sendHint(modifier)}</span>\n <Button onClick={send} disabled={!canSend} aria-busy={pending || undefined}>\n {pending ? (\n <Spinner label={null} className=\"size-4\" />\n ) : (\n <Send className=\"size-4 rtl:-scale-x-100\" aria-hidden />\n )}\n {labels.send}\n </Button>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,5 +1,6 @@
1
1
  export { DEFAULT_ATTACHMENT_ACCEPT, DEFAULT_FEEDBACK_ATTACHMENT_LABELS, DEFAULT_MAX_ATTACHMENT_BYTES, FeedbackAttachmentField, FeedbackAttachmentFieldLabels, pastedName } from './feedback/feedback-attachment.js';
2
- export { FeedbackAttachmentLabels, FeedbackCategoryOption, FeedbackDialog, FeedbackDialogLabels, FeedbackSubmission } from './feedback/feedback-dialog.js';
2
+ export { DEFAULT_FEEDBACK_DIALOG_LABELS, FeedbackAttachmentLabels, FeedbackCategoryOption, FeedbackDialog, FeedbackDialogLabels, FeedbackDialogTextLabels, FeedbackSubmission } from './feedback/feedback-dialog.js';
3
3
  export { FEEDBACK_CATEGORY_META, FEEDBACK_CATEGORY_ORDER, FEEDBACK_STATUS_META, FEEDBACK_STATUS_ORDER, FeedbackCategory, FeedbackCategoryBadge, FeedbackDetail, FeedbackDetailSection, FeedbackNoteAttachment, FeedbackNoteEditor, FeedbackProse, FeedbackStatus, FeedbackStatusBadge, FeedbackStatusTransitions, feedbackCategoryRank, nextFeedbackStatus, selectableFeedbackStatuses, visibleFeedbackStatuses } from './feedback/feedback-inbox.js';
4
+ export { DEFAULT_FEEDBACK_COMPOSER_LABELS, DEFAULT_FEEDBACK_THREAD_LABELS, FeedbackComposer, FeedbackComposerAttachment, FeedbackComposerLabels, FeedbackComposerProps, FeedbackThread, FeedbackThreadAttachment, FeedbackThreadLabels, FeedbackThreadMessage, FeedbackThreadProps } from './feedback/feedback-thread.js';
4
5
  import 'react';
5
6
  import 'lucide-react';
package/dist/feedback.js CHANGED
@@ -2,4 +2,5 @@
2
2
  export * from "./feedback/feedback-attachment.js";
3
3
  export * from "./feedback/feedback-dialog.js";
4
4
  export * from "./feedback/feedback-inbox.js";
5
+ export * from "./feedback/feedback-thread.js";
5
6
  //# sourceMappingURL=feedback.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/feedback.ts"],"sourcesContent":["// `@eifi1/ui-kit/feedback` — the report form and the parts an inbox is built from.\n//\n// A re-slicing of the main barrel, not a new API. The kit owns the status vocabulary,\n// the transition policy and the look; each app still wires its own API, columns,\n// strings and permissions — see the note at the top of feedback/feedback-inbox.tsx.\nexport * from \"./feedback/feedback-attachment\";\nexport * from \"./feedback/feedback-dialog\";\nexport * from \"./feedback/feedback-inbox\";\n"],"mappings":";AAKA,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/feedback.ts"],"sourcesContent":["// `@eifi1/ui-kit/feedback` — the report form and the parts an inbox is built from.\n//\n// A re-slicing of the main barrel, not a new API. The kit owns the status vocabulary,\n// the transition policy and the look; each app still wires its own API, columns,\n// strings and permissions — see the note at the top of feedback/feedback-inbox.tsx.\nexport * from \"./feedback/feedback-attachment\";\nexport * from \"./feedback/feedback-dialog\";\nexport * from \"./feedback/feedback-inbox\";\nexport * from \"./feedback/feedback-thread\";\n"],"mappings":";AAKA,cAAc;AACd,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Fetch a file with the app's own credentials and hand back an object URL for it.
3
+ *
4
+ * A plain `<img src="/api/v1/…">` cannot carry a Bearer token, so the browser gets a
5
+ * 401 and the image — and any "open it" link built on the same path — fails. Both apps
6
+ * solved this once each, by hand: kastlan's `shared/components/authed-image.tsx` (ported
7
+ * from keksdose) and its `useDocumentBlobUrl` for floor plans. This is that code, once,
8
+ * with the two things neither copy did: a loading/error state a caller can render, and
9
+ * an abort when the URL changes mid-flight (a stale response used to overwrite the new
10
+ * one's object URL — and leak the old one, since nothing revoked it).
11
+ *
12
+ * The kit does no auth of its own: the caller hands over a {@link AuthedFetcher} — its
13
+ * axios client, its `fetch` with the header — and the kit only manages the blob's life:
14
+ *
15
+ * - created when the bytes arrive,
16
+ * - revoked when the URL changes, the fetcher reports an error, or the component
17
+ * unmounts — so a long gallery session does not accumulate blobs,
18
+ * - never exposed as a token-bearing URL: the object URL is `blob:` and local.
19
+ */
20
+ /**
21
+ * The app's authenticated GET. Resolve to the file's bytes: a `Blob` (axios with
22
+ * `responseType: "blob"`), or a `Response` (plain `fetch`), whose `ok` is checked here.
23
+ *
24
+ * Honour `signal` if you can — a thumbnail scrolled past, or a lightbox paged on, then
25
+ * stops downloading. Ignoring it is safe; the result of a superseded call is discarded
26
+ * either way.
27
+ */
28
+ type AuthedFetcher = (url: string, init: {
29
+ signal: AbortSignal;
30
+ }) => Promise<Blob | Response>;
31
+ /** `idle`: no URL. `loading`: the fetch is in flight. `ready`: `src` is usable.
32
+ * `error`: the fetch failed (see `error`). */
33
+ type AuthedSrcStatus = "idle" | "loading" | "ready" | "error";
34
+ interface UseAuthedSrcOptions {
35
+ /**
36
+ * How to fetch. Left out (or `null`), the URL is used as it is — `src` is the URL and
37
+ * the status `ready` at once — so a component that takes an OPTIONAL fetcher (the
38
+ * image grid, the lightbox) has one code path for public and protected images.
39
+ */
40
+ fetcher?: AuthedFetcher | null;
41
+ /** `false` holds the fetch — a lightbox slide that is not on screen. Default `true`. */
42
+ enabled?: boolean;
43
+ }
44
+ interface UseAuthedSrcResult {
45
+ /** The object URL (or, without a fetcher, the URL itself); `null` until ready. */
46
+ src: string | null;
47
+ status: AuthedSrcStatus;
48
+ /** What the fetcher threw, when `status` is `error`. */
49
+ error: unknown;
50
+ /** The blob's MIME type, once fetched (`""` when the server sent none). `undefined`
51
+ * without a fetcher — nothing was fetched to ask. Lets a caller that does not know
52
+ * whether a URL is an image or a PDF find out. */
53
+ type: string | undefined;
54
+ /** Fetch again — after an `error`, or to pick up a changed file at the same URL. */
55
+ retry: () => void;
56
+ }
57
+ /**
58
+ * `useAuthedSrc(url, { fetcher })` — see the module note.
59
+ *
60
+ * ```tsx
61
+ * const fetcher: AuthedFetcher = (url, { signal }) =>
62
+ * apiClient.get(url, { responseType: "blob", signal }).then((r) => r.data);
63
+ * const { src, status } = useAuthedSrc(photo.file_path, { fetcher });
64
+ * ```
65
+ *
66
+ * The fetcher is read through a ref, so an inline arrow does not refetch on every
67
+ * render; a change of `url` (or `retry`) does.
68
+ */
69
+ declare function useAuthedSrc(url: string | null | undefined, { fetcher, enabled }?: UseAuthedSrcOptions): UseAuthedSrcResult;
70
+
71
+ export { type AuthedFetcher, type AuthedSrcStatus, type UseAuthedSrcOptions, type UseAuthedSrcResult, useAuthedSrc };
@@ -0,0 +1,63 @@
1
+ "use client";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
+ async function toBlob(res) {
4
+ if (typeof Response !== "undefined" && res instanceof Response) {
5
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
6
+ return res.blob();
7
+ }
8
+ return res;
9
+ }
10
+ function useAuthedSrc(url, { fetcher, enabled = true } = {}) {
11
+ const fetcherRef = useRef(fetcher);
12
+ useEffect(() => {
13
+ fetcherRef.current = fetcher;
14
+ });
15
+ const hasFetcher = !!fetcher;
16
+ const [attempt, setAttempt] = useState(0);
17
+ const key = url && enabled && hasFetcher ? `${attempt}|${url}` : null;
18
+ const [state, setState] = useState({
19
+ key: null,
20
+ src: null,
21
+ status: "idle",
22
+ error: void 0,
23
+ type: void 0
24
+ });
25
+ useEffect(() => {
26
+ const fetchFn = fetcherRef.current;
27
+ if (!key || !url || !fetchFn) return;
28
+ const controller = new AbortController();
29
+ let created = null;
30
+ let active = true;
31
+ let settled = false;
32
+ fetchFn(url, { signal: controller.signal }).then(toBlob).then((blob) => {
33
+ settled = true;
34
+ if (!active) return;
35
+ created = URL.createObjectURL(blob);
36
+ setState({ key, src: created, status: "ready", error: void 0, type: blob.type });
37
+ }).catch((error) => {
38
+ settled = true;
39
+ if (!active) return;
40
+ setState({ key, src: null, status: "error", error, type: void 0 });
41
+ });
42
+ return () => {
43
+ active = false;
44
+ if (!settled) controller.abort();
45
+ if (created) URL.revokeObjectURL(created);
46
+ };
47
+ }, [key, url]);
48
+ const retry = useCallback(() => setAttempt((n) => n + 1), []);
49
+ if (!url || !enabled) {
50
+ return { src: null, status: "idle", error: void 0, type: void 0, retry };
51
+ }
52
+ if (!hasFetcher) {
53
+ return { src: url, status: "ready", error: void 0, type: void 0, retry };
54
+ }
55
+ if (state.key !== key) {
56
+ return { src: null, status: "loading", error: void 0, type: void 0, retry };
57
+ }
58
+ return { src: state.src, status: state.status, error: state.error, type: state.type, retry };
59
+ }
60
+ export {
61
+ useAuthedSrc
62
+ };
63
+ //# sourceMappingURL=use-authed-src.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/use-authed-src.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from \"react\";\n\n/**\n * Fetch a file with the app's own credentials and hand back an object URL for it.\n *\n * A plain `<img src=\"/api/v1/…\">` cannot carry a Bearer token, so the browser gets a\n * 401 and the image — and any \"open it\" link built on the same path — fails. Both apps\n * solved this once each, by hand: kastlan's `shared/components/authed-image.tsx` (ported\n * from keksdose) and its `useDocumentBlobUrl` for floor plans. This is that code, once,\n * with the two things neither copy did: a loading/error state a caller can render, and\n * an abort when the URL changes mid-flight (a stale response used to overwrite the new\n * one's object URL — and leak the old one, since nothing revoked it).\n *\n * The kit does no auth of its own: the caller hands over a {@link AuthedFetcher} — its\n * axios client, its `fetch` with the header — and the kit only manages the blob's life:\n *\n * - created when the bytes arrive,\n * - revoked when the URL changes, the fetcher reports an error, or the component\n * unmounts — so a long gallery session does not accumulate blobs,\n * - never exposed as a token-bearing URL: the object URL is `blob:` and local.\n */\n\n/**\n * The app's authenticated GET. Resolve to the file's bytes: a `Blob` (axios with\n * `responseType: \"blob\"`), or a `Response` (plain `fetch`), whose `ok` is checked here.\n *\n * Honour `signal` if you can — a thumbnail scrolled past, or a lightbox paged on, then\n * stops downloading. Ignoring it is safe; the result of a superseded call is discarded\n * either way.\n */\nexport type AuthedFetcher = (url: string, init: { signal: AbortSignal }) => Promise<Blob | Response>;\n\n/** `idle`: no URL. `loading`: the fetch is in flight. `ready`: `src` is usable.\n * `error`: the fetch failed (see `error`). */\nexport type AuthedSrcStatus = \"idle\" | \"loading\" | \"ready\" | \"error\";\n\nexport interface UseAuthedSrcOptions {\n /**\n * How to fetch. Left out (or `null`), the URL is used as it is — `src` is the URL and\n * the status `ready` at once — so a component that takes an OPTIONAL fetcher (the\n * image grid, the lightbox) has one code path for public and protected images.\n */\n fetcher?: AuthedFetcher | null;\n /** `false` holds the fetch — a lightbox slide that is not on screen. Default `true`. */\n enabled?: boolean;\n}\n\nexport interface UseAuthedSrcResult {\n /** The object URL (or, without a fetcher, the URL itself); `null` until ready. */\n src: string | null;\n status: AuthedSrcStatus;\n /** What the fetcher threw, when `status` is `error`. */\n error: unknown;\n /** The blob's MIME type, once fetched (`\"\"` when the server sent none). `undefined`\n * without a fetcher — nothing was fetched to ask. Lets a caller that does not know\n * whether a URL is an image or a PDF find out. */\n type: string | undefined;\n /** Fetch again — after an `error`, or to pick up a changed file at the same URL. */\n retry: () => void;\n}\n\ninterface State {\n /** Which request this state belongs to — see the effect. */\n key: string | null;\n src: string | null;\n status: AuthedSrcStatus;\n error: unknown;\n type: string | undefined;\n}\n\nasync function toBlob(res: Blob | Response): Promise<Blob> {\n if (typeof Response !== \"undefined\" && res instanceof Response) {\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n return res.blob();\n }\n return res as Blob;\n}\n\n/**\n * `useAuthedSrc(url, { fetcher })` — see the module note.\n *\n * ```tsx\n * const fetcher: AuthedFetcher = (url, { signal }) =>\n * apiClient.get(url, { responseType: \"blob\", signal }).then((r) => r.data);\n * const { src, status } = useAuthedSrc(photo.file_path, { fetcher });\n * ```\n *\n * The fetcher is read through a ref, so an inline arrow does not refetch on every\n * render; a change of `url` (or `retry`) does.\n */\nexport function useAuthedSrc(\n url: string | null | undefined,\n { fetcher, enabled = true }: UseAuthedSrcOptions = {},\n): UseAuthedSrcResult {\n const fetcherRef = useRef(fetcher);\n useEffect(() => {\n fetcherRef.current = fetcher;\n });\n const hasFetcher = !!fetcher;\n const [attempt, setAttempt] = useState(0);\n const key = url && enabled && hasFetcher ? `${attempt}|${url}` : null;\n const [state, setState] = useState<State>({\n key: null,\n src: null,\n status: \"idle\",\n error: undefined,\n type: undefined,\n });\n\n useEffect(() => {\n const fetchFn = fetcherRef.current;\n if (!key || !url || !fetchFn) return;\n const controller = new AbortController();\n let created: string | null = null;\n let active = true;\n // Settled: the response is in (or failed). Aborting then would signal a request\n // that is long over — a fetcher that listens for `abort` (to log, to cancel a\n // retry it scheduled, to count cancellations) would see a cancellation that\n // never happened on every URL change and unmount.\n let settled = false;\n // No \"loading\" state is set here: a key the state does not describe yet already\n // reads as loading (see the end of the hook).\n fetchFn(url, { signal: controller.signal })\n .then(toBlob)\n .then((blob) => {\n settled = true;\n if (!active) return;\n created = URL.createObjectURL(blob);\n setState({ key, src: created, status: \"ready\", error: undefined, type: blob.type });\n })\n .catch((error: unknown) => {\n settled = true;\n if (!active) return;\n setState({ key, src: null, status: \"error\", error, type: undefined });\n });\n return () => {\n // `active` first: an in-flight response that lands after this must neither set\n // state on an unmounted component nor create a blob nobody will revoke.\n active = false;\n // Only a request still in flight is aborted; `active` above is the stale-response\n // guard either way.\n if (!settled) controller.abort();\n if (created) URL.revokeObjectURL(created);\n };\n }, [key, url]);\n\n const retry = useCallback(() => setAttempt((n) => n + 1), []);\n\n if (!url || !enabled) {\n return { src: null, status: \"idle\", error: undefined, type: undefined, retry };\n }\n if (!hasFetcher) {\n return { src: url, status: \"ready\", error: undefined, type: undefined, retry };\n }\n // Until the effect for THIS key has run, the state still describes the previous URL\n // — a render in between must not show the old picture under the new name.\n if (state.key !== key) {\n return { src: null, status: \"loading\", error: undefined, type: undefined, retry };\n }\n return { src: state.src, status: state.status, error: state.error, type: state.type, retry };\n}\n"],"mappings":";AAAA,SAAS,aAAa,WAAW,QAAQ,gBAAgB;AAsEzD,eAAe,OAAO,KAAqC;AACzD,MAAI,OAAO,aAAa,eAAe,eAAe,UAAU;AAC9D,QAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,QAAQ,IAAI,MAAM,EAAE;AACjD,WAAO,IAAI,KAAK;AAAA,EAClB;AACA,SAAO;AACT;AAcO,SAAS,aACd,KACA,EAAE,SAAS,UAAU,KAAK,IAAyB,CAAC,GAChC;AACpB,QAAM,aAAa,OAAO,OAAO;AACjC,YAAU,MAAM;AACd,eAAW,UAAU;AAAA,EACvB,CAAC;AACD,QAAM,aAAa,CAAC,CAAC;AACrB,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,CAAC;AACxC,QAAM,MAAM,OAAO,WAAW,aAAa,GAAG,OAAO,IAAI,GAAG,KAAK;AACjE,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAgB;AAAA,IACxC,KAAK;AAAA,IACL,KAAK;AAAA,IACL,QAAQ;AAAA,IACR,OAAO;AAAA,IACP,MAAM;AAAA,EACR,CAAC;AAED,YAAU,MAAM;AACd,UAAM,UAAU,WAAW;AAC3B,QAAI,CAAC,OAAO,CAAC,OAAO,CAAC,QAAS;AAC9B,UAAM,aAAa,IAAI,gBAAgB;AACvC,QAAI,UAAyB;AAC7B,QAAI,SAAS;AAKb,QAAI,UAAU;AAGd,YAAQ,KAAK,EAAE,QAAQ,WAAW,OAAO,CAAC,EACvC,KAAK,MAAM,EACX,KAAK,CAAC,SAAS;AACd,gBAAU;AACV,UAAI,CAAC,OAAQ;AACb,gBAAU,IAAI,gBAAgB,IAAI;AAClC,eAAS,EAAE,KAAK,KAAK,SAAS,QAAQ,SAAS,OAAO,QAAW,MAAM,KAAK,KAAK,CAAC;AAAA,IACpF,CAAC,EACA,MAAM,CAAC,UAAmB;AACzB,gBAAU;AACV,UAAI,CAAC,OAAQ;AACb,eAAS,EAAE,KAAK,KAAK,MAAM,QAAQ,SAAS,OAAO,MAAM,OAAU,CAAC;AAAA,IACtE,CAAC;AACH,WAAO,MAAM;AAGX,eAAS;AAGT,UAAI,CAAC,QAAS,YAAW,MAAM;AAC/B,UAAI,QAAS,KAAI,gBAAgB,OAAO;AAAA,IAC1C;AAAA,EACF,GAAG,CAAC,KAAK,GAAG,CAAC;AAEb,QAAM,QAAQ,YAAY,MAAM,WAAW,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC;AAE5D,MAAI,CAAC,OAAO,CAAC,SAAS;AACpB,WAAO,EAAE,KAAK,MAAM,QAAQ,QAAQ,OAAO,QAAW,MAAM,QAAW,MAAM;AAAA,EAC/E;AACA,MAAI,CAAC,YAAY;AACf,WAAO,EAAE,KAAK,KAAK,QAAQ,SAAS,OAAO,QAAW,MAAM,QAAW,MAAM;AAAA,EAC/E;AAGA,MAAI,MAAM,QAAQ,KAAK;AACrB,WAAO,EAAE,KAAK,MAAM,QAAQ,WAAW,OAAO,QAAW,MAAM,QAAW,MAAM;AAAA,EAClF;AACA,SAAO,EAAE,KAAK,MAAM,KAAK,QAAQ,MAAM,QAAQ,OAAO,MAAM,OAAO,MAAM,MAAM,MAAM,MAAM;AAC7F;","names":[]}