@eifi1/ui-kit 0.11.0 → 0.13.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 (397) hide show
  1. package/README.md +59 -34
  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/amount-input.d.ts +40 -4
  12. package/dist/components/amount-input.js +50 -12
  13. package/dist/components/amount-input.js.map +1 -1
  14. package/dist/components/authed-image.d.ts +68 -0
  15. package/dist/components/authed-image.js +117 -0
  16. package/dist/components/authed-image.js.map +1 -0
  17. package/dist/components/breadcrumbs.d.ts +2 -1
  18. package/dist/components/breadcrumbs.js +5 -2
  19. package/dist/components/breadcrumbs.js.map +1 -1
  20. package/dist/components/bulk-action-bar.d.ts +10 -2
  21. package/dist/components/bulk-action-bar.js.map +1 -1
  22. package/dist/components/button-group.d.ts +51 -2
  23. package/dist/components/button-group.js +5 -1
  24. package/dist/components/button-group.js.map +1 -1
  25. package/dist/components/calculator.d.ts +15 -4
  26. package/dist/components/calendar-heatmap.d.ts +41 -3
  27. package/dist/components/calendar-heatmap.js +48 -7
  28. package/dist/components/calendar-heatmap.js.map +1 -1
  29. package/dist/components/chart.js +14 -8
  30. package/dist/components/chart.js.map +1 -1
  31. package/dist/components/chip.d.ts +2 -1
  32. package/dist/components/chip.js +26 -18
  33. package/dist/components/chip.js.map +1 -1
  34. package/dist/components/choice-card.d.ts +35 -3
  35. package/dist/components/choice-card.js +83 -25
  36. package/dist/components/choice-card.js.map +1 -1
  37. package/dist/components/combobox-core.d.ts +3 -1
  38. package/dist/components/combobox-core.js +18 -5
  39. package/dist/components/combobox-core.js.map +1 -1
  40. package/dist/components/combobox.d.ts +2 -2
  41. package/dist/components/combobox.js +31 -9
  42. package/dist/components/combobox.js.map +1 -1
  43. package/dist/components/copy-button.d.ts +51 -70
  44. package/dist/components/copy-button.js +1 -1
  45. package/dist/components/copy-button.js.map +1 -1
  46. package/dist/components/data-table-cells.d.ts +63 -0
  47. package/dist/components/data-table-cells.js +64 -0
  48. package/dist/components/data-table-cells.js.map +1 -0
  49. package/dist/components/data-table-filter-popover.d.ts +1 -1
  50. package/dist/components/data-table-filter-popover.js +8 -1
  51. package/dist/components/data-table-filter-popover.js.map +1 -1
  52. package/dist/components/data-table-filters.d.ts +1 -1
  53. package/dist/components/data-table-filters.js +31 -1
  54. package/dist/components/data-table-filters.js.map +1 -1
  55. package/dist/components/data-table-labels.d.ts +13 -0
  56. package/dist/components/data-table-labels.js +8 -1
  57. package/dist/components/data-table-labels.js.map +1 -1
  58. package/dist/components/data-table.d.ts +1 -1
  59. package/dist/components/data-table.js +125 -27
  60. package/dist/components/data-table.js.map +1 -1
  61. package/dist/components/date-picker.d.ts +3 -1
  62. package/dist/components/date-picker.js +15 -2
  63. package/dist/components/date-picker.js.map +1 -1
  64. package/dist/components/description-list.d.ts +23 -3
  65. package/dist/components/description-list.js +23 -3
  66. package/dist/components/description-list.js.map +1 -1
  67. package/dist/components/dialog-frame.d.ts +1 -0
  68. package/dist/components/entity-combobox.d.ts +1 -1
  69. package/dist/components/entity-combobox.js +40 -20
  70. package/dist/components/entity-combobox.js.map +1 -1
  71. package/dist/components/error-boundary.d.ts +281 -0
  72. package/dist/components/error-boundary.js +302 -0
  73. package/dist/components/error-boundary.js.map +1 -0
  74. package/dist/components/field-sync.js +90 -83
  75. package/dist/components/field-sync.js.map +1 -1
  76. package/dist/components/field.d.ts +30 -4
  77. package/dist/components/field.js +40 -17
  78. package/dist/components/field.js.map +1 -1
  79. package/dist/components/file-button.d.ts +50 -208
  80. package/dist/components/file-dropzone.d.ts +49 -2
  81. package/dist/components/floating-panel.d.ts +13 -4
  82. package/dist/components/floating-panel.js +7 -3
  83. package/dist/components/floating-panel.js.map +1 -1
  84. package/dist/components/form-actions.d.ts +53 -0
  85. package/dist/components/form-actions.js +98 -0
  86. package/dist/components/form-actions.js.map +1 -0
  87. package/dist/components/full-bleed-dialog.js +4 -0
  88. package/dist/components/full-bleed-dialog.js.map +1 -1
  89. package/dist/components/hover-menu.js +64 -56
  90. package/dist/components/hover-menu.js.map +1 -1
  91. package/dist/components/image-grid.d.ts +67 -0
  92. package/dist/components/image-grid.js +112 -0
  93. package/dist/components/image-grid.js.map +1 -0
  94. package/dist/components/lightbox.d.ts +104 -0
  95. package/dist/components/lightbox.js +229 -0
  96. package/dist/components/lightbox.js.map +1 -0
  97. package/dist/components/line-items.d.ts +114 -0
  98. package/dist/components/line-items.js +238 -0
  99. package/dist/components/line-items.js.map +1 -0
  100. package/dist/components/list.d.ts +37 -4
  101. package/dist/components/list.js +35 -14
  102. package/dist/components/list.js.map +1 -1
  103. package/dist/components/loading-state.d.ts +24 -0
  104. package/dist/components/loading-state.js +32 -0
  105. package/dist/components/loading-state.js.map +1 -0
  106. package/dist/components/menu-item.d.ts +2 -1
  107. package/dist/components/menu-item.js +5 -1
  108. package/dist/components/menu-item.js.map +1 -1
  109. package/dist/components/mini-calendar.d.ts +13 -1
  110. package/dist/components/mini-calendar.js +46 -2
  111. package/dist/components/mini-calendar.js.map +1 -1
  112. package/dist/components/modal.d.ts +20 -1
  113. package/dist/components/modal.js +26 -2
  114. package/dist/components/modal.js.map +1 -1
  115. package/dist/components/month-picker.d.ts +21 -1
  116. package/dist/components/month-picker.js +139 -26
  117. package/dist/components/month-picker.js.map +1 -1
  118. package/dist/components/multi-entity-combobox.d.ts +1 -1
  119. package/dist/components/multi-entity-combobox.js +42 -20
  120. package/dist/components/multi-entity-combobox.js.map +1 -1
  121. package/dist/components/nav-pills.d.ts +2 -1
  122. package/dist/components/nav-pills.js +5 -1
  123. package/dist/components/nav-pills.js.map +1 -1
  124. package/dist/components/number-field.d.ts +24 -5
  125. package/dist/components/number-field.js +5 -7
  126. package/dist/components/number-field.js.map +1 -1
  127. package/dist/components/number-input.d.ts +25 -5
  128. package/dist/components/number-input.js +7 -1
  129. package/dist/components/number-input.js.map +1 -1
  130. package/dist/components/numpad-sheet.d.ts +20 -5
  131. package/dist/components/numpad-sheet.js +3 -2
  132. package/dist/components/numpad-sheet.js.map +1 -1
  133. package/dist/components/page-header.d.ts +35 -6
  134. package/dist/components/page-header.js +7 -1
  135. package/dist/components/page-header.js.map +1 -1
  136. package/dist/components/passkeys-setting.d.ts +67 -0
  137. package/dist/components/passkeys-setting.js +214 -0
  138. package/dist/components/passkeys-setting.js.map +1 -0
  139. package/dist/components/picker-sheet.js +5 -1
  140. package/dist/components/picker-sheet.js.map +1 -1
  141. package/dist/components/pie-chart-labels.d.ts +20 -0
  142. package/dist/components/pie-chart-labels.js +12 -0
  143. package/dist/components/pie-chart-labels.js.map +1 -0
  144. package/dist/components/pie-chart.d.ts +101 -0
  145. package/dist/components/pie-chart.js +283 -0
  146. package/dist/components/pie-chart.js.map +1 -0
  147. package/dist/components/popover.js +3 -0
  148. package/dist/components/popover.js.map +1 -1
  149. package/dist/components/progress-bar.d.ts +44 -3
  150. package/dist/components/progress-bar.js +58 -22
  151. package/dist/components/progress-bar.js.map +1 -1
  152. package/dist/components/qr-code.d.ts +34 -0
  153. package/dist/components/qr-code.js +57 -0
  154. package/dist/components/qr-code.js.map +1 -0
  155. package/dist/components/search-field.d.ts +15 -3
  156. package/dist/components/search-field.js +5 -1
  157. package/dist/components/search-field.js.map +1 -1
  158. package/dist/components/settings-fields.d.ts +49 -1
  159. package/dist/components/signed-amount.d.ts +122 -0
  160. package/dist/components/signed-amount.js +152 -0
  161. package/dist/components/signed-amount.js.map +1 -0
  162. package/dist/components/skeleton.d.ts +10 -3
  163. package/dist/components/skeleton.js +12 -2
  164. package/dist/components/skeleton.js.map +1 -1
  165. package/dist/components/stat-tile.d.ts +2 -1
  166. package/dist/components/stat-tile.js +6 -3
  167. package/dist/components/stat-tile.js.map +1 -1
  168. package/dist/components/swipeable-row.js +4 -3
  169. package/dist/components/swipeable-row.js.map +1 -1
  170. package/dist/components/table.d.ts +39 -5
  171. package/dist/components/table.js +45 -11
  172. package/dist/components/table.js.map +1 -1
  173. package/dist/components/text-link.d.ts +238 -0
  174. package/dist/components/text-link.js +144 -0
  175. package/dist/components/text-link.js.map +1 -0
  176. package/dist/components/text.d.ts +26 -2
  177. package/dist/components/text.js +16 -2
  178. package/dist/components/text.js.map +1 -1
  179. package/dist/components/time-input.d.ts +49 -1
  180. package/dist/components/toggle-group.d.ts +35 -0
  181. package/dist/components/toggle-group.js +43 -14
  182. package/dist/components/toggle-group.js.map +1 -1
  183. package/dist/components/toggle-legend.d.ts +53 -3
  184. package/dist/components/toggle-legend.js +54 -1
  185. package/dist/components/toggle-legend.js.map +1 -1
  186. package/dist/components/tooltip.d.ts +51 -1
  187. package/dist/components/tooltip.js +59 -35
  188. package/dist/components/tooltip.js.map +1 -1
  189. package/dist/components/ui.d.ts +51 -779
  190. package/dist/components/ui.js +364 -34
  191. package/dist/components/ui.js.map +1 -1
  192. package/dist/components/use-table-state.d.ts +3 -52
  193. package/dist/components/use-table-state.js +128 -30
  194. package/dist/components/use-table-state.js.map +1 -1
  195. package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
  196. package/dist/data-table.d.ts +2 -1
  197. package/dist/data-table.js +5 -1
  198. package/dist/data-table.js.map +1 -1
  199. package/dist/feedback/feedback-attachment.js +4 -6
  200. package/dist/feedback/feedback-attachment.js.map +1 -1
  201. package/dist/feedback/feedback-dialog.d.ts +26 -3
  202. package/dist/feedback/feedback-dialog.js +21 -5
  203. package/dist/feedback/feedback-dialog.js.map +1 -1
  204. package/dist/feedback/feedback-inbox.js +86 -75
  205. package/dist/feedback/feedback-inbox.js.map +1 -1
  206. package/dist/feedback/feedback-thread.d.ts +173 -0
  207. package/dist/feedback/feedback-thread.js +264 -0
  208. package/dist/feedback/feedback-thread.js.map +1 -0
  209. package/dist/feedback.d.ts +2 -1
  210. package/dist/feedback.js +1 -0
  211. package/dist/feedback.js.map +1 -1
  212. package/dist/hooks/use-authed-src.d.ts +71 -0
  213. package/dist/hooks/use-authed-src.js +63 -0
  214. package/dist/hooks/use-authed-src.js.map +1 -0
  215. package/dist/hooks/use-file-drop.d.ts +49 -2
  216. package/dist/hooks/use-hotkey.d.ts +59 -0
  217. package/dist/hooks/use-hotkey.js +105 -0
  218. package/dist/hooks/use-hotkey.js.map +1 -0
  219. package/dist/hooks/use-search-param-state.d.ts +83 -0
  220. package/dist/hooks/use-search-param-state.js +94 -0
  221. package/dist/hooks/use-search-param-state.js.map +1 -0
  222. package/dist/i18n/defaults.d.ts +15 -4
  223. package/dist/i18n/defaults.js +31 -1
  224. package/dist/i18n/defaults.js.map +1 -1
  225. package/dist/i18n/kit-labels.d.ts +46 -312
  226. package/dist/i18n/kit-labels.js +20 -5
  227. package/dist/i18n/kit-labels.js.map +1 -1
  228. package/dist/i18n/locales/de-CH-informal.d.ts +15 -4
  229. package/dist/i18n/locales/de-CH.d.ts +15 -4
  230. package/dist/i18n/locales/de-informal.d.ts +15 -4
  231. package/dist/i18n/locales/de.d.ts +15 -4
  232. package/dist/i18n/locales/de.js +164 -3
  233. package/dist/i18n/locales/de.js.map +1 -1
  234. package/dist/i18n/locales/es.d.ts +15 -4
  235. package/dist/i18n/locales/es.js +161 -3
  236. package/dist/i18n/locales/es.js.map +1 -1
  237. package/dist/i18n/locales/fr.d.ts +15 -4
  238. package/dist/i18n/locales/fr.js +161 -3
  239. package/dist/i18n/locales/fr.js.map +1 -1
  240. package/dist/i18n/locales/hu.d.ts +15 -4
  241. package/dist/i18n/locales/hu.js +161 -3
  242. package/dist/i18n/locales/hu.js.map +1 -1
  243. package/dist/i18n/locales/it.d.ts +15 -4
  244. package/dist/i18n/locales/it.js +161 -3
  245. package/dist/i18n/locales/it.js.map +1 -1
  246. package/dist/i18n/locales/zh.d.ts +15 -4
  247. package/dist/i18n/locales/zh.js +161 -3
  248. package/dist/i18n/locales/zh.js.map +1 -1
  249. package/dist/index.d.ts +39 -19
  250. package/dist/index.js +40 -1
  251. package/dist/index.js.map +1 -1
  252. package/dist/kit-labels-Mz8zZHIF.d.ts +1744 -0
  253. package/dist/lib/format.d.ts +162 -0
  254. package/dist/lib/format.js +151 -0
  255. package/dist/lib/format.js.map +1 -0
  256. package/dist/lib/qr-encode.d.ts +44 -0
  257. package/dist/lib/qr-encode.js +338 -0
  258. package/dist/lib/qr-encode.js.map +1 -0
  259. package/dist/rhf/fields.d.ts +283 -0
  260. package/dist/rhf/fields.js +631 -0
  261. package/dist/rhf/fields.js.map +1 -0
  262. package/dist/rhf/form.d.ts +49 -1
  263. package/dist/rhf/line-items.d.ts +31 -0
  264. package/dist/rhf/line-items.js +59 -0
  265. package/dist/rhf/line-items.js.map +1 -0
  266. package/dist/rhf.d.ts +61 -1
  267. package/dist/rhf.js +2 -0
  268. package/dist/rhf.js.map +1 -1
  269. package/dist/search/command-palette.js +20 -14
  270. package/dist/search/command-palette.js.map +1 -1
  271. package/dist/shell/app-shell.d.ts +99 -2
  272. package/dist/shell/app-shell.js +120 -57
  273. package/dist/shell/app-shell.js.map +1 -1
  274. package/dist/shell/auth-layout.d.ts +74 -0
  275. package/dist/shell/auth-layout.js +97 -0
  276. package/dist/shell/auth-layout.js.map +1 -0
  277. package/dist/shell/top-bar-brand.d.ts +81 -0
  278. package/dist/shell/top-bar-brand.js +47 -0
  279. package/dist/shell/top-bar-brand.js.map +1 -0
  280. package/dist/shell/topbar-action-menu.d.ts +21 -4
  281. package/dist/shell/topbar-action-menu.js +18 -2
  282. package/dist/shell/topbar-action-menu.js.map +1 -1
  283. package/dist/shell.d.ts +56 -1
  284. package/dist/shell.js +2 -0
  285. package/dist/shell.js.map +1 -1
  286. package/dist/tour/tour.js +10 -4
  287. package/dist/tour/tour.js.map +1 -1
  288. package/dist/wizard/stepper-nav.d.ts +48 -1
  289. package/dist/wizard/use-wizard.js +4 -2
  290. package/dist/wizard/use-wizard.js.map +1 -1
  291. package/dist/wizard/wizard-context.d.ts +24 -1
  292. package/dist/wizard/wizard-context.js +19 -2
  293. package/dist/wizard/wizard-context.js.map +1 -1
  294. package/dist/wizard/wizard-step.d.ts +30 -5
  295. package/dist/wizard/wizard-step.js +34 -3
  296. package/dist/wizard/wizard-step.js.map +1 -1
  297. package/dist/wizard.d.ts +50 -3
  298. package/package.json +6 -6
  299. package/src/chart.ts +4 -0
  300. package/src/components/account-settings-labels.ts +190 -0
  301. package/src/components/account-settings.tsx +110 -63
  302. package/src/components/amount-input.tsx +109 -10
  303. package/src/components/authed-image.tsx +190 -0
  304. package/src/components/breadcrumbs.tsx +8 -2
  305. package/src/components/bulk-action-bar.tsx +16 -3
  306. package/src/components/button-group.tsx +7 -2
  307. package/src/components/calendar-heatmap.tsx +105 -10
  308. package/src/components/chart.tsx +19 -8
  309. package/src/components/chip.tsx +14 -4
  310. package/src/components/choice-card.tsx +136 -25
  311. package/src/components/combobox-core.tsx +33 -9
  312. package/src/components/combobox.tsx +33 -9
  313. package/src/components/copy-button.tsx +3 -2
  314. package/src/components/data-table-cells.tsx +119 -0
  315. package/src/components/data-table-filter-popover.tsx +13 -5
  316. package/src/components/data-table-filters.ts +77 -0
  317. package/src/components/data-table-labels.ts +23 -0
  318. package/src/components/data-table.tsx +230 -13
  319. package/src/components/date-picker.tsx +19 -3
  320. package/src/components/description-list.tsx +52 -3
  321. package/src/components/entity-combobox.tsx +25 -7
  322. package/src/components/error-boundary.tsx +640 -0
  323. package/src/components/field-sync.tsx +48 -40
  324. package/src/components/field.tsx +56 -5
  325. package/src/components/floating-panel.tsx +20 -7
  326. package/src/components/form-actions.tsx +191 -0
  327. package/src/components/full-bleed-dialog.tsx +4 -0
  328. package/src/components/hover-menu.tsx +26 -17
  329. package/src/components/image-grid.tsx +181 -0
  330. package/src/components/lightbox.tsx +374 -0
  331. package/src/components/line-items.tsx +385 -0
  332. package/src/components/list.tsx +82 -19
  333. package/src/components/loading-state.tsx +47 -0
  334. package/src/components/menu-item.tsx +7 -2
  335. package/src/components/mini-calendar.tsx +72 -3
  336. package/src/components/modal.tsx +65 -3
  337. package/src/components/month-picker.tsx +181 -26
  338. package/src/components/multi-entity-combobox.tsx +27 -7
  339. package/src/components/nav-pills.tsx +8 -1
  340. package/src/components/number-field.tsx +15 -10
  341. package/src/components/number-input.tsx +17 -1
  342. package/src/components/numpad-sheet.tsx +6 -1
  343. package/src/components/page-header.tsx +42 -5
  344. package/src/components/passkeys-setting.tsx +331 -0
  345. package/src/components/picker-sheet.tsx +10 -1
  346. package/src/components/pie-chart-labels.ts +32 -0
  347. package/src/components/pie-chart.tsx +479 -0
  348. package/src/components/popover.tsx +3 -0
  349. package/src/components/progress-bar.tsx +129 -19
  350. package/src/components/qr-code.tsx +83 -0
  351. package/src/components/search-field.tsx +20 -4
  352. package/src/components/signed-amount.tsx +295 -0
  353. package/src/components/skeleton.tsx +21 -3
  354. package/src/components/stat-tile.tsx +11 -3
  355. package/src/components/swipeable-row.tsx +7 -7
  356. package/src/components/table.tsx +102 -13
  357. package/src/components/text-link.tsx +356 -0
  358. package/src/components/text.tsx +46 -2
  359. package/src/components/toggle-group.tsx +93 -20
  360. package/src/components/toggle-legend.tsx +108 -2
  361. package/src/components/tooltip.tsx +79 -8
  362. package/src/components/ui.tsx +707 -49
  363. package/src/components/use-table-state.ts +265 -33
  364. package/src/data-table.ts +10 -0
  365. package/src/feedback/feedback-attachment.tsx +10 -7
  366. package/src/feedback/feedback-dialog.tsx +49 -7
  367. package/src/feedback/feedback-inbox.tsx +7 -0
  368. package/src/feedback/feedback-thread.tsx +510 -0
  369. package/src/feedback.ts +1 -0
  370. package/src/hooks/use-authed-src.ts +161 -0
  371. package/src/hooks/use-hotkey.ts +172 -0
  372. package/src/hooks/use-search-param-state.ts +187 -0
  373. package/src/i18n/defaults.ts +30 -0
  374. package/src/i18n/kit-labels.tsx +118 -8
  375. package/src/i18n/locales/de.ts +161 -0
  376. package/src/i18n/locales/es.ts +158 -0
  377. package/src/i18n/locales/fr.ts +161 -0
  378. package/src/i18n/locales/hu.ts +159 -0
  379. package/src/i18n/locales/it.ts +159 -0
  380. package/src/i18n/locales/zh.ts +158 -0
  381. package/src/index.ts +59 -0
  382. package/src/lib/format.ts +348 -0
  383. package/src/lib/qr-encode.ts +420 -0
  384. package/src/rhf/fields.tsx +1135 -0
  385. package/src/rhf/line-items.tsx +130 -0
  386. package/src/rhf.ts +6 -0
  387. package/src/search/command-palette.tsx +29 -19
  388. package/src/shell/app-shell.tsx +153 -3
  389. package/src/shell/auth-layout.tsx +190 -0
  390. package/src/shell/top-bar-brand.tsx +76 -0
  391. package/src/shell/topbar-action-menu.tsx +32 -4
  392. package/src/shell.ts +2 -0
  393. package/src/tour/tour.tsx +16 -5
  394. package/src/wizard/use-wizard.ts +8 -4
  395. package/src/wizard/wizard-context.tsx +41 -1
  396. package/src/wizard/wizard-step.tsx +75 -6
  397. package/tokens.css +12 -0
@@ -0,0 +1,640 @@
1
+ import { Component } from "react";
2
+ import type { ErrorInfo, ReactNode } from "react";
3
+ import { AlertTriangle, RefreshCw, WifiOff } from "lucide-react";
4
+ import { useKitLabels } from "../i18n/kit-labels";
5
+ import { Button, EmptyState } from "./ui";
6
+ import { CopyButton } from "./copy-button";
7
+ import { Disclosure } from "./disclosure";
8
+
9
+ /** The words of the default fallback. */
10
+ export interface ErrorBoundaryLabels {
11
+ /** The fallback's heading. */
12
+ title: string;
13
+ /** The line under it. */
14
+ message: string;
15
+ /** The button that clears the error and renders the children again. */
16
+ retry: string;
17
+ /** The disclosure that opens the full error report (`showDetails`). */
18
+ details: string;
19
+ /** The button that reloads the whole page (0.13). */
20
+ reload: string;
21
+ /** Heading when a lazy chunk failed to load while online — a deploy replaced the
22
+ * files this page was built from (0.13). */
23
+ updateTitle: string;
24
+ updateMessage: string;
25
+ /** Heading when the browser says it is offline (0.13). */
26
+ offlineTitle: string;
27
+ offlineMessage: string;
28
+ /** The copy button's words: what it copies is the plain-text report (0.13). */
29
+ copyReport: string;
30
+ /** Shown once `onReport` has filed the crash and returned no reference (0.13). */
31
+ reported: string;
32
+ /** Shown once `onReport` has filed the crash under a reference — rendered as the
33
+ * app returned it, so return `"#123"` if the hash belongs to it (0.13). */
34
+ reportedAs: (reference: string) => string;
35
+ }
36
+
37
+ export const DEFAULT_ERROR_BOUNDARY_LABELS: ErrorBoundaryLabels = {
38
+ title: "Something went wrong",
39
+ message: "This part of the page could not be shown. Try again, or reload the page.",
40
+ retry: "Try again",
41
+ details: "Error details",
42
+ reload: "Reload",
43
+ updateTitle: "A new version is available",
44
+ updateMessage: "Part of the app has changed since this page was loaded. Reload to get the new version.",
45
+ offlineTitle: "You’re offline",
46
+ offlineMessage: "This page could not be loaded without a connection. Reconnect, then reload the page.",
47
+ copyReport: "Copy error report",
48
+ reported: "This error has been reported automatically.",
49
+ reportedAs: (reference) => `Reported as ${reference}`,
50
+ };
51
+
52
+ /**
53
+ * What was thrown, as flat strings, read ONCE when it arrives.
54
+ *
55
+ * React stores whatever was thrown, hostile or not — `throw { get message() { throw … } }`,
56
+ * `throw { name: Symbol() }`, `throw undefined` — and reading one of those during the
57
+ * fallback's own render would throw INSIDE the boundary, escalating to the next one up
58
+ * (keksdose's error-boundary.tsx learned this as feedback #160: at the app level there is
59
+ * none, and the result is the white screen the boundary exists to prevent).
60
+ */
61
+ export interface ErrorBoundaryDetails {
62
+ name: string;
63
+ message: string;
64
+ stack?: string;
65
+ }
66
+
67
+ function read(get: () => unknown): string | undefined {
68
+ try {
69
+ const v = get();
70
+ return typeof v === "string" ? v : v === undefined || v === null ? undefined : String(v);
71
+ } catch {
72
+ return undefined;
73
+ }
74
+ }
75
+
76
+ /** {@link ErrorBoundaryDetails} of any thrown value, without ever throwing. */
77
+ export function describeThrown(error: unknown): ErrorBoundaryDetails {
78
+ if (typeof error === "object" && error !== null) {
79
+ const e = error as { name?: unknown; message?: unknown; stack?: unknown };
80
+ return {
81
+ name: read(() => e.name) ?? "Error",
82
+ message: read(() => e.message) ?? "",
83
+ stack: read(() => e.stack),
84
+ };
85
+ }
86
+ return { name: "Error", message: read(() => error) ?? "" };
87
+ }
88
+
89
+ /**
90
+ * Is this a failed lazy import — a route chunk that did not arrive?
91
+ *
92
+ * Two causes, and neither is a bug in the page: the device is offline and the chunk was
93
+ * never cached, or a deploy replaced the hashed files the running page still points at.
94
+ * Both are cured by a reload and by nothing else — `React.lazy` caches the rejected
95
+ * import, so Try again re-throws the same error without fetching. The patterns are
96
+ * keksdose's (crash-report.ts), which covers Vite ("dynamically imported module",
97
+ * "module script failed"), webpack ("ChunkLoadError", "Loading chunk") and the bare
98
+ * `fetch` failure a service worker hands back.
99
+ *
100
+ * Never throws: it runs inside the fallback's own render, where a throw is a white
101
+ * screen. An error it cannot read is not a chunk failure.
102
+ */
103
+ export function isChunkLoadError(
104
+ error: { name?: string | null; message?: string | null } | null | undefined,
105
+ ): boolean {
106
+ try {
107
+ const s = `${error?.name ?? ""} ${error?.message ?? ""}`;
108
+ return /ChunkLoadError|dynamically imported module|module script failed|Loading chunk|Failed to fetch/i.test(s);
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Everything a crash report carries, read once when the boundary catches.
116
+ *
117
+ * Plain strings only, so it survives `JSON.stringify` into an app's crash endpoint as is
118
+ * and {@link formatCrashReport} can print it without a single guard. The wording of the
119
+ * printed report is English on purpose: it is read by whoever fixes the bug, not by the
120
+ * user who copies it, and a report in nine languages cannot be searched.
121
+ */
122
+ export interface CrashReport {
123
+ /** The thrown value's `name` ("TypeError"), "Error" for a non-object. */
124
+ name: string;
125
+ message: string;
126
+ stack?: string;
127
+ /** React's component stack from `componentDidCatch` — the part that names the
128
+ * component, which a minified JS stack no longer does. */
129
+ componentStack?: string;
130
+ /** `pathname + search` at the moment of the crash. The search may carry what the
131
+ * user typed — see `redact`. */
132
+ page: string;
133
+ /** ISO 8601, UTC. */
134
+ time: string;
135
+ userAgent: string;
136
+ /** The boundary's `appVersion` prop. */
137
+ appVersion?: string;
138
+ /** `navigator.onLine` at the moment of the crash; left out where there is none. */
139
+ online?: boolean;
140
+ /** {@link isChunkLoadError} of the thrown value. Such a crash is never handed to
141
+ * `onReport` — it is a deploy or a dropped connection, not a defect — but it is
142
+ * still in the copied report. */
143
+ chunkLoad: boolean;
144
+ /** The boundary's `reportExtras()`, stringified. */
145
+ extras: Record<string, string>;
146
+ }
147
+
148
+ /**
149
+ * The report as one plain-text block — what the copy button puts on the clipboard and
150
+ * the details disclosure shows.
151
+ *
152
+ * lenkbank's layout, because that is the one that was already being pasted into
153
+ * feedback messages: the error line first (it is what a search for the bug matches),
154
+ * then where / when / what, then the two stacks.
155
+ */
156
+ export function formatCrashReport(report: CrashReport): string {
157
+ const lines = [
158
+ report.message ? `${report.name}: ${report.message}` : report.name,
159
+ `Page: ${report.page}`,
160
+ `Time: ${report.time}`,
161
+ `User agent: ${report.userAgent}`,
162
+ ];
163
+ if (report.appVersion) lines.push(`App version: ${report.appVersion}`);
164
+ if (report.online === false) lines.push("Online: no");
165
+ for (const [key, value] of Object.entries(report.extras)) lines.push(`${key}: ${value}`);
166
+ if (report.stack) lines.push("", "Stack:", report.stack.trim());
167
+ if (report.componentStack) lines.push("", "Component stack:", report.componentStack.trim());
168
+ return lines.join("\n");
169
+ }
170
+
171
+ /**
172
+ * A key for "the same crash again": the error's name and message and the first frames
173
+ * of its stack.
174
+ *
175
+ * Page-load local, not a server-side grouping key (keksdose's crash-report.ts draws the
176
+ * same line): its only job is to stop one page load filing one bug twice — React's dev
177
+ * StrictMode, a Try again that crashes again, a render loop that trips the boundary many
178
+ * times a second. The page and the time are left out on purpose: the same bug on two
179
+ * routes is still one bug.
180
+ */
181
+ export function crashFingerprint(report: Pick<CrashReport, "name" | "message" | "stack">): string {
182
+ const frames = (report.stack ?? "").split("\n").slice(0, 4).join("|");
183
+ return `${report.name}::${report.message}::${frames}`;
184
+ }
185
+
186
+ /**
187
+ * What `onReport` may resolve with. `{ reference }` is shown ("Reported as …");
188
+ * nothing (or `{}`) means filed without one. `{ filed: false }` says the app decided NOT
189
+ * to file (a demo session, a hot-reload artefact, a report buffered for later): the
190
+ * screen then claims nothing and keeps the copy button — claiming a report that was
191
+ * never sent is the same false statement as hiding one that was.
192
+ */
193
+ export type CrashReportResult = { reference?: string; filed?: boolean } | void;
194
+
195
+ interface Filing {
196
+ filed: boolean;
197
+ reference?: string;
198
+ }
199
+
200
+ const NOT_FILED: Filing = { filed: false };
201
+
202
+ /** A render loop can trip a boundary dozens of times a second with a different message
203
+ * each time (a counter in it); without a ceiling the first bad deploy files hundreds of
204
+ * rows. keksdose's number. */
205
+ const MAX_REPORTS_PER_PAGE_LOAD = 5;
206
+
207
+ /** Fingerprint → the one filing for it in this page load. Module scope IS the page
208
+ * load: a reload starts it empty. A duplicate gets the first filing's promise, so the
209
+ * second screen shows the same reference instead of claiming nothing. */
210
+ const filings = new Map<string, Promise<Filing>>();
211
+
212
+ function settle(out: unknown): Filing {
213
+ try {
214
+ if (out && typeof out === "object") {
215
+ const o = out as { filed?: unknown; reference?: unknown };
216
+ if (o.filed === false) return NOT_FILED;
217
+ const reference = typeof o.reference === "string" && o.reference ? o.reference : undefined;
218
+ return { filed: true, reference };
219
+ }
220
+ return { filed: true };
221
+ } catch {
222
+ return NOT_FILED;
223
+ }
224
+ }
225
+
226
+ /** Hand the report to the app, once per fingerprint per page load, and never throw —
227
+ * not for a synchronous throw, a rejection, or a hostile thenable. */
228
+ function fileOnce(report: CrashReport, onReport: (report: CrashReport) => unknown): Promise<Filing> {
229
+ const key = crashFingerprint(report);
230
+ const known = filings.get(key);
231
+ if (known) return known;
232
+ if (filings.size >= MAX_REPORTS_PER_PAGE_LOAD) return Promise.resolve(NOT_FILED);
233
+ let filing: Promise<Filing>;
234
+ try {
235
+ filing = Promise.resolve(onReport(report)).then(settle, () => NOT_FILED);
236
+ } catch {
237
+ filing = Promise.resolve(NOT_FILED);
238
+ }
239
+ // Claimed before the first await: two catches in the same tick (StrictMode) must not
240
+ // both get past the lookup above.
241
+ filings.set(key, filing);
242
+ return filing;
243
+ }
244
+
245
+ function isOffline(): boolean {
246
+ try {
247
+ return typeof navigator !== "undefined" && navigator.onLine === false;
248
+ } catch {
249
+ return false;
250
+ }
251
+ }
252
+
253
+ function reloadPage(): void {
254
+ try {
255
+ window.location.reload();
256
+ } catch {
257
+ /* nothing left to do from here */
258
+ }
259
+ }
260
+
261
+ /** What a render-prop `fallback` is handed. */
262
+ export interface ErrorBoundaryFallbackProps {
263
+ /** The thrown value, as it was thrown. Read it with care — see {@link describeThrown}. */
264
+ error: unknown;
265
+ details: ErrorBoundaryDetails;
266
+ /** Clear the error and render the children again. */
267
+ reset: () => void;
268
+ /** The crash report, once `componentDidCatch` has built it (null on the very first
269
+ * render of the fallback, which happens before it). Already `redact`ed. */
270
+ report: CrashReport | null;
271
+ }
272
+
273
+ export interface ErrorBoundaryProps {
274
+ children?: ReactNode;
275
+ /** Replaces the default fallback: a node, or a render function handed the error and
276
+ * a `reset`. The same rule applies to it as to the default: see the component docs. */
277
+ fallback?: ReactNode | ((props: ErrorBoundaryFallbackProps) => ReactNode);
278
+ /** Called once per caught error with the raw value — log it. A throw from it is
279
+ * swallowed: the boundary is the last thing standing. For filing a report, prefer
280
+ * `onReport`, which gets the finished report, dedupe and the reference on screen. */
281
+ onError?: (error: unknown, info: ErrorInfo) => void;
282
+ /** Called when the error is cleared, by Try again, `reset` or a `resetKeys` change —
283
+ * drop a cache, refetch. */
284
+ onReset?: () => void;
285
+ /**
286
+ * When any of these changes (compared with `Object.is`) AFTER an error was caught, the
287
+ * error is cleared and the children render again — a change in the same update that
288
+ * threw does not count (it is what the error was shown under) — pass the route path so leaving a broken page does not
289
+ * carry its fallback to the next one (keksdose's `resetKey`).
290
+ */
291
+ resetKeys?: ReadonlyArray<unknown>;
292
+ /** Quote the error's `name: message` in a monospace box under the hint. Default on
293
+ * (0.13): three sentences true of every crash identify none of them, and the one
294
+ * line that does is what a user can read out or screenshot (lenkbank P7). Not shown
295
+ * for a chunk-load / offline screen, where the message is a URL that explains
296
+ * nothing the title does not. */
297
+ showMessage?: boolean;
298
+ /** A collapsed disclosure holding the full plain-text report. Default on since 0.13
299
+ * (it was an opt-in showing name, message and stack): closed, it costs one line, and
300
+ * it is where a support conversation asks the user to look. */
301
+ showDetails?: boolean;
302
+ /** The copy button for the plain-text report (the kit's CopyButton, which says when
303
+ * the clipboard refused). Default on. Hidden once `onReport` has filed the crash —
304
+ * there is nothing left to paste anywhere — and shown again whenever it did not. */
305
+ copyReport?: boolean;
306
+ /** The app's version, printed in the report — the first question on every crash. */
307
+ appVersion?: string;
308
+ /** More `key: value` lines for the report, read when the boundary catches (a tenant
309
+ * id, the feature flags). A throw from it is swallowed and the lines are left out. */
310
+ reportExtras?: () => Record<string, string>;
311
+ /**
312
+ * Rewrite the report before anything sees it: the screen, the clipboard and
313
+ * `onReport` all get the returned one.
314
+ *
315
+ * THE REPORT IS STRUCTURE, NEVER CONTENT — and only the app knows which of its query
316
+ * params hold content. keksdose's register put the free-text search in `?q=` and payee
317
+ * names in the `f.*` filters, and its crash channel sent them in the clear to a column
318
+ * every admin reads (crash-report.ts `safeUrl`). The cure there is an ALLOW-list, so a
319
+ * param added later is dropped until somebody decides otherwise — rebuild `page`
320
+ * from `new URL(r.page, location.origin)`, keeping only the listed `searchParams`.
321
+ * `extras` are the app's own and need no second pass. A throw from `redact` falls
322
+ * back to the pathname alone and no extras — failing closed.
323
+ *
324
+ * The same holds inside `onReport`, which is the other place to do it: clamp fields
325
+ * to what the endpoint accepts, drop what it must not store. The kit cannot know.
326
+ */
327
+ redact?: (report: CrashReport) => CrashReport;
328
+ /**
329
+ * File the report automatically. Called from `componentDidCatch`, at most once per
330
+ * {@link crashFingerprint} per page load (and at most five times in all), never for a
331
+ * chunk-load error. It can NOT throw into the boundary: a synchronous throw and a
332
+ * rejection both read as "not filed", and the copy button stays.
333
+ *
334
+ * Resolve with `{ reference }` and the screen says "Reported as …" instead of the copy
335
+ * button; see {@link CrashReportResult} for "filed without a reference" and "decided not
336
+ * to file". Whatever the app's reporter does, it must do without the router, the query
337
+ * client or the auth store's hooks — a bare `fetch` with `keepalive`, so it survives
338
+ * the reload the user is about to press (keksdose crash-report.ts `post`).
339
+ */
340
+ onReport?: (report: CrashReport) => Promise<CrashReportResult> | CrashReportResult;
341
+ /** The app's own escape hatches, beside Reload / Try again — keksdose's "Clear local
342
+ * data" for a poisoned persisted cache that no in-app action could otherwise break.
343
+ * A full-width hint under it: wrap it in `<div className="basis-full">`. */
344
+ actions?: ReactNode;
345
+ /** The default fallback's heading level. Default `h2`: the fallback often IS the page. */
346
+ headingAs?: "h2" | "h3" | "h4" | "h5" | "h6";
347
+ /** Classes for the default fallback's box (`m-8 min-h-[400px]`, kastlan). */
348
+ className?: string;
349
+ labels?: Partial<ErrorBoundaryLabels>;
350
+ }
351
+
352
+ type FilingState = "none" | "pending" | "filed" | "failed";
353
+
354
+ interface ErrorBoundaryState {
355
+ /** A flag of its own, so `throw undefined` is still an error shown. */
356
+ failed: boolean;
357
+ error: unknown;
358
+ details: ErrorBoundaryDetails | null;
359
+ report: CrashReport | null;
360
+ filing: FilingState;
361
+ reference: string | undefined;
362
+ }
363
+
364
+ const CLEAR: ErrorBoundaryState = {
365
+ failed: false,
366
+ error: null,
367
+ details: null,
368
+ report: null,
369
+ filing: "none",
370
+ reference: undefined,
371
+ };
372
+
373
+ function keysChanged(a: ReadonlyArray<unknown> | undefined, b: ReadonlyArray<unknown> | undefined): boolean {
374
+ if (a === b) return false;
375
+ if (!a || !b || a.length !== b.length) return true;
376
+ return a.some((v, i) => !Object.is(v, b[i]));
377
+ }
378
+
379
+ function currentPage(): { page: string; path: string } {
380
+ try {
381
+ const { pathname, search } = window.location;
382
+ return { page: `${pathname}${search}`, path: pathname };
383
+ } catch {
384
+ return { page: "", path: "" };
385
+ }
386
+ }
387
+
388
+ function buildReport(details: ErrorBoundaryDetails, componentStack: string | undefined, props: ErrorBoundaryProps): CrashReport {
389
+ const { page, path } = currentPage();
390
+ const extras: Record<string, string> = {};
391
+ try {
392
+ const more = props.reportExtras?.();
393
+ if (more) for (const [k, v] of Object.entries(more)) extras[k] = String(v);
394
+ } catch {
395
+ /* documented: the lines are left out */
396
+ }
397
+ let userAgent = "";
398
+ let online: boolean | undefined;
399
+ try {
400
+ userAgent = navigator.userAgent;
401
+ online = typeof navigator.onLine === "boolean" ? navigator.onLine : undefined;
402
+ } catch {
403
+ /* no navigator (SSR, a worker test) */
404
+ }
405
+ const report: CrashReport = {
406
+ name: details.name,
407
+ message: details.message,
408
+ stack: details.stack,
409
+ componentStack: componentStack ?? undefined,
410
+ page,
411
+ time: new Date().toISOString(),
412
+ userAgent,
413
+ appVersion: props.appVersion,
414
+ online,
415
+ chunkLoad: isChunkLoadError(details),
416
+ extras,
417
+ };
418
+ if (!props.redact) return report;
419
+ try {
420
+ return props.redact(report);
421
+ } catch {
422
+ // Fail closed: an app that asked for redaction must not get the unredacted page.
423
+ return { ...report, page: path, extras: {} };
424
+ }
425
+ }
426
+
427
+ /**
428
+ * Catch a render error below it and show ONE crash screen instead of an empty page.
429
+ *
430
+ * The three apps each built one. kastlan's and keksdose's sat on `EmptyState
431
+ * tone="danger"`; lenkbank's quoted the error and offered a plain-text report to copy;
432
+ * keksdose's filed the crash itself, told a lazy-chunk failure apart as "offline", and
433
+ * offered "clear local data" when a poisoned cache made Retry useless (feedback #160).
434
+ * This is all of that, so the apps reduce to
435
+ * `<ErrorBoundary resetKeys={[pathname]} appVersion={__VERSION__} />` plus, for an app
436
+ * with a crash endpoint, `onReport` and `actions`:
437
+ *
438
+ * - the error's `name: message`, quoted (`showMessage`);
439
+ * - Try again (reset the boundary) AND Reload (the page) — Try again alone re-renders
440
+ * the same subtree from the same state, which a deterministic crash survives;
441
+ * - a chunk-load error or an offline browser gets its own title and Reload first
442
+ * ("A new version is available" / "You're offline");
443
+ * - the copy button and the report in a disclosure; `onReport` for filing it, deduped
444
+ * per page load, with its reference on screen.
445
+ *
446
+ * THE RULE for this fallback — and for a `fallback` or `actions` an app passes in: it
447
+ * must not depend on the router, the query client or the auth store. A crash is exactly
448
+ * when those may be what broke, and a fallback that throws escalates to the next
449
+ * boundary up — at the app level there is none, and the user gets the white screen this
450
+ * exists to prevent. So no `useNavigate`, no `useQuery`, no `useAuth()` in it; reload
451
+ * with `window.location`, not the router. The kit's own label hooks are the one context
452
+ * it does read (`errorBoundary.*` from `UiKitProvider`), and they degrade to the English
453
+ * defaults when there is no provider above — which is why the app-level boundary may
454
+ * sit OUTSIDE the provider and still render.
455
+ *
456
+ * The fallback is `role="alert"`: a page replaced by an error is news, and a reader
457
+ * that was on it is told.
458
+ */
459
+ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
460
+ state: ErrorBoundaryState = CLEAR;
461
+
462
+ /** Bumped by every catch and reset, so a filing that settles after the screen moved
463
+ * on (Try again, a route change, a second crash) does not write into it. */
464
+ private generation = 0;
465
+
466
+ /**
467
+ * Forget which crashes this page load has filed, and the count towards the cap.
468
+ *
469
+ * For tests: the dedupe lives in module scope because a page load is what it spans,
470
+ * and a test file is one page load — so without this, the sixth test that files a
471
+ * report finds the cap reached and `onReport` never called. Call it in `beforeEach`.
472
+ * Never needed in an app: a reload is the reset.
473
+ */
474
+ static forgetReports(): void {
475
+ filings.clear();
476
+ }
477
+
478
+ static getDerivedStateFromError(error: unknown): Partial<ErrorBoundaryState> {
479
+ return { failed: true, error, details: describeThrown(error), report: null, filing: "none", reference: undefined };
480
+ }
481
+
482
+ componentDidCatch(error: unknown, info: ErrorInfo): void {
483
+ try {
484
+ this.props.onError?.(error, info);
485
+ } catch {
486
+ /* documented: a logger must not take the fallback down */
487
+ }
488
+ const details = this.state.details ?? describeThrown(error);
489
+ const report = buildReport(details, info.componentStack ?? undefined, this.props);
490
+ const { onReport } = this.props;
491
+ const willFile = onReport !== undefined && !report.chunkLoad;
492
+ const generation = ++this.generation;
493
+ this.setState({ report, filing: willFile ? "pending" : "none", reference: undefined });
494
+ if (!willFile) return;
495
+ void fileOnce(report, onReport).then((filing) => {
496
+ if (generation !== this.generation) return;
497
+ this.setState(
498
+ filing.filed ? { filing: "filed", reference: filing.reference } : { filing: "failed", reference: undefined },
499
+ );
500
+ });
501
+ }
502
+
503
+ componentDidUpdate(prev: ErrorBoundaryProps, prevState: ErrorBoundaryState): void {
504
+ // Only a change AFTER the error was caught clears it (react-error-boundary's rule).
505
+ // `prevState.failed` is false in the update that caught it: that update may well be
506
+ // the one that changed the keys too — a navigation to a page that throws, with
507
+ // `resetKeys={[pathname]}` — and resetting then would fire `onReset` for nothing and
508
+ // render the failing child a second time. From then on `prev.resetKeys` are the
509
+ // keys the error was shown under, so any change clears it.
510
+ if (this.state.failed && prevState.failed && keysChanged(prev.resetKeys, this.props.resetKeys)) {
511
+ this.reset();
512
+ }
513
+ }
514
+
515
+ reset = (): void => {
516
+ this.generation++;
517
+ this.props.onReset?.();
518
+ this.setState(CLEAR);
519
+ };
520
+
521
+ render(): ReactNode {
522
+ const { failed, error, details, report, filing, reference } = this.state;
523
+ if (!failed || !details) return this.props.children;
524
+ const { fallback } = this.props;
525
+ if (typeof fallback === "function") return fallback({ error, details, reset: this.reset, report });
526
+ if (fallback !== undefined) return fallback;
527
+ return (
528
+ <ErrorFallback
529
+ details={details}
530
+ report={report}
531
+ filing={filing}
532
+ reference={reference}
533
+ reset={this.reset}
534
+ showMessage={this.props.showMessage ?? true}
535
+ showDetails={this.props.showDetails ?? true}
536
+ copyReport={this.props.copyReport ?? true}
537
+ actions={this.props.actions}
538
+ headingAs={this.props.headingAs ?? "h2"}
539
+ className={this.props.className}
540
+ labels={this.props.labels}
541
+ />
542
+ );
543
+ }
544
+ }
545
+
546
+ function ErrorFallback({
547
+ details,
548
+ report,
549
+ filing,
550
+ reference,
551
+ reset,
552
+ showMessage,
553
+ showDetails,
554
+ copyReport,
555
+ actions,
556
+ headingAs,
557
+ className,
558
+ labels,
559
+ }: {
560
+ details: ErrorBoundaryDetails;
561
+ report: CrashReport | null;
562
+ filing: FilingState;
563
+ reference: string | undefined;
564
+ reset: () => void;
565
+ showMessage: boolean;
566
+ showDetails: boolean;
567
+ copyReport: boolean;
568
+ actions?: ReactNode;
569
+ headingAs: NonNullable<ErrorBoundaryProps["headingAs"]>;
570
+ className?: string;
571
+ labels?: Partial<ErrorBoundaryLabels>;
572
+ }) {
573
+ const text = useKitLabels("errorBoundary", DEFAULT_ERROR_BOUNDARY_LABELS, labels);
574
+ const chunk = isChunkLoadError(details);
575
+ // Offline first, as keksdose has it: offline, a chunk failure is the connection, and
576
+ // any other crash most likely a request that could not be made — either way "reload
577
+ // once you are back" is the advice, and "a new version" would be a false one.
578
+ const mode = isOffline() ? "offline" : chunk ? "update" : "error";
579
+ const title = mode === "offline" ? text.offlineTitle : mode === "update" ? text.updateTitle : text.title;
580
+ const hint = mode === "offline" ? text.offlineMessage : mode === "update" ? text.updateMessage : text.message;
581
+ const Icon = mode === "offline" ? WifiOff : mode === "update" ? RefreshCw : AlertTriangle;
582
+ const reportText = report ? formatCrashReport(report) : null;
583
+ const errorLine = details.message ? `${details.name}: ${details.message}` : details.name;
584
+
585
+ const reloadButton = (
586
+ <Button key="reload" variant={mode === "error" ? "secondary" : "primary"} onClick={reloadPage}>
587
+ {text.reload}
588
+ </Button>
589
+ );
590
+ // Not after a chunk failure: `React.lazy` keeps the rejected import, so Try again
591
+ // could only throw the same error again.
592
+ const retryButton = chunk ? null : (
593
+ <Button key="retry" variant={mode === "error" ? "primary" : "secondary"} onClick={reset}>
594
+ {text.retry}
595
+ </Button>
596
+ );
597
+
598
+ return (
599
+ <EmptyState
600
+ role="alert"
601
+ tone={mode === "error" ? "danger" : undefined}
602
+ headingAs={headingAs}
603
+ className={className}
604
+ icon={<Icon />}
605
+ title={title}
606
+ hint={hint}
607
+ action={
608
+ <>
609
+ {showMessage && mode === "error" && (
610
+ // A quotation, not prose: left-aligned and monospaced, so it reads as the
611
+ // program's words and can be read out or screenshotted verbatim.
612
+ <p
613
+ data-error-message=""
614
+ className="-mt-1 mb-1 basis-full whitespace-pre-wrap break-words rounded-md border border-[var(--danger-border)] bg-[var(--danger-bg)] px-3 py-2 text-start font-mono text-xs text-[var(--text-primary)]"
615
+ >
616
+ {errorLine}
617
+ </p>
618
+ )}
619
+ {filing === "filed" && (
620
+ <p className="-mt-1 mb-1 basis-full text-xs">
621
+ {reference ? text.reportedAs(reference) : text.reported}
622
+ </p>
623
+ )}
624
+ {mode === "error" ? [retryButton, reloadButton] : [reloadButton, retryButton]}
625
+ {copyReport && reportText !== null && filing !== "filed" && (
626
+ <CopyButton variant="label" buttonVariant="ghost" label={text.copyReport} text={reportText} />
627
+ )}
628
+ {actions}
629
+ {showDetails && reportText !== null && (
630
+ <Disclosure variant="bare" title={text.details} className="basis-full text-start">
631
+ <pre className="max-h-60 overflow-auto whitespace-pre-wrap break-words text-xs text-[var(--text-secondary)]">
632
+ {reportText}
633
+ </pre>
634
+ </Disclosure>
635
+ )}
636
+ </>
637
+ }
638
+ />
639
+ );
640
+ }