@eifi1/ui-kit 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (382) hide show
  1. package/README.md +66 -37
  2. package/dist/chart.d.ts +3 -1
  3. package/dist/chart.js +2 -0
  4. package/dist/chart.js.map +1 -1
  5. package/dist/components/account-settings-labels.d.ts +105 -0
  6. package/dist/components/account-settings-labels.js +81 -0
  7. package/dist/components/account-settings-labels.js.map +1 -0
  8. package/dist/components/account-settings.d.ts +36 -52
  9. package/dist/components/account-settings.js +44 -8
  10. package/dist/components/account-settings.js.map +1 -1
  11. package/dist/components/alert-banner.d.ts +26 -1
  12. package/dist/components/alert-banner.js +10 -7
  13. package/dist/components/alert-banner.js.map +1 -1
  14. package/dist/components/amount-input.d.ts +26 -4
  15. package/dist/components/amount-input.js +5 -2
  16. package/dist/components/amount-input.js.map +1 -1
  17. package/dist/components/authed-image.d.ts +68 -0
  18. package/dist/components/authed-image.js +117 -0
  19. package/dist/components/authed-image.js.map +1 -0
  20. package/dist/components/breadcrumbs.d.ts +2 -1
  21. package/dist/components/breadcrumbs.js +5 -2
  22. package/dist/components/breadcrumbs.js.map +1 -1
  23. package/dist/components/bulk-action-bar.d.ts +51 -5
  24. package/dist/components/bulk-action-bar.js +88 -41
  25. package/dist/components/bulk-action-bar.js.map +1 -1
  26. package/dist/components/button-group.d.ts +107 -3
  27. package/dist/components/button-group.js +60 -5
  28. package/dist/components/button-group.js.map +1 -1
  29. package/dist/components/calculator.d.ts +17 -4
  30. package/dist/components/calendar-heatmap.d.ts +162 -0
  31. package/dist/components/calendar-heatmap.js +336 -0
  32. package/dist/components/calendar-heatmap.js.map +1 -0
  33. package/dist/components/chip.d.ts +2 -1
  34. package/dist/components/chip.js +6 -3
  35. package/dist/components/chip.js.map +1 -1
  36. package/dist/components/choice-card.d.ts +66 -2
  37. package/dist/components/choice-card.js +108 -1
  38. package/dist/components/choice-card.js.map +1 -1
  39. package/dist/components/combobox-core.js +2 -0
  40. package/dist/components/combobox-core.js.map +1 -1
  41. package/dist/components/combobox.d.ts +2 -2
  42. package/dist/components/combobox.js +31 -9
  43. package/dist/components/combobox.js.map +1 -1
  44. package/dist/components/copy-button.d.ts +52 -70
  45. package/dist/components/copy-button.js +1 -1
  46. package/dist/components/copy-button.js.map +1 -1
  47. package/dist/components/data-table-cells.d.ts +63 -0
  48. package/dist/components/data-table-cells.js +64 -0
  49. package/dist/components/data-table-cells.js.map +1 -0
  50. package/dist/components/data-table-filter-popover.d.ts +1 -1
  51. package/dist/components/data-table-filters.d.ts +1 -1
  52. package/dist/components/data-table-filters.js +31 -1
  53. package/dist/components/data-table-filters.js.map +1 -1
  54. package/dist/components/data-table-labels.d.ts +13 -0
  55. package/dist/components/data-table-labels.js +8 -1
  56. package/dist/components/data-table-labels.js.map +1 -1
  57. package/dist/components/data-table.d.ts +1 -1
  58. package/dist/components/data-table.js +104 -12
  59. package/dist/components/data-table.js.map +1 -1
  60. package/dist/components/date-picker.d.ts +3 -1
  61. package/dist/components/date-picker.js +15 -2
  62. package/dist/components/date-picker.js.map +1 -1
  63. package/dist/components/description-list.d.ts +23 -3
  64. package/dist/components/description-list.js +23 -3
  65. package/dist/components/description-list.js.map +1 -1
  66. package/dist/components/dialog-frame.d.ts +1 -0
  67. package/dist/components/entity-combobox.d.ts +1 -1
  68. package/dist/components/entity-combobox.js +18 -3
  69. package/dist/components/entity-combobox.js.map +1 -1
  70. package/dist/components/error-boundary.d.ts +92 -0
  71. package/dist/components/error-boundary.js +111 -0
  72. package/dist/components/error-boundary.js.map +1 -0
  73. package/dist/components/field.d.ts +69 -0
  74. package/dist/components/field.js +75 -0
  75. package/dist/components/field.js.map +1 -0
  76. package/dist/components/file-button.d.ts +51 -208
  77. package/dist/components/file-dropzone.d.ts +50 -2
  78. package/dist/components/floating-panel.d.ts +156 -4
  79. package/dist/components/floating-panel.js +193 -29
  80. package/dist/components/floating-panel.js.map +1 -1
  81. package/dist/components/form-actions.d.ts +53 -0
  82. package/dist/components/form-actions.js +98 -0
  83. package/dist/components/form-actions.js.map +1 -0
  84. package/dist/components/image-grid.d.ts +67 -0
  85. package/dist/components/image-grid.js +112 -0
  86. package/dist/components/image-grid.js.map +1 -0
  87. package/dist/components/lightbox.d.ts +104 -0
  88. package/dist/components/lightbox.js +229 -0
  89. package/dist/components/lightbox.js.map +1 -0
  90. package/dist/components/line-items.d.ts +114 -0
  91. package/dist/components/line-items.js +238 -0
  92. package/dist/components/line-items.js.map +1 -0
  93. package/dist/components/list.d.ts +37 -3
  94. package/dist/components/list.js +39 -14
  95. package/dist/components/list.js.map +1 -1
  96. package/dist/components/loading-state.d.ts +24 -0
  97. package/dist/components/loading-state.js +32 -0
  98. package/dist/components/loading-state.js.map +1 -0
  99. package/dist/components/menu-item.d.ts +13 -2
  100. package/dist/components/menu-item.js +11 -2
  101. package/dist/components/menu-item.js.map +1 -1
  102. package/dist/components/mini-calendar.d.ts +82 -5
  103. package/dist/components/mini-calendar.js +212 -64
  104. package/dist/components/mini-calendar.js.map +1 -1
  105. package/dist/components/modal.d.ts +20 -1
  106. package/dist/components/modal.js +26 -2
  107. package/dist/components/modal.js.map +1 -1
  108. package/dist/components/month-picker.d.ts +21 -1
  109. package/dist/components/month-picker.js +139 -26
  110. package/dist/components/month-picker.js.map +1 -1
  111. package/dist/components/multi-entity-combobox.d.ts +1 -1
  112. package/dist/components/multi-entity-combobox.js +19 -3
  113. package/dist/components/multi-entity-combobox.js.map +1 -1
  114. package/dist/components/nav-pills.d.ts +67 -0
  115. package/dist/components/nav-pills.js +69 -0
  116. package/dist/components/nav-pills.js.map +1 -0
  117. package/dist/components/number-field.d.ts +26 -5
  118. package/dist/components/number-field.js +5 -7
  119. package/dist/components/number-field.js.map +1 -1
  120. package/dist/components/number-input.d.ts +27 -5
  121. package/dist/components/number-input.js +7 -1
  122. package/dist/components/number-input.js.map +1 -1
  123. package/dist/components/numpad-sheet.d.ts +17 -4
  124. package/dist/components/passkeys-setting.d.ts +67 -0
  125. package/dist/components/passkeys-setting.js +214 -0
  126. package/dist/components/passkeys-setting.js.map +1 -0
  127. package/dist/components/picker-sheet.js +2 -1
  128. package/dist/components/picker-sheet.js.map +1 -1
  129. package/dist/components/pie-chart-labels.d.ts +20 -0
  130. package/dist/components/pie-chart-labels.js +12 -0
  131. package/dist/components/pie-chart-labels.js.map +1 -0
  132. package/dist/components/pie-chart.d.ts +101 -0
  133. package/dist/components/pie-chart.js +283 -0
  134. package/dist/components/pie-chart.js.map +1 -0
  135. package/dist/components/progress-bar.d.ts +73 -3
  136. package/dist/components/progress-bar.js +110 -8
  137. package/dist/components/progress-bar.js.map +1 -1
  138. package/dist/components/qr-code.d.ts +34 -0
  139. package/dist/components/qr-code.js +57 -0
  140. package/dist/components/qr-code.js.map +1 -0
  141. package/dist/components/search-field.d.ts +15 -3
  142. package/dist/components/search-field.js +5 -1
  143. package/dist/components/search-field.js.map +1 -1
  144. package/dist/components/series-chart.d.ts +16 -0
  145. package/dist/components/series-chart.js +4 -1
  146. package/dist/components/series-chart.js.map +1 -1
  147. package/dist/components/settings-fields.d.ts +50 -1
  148. package/dist/components/signed-amount.d.ts +121 -0
  149. package/dist/components/signed-amount.js +152 -0
  150. package/dist/components/signed-amount.js.map +1 -0
  151. package/dist/components/skeleton.d.ts +10 -3
  152. package/dist/components/skeleton.js +12 -2
  153. package/dist/components/skeleton.js.map +1 -1
  154. package/dist/components/stat-tile.d.ts +2 -1
  155. package/dist/components/stat-tile.js +5 -2
  156. package/dist/components/stat-tile.js.map +1 -1
  157. package/dist/components/status-dot.d.ts +6 -2
  158. package/dist/components/status-dot.js +6 -1
  159. package/dist/components/status-dot.js.map +1 -1
  160. package/dist/components/table.d.ts +77 -11
  161. package/dist/components/table.js +85 -17
  162. package/dist/components/table.js.map +1 -1
  163. package/dist/components/text-link.d.ts +176 -0
  164. package/dist/components/text-link.js +109 -0
  165. package/dist/components/text-link.js.map +1 -0
  166. package/dist/components/text.d.ts +8 -1
  167. package/dist/components/text.js +1 -0
  168. package/dist/components/text.js.map +1 -1
  169. package/dist/components/time-input.d.ts +50 -1
  170. package/dist/components/toggle-group.d.ts +28 -0
  171. package/dist/components/toggle-group.js +58 -9
  172. package/dist/components/toggle-group.js.map +1 -1
  173. package/dist/components/toggle-legend.d.ts +36 -3
  174. package/dist/components/toggle-legend.js +49 -0
  175. package/dist/components/toggle-legend.js.map +1 -1
  176. package/dist/components/tooltip.d.ts +11 -3
  177. package/dist/components/tooltip.js +2 -0
  178. package/dist/components/tooltip.js.map +1 -1
  179. package/dist/components/ui.d.ts +52 -729
  180. package/dist/components/ui.js +296 -27
  181. package/dist/components/ui.js.map +1 -1
  182. package/dist/components/use-table-state.d.ts +3 -52
  183. package/dist/components/use-table-state.js +128 -30
  184. package/dist/components/use-table-state.js.map +1 -1
  185. package/dist/components/user-avatar.d.ts +25 -3
  186. package/dist/components/user-avatar.js +32 -3
  187. package/dist/components/user-avatar.js.map +1 -1
  188. package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
  189. package/dist/data-table.d.ts +2 -1
  190. package/dist/data-table.js +5 -1
  191. package/dist/data-table.js.map +1 -1
  192. package/dist/feedback/feedback-dialog.d.ts +26 -3
  193. package/dist/feedback/feedback-dialog.js +16 -2
  194. package/dist/feedback/feedback-dialog.js.map +1 -1
  195. package/dist/feedback/feedback-thread.d.ts +133 -0
  196. package/dist/feedback/feedback-thread.js +237 -0
  197. package/dist/feedback/feedback-thread.js.map +1 -0
  198. package/dist/feedback.d.ts +2 -1
  199. package/dist/feedback.js +1 -0
  200. package/dist/feedback.js.map +1 -1
  201. package/dist/hooks/use-authed-src.d.ts +71 -0
  202. package/dist/hooks/use-authed-src.js +63 -0
  203. package/dist/hooks/use-authed-src.js.map +1 -0
  204. package/dist/hooks/use-file-drop.d.ts +50 -2
  205. package/dist/hooks/use-hotkey.d.ts +59 -0
  206. package/dist/hooks/use-hotkey.js +105 -0
  207. package/dist/hooks/use-hotkey.js.map +1 -0
  208. package/dist/hooks/use-search-param-state.d.ts +83 -0
  209. package/dist/hooks/use-search-param-state.js +94 -0
  210. package/dist/hooks/use-search-param-state.js.map +1 -0
  211. package/dist/i18n/defaults.d.ts +17 -4
  212. package/dist/i18n/defaults.js +33 -1
  213. package/dist/i18n/defaults.js.map +1 -1
  214. package/dist/i18n/kit-labels.d.ts +47 -310
  215. package/dist/i18n/kit-labels.js +20 -5
  216. package/dist/i18n/kit-labels.js.map +1 -1
  217. package/dist/i18n/locales/de-CH-informal.d.ts +17 -4
  218. package/dist/i18n/locales/de-CH.d.ts +17 -4
  219. package/dist/i18n/locales/de-informal.d.ts +17 -4
  220. package/dist/i18n/locales/de.d.ts +17 -4
  221. package/dist/i18n/locales/de.js +165 -4
  222. package/dist/i18n/locales/de.js.map +1 -1
  223. package/dist/i18n/locales/es.d.ts +17 -4
  224. package/dist/i18n/locales/es.js +162 -4
  225. package/dist/i18n/locales/es.js.map +1 -1
  226. package/dist/i18n/locales/fr.d.ts +17 -4
  227. package/dist/i18n/locales/fr.js +162 -4
  228. package/dist/i18n/locales/fr.js.map +1 -1
  229. package/dist/i18n/locales/hu.d.ts +17 -4
  230. package/dist/i18n/locales/hu.js +162 -4
  231. package/dist/i18n/locales/hu.js.map +1 -1
  232. package/dist/i18n/locales/it.d.ts +17 -4
  233. package/dist/i18n/locales/it.js +162 -4
  234. package/dist/i18n/locales/it.js.map +1 -1
  235. package/dist/i18n/locales/zh.d.ts +17 -4
  236. package/dist/i18n/locales/zh.js +162 -4
  237. package/dist/i18n/locales/zh.js.map +1 -1
  238. package/dist/index.d.ts +45 -21
  239. package/dist/index.js +43 -1
  240. package/dist/index.js.map +1 -1
  241. package/dist/kit-labels-v3biUF1L.d.ts +1645 -0
  242. package/dist/lib/clipping.d.ts +21 -5
  243. package/dist/lib/clipping.js +3 -0
  244. package/dist/lib/clipping.js.map +1 -1
  245. package/dist/lib/dates.d.ts +17 -1
  246. package/dist/lib/dates.js +19 -0
  247. package/dist/lib/dates.js.map +1 -1
  248. package/dist/lib/format.d.ts +144 -0
  249. package/dist/lib/format.js +149 -0
  250. package/dist/lib/format.js.map +1 -0
  251. package/dist/lib/qr-encode.d.ts +44 -0
  252. package/dist/lib/qr-encode.js +338 -0
  253. package/dist/lib/qr-encode.js.map +1 -0
  254. package/dist/rhf/fields.d.ts +241 -0
  255. package/dist/rhf/fields.js +601 -0
  256. package/dist/rhf/fields.js.map +1 -0
  257. package/dist/rhf/form.d.ts +50 -1
  258. package/dist/rhf/line-items.d.ts +31 -0
  259. package/dist/rhf/line-items.js +59 -0
  260. package/dist/rhf/line-items.js.map +1 -0
  261. package/dist/rhf/use-rhf-wizard-step.d.ts +22 -0
  262. package/dist/rhf/use-rhf-wizard-step.js +38 -0
  263. package/dist/rhf/use-rhf-wizard-step.js.map +1 -0
  264. package/dist/rhf.d.ts +63 -1
  265. package/dist/rhf.js +3 -0
  266. package/dist/rhf.js.map +1 -1
  267. package/dist/shell/app-shell.d.ts +99 -2
  268. package/dist/shell/app-shell.js +58 -3
  269. package/dist/shell/app-shell.js.map +1 -1
  270. package/dist/shell/auth-layout.d.ts +74 -0
  271. package/dist/shell/auth-layout.js +97 -0
  272. package/dist/shell/auth-layout.js.map +1 -0
  273. package/dist/shell/top-bar-brand.d.ts +81 -0
  274. package/dist/shell/top-bar-brand.js +47 -0
  275. package/dist/shell/top-bar-brand.js.map +1 -0
  276. package/dist/shell/topbar-action-menu.d.ts +84 -3
  277. package/dist/shell/topbar-action-menu.js +115 -27
  278. package/dist/shell/topbar-action-menu.js.map +1 -1
  279. package/dist/shell.d.ts +58 -2
  280. package/dist/shell.js +2 -0
  281. package/dist/shell.js.map +1 -1
  282. package/dist/wizard/stepper-nav.d.ts +49 -1
  283. package/dist/wizard/wizard-context.d.ts +30 -1
  284. package/dist/wizard/wizard-context.js +23 -2
  285. package/dist/wizard/wizard-context.js.map +1 -1
  286. package/dist/wizard/wizard-step.d.ts +20 -5
  287. package/dist/wizard/wizard-step.js +16 -3
  288. package/dist/wizard/wizard-step.js.map +1 -1
  289. package/dist/wizard.d.ts +51 -3
  290. package/package.json +1 -1
  291. package/src/chart.ts +4 -0
  292. package/src/components/account-settings-labels.ts +190 -0
  293. package/src/components/account-settings.tsx +110 -63
  294. package/src/components/alert-banner.tsx +36 -7
  295. package/src/components/amount-input.tsx +13 -1
  296. package/src/components/authed-image.tsx +190 -0
  297. package/src/components/breadcrumbs.tsx +8 -2
  298. package/src/components/bulk-action-bar.tsx +163 -57
  299. package/src/components/button-group.tsx +129 -4
  300. package/src/components/calendar-heatmap.tsx +599 -0
  301. package/src/components/chip.tsx +8 -4
  302. package/src/components/choice-card.tsx +208 -1
  303. package/src/components/combobox-core.tsx +5 -0
  304. package/src/components/combobox.tsx +31 -9
  305. package/src/components/copy-button.tsx +3 -2
  306. package/src/components/data-table-cells.tsx +119 -0
  307. package/src/components/data-table-filters.ts +77 -0
  308. package/src/components/data-table-labels.ts +23 -0
  309. package/src/components/data-table.tsx +231 -13
  310. package/src/components/date-picker.tsx +19 -3
  311. package/src/components/description-list.tsx +52 -3
  312. package/src/components/entity-combobox.tsx +18 -3
  313. package/src/components/error-boundary.tsx +216 -0
  314. package/src/components/field.tsx +188 -0
  315. package/src/components/floating-panel.tsx +380 -16
  316. package/src/components/form-actions.tsx +191 -0
  317. package/src/components/image-grid.tsx +181 -0
  318. package/src/components/lightbox.tsx +374 -0
  319. package/src/components/line-items.tsx +385 -0
  320. package/src/components/list.tsx +93 -18
  321. package/src/components/loading-state.tsx +47 -0
  322. package/src/components/menu-item.tsx +29 -3
  323. package/src/components/mini-calendar.tsx +317 -57
  324. package/src/components/modal.tsx +61 -3
  325. package/src/components/month-picker.tsx +181 -26
  326. package/src/components/multi-entity-combobox.tsx +19 -3
  327. package/src/components/nav-pills.tsx +152 -0
  328. package/src/components/number-field.tsx +15 -10
  329. package/src/components/number-input.tsx +16 -1
  330. package/src/components/passkeys-setting.tsx +331 -0
  331. package/src/components/picker-sheet.tsx +7 -1
  332. package/src/components/pie-chart-labels.ts +32 -0
  333. package/src/components/pie-chart.tsx +479 -0
  334. package/src/components/progress-bar.tsx +241 -13
  335. package/src/components/qr-code.tsx +83 -0
  336. package/src/components/search-field.tsx +20 -4
  337. package/src/components/series-chart.tsx +21 -0
  338. package/src/components/signed-amount.tsx +286 -0
  339. package/src/components/skeleton.tsx +21 -3
  340. package/src/components/stat-tile.tsx +8 -2
  341. package/src/components/status-dot.tsx +10 -2
  342. package/src/components/table.tsx +186 -23
  343. package/src/components/text-link.tsx +248 -0
  344. package/src/components/text.tsx +9 -1
  345. package/src/components/toggle-group.tsx +106 -13
  346. package/src/components/toggle-legend.tsx +87 -2
  347. package/src/components/tooltip.tsx +17 -3
  348. package/src/components/ui.tsx +572 -44
  349. package/src/components/use-table-state.ts +265 -33
  350. package/src/components/user-avatar.tsx +57 -3
  351. package/src/data-table.ts +10 -0
  352. package/src/feedback/feedback-dialog.tsx +42 -3
  353. package/src/feedback/feedback-thread.tsx +434 -0
  354. package/src/feedback.ts +1 -0
  355. package/src/hooks/use-authed-src.ts +161 -0
  356. package/src/hooks/use-hotkey.ts +172 -0
  357. package/src/hooks/use-search-param-state.ts +187 -0
  358. package/src/i18n/defaults.ts +32 -0
  359. package/src/i18n/kit-labels.tsx +104 -8
  360. package/src/i18n/locales/de.ts +162 -0
  361. package/src/i18n/locales/es.ts +159 -0
  362. package/src/i18n/locales/fr.ts +162 -0
  363. package/src/i18n/locales/hu.ts +160 -0
  364. package/src/i18n/locales/it.ts +160 -0
  365. package/src/i18n/locales/zh.ts +159 -0
  366. package/src/index.ts +64 -0
  367. package/src/lib/clipping.ts +22 -4
  368. package/src/lib/dates.ts +37 -0
  369. package/src/lib/format.ts +325 -0
  370. package/src/lib/qr-encode.ts +420 -0
  371. package/src/rhf/fields.tsx +1054 -0
  372. package/src/rhf/line-items.tsx +130 -0
  373. package/src/rhf/use-rhf-wizard-step.ts +113 -0
  374. package/src/rhf.ts +8 -0
  375. package/src/shell/app-shell.tsx +147 -2
  376. package/src/shell/auth-layout.tsx +190 -0
  377. package/src/shell/top-bar-brand.tsx +76 -0
  378. package/src/shell/topbar-action-menu.tsx +260 -49
  379. package/src/shell.ts +2 -0
  380. package/src/wizard/wizard-context.tsx +50 -1
  381. package/src/wizard/wizard-step.tsx +43 -6
  382. package/tokens.css +12 -0
@@ -0,0 +1,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
+ }
@@ -1,5 +1,6 @@
1
1
  import { DEFAULT_DATA_TABLE_LABELS } from "../components/data-table-labels";
2
2
  import { DEFAULT_MINI_CALENDAR_LABELS } from "../components/mini-calendar";
3
+ import { DEFAULT_CALENDAR_HEATMAP_LABELS } from "../components/calendar-heatmap";
3
4
  import { DEFAULT_MONTH_PICKER_LABELS } from "../components/month-picker";
4
5
  import { DEFAULT_PAGE_CONTENTS_LABELS } from "../components/page-contents";
5
6
  import { DEFAULT_POPOVER_LABELS } from "../components/popover";
@@ -11,6 +12,7 @@ import { DEFAULT_TOUR_LABELS } from "../tour/tour";
11
12
  import { DEFAULT_COMMAND_PALETTE_LABELS } from "../search/command-palette";
12
13
  import { DEFAULT_GLOBAL_SEARCH_LABELS } from "../search/global-search";
13
14
  import { DEFAULT_SERIES_CHART_LABELS } from "../components/series-chart-labels";
15
+ import { DEFAULT_PIE_CHART_LABELS } from "../components/pie-chart-labels";
14
16
  import { DEFAULT_SPARKLINE_LABELS } from "../components/sparkline";
15
17
  import { DEFAULT_STAT_TILE_LABELS } from "../components/stat-tile";
16
18
  import { DEFAULT_SIGNATURE_PAD_LABELS } from "../components/signature-pad";
@@ -24,11 +26,26 @@ import { DEFAULT_MEASURED_GRID_LABELS } from "../components/measured-grid";
24
26
  import { DEFAULT_CONFIRM_DIALOG_LABELS } from "../components/confirm-dialog";
25
27
  import { DEFAULT_FLOATING_PANEL_LABELS } from "../components/floating-panel";
26
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";
27
35
  import { DEFAULT_COPY_BUTTON_LABELS } from "../components/copy-button";
28
36
  import { DEFAULT_BULK_ACTION_BAR_LABELS } from "../components/bulk-action-bar";
29
37
  import { DEFAULT_LIST_LABELS } from "../components/list";
30
38
  import { DEFAULT_BREADCRUMBS_LABELS } from "../components/breadcrumbs";
31
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";
32
49
  import {
33
50
  DEFAULT_APP_SHELL_LABELS,
34
51
  DEFAULT_CALCULATOR_LABELS,
@@ -56,6 +73,7 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
56
73
  common: DEFAULT_COMMON_LABELS,
57
74
  dataTable: DEFAULT_DATA_TABLE_LABELS,
58
75
  miniCalendar: DEFAULT_MINI_CALENDAR_LABELS,
76
+ calendarHeatmap: DEFAULT_CALENDAR_HEATMAP_LABELS,
59
77
  datePicker: DEFAULT_DATE_PICKER_LABELS,
60
78
  monthPicker: DEFAULT_MONTH_PICKER_LABELS,
61
79
  popover: DEFAULT_POPOVER_LABELS,
@@ -78,6 +96,7 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
78
96
  commandPalette: DEFAULT_COMMAND_PALETTE_LABELS,
79
97
  globalSearch: DEFAULT_GLOBAL_SEARCH_LABELS,
80
98
  seriesChart: DEFAULT_SERIES_CHART_LABELS,
99
+ pieChart: DEFAULT_PIE_CHART_LABELS,
81
100
  sparkline: DEFAULT_SPARKLINE_LABELS,
82
101
  statTile: DEFAULT_STAT_TILE_LABELS,
83
102
  signaturePad: DEFAULT_SIGNATURE_PAD_LABELS,
@@ -89,6 +108,10 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
89
108
  filePicker: DEFAULT_FILE_PICKER_LABELS,
90
109
  measuredGrid: DEFAULT_MEASURED_GRID_LABELS,
91
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,
92
115
  confirmDialog: DEFAULT_CONFIRM_DIALOG_LABELS,
93
116
  floatingPanel: DEFAULT_FLOATING_PANEL_LABELS,
94
117
  copyButton: DEFAULT_COPY_BUTTON_LABELS,
@@ -96,4 +119,13 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
96
119
  list: DEFAULT_LIST_LABELS,
97
120
  breadcrumbs: DEFAULT_BREADCRUMBS_LABELS,
98
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,
99
131
  };
@@ -1,7 +1,9 @@
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";
6
+ import type { CalendarHeatmapLabels } from "../components/calendar-heatmap";
5
7
  import type { PopoverLabels } from "../components/popover";
6
8
  import type { ChipInputLabels } from "../components/chip";
7
9
  import type { FieldSyncLabels } from "../components/field-sync";
@@ -13,6 +15,7 @@ import type { GlobalSearchLabels } from "../search/global-search";
13
15
  import type { MonthPickerLabels } from "../components/month-picker";
14
16
  import type { PageContentsLabels } from "../components/page-contents";
15
17
  import type { SeriesChartLabels } from "../components/series-chart-labels";
18
+ import type { PieChartLabels } from "../components/pie-chart-labels";
16
19
  import type { SparklineLabels } from "../components/sparkline";
17
20
  import type { StatTileLabels } from "../components/stat-tile";
18
21
  import type { SignaturePadLabels } from "../components/signature-pad";
@@ -24,6 +27,9 @@ import type { DialogFrameLabels } from "../components/dialog-frame";
24
27
  import type { FilePickerLabels } from "../components/file-button";
25
28
  import type { MeasuredGridLabels } from "../components/measured-grid";
26
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";
27
33
  import type { ConfirmDialogLabels } from "../components/confirm-dialog";
28
34
  import type { FloatingPanelLabels } from "../components/floating-panel";
29
35
  import type { CopyButtonLabels } from "../components/copy-button";
@@ -31,6 +37,15 @@ import type { BulkActionBarLabels } from "../components/bulk-action-bar";
31
37
  import type { ListLabels } from "../components/list";
32
38
  import type { BreadcrumbsLabels } from "../components/breadcrumbs";
33
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";
34
49
 
35
50
  /**
36
51
  * EVERY string the kit renders, as one typed tree — and an optional provider that
@@ -83,6 +98,9 @@ export interface CommonLabels {
83
98
  /** The × that puts away a banner or a notice (`AlertBanner onDismiss`). Not `close`:
84
99
  * nothing opened, and "Close" on a banner reads as closing the page it sits on. */
85
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;
86
104
  }
87
105
 
88
106
  /** `DatePicker` / `DateRangePicker` chrome. The calendar inside has its own
@@ -197,6 +215,7 @@ export interface UiKitLabels {
197
215
  common: CommonLabels;
198
216
  dataTable: DataTableLabels;
199
217
  miniCalendar: MiniCalendarLabels;
218
+ calendarHeatmap: CalendarHeatmapLabels;
200
219
  datePicker: DatePickerLabels;
201
220
  monthPicker: MonthPickerLabels;
202
221
  popover: PopoverLabels;
@@ -219,6 +238,7 @@ export interface UiKitLabels {
219
238
  commandPalette: CommandPaletteLabels;
220
239
  globalSearch: GlobalSearchLabels;
221
240
  seriesChart: SeriesChartLabels;
241
+ pieChart: PieChartLabels;
222
242
  sparkline: SparklineLabels;
223
243
  statTile: StatTileLabels;
224
244
  signaturePad: SignaturePadLabels;
@@ -230,6 +250,14 @@ export interface UiKitLabels {
230
250
  filePicker: FilePickerLabels;
231
251
  measuredGrid: MeasuredGridLabels;
232
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;
233
261
  confirmDialog: ConfirmDialogLabels;
234
262
  floatingPanel: FloatingPanelLabels;
235
263
  copyButton: CopyButtonLabels;
@@ -237,11 +265,36 @@ export interface UiKitLabels {
237
265
  list: ListLabels;
238
266
  breadcrumbs: BreadcrumbsLabels;
239
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;
240
277
  }
241
278
 
242
- /** Any subset of the tree, one level deep — each namespace may be partial, and a
243
- * namespace that holds a record (`dataTable.presets`) is merged key by key. */
244
- 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]> };
245
298
 
246
299
  /* ── English defaults for the namespaces defined here ────────────────────── */
247
300
 
@@ -259,6 +312,7 @@ export const DEFAULT_COMMON_LABELS: CommonLabels = {
259
312
  noResults: "No results",
260
313
  fieldValue: (field, value) => `${field}: ${value}`,
261
314
  dismiss: "Dismiss",
315
+ opensInNewTab: "opens in a new tab",
262
316
  };
263
317
 
264
318
  export const DEFAULT_DATE_PICKER_LABELS: DatePickerLabels = {
@@ -362,8 +416,20 @@ interface KitI18n {
362
416
  labels?: UiKitLabelOverrides;
363
417
  locale?: string;
364
418
  weekStartsOn?: WeekDay;
419
+ linkComponent?: KitLinkComponent;
365
420
  }
366
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
+ export type KitLinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & {
428
+ href: string;
429
+ ref?: Ref<HTMLAnchorElement>;
430
+ };
431
+ export type KitLinkComponent = (props: KitLinkProps) => ReactElement;
432
+
367
433
  const KitI18nContext = createContext<KitI18n>({});
368
434
 
369
435
  export interface UiKitProviderProps {
@@ -385,6 +451,14 @@ export interface UiKitProviderProps {
385
451
  * and switching it to `en-GB` to get Monday changes every date and number format.
386
452
  */
387
453
  weekStartsOn?: WeekDay;
454
+ /**
455
+ * The app's router link, used by every kit component that renders an in-app link
456
+ * and was not handed its own `renderLink` — so `renderLink` stops being repeated on
457
+ * each ListItem, Breadcrumbs, StatTile, Chip… (kastlan). A component's own
458
+ * `renderLink` wins; external links and in-page `#anchor`s stay plain `<a>`, and a
459
+ * hash-router `#/path` is handed to it like `/path` (see `pickLinkRenderer`).
460
+ */
461
+ linkComponent?: KitLinkComponent;
388
462
  children: ReactNode;
389
463
  }
390
464
 
@@ -396,15 +470,22 @@ export interface UiKitProviderProps {
396
470
  * it names, so a page can re-label one table's `dataTable.table` without restating
397
471
  * the language.
398
472
  */
399
- export function UiKitProvider({ labels, locale, weekStartsOn, children }: UiKitProviderProps) {
473
+ export function UiKitProvider({
474
+ labels,
475
+ locale,
476
+ weekStartsOn,
477
+ linkComponent,
478
+ children,
479
+ }: UiKitProviderProps) {
400
480
  const outer = useContext(KitI18nContext);
401
481
  const value = useMemo<KitI18n>(
402
482
  () => ({
403
483
  locale: locale ?? outer.locale,
404
484
  weekStartsOn: weekStartsOn ?? outer.weekStartsOn,
405
485
  labels: mergeOverrides(outer.labels, labels),
486
+ linkComponent: linkComponent ?? outer.linkComponent,
406
487
  }),
407
- [outer, labels, locale, weekStartsOn],
488
+ [outer, labels, locale, weekStartsOn, linkComponent],
408
489
  );
409
490
  return <KitI18nContext.Provider value={value}>{children}</KitI18nContext.Provider>;
410
491
  }
@@ -413,14 +494,23 @@ function isRecord(v: unknown): v is Record<string, unknown> {
413
494
  return typeof v === "object" && v !== null && !Array.isArray(v);
414
495
  }
415
496
 
416
- /** Two levels: namespaces, and inside a namespace a record-valued key (presets). */
417
- function mergeNamespace<T extends object>(base: T, over: Partial<T> | undefined): T {
497
+ /** A record of labels to merge into — not a React element, which is an object too but
498
+ * is a label given whole. */
499
+ function isLabelRecord(v: unknown): v is Record<string, unknown> {
500
+ return isRecord(v) && !("$$typeof" in v);
501
+ }
502
+
503
+ /** Merged key by key at EVERY depth — a namespace, a record inside it
504
+ * (`accountSettings.passkeys`, `dataTable.presets`), and so on down — to match
505
+ * {@link LabelOverride}. An `undefined` is skipped at every depth, so it never blanks
506
+ * out what is under it. */
507
+ function mergeNamespace<T extends object>(base: T, over: LabelOverride<T> | Partial<T> | undefined): T {
418
508
  if (!over) return base;
419
509
  const out = { ...base } as Record<string, unknown>;
420
510
  for (const [k, v] of Object.entries(over)) {
421
511
  if (v === undefined) continue;
422
512
  const prev = out[k];
423
- out[k] = isRecord(prev) && isRecord(v) ? { ...prev, ...v } : v;
513
+ out[k] = isLabelRecord(prev) && isLabelRecord(v) ? mergeNamespace(prev, v) : v;
424
514
  }
425
515
  return out as T;
426
516
  }
@@ -473,6 +563,12 @@ export function useKitLocale(prop?: string): string | undefined {
473
563
  /** The week start the nearest `<UiKitProvider weekStartsOn>` pins, else `undefined`
474
564
  * (the caller then asks the locale). A component's own prop goes first:
475
565
  * `prop ?? useKitWeekStart()`. */
566
+ /** The provider's router link, or `undefined` — a component's own `renderLink` should
567
+ * win over it: `const Link = renderLink ?? useKitLink()`. */
568
+ export function useKitLink(): KitLinkComponent | undefined {
569
+ return useContext(KitI18nContext).linkComponent;
570
+ }
571
+
476
572
  export function useKitWeekStart(): WeekDay | undefined {
477
573
  return useContext(KitI18nContext).weekStartsOn;
478
574
  }