@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,190 @@
1
+ import { useState } from "react";
2
+ import type { ImgHTMLAttributes, ReactNode } from "react";
3
+ import { ImageOff } from "lucide-react";
4
+
5
+ import { cn } from "../lib/cn";
6
+ import { useKitLabels } from "../i18n/kit-labels";
7
+ import { SKELETON_CLASS } from "./skeleton";
8
+ import { useAuthedSrc } from "../hooks/use-authed-src";
9
+ import type { AuthedFetcher, UseAuthedSrcResult } from "../hooks/use-authed-src";
10
+
11
+ /** The two states an image can be in besides "there". */
12
+ export interface AuthedImageLabels {
13
+ /** Read out while the bytes are on their way (the skeleton itself is silent). */
14
+ loading: string;
15
+ /** Shown, small, in place of an image that could not be fetched or decoded. */
16
+ loadError: string;
17
+ /** The failed tile's accessible name, given the image's alt. */
18
+ failedImage: (alt: string) => string;
19
+ }
20
+
21
+ export const DEFAULT_AUTHED_IMAGE_LABELS: AuthedImageLabels = {
22
+ loading: "Loading image…",
23
+ loadError: "Image couldn’t be loaded",
24
+ failedImage: (alt) => `${alt}: image couldn’t be loaded`,
25
+ };
26
+
27
+ export interface AuthedImageProps
28
+ extends Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt" | "children"> {
29
+ /** The image's URL — an API path when a {@link fetcher} is given. */
30
+ src: string | null | undefined;
31
+ /** Required: an image without a text alternative is a gap a reader falls into.
32
+ * `""` for a purely decorative one. */
33
+ alt: string;
34
+ /** The app's authenticated GET (see {@link useAuthedSrc}). Left out, `src` is loaded
35
+ * by the browser as usual and this is an `<img>` with loading and error states. */
36
+ fetcher?: AuthedFetcher | null;
37
+ /** Classes for the box around the image (the placeholders fill it). */
38
+ wrapperClassName?: string;
39
+ /**
40
+ * Wrap the image in a link to itself, opening in a new tab — kastlan's
41
+ * `AuthedImage` did, because an object URL is the only address of a protected image
42
+ * a browser tab can open. The link's href is the `blob:` URL, never the API path.
43
+ */
44
+ link?: boolean;
45
+ /** Replaces the default skeleton while loading. */
46
+ loadingFallback?: ReactNode;
47
+ /** Replaces the default "couldn't load" tile. */
48
+ errorFallback?: ReactNode;
49
+ labels?: Partial<AuthedImageLabels>;
50
+ }
51
+
52
+ /**
53
+ * An image with a loading state, an error state and — given a `fetcher` — the app's
54
+ * authentication. Replaces kastlan's `shared/components/authed-image.tsx`, which
55
+ * rendered NOTHING until the blob arrived and nothing at all if it never did: a card
56
+ * with a photo slot either had the photo or was silently shorter.
57
+ *
58
+ * Two failures are one state here: the fetch failing (401, 404, offline) and the
59
+ * browser failing to decode what arrived (`onError` on the `<img>`, e.g. a PDF served
60
+ * where an image was expected).
61
+ */
62
+ export function AuthedImage({
63
+ src,
64
+ alt,
65
+ fetcher,
66
+ wrapperClassName,
67
+ className,
68
+ link,
69
+ loadingFallback,
70
+ errorFallback,
71
+ labels: labelsProp,
72
+ onLoad,
73
+ onError,
74
+ ...rest
75
+ }: AuthedImageProps) {
76
+ const labels = useKitLabels("authedImage", DEFAULT_AUTHED_IMAGE_LABELS, labelsProp);
77
+ const fetched = useAuthedSrc(src, { fetcher });
78
+ return (
79
+ <ImageStates
80
+ fetched={fetched}
81
+ alt={alt}
82
+ labels={labels}
83
+ className={className}
84
+ wrapperClassName={wrapperClassName}
85
+ link={link}
86
+ loadingFallback={loadingFallback}
87
+ errorFallback={errorFallback}
88
+ imgProps={rest}
89
+ onLoad={onLoad}
90
+ onError={onError}
91
+ />
92
+ );
93
+ }
94
+
95
+ /**
96
+ * The rendering half of {@link AuthedImage}, for a caller that already holds a
97
+ * {@link useAuthedSrc} result (the lightbox needs the object URL for its download link
98
+ * as well). Internal to the package.
99
+ */
100
+ export function ImageStates({
101
+ fetched,
102
+ alt,
103
+ labels,
104
+ className,
105
+ wrapperClassName,
106
+ link,
107
+ loadingFallback,
108
+ errorFallback,
109
+ imgProps,
110
+ onLoad,
111
+ onError,
112
+ }: {
113
+ fetched: UseAuthedSrcResult;
114
+ alt: string;
115
+ labels: AuthedImageLabels;
116
+ className?: string;
117
+ wrapperClassName?: string;
118
+ link?: boolean;
119
+ loadingFallback?: ReactNode;
120
+ errorFallback?: ReactNode;
121
+ imgProps?: Omit<ImgHTMLAttributes<HTMLImageElement>, "src" | "alt" | "children" | "onLoad" | "onError">;
122
+ onLoad?: ImgHTMLAttributes<HTMLImageElement>["onLoad"];
123
+ onError?: ImgHTMLAttributes<HTMLImageElement>["onError"];
124
+ }) {
125
+ // Decode state, per src: keyed so a new picture starts over rather than inheriting
126
+ // the previous one's "loaded" or "broken".
127
+ const [decode, setDecode] = useState<{ src: string | null; state: "loaded" | "error" } | null>(null);
128
+ const decoded = decode && decode.src === fetched.src ? decode.state : "pending";
129
+ const failed = fetched.status === "error" || decoded === "error";
130
+ const waiting = !failed && (fetched.status === "loading" || (fetched.src !== null && decoded === "pending"));
131
+
132
+ const img = fetched.src !== null && !failed && (
133
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- load/error are resource events, not interactions
134
+ <img
135
+ {...imgProps}
136
+ src={fetched.src}
137
+ alt={alt}
138
+ onLoad={(e) => {
139
+ setDecode({ src: fetched.src, state: "loaded" });
140
+ onLoad?.(e);
141
+ }}
142
+ onError={(e) => {
143
+ setDecode({ src: fetched.src, state: "error" });
144
+ onError?.(e);
145
+ }}
146
+ className={cn(
147
+ // Kept in the tree while it decodes (the browser needs it there to load at
148
+ // all), but invisible, so the skeleton is what shows.
149
+ decoded === "pending" && "opacity-0",
150
+ className,
151
+ )}
152
+ />
153
+ );
154
+
155
+ return (
156
+ <span
157
+ data-image-state={failed ? "error" : waiting ? "loading" : fetched.status === "idle" ? "idle" : "ready"}
158
+ className={cn("relative block overflow-hidden", wrapperClassName)}
159
+ aria-busy={waiting || undefined}
160
+ >
161
+ {link && img && fetched.src ? (
162
+ <a href={fetched.src} target="_blank" rel="noreferrer noopener" className="block h-full w-full">
163
+ {img}
164
+ </a>
165
+ ) : (
166
+ img
167
+ )}
168
+ {waiting &&
169
+ (loadingFallback ?? (
170
+ <span className={cn(SKELETON_CLASS, "absolute inset-0 block rounded-none")}>
171
+ <span className="sr-only">{labels.loading}</span>
172
+ </span>
173
+ ))}
174
+ {failed &&
175
+ (errorFallback ?? (
176
+ <span
177
+ // `role="img"` with the alt as its name: the picture that failed still
178
+ // occupies its place in the reading order, and "couldn't be loaded" is
179
+ // the one fact a reader needs about it.
180
+ role="img"
181
+ aria-label={alt ? labels.failedImage(alt) : labels.loadError}
182
+ className="flex h-full min-h-16 w-full flex-col items-center justify-center gap-1 bg-[var(--bg-hover)] p-2 text-center text-xs text-[var(--text-muted)]"
183
+ >
184
+ <ImageOff aria-hidden className="size-5 shrink-0" />
185
+ <span aria-hidden className="line-clamp-2">{labels.loadError}</span>
186
+ </span>
187
+ ))}
188
+ </span>
189
+ );
190
+ }
@@ -2,7 +2,8 @@ import { Fragment, useEffect, useRef, useState } from "react";
2
2
  import type { ComponentPropsWithoutRef, ReactElement, ReactNode } from "react";
3
3
  import { ChevronRight, MoreHorizontal } from "lucide-react";
4
4
  import { cn } from "../lib/cn";
5
- import { useKitLabels } from "../i18n/kit-labels";
5
+ import { useKitLabels, useKitLink } from "../i18n/kit-labels";
6
+ import { pickLinkRenderer, RenderedKitLink } from "./text-link";
6
7
 
7
8
  export interface BreadcrumbsLabels {
8
9
  /** The `nav` landmark's name — what a screen reader lists it as. */
@@ -35,7 +36,8 @@ export interface BreadcrumbLinkProps {
35
36
 
36
37
  export interface BreadcrumbsProps extends Omit<ComponentPropsWithoutRef<"nav">, "children"> {
37
38
  items: BreadcrumbItem[];
38
- /** Your router's link. Default `<a>`. The API `Chip`, `StatTile` and `ListItem` share. */
39
+ /** Your router's link. Default: the `<UiKitProvider linkComponent>` (for an in-app
40
+ * `href`), then `<a>`. The API `Chip`, `StatTile` and `ListItem` share. */
39
41
  renderLink?: (props: BreadcrumbLinkProps) => ReactElement;
40
42
  /**
41
43
  * The mark between crumbs. Default a chevron, mirrored in RTL — a chevron pointing
@@ -78,6 +80,7 @@ export function Breadcrumbs({
78
80
  ...rest
79
81
  }: BreadcrumbsProps) {
80
82
  const labels = useKitLabels("breadcrumbs", DEFAULT_BREADCRUMBS_LABELS, labelsProp);
83
+ const kitLink = useKitLink();
81
84
  const [expanded, setExpanded] = useState(false);
82
85
  // The "…" button vanishes when pressed; focus goes to the first crumb it revealed,
83
86
  // not back to the top of the document.
@@ -121,8 +124,11 @@ export function Breadcrumbs({
121
124
  );
122
125
  } else if (item.href !== undefined) {
123
126
  const props: BreadcrumbLinkProps = { href: item.href, className: linkClass, children: item.label };
127
+ const kit = pickLinkRenderer<BreadcrumbLinkProps>(undefined, kitLink, item.href);
124
128
  content = renderLink ? (
125
129
  renderLink(props)
130
+ ) : kit ? (
131
+ <RenderedKitLink render={kit} props={props} />
126
132
  ) : (
127
133
  <a href={props.href} className={props.className}>
128
134
  {props.children}
@@ -99,8 +99,16 @@ export interface BulkActionBarProps extends Omit<ComponentPropsWithoutRef<"div">
99
99
  open?: boolean;
100
100
  /** Clear the selection / leave selection mode. */
101
101
  onClear: () => void;
102
- /** The actions: buttons (`IconButton size="lg"` on a phone, `Button` from md up),
103
- * in the caller's order. They are the toolbar's arrow-key stops, after the clear. */
102
+ /**
103
+ * The actions: buttons (`IconButton size="lg"` on a phone, `Button` from md up),
104
+ * in the caller's order. They are the toolbar's arrow-key stops, after the clear.
105
+ *
106
+ * LINKS rove too (keksdose F8): a `<Button href>` — "View report", which navigates
107
+ * rather than acts — is an `<a href>`, and it is one more arrow-key stop in its place
108
+ * among the buttons, not a second Tab stop beside the toolbar's one. A disabled
109
+ * `<Button href disabled>` has no `href` and is skipped, as a disabled button is. Any
110
+ * other focusable of your own joins with `data-toolbar-item`.
111
+ */
104
112
  children?: ReactNode;
105
113
  /** Default `floating`. See {@link BulkActionBarVariant}; an object picks one per
106
114
  * breakpoint, see {@link ResponsiveBulkActionBarVariant}. */
@@ -135,7 +143,12 @@ const FLOATING_STYLE: CSSProperties = {
135
143
 
136
144
  /** What the arrow keys walk: the toolbar's enabled controls. Matched by element, not by
137
145
  * `tabindex`, because the roving below sets every one but the current to `-1` — a
138
- * custom control joins with `data-toolbar-item`. */
146
+ * custom control joins with `data-toolbar-item`.
147
+ *
148
+ * `a[href]` is here on purpose (keksdose F8, a "View report" `Button href` in the bar):
149
+ * a link in a toolbar is a toolbar item like any other, and left out it would keep its
150
+ * own Tab stop, so Tab would stop twice in a bar that promises one. `[href]` rather than `a`: the kit's disabled link is an `<a>`
151
+ * WITHOUT one, and is skipped as a disabled button is. */
139
152
  const ITEMS =
140
153
  "button:not([disabled]),a[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[data-toolbar-item]";
141
154
 
@@ -1,6 +1,8 @@
1
1
  import type { ComponentPropsWithoutRef, MouseEvent, ReactElement, ReactNode, Ref } from "react";
2
2
  import { cn } from "../lib/cn";
3
3
  import { buttonClasses, type ButtonSize, type ButtonVariant } from "./ui";
4
+ import { useKitLink } from "../i18n/kit-labels";
5
+ import { pickLinkRenderer } from "./text-link";
4
6
 
5
7
  // Every class below is spelled out in full rather than assembled from parts: the
6
8
  // consumer's Tailwind build finds classes by scanning this package's source as text,
@@ -147,7 +149,8 @@ export interface ButtonGroupLinkProps extends Omit<ComponentPropsWithoutRef<"a">
147
149
  /** The member for the page you are on: `aria-current="page"` and the "on" look of a
148
150
  * pressed member, so a link row reads as a segmented switch between pages. */
149
151
  current?: boolean;
150
- /** Render through your router's `Link` — see {@link ButtonGroupLinkRenderProps}. */
152
+ /** Render through your router's `Link` — see {@link ButtonGroupLinkRenderProps}.
153
+ * Default: the `<UiKitProvider linkComponent>`, then `<a>`. */
151
154
  renderLink?: (props: ButtonGroupLinkRenderProps) => ReactElement;
152
155
  className?: string;
153
156
  children: ReactNode;
@@ -174,6 +177,7 @@ export function ButtonGroupLink({
174
177
  children,
175
178
  ...rest
176
179
  }: ButtonGroupLinkProps) {
180
+ const kitLink = useKitLink();
177
181
  const props: ButtonGroupLinkRenderProps = {
178
182
  ...rest,
179
183
  href,
@@ -181,7 +185,8 @@ export function ButtonGroupLink({
181
185
  className: buttonClasses(variant, { size, className: cn(current && LINK_CURRENT, className) }),
182
186
  children,
183
187
  };
184
- if (renderLink) return <RenderedGroupLink render={renderLink} {...props} />;
188
+ const render = pickLinkRenderer(renderLink, kitLink, href);
189
+ if (render) return <RenderedGroupLink render={render} {...props} />;
185
190
  const { children: content, ...anchor } = props;
186
191
  return <a {...anchor}>{content}</a>;
187
192
  }
@@ -4,6 +4,8 @@ import { cn } from "../lib/cn";
4
4
  import { dirOf, horizontalStep } from "../lib/direction";
5
5
  import { addMonthsClamped, localeWeekStart, parseIsoDate, toLocalIso } from "../lib/dates";
6
6
  import { useKitLabels, useKitLocale, useKitWeekStart } from "../i18n/kit-labels";
7
+ import { lerpHex, textOn } from "../theme/chart-palette";
8
+ import { parseHex } from "../theme/color";
7
9
  import { Tooltip } from "./tooltip";
8
10
  import type { WeekDay } from "./mini-calendar";
9
11
 
@@ -103,6 +105,39 @@ export interface CalendarHeatmapProps extends Omit<ComponentPropsWithoutRef<"div
103
105
  /** The scale's colour, as any CSS colour. Default `var(--brand)`; each step is this
104
106
  * mixed into `--bg-surface`, so the ramp follows the theme. */
105
107
  color?: string;
108
+ /**
109
+ * A CONTINUOUS scale from `colorFrom` (the smallest value) to `colorTo` (the busiest
110
+ * day), instead of `levels` steps of `color` — keksdose's palette-store ramp,
111
+ * `seqLow` → `seqHigh`. Both are needed; `color` is then unused. Two `#rrggbb`
112
+ * colours are interpolated in JS, so the fill is a concrete hex and the day number's
113
+ * ink (in `"month"`) is MEASURED against it; anything else (a `var()`) is mixed with
114
+ * `color-mix()`. `levels` still sets `data-level` and the legend's swatch count.
115
+ */
116
+ colorFrom?: string;
117
+ colorTo?: string;
118
+ /**
119
+ * A fill of the caller's own for a day with a value: handed the value and the
120
+ * scale's `max`, returns any CSS colour. Wins over `color` and `colorFrom`/`colorTo`.
121
+ * Also paints the legend, at `max × level / levels`.
122
+ */
123
+ fill?: (value: number, max: number) => string;
124
+ /** The fill of a day with no value (or a value of 0). Default: a wash of the text
125
+ * colour over the surface, which reads as a cell on both themes. */
126
+ emptyColor?: string;
127
+ /**
128
+ * Where the window ENDS — the day `maxDays` counts back from, and the last one drawn:
129
+ *
130
+ * - `"to"` (default): on `to`.
131
+ * - `"today"`: on today.
132
+ * - `"latest"`: on the last day that can carry a figure — today, or a later day that
133
+ * actually has a value. A window running into the future (a report's "all" preset
134
+ * ends a year ahead) then spends its cells on the days a person is looking for,
135
+ * not on a year of empty ones.
136
+ * - a `"YYYY-MM-DD"`: on that day.
137
+ *
138
+ * Always kept inside `from`…`to`.
139
+ */
140
+ anchor?: "to" | "today" | "latest" | (string & {});
106
141
  /**
107
142
  * The most days to draw. A longer window keeps its LATEST `maxDays` and says how many
108
143
  * it left out (`labels.truncated`) rather than silently ending. Default: no cap.
@@ -131,6 +166,26 @@ const EMPTY_FILL = "color-mix(in oklab, var(--text-primary) 8%, var(--bg-surface
131
166
 
132
167
  const weekdayIndex = (d: Date, weekStart: WeekDay) => (d.getDay() - weekStart + 7) % 7;
133
168
 
169
+ /** Where the window ends for an {@link CalendarHeatmapProps.anchor}, kept in `[from, to]`.
170
+ * `latestData` is the latest ISO date carrying a value, or `""`. */
171
+ export function heatmapWindowEnd(
172
+ from: string,
173
+ to: string,
174
+ anchor: string,
175
+ latestData: string,
176
+ today: string = toLocalIso(new Date()),
177
+ ): string {
178
+ let end: string;
179
+ if (anchor === "today") end = today;
180
+ else if (anchor === "latest") end = latestData > today ? latestData : today;
181
+ else if (anchor === "to" || !parseIsoDate(anchor)) end = to;
182
+ else end = anchor;
183
+ // ISO dates sort as strings, so the clamp is plain comparison.
184
+ if (end > to) end = to;
185
+ if (end < from) end = from;
186
+ return end;
187
+ }
188
+
134
189
  /** The step a value falls on: 0 for nothing, else 1…levels by its share of `max`. */
135
190
  export function heatmapLevel(value: number, max: number, levels: number): number {
136
191
  if (!(value > 0) || !(max > 0) || levels < 1) return 0;
@@ -159,7 +214,9 @@ function levelFill(level: number, levels: number, color: string): string {
159
214
  * the other way round, mirrored in RTL — PageUp/PageDown move a month and Home/End go
160
215
  * to the window's first/last day. Each cell is named with its whole date AND its value,
161
216
  * because the colour is not something a screen reader can say, and carries
162
- * `data-day="YYYY-MM-DD"` as its identity for tests and for the host.
217
+ * `data-day="YYYY-MM-DD"` as its identity for tests and for the host. Note that in
218
+ * `"weeks"` the cells are in DOM order by WEEKDAY ROW (all Mondays, then all
219
+ * Tuesdays…), not by date — query by `data-day`, not by position.
163
220
  *
164
221
  * The tooltip is portalled (a year of squares always sits in a sideways scroller) and
165
222
  * `data-private` unless `sensitive={false}`. Cell size in `"weeks"` is the CSS variable
@@ -178,6 +235,11 @@ export function CalendarHeatmap({
178
235
  max: maxProp,
179
236
  levels = 4,
180
237
  color = "var(--brand)",
238
+ colorFrom,
239
+ colorTo,
240
+ fill: fillProp,
241
+ emptyColor,
242
+ anchor = "to",
181
243
  maxDays,
182
244
  legend = true,
183
245
  locale: localeProp,
@@ -202,9 +264,16 @@ export function CalendarHeatmap({
202
264
 
203
265
  // The window, trimmed from the FRONT: the days a person looks for are the recent
204
266
  // ones, and a cap that kept the oldest would end the calendar before today.
267
+ const latestData = useMemo(() => {
268
+ let latest = "";
269
+ for (const [iso, v] of values) if (v !== 0 && iso > latest) latest = iso;
270
+ return latest;
271
+ }, [values]);
272
+ const windowEnd = heatmapWindowEnd(from, to, anchor, latestData);
273
+
205
274
  const { days, dropped } = useMemo(() => {
206
275
  const start = parseIsoDate(from);
207
- const end = parseIsoDate(to);
276
+ const end = parseIsoDate(windowEnd);
208
277
  const list: Date[] = [];
209
278
  if (!start || !end || end < start) return { days: list, dropped: 0 };
210
279
  const cursor = new Date(end);
@@ -216,7 +285,7 @@ export function CalendarHeatmap({
216
285
  const first = list[0];
217
286
  const trimmed = first ? Math.round((first.getTime() - start.getTime()) / 86_400_000) : 0;
218
287
  return { days: list, dropped: Math.max(0, trimmed) };
219
- }, [from, to, maxDays]);
288
+ }, [from, windowEnd, maxDays]);
220
289
 
221
290
  const firstIso = days.length ? toLocalIso(days[0]) : "";
222
291
  const lastIso = days.length ? toLocalIso(days[days.length - 1]) : "";
@@ -341,13 +410,43 @@ export function CalendarHeatmap({
341
410
  };
342
411
 
343
412
  const legendLevels = Array.from({ length: levels + 1 }, (_, l) => l);
413
+ const empty = emptyColor ?? EMPTY_FILL;
414
+ const continuous = colorFrom !== undefined && colorTo !== undefined;
415
+ const hexRamp = continuous && parseHex(colorFrom) != null && parseHex(colorTo) != null;
416
+
417
+ /** A day's fill, and the ink its number is set in (`"month"`). */
418
+ const paint = (value: number, level: number): { fill: string; ink: string } => {
419
+ if (level === 0) return { fill: empty, ink: "var(--text-primary)" };
420
+ if (fillProp || continuous) {
421
+ const t = max > 0 ? Math.min(1, Math.max(0, value / max)) : 1;
422
+ const fill = fillProp
423
+ ? fillProp(value, max)
424
+ : hexRamp
425
+ ? lerpHex(colorFrom!, colorTo!, t)
426
+ : `color-mix(in oklab, ${colorTo} ${Math.round(t * 100)}%, ${colorFrom})`;
427
+ // Measured where the fill is a colour; a mix of references cannot be measured,
428
+ // and the page's own text colour is the safe answer there.
429
+ const measured = textOn(fill);
430
+ return { fill, ink: measured === "currentColor" ? "var(--text-primary)" : measured };
431
+ }
432
+ // Light text once the fill is the stronger half of the mix — `--bg-surface` is the
433
+ // colour the scale is mixed INTO, so it is the one that contrasts with its far end
434
+ // on either theme.
435
+ const strong = levelPercent(level, levels) >= 45;
436
+ return {
437
+ fill: levelFill(level, levels, color),
438
+ ink: strong ? "var(--bg-surface)" : "var(--text-primary)",
439
+ };
440
+ };
441
+ // The legend's swatches: a value that lands on each step (the top one on `max`).
442
+ const legendFill = (l: number) => paint(((max > 0 ? max : 1) * l) / levels, l).fill;
344
443
 
345
444
  const cell = (d: Date, wrapClass: string, cellClass: string, withNumber: boolean) => {
346
445
  const iso = toLocalIso(d);
347
446
  const raw = values.get(iso);
348
447
  const value = raw ?? 0;
349
448
  const level = heatmapLevel(value, max, levels);
350
- const fill = levelFill(level, levels, color);
449
+ const { fill, ink } = paint(value, level);
351
450
  const formattedDate = formatDay(d);
352
451
  const formattedValue = formatValue(value);
353
452
  const name = labels.day(formattedDate, formattedValue);
@@ -355,10 +454,6 @@ export function CalendarHeatmap({
355
454
  ? tooltip({ iso, date: d, value, hasData: raw !== undefined, level, formattedDate, formattedValue })
356
455
  : name;
357
456
  const isSelected = selected === iso;
358
- // Light text once the fill is the stronger half of the mix — `--bg-surface` is the
359
- // colour the scale is mixed INTO, so it is the one that contrasts with its far end
360
- // on either theme.
361
- const strong = level > 0 && levelPercent(level, levels) >= 45;
362
457
  const shared = {
363
458
  role: "gridcell",
364
459
  "data-day": iso,
@@ -370,7 +465,7 @@ export function CalendarHeatmap({
370
465
  onFocus: () => {
371
466
  if (iso !== activeIso) setActiveIso(iso);
372
467
  },
373
- style: { background: fill, color: strong ? "var(--bg-surface)" : "var(--text-primary)" },
468
+ style: { background: fill, color: ink },
374
469
  className: cn(
375
470
  cellClass,
376
471
  isSelected && "ring-2 ring-[var(--text-primary)]",
@@ -493,7 +588,7 @@ export function CalendarHeatmap({
493
588
  key={l}
494
589
  data-legend-level={l}
495
590
  className="size-2.5 rounded-sm"
496
- style={{ background: levelFill(l, levels, color) }}
591
+ style={{ background: legendFill(l) }}
497
592
  />
498
593
  ))}
499
594
  <span className="ms-0.5">{labels.more}</span>
@@ -8,7 +8,7 @@
8
8
  // The value on show is the *structure*: config-driven colors injected as CSS
9
9
  // vars, muted axis/grid styling, and a polished tooltip/legend — the things
10
10
  // that make shadcn charts read as "designed" rather than "default Recharts".
11
- import { createContext, useContext, useId, useRef } from "react";
11
+ import { createContext, useContext, useId, useLayoutEffect, useRef, useState } from "react";
12
12
  import type { ComponentProps, CSSProperties, ReactNode, RefObject } from "react";
13
13
  import {
14
14
  Legend as RechartsLegend,
@@ -263,7 +263,7 @@ export function ChartTooltipContent({
263
263
  // `ChartTooltip` keeps null values (so they can print "—"), which also keeps the
264
264
  // series a legend toggle switched off with `hide`; those go here instead.
265
265
  const items = includeHidden ? payload : payload?.filter((item) => item.hide !== true);
266
- if (!active || !items?.length) return null;
266
+ const visible = Boolean(active && items?.length);
267
267
  // Recharts anchors the tooltip at coordinate.x inside the full (scrolled) chart
268
268
  // width; subtract the container's scrollLeft to get its on-screen x, then flip
269
269
  // left if the tooltip's own width would run past the visible right edge.
@@ -272,16 +272,27 @@ export function ChartTooltipContent({
272
272
  // `ChartContainer`), but an RTL scroller counts `scrollLeft` from its right edge —
273
273
  // 0 at the start, negative going left — so there the content's left edge sits
274
274
  // `scrollWidth - clientWidth + scrollLeft` px to the left of the visible one.
275
- let flip = false;
276
- const boundary = boundaryRef?.current;
277
- if (boundary && coordinate?.x != null) {
275
+ //
276
+ // Measured in a layout effect (the DOM is not readable during render), so it commits
277
+ // before paint. `payload` and `label` are deps because they change the tooltip's own
278
+ // width, and recharts hands a fresh `payload` on every tooltip render, so a scroll
279
+ // followed by any re-render is re-measured just as it was when this ran in render.
280
+ const [flip, setFlip] = useState(false);
281
+ const x = coordinate?.x;
282
+ useLayoutEffect(() => {
283
+ const boundary = boundaryRef?.current;
284
+ if (!visible || !boundary || x == null) {
285
+ setFlip(false);
286
+ return;
287
+ }
278
288
  const offset = isRtl(boundary)
279
289
  ? boundary.scrollWidth - boundary.clientWidth + boundary.scrollLeft
280
290
  : boundary.scrollLeft;
281
- const visibleX = coordinate.x - offset;
291
+ const visibleX = x - offset;
282
292
  const tipWidth = tipRef.current?.offsetWidth ?? 160;
283
- flip = visibleX + tipWidth + 12 > boundary.clientWidth;
284
- }
293
+ setFlip(visibleX + tipWidth + 12 > boundary.clientWidth);
294
+ }, [visible, x, boundaryRef, payload, label]);
295
+ if (!visible || !items) return null;
285
296
  return (
286
297
  <div
287
298
  ref={tipRef}
@@ -5,7 +5,8 @@ import type { LucideIcon } from "lucide-react";
5
5
  import { cn } from "../lib/cn";
6
6
  import { horizontalStep } from "../lib/direction";
7
7
  import { FIELD_INVALID } from "./ui";
8
- import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from "../i18n/kit-labels";
8
+ import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink, useKitLocale } from "../i18n/kit-labels";
9
+ import { pickLinkRenderer } from "./text-link";
9
10
 
10
11
  /**
11
12
  * A chip: a compact pill carrying one value.
@@ -384,7 +385,8 @@ export type ChipProps = ChipBaseProps &
384
385
  /**
385
386
  * Renders the link for `href` — pass your router's `<Link>` here, since a
386
387
  * plain `<a>` reloads a single-page app (keksdose's account-menu admin pill).
387
- * Default: `<a>`. Ignored without `href`.
388
+ * Default: the `<UiKitProvider linkComponent>`, then `<a>`. Ignored without
389
+ * `href`.
388
390
  *
389
391
  * A render function rather than an `as={Link}`, for three reasons. It is the
390
392
  * API `StatTile` already has (`renderLink`), so a consumer learns it once.
@@ -455,6 +457,7 @@ export const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(
455
457
  ref,
456
458
  ) {
457
459
  const common = useKitLabels("common", DEFAULT_COMMON_LABELS, { remove: removeLabel });
460
+ const kitLink = useKitLink();
458
461
  const s = SIZE[size];
459
462
  const interactive = !!href || !!onClick;
460
463
  // One radius for every rounded piece — the pill, the body inside a split pill, the ×
@@ -570,10 +573,11 @@ export const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(
570
573
  className: remove ? inner : look,
571
574
  children: body,
572
575
  };
573
- const link = renderLink ? (
576
+ const render = pickLinkRenderer(renderLink, kitLink, href);
577
+ const link = render ? (
574
578
  // Through a component rather than called here, so the forwarded ref arrives as
575
579
  // an ordinary prop of an element and is never handed to a function mid-render.
576
- <RenderedLink render={renderLink} {...linkProps} />
580
+ <RenderedLink render={render} {...linkProps} />
577
581
  ) : (
578
582
  <a
579
583
  ref={ref as React.Ref<HTMLAnchorElement>}
@@ -617,6 +621,9 @@ export const Chip = forwardRef<HTMLElement, ChipProps>(function Chip(
617
621
  }
618
622
 
619
623
  return (
624
+ /* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- the handlers are
625
+ the roving-focus hooks, live only when the container passes `role` and `tabIndex`
626
+ (ChipInput does); the role is a prop, so the rule cannot see it. */
620
627
  <span
621
628
  ref={ref as React.Ref<HTMLSpanElement>}
622
629
  className={look}
@@ -889,6 +896,9 @@ export function ChipInput({
889
896
  {label}
890
897
  </label>
891
898
  )}
899
+ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- a
900
+ pointer convenience that widens the input's hit area to the whole field; the
901
+ keyboard path is the <input> itself, which is in the tab order. */}
892
902
  <div
893
903
  // `relative` so the live region below can be `sr-only` without escaping to the
894
904
  // initial containing block and inflating the document height.