@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,172 @@
1
+ import { useEffect, useRef } from "react";
2
+
3
+ /**
4
+ * A key combination: modifiers and one key, joined by "+", case-insensitive —
5
+ * `"Ctrl+Shift+F"`, `"Mod+K"`, `"Alt+ArrowUp"`, `"Escape"`, `"?"`.
6
+ *
7
+ * Modifiers: `Ctrl` (`Control`), `Meta` (`Cmd`, `Command`), `Shift`, `Alt` (`Option`),
8
+ * and `Mod` — Cmd on Apple platforms, Ctrl everywhere else, the one to use for an
9
+ * app shortcut. The key is `KeyboardEvent.key` ("f", "Enter", "ArrowUp", "/"), or a
10
+ * `KeyboardEvent.code` ("KeyF", "Digit1") when the physical key is what matters.
11
+ * Several combos: pass an array.
12
+ */
13
+ export type Hotkey = string;
14
+
15
+ export interface UseHotkeyOptions {
16
+ /** Off while `false` (default `true`). */
17
+ enabled?: boolean;
18
+ /** Call `preventDefault()` on a match (default `true`): Ctrl+Shift+F must not also
19
+ * open the browser's own dialog. */
20
+ preventDefault?: boolean;
21
+ /** Where to listen. Default `document`. A ref, an element, or `window`. */
22
+ target?: EventTarget | null | { current: EventTarget | null };
23
+ /**
24
+ * Fire while the focus is in a text field, select or contenteditable too. Default
25
+ * `false`: a shortcut must not steal the letters someone is typing. A combo with Ctrl,
26
+ * Meta or Alt is not typing — those still fire unless this is `"never"`.
27
+ */
28
+ allowInInputs?: boolean | "never";
29
+ /** `keydown` (default) or `keyup`. */
30
+ event?: "keydown" | "keyup";
31
+ }
32
+
33
+ export interface ParsedHotkey {
34
+ ctrl: boolean;
35
+ meta: boolean;
36
+ shift: boolean;
37
+ alt: boolean;
38
+ key: string;
39
+ }
40
+
41
+ /** Whether this is an Apple platform, where `Mod` means Cmd. */
42
+ export function isApplePlatform(): boolean {
43
+ if (typeof navigator === "undefined") return false;
44
+ const nav = navigator as Navigator & { userAgentData?: { platform?: string } };
45
+ return /mac|iphone|ipad|ipod/i.test(nav.userAgentData?.platform || nav.platform || nav.userAgent);
46
+ }
47
+
48
+ /** Parse one combo. `apple` decides what `Mod` is. Throws on an empty key. */
49
+ export function parseHotkey(combo: Hotkey, apple = isApplePlatform()): ParsedHotkey {
50
+ const parts = combo
51
+ .split("+")
52
+ .map((p) => p.trim())
53
+ // "Ctrl++" is Ctrl and "+": an empty part after a "+" is the plus key itself.
54
+ .map((p, i, all) => (p === "" && i === all.length - 1 ? "+" : p))
55
+ .filter((p) => p !== "");
56
+ const out: ParsedHotkey = { ctrl: false, meta: false, shift: false, alt: false, key: "" };
57
+ for (const part of parts) {
58
+ switch (part.toLowerCase()) {
59
+ case "ctrl":
60
+ case "control":
61
+ out.ctrl = true;
62
+ break;
63
+ case "meta":
64
+ case "cmd":
65
+ case "command":
66
+ out.meta = true;
67
+ break;
68
+ case "mod":
69
+ if (apple) out.meta = true;
70
+ else out.ctrl = true;
71
+ break;
72
+ case "shift":
73
+ out.shift = true;
74
+ break;
75
+ case "alt":
76
+ case "option":
77
+ out.alt = true;
78
+ break;
79
+ default:
80
+ out.key = part;
81
+ }
82
+ }
83
+ if (!out.key) throw new Error(`useHotkey: "${combo}" names no key`);
84
+ return out;
85
+ }
86
+
87
+ const KEY_ALIASES: Record<string, string> = {
88
+ esc: "escape",
89
+ space: " ",
90
+ del: "delete",
91
+ up: "arrowup",
92
+ down: "arrowdown",
93
+ left: "arrowleft",
94
+ right: "arrowright",
95
+ };
96
+
97
+ /** Whether `e` is `combo` — every modifier as named (no more, no fewer), and the key. */
98
+ export function matchesHotkey(e: KeyboardEvent, combo: ParsedHotkey): boolean {
99
+ if (e.ctrlKey !== combo.ctrl || e.metaKey !== combo.meta || e.altKey !== combo.alt) return false;
100
+ const want = KEY_ALIASES[combo.key.toLowerCase()] ?? combo.key.toLowerCase();
101
+ const key = (e.key ?? "").toLowerCase();
102
+ const code = (e.code ?? "").toLowerCase();
103
+ const keyMatches = key === want || code === want;
104
+ if (!keyMatches) return false;
105
+ // Shift must match exactly for a letter or a named key; for a symbol it is how the
106
+ // symbol is typed ("?" is Shift+/ on a US layout, a plain key on others), so an
107
+ // unnamed Shift is not held against it.
108
+ const symbol = want.length === 1 && !/[a-z0-9]/.test(want);
109
+ return symbol && !combo.shift ? true : e.shiftKey === combo.shift;
110
+ }
111
+
112
+ /** Is the focus somewhere that takes typed characters? */
113
+ function isTypingTarget(target: EventTarget | null): boolean {
114
+ if (!(target instanceof HTMLElement)) return false;
115
+ if (target.isContentEditable) return true;
116
+ if (target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement) return true;
117
+ if (target instanceof HTMLInputElement) {
118
+ return !["button", "checkbox", "radio", "submit", "reset", "range", "color", "file", "image"].includes(target.type);
119
+ }
120
+ return false;
121
+ }
122
+
123
+ /**
124
+ * Call `handler` when `combo` is pressed — kastlan's feedback button (Ctrl+Shift+F,
125
+ * feedback-button:60) wired it by hand, and so will every next shortcut.
126
+ *
127
+ * - `Mod` is Cmd on a Mac and Ctrl elsewhere, read once per mount.
128
+ * - Modifiers match EXACTLY: `"Ctrl+K"` does not fire on Ctrl+Shift+K.
129
+ * - Typing in a field is left alone unless `allowInInputs`, except for a combo with
130
+ * Ctrl / Meta / Alt, which no one types as text.
131
+ * - A key held down fires once, not on every repeat.
132
+ * - The newest `handler` is always called; changing it does not re-attach the listener.
133
+ *
134
+ * `useCommandKey` (the ⌘K palette hook) is unchanged; this is its general form.
135
+ */
136
+ export function useHotkey(
137
+ combo: Hotkey | ReadonlyArray<Hotkey>,
138
+ handler: (event: KeyboardEvent) => void,
139
+ options: UseHotkeyOptions = {},
140
+ ): void {
141
+ const { enabled = true, preventDefault = true, target, allowInInputs = false, event = "keydown" } = options;
142
+ const handlerRef = useRef(handler);
143
+ useEffect(() => {
144
+ handlerRef.current = handler;
145
+ });
146
+ const key = typeof combo === "string" ? combo : combo.join("\u0000");
147
+ const targetEl = target && "current" in target ? undefined : target;
148
+ const targetRef = target && "current" in target ? target : undefined;
149
+
150
+ useEffect(() => {
151
+ if (!enabled) return;
152
+ const node: EventTarget | null =
153
+ targetRef?.current ?? targetEl ?? (typeof document === "undefined" ? null : document);
154
+ if (!node) return;
155
+ const apple = isApplePlatform();
156
+ const parsed = key.split("\u0000").map((c) => parseHotkey(c, apple));
157
+ const listener = (raw: Event) => {
158
+ const e = raw as KeyboardEvent;
159
+ if (e.repeat) return;
160
+ const hit = parsed.find((p) => matchesHotkey(e, p));
161
+ if (!hit) return;
162
+ if (allowInInputs !== true && isTypingTarget(e.target)) {
163
+ const chorded = hit.ctrl || hit.meta || hit.alt;
164
+ if (allowInInputs === "never" || !chorded) return;
165
+ }
166
+ if (preventDefault) e.preventDefault();
167
+ handlerRef.current(e);
168
+ };
169
+ node.addEventListener(event, listener);
170
+ return () => node.removeEventListener(event, listener);
171
+ }, [key, enabled, preventDefault, allowInInputs, event, targetEl, targetRef]);
172
+ }
@@ -0,0 +1,187 @@
1
+ import { useLocation, useNavigate, useSearchParams } from "react-router";
2
+
3
+ type Updater<T> = T | ((prev: T) => T);
4
+
5
+ export interface SearchParamStateOptions<T> {
6
+ /**
7
+ * The value of a param that IS present. Return `undefined` (or throw) for a value
8
+ * that does not parse — a hand-edited or stale link — and the default is used.
9
+ * Default: the raw string.
10
+ */
11
+ parse?: (raw: string) => T | undefined;
12
+ /**
13
+ * The param for a value, or `null` to remove it. Default: `String(value)`, and
14
+ * `null` for `null` / `undefined`. A value that serialises the same as the default
15
+ * removes the param too, so the default is always the clean URL.
16
+ */
17
+ serialize?: (value: T) => string | null;
18
+ /** Shorthand for `history: "replace"`. */
19
+ replace?: boolean;
20
+ /**
21
+ * What a change does to the browser history:
22
+ * - `"push"` (default): a new entry, so Back undoes the change.
23
+ * - `"replace"`: the current entry is rewritten — a tab, a sort, a view toggle,
24
+ * which are not steps anyone wants Back to walk through.
25
+ * - `"replace-on-clear"`: push when a value is set, replace when it is cleared (the
26
+ * "open a form, close it" pattern).
27
+ */
28
+ history?: "push" | "replace" | "replace-on-clear";
29
+ }
30
+
31
+ const defaultSerialize = (value: unknown): string | null =>
32
+ value === null || value === undefined ? null : String(value);
33
+
34
+ /** The route state of the current entry, carried over a `replace` so a query rewrite
35
+ * does not drop what a navigation put there (a dialog's opener mark, below). */
36
+ function carriedState(state: unknown): Record<string, unknown> | undefined {
37
+ return state && typeof state === "object" ? (state as Record<string, unknown>) : undefined;
38
+ }
39
+
40
+ /**
41
+ * One URL search param as React state: `const [view, setView] = useSearchParamState("view", "list")`.
42
+ *
43
+ * Replaces the hand-rolled `setParams((prev) => { const p = new URLSearchParams(prev);
44
+ * …set/delete…; return p })` blocks that keksdose had in a dozen pages and kastlan ported
45
+ * as its own copy of this hook (shared/hooks/use-search-param-state.ts, both apps). The
46
+ * kit's version takes the DEFAULT as an argument rather than inside `parse`, so the
47
+ * two rules that follow from it cannot be forgotten at a call site:
48
+ *
49
+ * * an absent or unparseable param reads as the default, and
50
+ * * setting the default removes the param — the default view is the clean URL.
51
+ *
52
+ * Every other param in the query string is left as it is. Two updates in one tick do
53
+ * not compose — react-router resolves each against the params of the last render.
54
+ * Requires a react-router Router.
55
+ */
56
+ export function useSearchParamState<T = string>(
57
+ key: string,
58
+ defaultValue: T,
59
+ options: SearchParamStateOptions<T> = {},
60
+ ): [T, (next: Updater<T>) => void] {
61
+ const { parse, serialize = defaultSerialize as (value: T) => string | null } = options;
62
+ const history = options.history ?? (options.replace ? "replace" : "push");
63
+ const [params, setParams] = useSearchParams();
64
+ const location = useLocation();
65
+
66
+ const read = (raw: string | null): T => {
67
+ if (raw === null) return defaultValue;
68
+ if (!parse) return raw as unknown as T;
69
+ try {
70
+ const parsed = parse(raw);
71
+ return parsed === undefined ? defaultValue : parsed;
72
+ } catch {
73
+ return defaultValue;
74
+ }
75
+ };
76
+ const value = read(params.get(key));
77
+ const defaultSerialized = serialize(defaultValue);
78
+ const toParam = (v: T): string | null => {
79
+ const s = serialize(v);
80
+ return s === defaultSerialized ? null : s;
81
+ };
82
+
83
+ const setValue = (next: Updater<T>) => {
84
+ // Resolved against the rendered value only to choose the history mode; the write
85
+ // resolves again inside react-router's updater, against the params it hands over.
86
+ const preview = typeof next === "function" ? (next as (prev: T) => T)(value) : next;
87
+ const replace = history === "replace" || (history === "replace-on-clear" && toParam(preview) === null);
88
+ setParams(
89
+ (prev) => {
90
+ const p = new URLSearchParams(prev);
91
+ const resolved = typeof next === "function" ? (next as (prev: T) => T)(read(p.get(key))) : next;
92
+ const serialized = toParam(resolved);
93
+ if (serialized === null) p.delete(key);
94
+ else p.set(key, serialized);
95
+ return p;
96
+ },
97
+ replace ? { replace, state: carriedState(location.state) } : undefined,
98
+ );
99
+ };
100
+
101
+ return [value, setValue];
102
+ }
103
+
104
+ /**
105
+ * The active tab in `?tab=` (or `key`), with the default tab as the clean URL, for the
106
+ * kit's `Tabs`:
107
+ *
108
+ * const [tab, setTab] = useTabParam("overview", { tabs: ["overview", "history"] });
109
+ * <Tabs tabs={…} active={tab} onChange={setTab} />
110
+ *
111
+ * Switching tabs REPLACES the history entry — a tab is a view of this page, not a
112
+ * place Back should step through. `tabs`, when given, turns a param naming no tab (a
113
+ * renamed tab, an old link) into the default rather than into a blank panel.
114
+ * kastlan's use-tab-param.ts, with the validation it did not have.
115
+ */
116
+ export function useTabParam<T extends string>(
117
+ defaultTab: T,
118
+ options: { key?: string; tabs?: readonly T[] } = {},
119
+ ): [T, (tab: T) => void] {
120
+ const { key = "tab", tabs } = options;
121
+ return useSearchParamState<T>(key, defaultTab, {
122
+ parse: (raw) => (!tabs || (tabs as readonly string[]).includes(raw) ? (raw as T) : undefined),
123
+ history: "replace",
124
+ });
125
+ }
126
+
127
+ /** Where a dialog's open state lives: a param's presence (`"edit"` → `?edit=1`), or one
128
+ * value of a shared param (`{ key: "dialog", value: "create-lease" }`). */
129
+ export type DialogParam = string | { key: string; value: string };
130
+
131
+ /** The route-state mark an opener leaves on the entry it pushes, so the close knows it
132
+ * may go BACK over it rather than add a second entry. */
133
+ const OPENED_BY_KIT = "__uiKitDialogParam";
134
+
135
+ /**
136
+ * A dialog's open state in the URL, for `Modal`'s `urlParam` and anything else that
137
+ * opens one: `const [open, setOpen] = useDialogParam({ key: "dialog", value: "create" })`.
138
+ *
139
+ * * **Opening pushes** an entry, so the platform Back gesture closes the dialog — by
140
+ * leaving the entry — and Forward opens it again, like any other step.
141
+ * * **Closing goes back** over that entry when this hook pushed it, so a closed dialog
142
+ * leaves no dead entry behind. A dialog that arrived OPEN (a deep link, a reload, a
143
+ * plain `<Link>` to it) has nothing of ours to go back over, and its close removes the
144
+ * param in place instead — Back from there leaves the page, as it should.
145
+ *
146
+ * That is why it is not `useSearchParamState`: going back is the only close that
147
+ * leaves the history as it was before the dialog, and only the opener knows whether it
148
+ * may.
149
+ */
150
+ export function useDialogParam(param: DialogParam): [boolean, (open: boolean) => void] {
151
+ const { key, value } = typeof param === "string" ? { key: param, value: "1" } : param;
152
+ const [params, setParams] = useSearchParams();
153
+ const location = useLocation();
154
+ const navigate = useNavigate();
155
+ const open = typeof param === "string" ? params.has(key) : params.get(key) === value;
156
+ const mark = `${key}=${value}`;
157
+
158
+ const setOpen = (next: boolean) => {
159
+ if (next === open) return;
160
+ const state = carriedState(location.state);
161
+ if (next) {
162
+ setParams(
163
+ (prev) => {
164
+ const p = new URLSearchParams(prev);
165
+ p.set(key, value);
166
+ return p;
167
+ },
168
+ { state: { ...state, [OPENED_BY_KIT]: mark } },
169
+ );
170
+ return;
171
+ }
172
+ if (state?.[OPENED_BY_KIT] === mark) {
173
+ void navigate(-1);
174
+ return;
175
+ }
176
+ setParams(
177
+ (prev) => {
178
+ const p = new URLSearchParams(prev);
179
+ p.delete(key);
180
+ return p;
181
+ },
182
+ { replace: true, state },
183
+ );
184
+ };
185
+
186
+ return [open, setOpen];
187
+ }
@@ -12,6 +12,7 @@ import { DEFAULT_TOUR_LABELS } from "../tour/tour";
12
12
  import { DEFAULT_COMMAND_PALETTE_LABELS } from "../search/command-palette";
13
13
  import { DEFAULT_GLOBAL_SEARCH_LABELS } from "../search/global-search";
14
14
  import { DEFAULT_SERIES_CHART_LABELS } from "../components/series-chart-labels";
15
+ import { DEFAULT_PIE_CHART_LABELS } from "../components/pie-chart-labels";
15
16
  import { DEFAULT_SPARKLINE_LABELS } from "../components/sparkline";
16
17
  import { DEFAULT_STAT_TILE_LABELS } from "../components/stat-tile";
17
18
  import { DEFAULT_SIGNATURE_PAD_LABELS } from "../components/signature-pad";
@@ -25,11 +26,26 @@ import { DEFAULT_MEASURED_GRID_LABELS } from "../components/measured-grid";
25
26
  import { DEFAULT_CONFIRM_DIALOG_LABELS } from "../components/confirm-dialog";
26
27
  import { DEFAULT_FLOATING_PANEL_LABELS } from "../components/floating-panel";
27
28
  import { DEFAULT_FEEDBACK_ATTACHMENT_LABELS } from "../feedback/feedback-attachment";
29
+ import { DEFAULT_FEEDBACK_DIALOG_LABELS } from "../feedback/feedback-dialog";
30
+ import {
31
+ DEFAULT_FEEDBACK_COMPOSER_LABELS,
32
+ DEFAULT_FEEDBACK_THREAD_LABELS,
33
+ } from "../feedback/feedback-thread";
34
+ import { DEFAULT_ACCOUNT_SETTINGS_LABELS } from "../components/account-settings-labels";
28
35
  import { DEFAULT_COPY_BUTTON_LABELS } from "../components/copy-button";
29
36
  import { DEFAULT_BULK_ACTION_BAR_LABELS } from "../components/bulk-action-bar";
30
37
  import { DEFAULT_LIST_LABELS } from "../components/list";
31
38
  import { DEFAULT_BREADCRUMBS_LABELS } from "../components/breadcrumbs";
32
39
  import { DEFAULT_TOAST_LABELS } from "../components/toast";
40
+ import { DEFAULT_FORM_ACTIONS_LABELS } from "../components/form-actions";
41
+ import { DEFAULT_DESCRIPTION_LIST_LABELS } from "../components/description-list";
42
+ import { DEFAULT_LINE_ITEMS_LABELS } from "../components/line-items";
43
+ import { DEFAULT_PROGRESS_BAR_LABELS } from "../components/progress-bar";
44
+ import { DEFAULT_SIGNED_AMOUNT_LABELS } from "../components/signed-amount";
45
+ import { DEFAULT_ERROR_BOUNDARY_LABELS } from "../components/error-boundary";
46
+ import { DEFAULT_AUTHED_IMAGE_LABELS } from "../components/authed-image";
47
+ import { DEFAULT_IMAGE_GRID_LABELS } from "../components/image-grid";
48
+ import { DEFAULT_LIGHTBOX_LABELS } from "../components/lightbox";
33
49
  import {
34
50
  DEFAULT_APP_SHELL_LABELS,
35
51
  DEFAULT_CALCULATOR_LABELS,
@@ -80,6 +96,7 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
80
96
  commandPalette: DEFAULT_COMMAND_PALETTE_LABELS,
81
97
  globalSearch: DEFAULT_GLOBAL_SEARCH_LABELS,
82
98
  seriesChart: DEFAULT_SERIES_CHART_LABELS,
99
+ pieChart: DEFAULT_PIE_CHART_LABELS,
83
100
  sparkline: DEFAULT_SPARKLINE_LABELS,
84
101
  statTile: DEFAULT_STAT_TILE_LABELS,
85
102
  signaturePad: DEFAULT_SIGNATURE_PAD_LABELS,
@@ -91,6 +108,10 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
91
108
  filePicker: DEFAULT_FILE_PICKER_LABELS,
92
109
  measuredGrid: DEFAULT_MEASURED_GRID_LABELS,
93
110
  feedbackAttachment: DEFAULT_FEEDBACK_ATTACHMENT_LABELS,
111
+ feedbackDialog: DEFAULT_FEEDBACK_DIALOG_LABELS,
112
+ feedbackThread: DEFAULT_FEEDBACK_THREAD_LABELS,
113
+ feedbackComposer: DEFAULT_FEEDBACK_COMPOSER_LABELS,
114
+ accountSettings: DEFAULT_ACCOUNT_SETTINGS_LABELS,
94
115
  confirmDialog: DEFAULT_CONFIRM_DIALOG_LABELS,
95
116
  floatingPanel: DEFAULT_FLOATING_PANEL_LABELS,
96
117
  copyButton: DEFAULT_COPY_BUTTON_LABELS,
@@ -98,4 +119,13 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
98
119
  list: DEFAULT_LIST_LABELS,
99
120
  breadcrumbs: DEFAULT_BREADCRUMBS_LABELS,
100
121
  toast: DEFAULT_TOAST_LABELS,
122
+ form: DEFAULT_FORM_ACTIONS_LABELS,
123
+ descriptionList: DEFAULT_DESCRIPTION_LIST_LABELS,
124
+ lineItems: DEFAULT_LINE_ITEMS_LABELS,
125
+ progressBar: DEFAULT_PROGRESS_BAR_LABELS,
126
+ signedAmount: DEFAULT_SIGNED_AMOUNT_LABELS,
127
+ errorBoundary: DEFAULT_ERROR_BOUNDARY_LABELS,
128
+ authedImage: DEFAULT_AUTHED_IMAGE_LABELS,
129
+ imageGrid: DEFAULT_IMAGE_GRID_LABELS,
130
+ lightbox: DEFAULT_LIGHTBOX_LABELS,
101
131
  };
@@ -1,4 +1,5 @@
1
1
  import { createContext, useContext, useMemo } from "react";
2
+ import type { AnchorHTMLAttributes, ReactElement, Ref } from "react";
2
3
  import type { ReactNode } from "react";
3
4
  import type { DataTableLabels } from "../components/data-table-labels";
4
5
  import type { MiniCalendarLabels, WeekDay } from "../components/mini-calendar";
@@ -14,6 +15,7 @@ import type { GlobalSearchLabels } from "../search/global-search";
14
15
  import type { MonthPickerLabels } from "../components/month-picker";
15
16
  import type { PageContentsLabels } from "../components/page-contents";
16
17
  import type { SeriesChartLabels } from "../components/series-chart-labels";
18
+ import type { PieChartLabels } from "../components/pie-chart-labels";
17
19
  import type { SparklineLabels } from "../components/sparkline";
18
20
  import type { StatTileLabels } from "../components/stat-tile";
19
21
  import type { SignaturePadLabels } from "../components/signature-pad";
@@ -25,6 +27,9 @@ import type { DialogFrameLabels } from "../components/dialog-frame";
25
27
  import type { FilePickerLabels } from "../components/file-button";
26
28
  import type { MeasuredGridLabels } from "../components/measured-grid";
27
29
  import type { FeedbackAttachmentFieldLabels } from "../feedback/feedback-attachment";
30
+ import type { FeedbackDialogTextLabels } from "../feedback/feedback-dialog";
31
+ import type { FeedbackComposerLabels, FeedbackThreadLabels } from "../feedback/feedback-thread";
32
+ import type { AccountSettingsLabels } from "../components/account-settings-labels";
28
33
  import type { ConfirmDialogLabels } from "../components/confirm-dialog";
29
34
  import type { FloatingPanelLabels } from "../components/floating-panel";
30
35
  import type { CopyButtonLabels } from "../components/copy-button";
@@ -32,6 +37,15 @@ import type { BulkActionBarLabels } from "../components/bulk-action-bar";
32
37
  import type { ListLabels } from "../components/list";
33
38
  import type { BreadcrumbsLabels } from "../components/breadcrumbs";
34
39
  import type { ToastLabels } from "../components/toast";
40
+ import type { FormActionsLabels } from "../components/form-actions";
41
+ import type { DescriptionListLabels } from "../components/description-list";
42
+ import type { LineItemsLabels } from "../components/line-items";
43
+ import type { ProgressBarLabels } from "../components/progress-bar";
44
+ import type { SignedAmountLabels } from "../components/signed-amount";
45
+ import type { ErrorBoundaryLabels } from "../components/error-boundary";
46
+ import type { AuthedImageLabels } from "../components/authed-image";
47
+ import type { ImageGridLabels } from "../components/image-grid";
48
+ import type { LightboxLabels } from "../components/lightbox";
35
49
 
36
50
  /**
37
51
  * EVERY string the kit renders, as one typed tree — and an optional provider that
@@ -84,6 +98,9 @@ export interface CommonLabels {
84
98
  /** The × that puts away a banner or a notice (`AlertBanner onDismiss`). Not `close`:
85
99
  * nothing opened, and "Close" on a banner reads as closing the page it sits on. */
86
100
  dismiss: string;
101
+ /** Read after a link that opens a new tab (`<Button href external>`, TextLink):
102
+ * nothing else tells a screen reader the page is about to change tabs. */
103
+ opensInNewTab: string;
87
104
  }
88
105
 
89
106
  /** `DatePicker` / `DateRangePicker` chrome. The calendar inside has its own
@@ -221,6 +238,7 @@ export interface UiKitLabels {
221
238
  commandPalette: CommandPaletteLabels;
222
239
  globalSearch: GlobalSearchLabels;
223
240
  seriesChart: SeriesChartLabels;
241
+ pieChart: PieChartLabels;
224
242
  sparkline: SparklineLabels;
225
243
  statTile: StatTileLabels;
226
244
  signaturePad: SignaturePadLabels;
@@ -232,6 +250,14 @@ export interface UiKitLabels {
232
250
  filePicker: FilePickerLabels;
233
251
  measuredGrid: MeasuredGridLabels;
234
252
  feedbackAttachment: FeedbackAttachmentFieldLabels;
253
+ /** 0.12.0: `FeedbackDialog`'s own strings; `FeedbackThread` / `FeedbackComposer`. */
254
+ feedbackDialog: FeedbackDialogTextLabels;
255
+ feedbackThread: FeedbackThreadLabels;
256
+ feedbackComposer: FeedbackComposerLabels;
257
+ /** 0.12.0: `ProfileSetting`, `PasswordSetting`, `TwoFactorSetting`, `PasskeysSetting`
258
+ * — one record per section. The provider takes a whole section record; the
259
+ * components merge it key by key, as a prop. */
260
+ accountSettings: AccountSettingsLabels;
235
261
  confirmDialog: ConfirmDialogLabels;
236
262
  floatingPanel: FloatingPanelLabels;
237
263
  copyButton: CopyButtonLabels;
@@ -239,11 +265,36 @@ export interface UiKitLabels {
239
265
  list: ListLabels;
240
266
  breadcrumbs: BreadcrumbsLabels;
241
267
  toast: ToastLabels;
268
+ form: FormActionsLabels;
269
+ descriptionList: DescriptionListLabels;
270
+ lineItems: LineItemsLabels;
271
+ progressBar: ProgressBarLabels;
272
+ signedAmount: SignedAmountLabels;
273
+ errorBoundary: ErrorBoundaryLabels;
274
+ authedImage: AuthedImageLabels;
275
+ imageGrid: ImageGridLabels;
276
+ lightbox: LightboxLabels;
242
277
  }
243
278
 
244
- /** Any subset of the tree, one level deep — each namespace may be partial, and a
245
- * namespace that holds a record (`dataTable.presets`) is merged key by key. */
246
- export type UiKitLabelOverrides = { [K in keyof UiKitLabels]?: Partial<UiKitLabels[K]> };
279
+ /**
280
+ * A label override, as deep as the labels go: an object of labels (a namespace, or a
281
+ * record inside one such as `accountSettings.passkeys` or `dataTable.presets`) may be
282
+ * partial at every level, because the provider merges it key by key at every level.
283
+ * A leaf stays as it is: a string, a function-valued label (`(n) => …`), and anything
284
+ * that is not purely an object (a `ReactNode` union) is given whole.
285
+ */
286
+ export type LabelOverride<T> = [T] extends [(...args: never[]) => unknown]
287
+ ? T
288
+ : [T] extends [readonly unknown[]]
289
+ ? T
290
+ : [T] extends [object]
291
+ ? { [K in keyof T]?: LabelOverride<T[K]> }
292
+ : T;
293
+
294
+ /** Any subset of the tree — each namespace may be partial, and so may any record
295
+ * inside one (`accountSettings.passkeys`, `dataTable.presets`): they are merged key
296
+ * by key, at every depth. */
297
+ export type UiKitLabelOverrides = { [K in keyof UiKitLabels]?: LabelOverride<UiKitLabels[K]> };
247
298
 
248
299
  /* ── English defaults for the namespaces defined here ────────────────────── */
249
300
 
@@ -261,6 +312,7 @@ export const DEFAULT_COMMON_LABELS: CommonLabels = {
261
312
  noResults: "No results",
262
313
  fieldValue: (field, value) => `${field}: ${value}`,
263
314
  dismiss: "Dismiss",
315
+ opensInNewTab: "opens in a new tab",
264
316
  };
265
317
 
266
318
  export const DEFAULT_DATE_PICKER_LABELS: DatePickerLabels = {
@@ -364,8 +416,36 @@ interface KitI18n {
364
416
  labels?: UiKitLabelOverrides;
365
417
  locale?: string;
366
418
  weekStartsOn?: WeekDay;
419
+ linkComponent?: KitLinkComponent;
367
420
  }
368
421
 
422
+ /**
423
+ * What every kit link receives: the anchor attributes the kit decided (href, class,
424
+ * aria-current, handlers, children, ref). An app's router link maps them —
425
+ * `({ href, ...p }) => <Link to={href} {...p} />` — ONCE, on the provider.
426
+ *
427
+ * `replace` is the one prop that is not an anchor attribute: see there. Map it
428
+ * explicitly if your router link is not react-router's —
429
+ * `({ href, replace, ...p }) => <Link to={href} replace={replace} {...p} />` — and never
430
+ * spread it onto a DOM `<a>`, where React warns about an unknown attribute.
431
+ */
432
+ export type KitLinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & {
433
+ href: string;
434
+ ref?: Ref<HTMLAnchorElement>;
435
+ /**
436
+ * Navigate by REPLACING the current history entry, so Back skips it — keksdose F1:
437
+ * a one-shot URL (a magic-link landing, `?checkout=done`, a wizard's "finish" hop)
438
+ * that must not come back when the user presses Back. Present ONLY when the kit link
439
+ * was given `replace` (`true`), so a provider link that spreads its props onto a
440
+ * plain `<a>` keeps working for every link that never asks for it. react-router's
441
+ * `<Link>` takes the same prop, so `({ href, ...p }) => <Link to={href} {...p} />`
442
+ * honours it with no change; any other router maps it to its own replace option.
443
+ * The kit never puts it on a DOM element itself.
444
+ */
445
+ replace?: boolean;
446
+ };
447
+ export type KitLinkComponent = (props: KitLinkProps) => ReactElement;
448
+
369
449
  const KitI18nContext = createContext<KitI18n>({});
370
450
 
371
451
  export interface UiKitProviderProps {
@@ -387,6 +467,14 @@ export interface UiKitProviderProps {
387
467
  * and switching it to `en-GB` to get Monday changes every date and number format.
388
468
  */
389
469
  weekStartsOn?: WeekDay;
470
+ /**
471
+ * The app's router link, used by every kit component that renders an in-app link
472
+ * and was not handed its own `renderLink` — so `renderLink` stops being repeated on
473
+ * each ListItem, Breadcrumbs, StatTile, Chip… (kastlan). A component's own
474
+ * `renderLink` wins; external links and in-page `#anchor`s stay plain `<a>`, and a
475
+ * hash-router `#/path` is handed to it like `/path` (see `pickLinkRenderer`).
476
+ */
477
+ linkComponent?: KitLinkComponent;
390
478
  children: ReactNode;
391
479
  }
392
480
 
@@ -398,15 +486,22 @@ export interface UiKitProviderProps {
398
486
  * it names, so a page can re-label one table's `dataTable.table` without restating
399
487
  * the language.
400
488
  */
401
- export function UiKitProvider({ labels, locale, weekStartsOn, children }: UiKitProviderProps) {
489
+ export function UiKitProvider({
490
+ labels,
491
+ locale,
492
+ weekStartsOn,
493
+ linkComponent,
494
+ children,
495
+ }: UiKitProviderProps) {
402
496
  const outer = useContext(KitI18nContext);
403
497
  const value = useMemo<KitI18n>(
404
498
  () => ({
405
499
  locale: locale ?? outer.locale,
406
500
  weekStartsOn: weekStartsOn ?? outer.weekStartsOn,
407
501
  labels: mergeOverrides(outer.labels, labels),
502
+ linkComponent: linkComponent ?? outer.linkComponent,
408
503
  }),
409
- [outer, labels, locale, weekStartsOn],
504
+ [outer, labels, locale, weekStartsOn, linkComponent],
410
505
  );
411
506
  return <KitI18nContext.Provider value={value}>{children}</KitI18nContext.Provider>;
412
507
  }
@@ -415,14 +510,23 @@ function isRecord(v: unknown): v is Record<string, unknown> {
415
510
  return typeof v === "object" && v !== null && !Array.isArray(v);
416
511
  }
417
512
 
418
- /** Two levels: namespaces, and inside a namespace a record-valued key (presets). */
419
- function mergeNamespace<T extends object>(base: T, over: Partial<T> | undefined): T {
513
+ /** A record of labels to merge into — not a React element, which is an object too but
514
+ * is a label given whole. */
515
+ function isLabelRecord(v: unknown): v is Record<string, unknown> {
516
+ return isRecord(v) && !("$$typeof" in v);
517
+ }
518
+
519
+ /** Merged key by key at EVERY depth — a namespace, a record inside it
520
+ * (`accountSettings.passkeys`, `dataTable.presets`), and so on down — to match
521
+ * {@link LabelOverride}. An `undefined` is skipped at every depth, so it never blanks
522
+ * out what is under it. */
523
+ function mergeNamespace<T extends object>(base: T, over: LabelOverride<T> | Partial<T> | undefined): T {
420
524
  if (!over) return base;
421
525
  const out = { ...base } as Record<string, unknown>;
422
526
  for (const [k, v] of Object.entries(over)) {
423
527
  if (v === undefined) continue;
424
528
  const prev = out[k];
425
- out[k] = isRecord(prev) && isRecord(v) ? { ...prev, ...v } : v;
529
+ out[k] = isLabelRecord(prev) && isLabelRecord(v) ? mergeNamespace(prev, v) : v;
426
530
  }
427
531
  return out as T;
428
532
  }
@@ -475,6 +579,12 @@ export function useKitLocale(prop?: string): string | undefined {
475
579
  /** The week start the nearest `<UiKitProvider weekStartsOn>` pins, else `undefined`
476
580
  * (the caller then asks the locale). A component's own prop goes first:
477
581
  * `prop ?? useKitWeekStart()`. */
582
+ /** The provider's router link, or `undefined` — a component's own `renderLink` should
583
+ * win over it: `const Link = renderLink ?? useKitLink()`. */
584
+ export function useKitLink(): KitLinkComponent | undefined {
585
+ return useContext(KitI18nContext).linkComponent;
586
+ }
587
+
478
588
  export function useKitWeekStart(): WeekDay | undefined {
479
589
  return useContext(KitI18nContext).weekStartsOn;
480
590
  }