@eifi1/ui-kit 0.11.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 (342) hide show
  1. package/README.md +57 -32
  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 +24 -4
  12. package/dist/components/amount-input.js +5 -2
  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/button-group.d.ts +51 -2
  21. package/dist/components/button-group.js +5 -1
  22. package/dist/components/button-group.js.map +1 -1
  23. package/dist/components/calculator.d.ts +15 -4
  24. package/dist/components/calendar-heatmap.d.ts +41 -3
  25. package/dist/components/calendar-heatmap.js +48 -7
  26. package/dist/components/calendar-heatmap.js.map +1 -1
  27. package/dist/components/chip.d.ts +2 -1
  28. package/dist/components/chip.js +6 -3
  29. package/dist/components/chip.js.map +1 -1
  30. package/dist/components/choice-card.d.ts +35 -3
  31. package/dist/components/choice-card.js +83 -25
  32. package/dist/components/choice-card.js.map +1 -1
  33. package/dist/components/combobox-core.js +2 -0
  34. package/dist/components/combobox-core.js.map +1 -1
  35. package/dist/components/combobox.d.ts +2 -2
  36. package/dist/components/combobox.js +31 -9
  37. package/dist/components/combobox.js.map +1 -1
  38. package/dist/components/copy-button.d.ts +51 -70
  39. package/dist/components/copy-button.js +1 -1
  40. package/dist/components/copy-button.js.map +1 -1
  41. package/dist/components/data-table-cells.d.ts +63 -0
  42. package/dist/components/data-table-cells.js +64 -0
  43. package/dist/components/data-table-cells.js.map +1 -0
  44. package/dist/components/data-table-filter-popover.d.ts +1 -1
  45. package/dist/components/data-table-filters.d.ts +1 -1
  46. package/dist/components/data-table-filters.js +31 -1
  47. package/dist/components/data-table-filters.js.map +1 -1
  48. package/dist/components/data-table-labels.d.ts +13 -0
  49. package/dist/components/data-table-labels.js +8 -1
  50. package/dist/components/data-table-labels.js.map +1 -1
  51. package/dist/components/data-table.d.ts +1 -1
  52. package/dist/components/data-table.js +102 -12
  53. package/dist/components/data-table.js.map +1 -1
  54. package/dist/components/date-picker.d.ts +3 -1
  55. package/dist/components/date-picker.js +15 -2
  56. package/dist/components/date-picker.js.map +1 -1
  57. package/dist/components/description-list.d.ts +23 -3
  58. package/dist/components/description-list.js +23 -3
  59. package/dist/components/description-list.js.map +1 -1
  60. package/dist/components/dialog-frame.d.ts +1 -0
  61. package/dist/components/entity-combobox.d.ts +1 -1
  62. package/dist/components/entity-combobox.js +18 -3
  63. package/dist/components/entity-combobox.js.map +1 -1
  64. package/dist/components/error-boundary.d.ts +92 -0
  65. package/dist/components/error-boundary.js +111 -0
  66. package/dist/components/error-boundary.js.map +1 -0
  67. package/dist/components/field.d.ts +30 -4
  68. package/dist/components/field.js +40 -17
  69. package/dist/components/field.js.map +1 -1
  70. package/dist/components/file-button.d.ts +50 -208
  71. package/dist/components/file-dropzone.d.ts +49 -2
  72. package/dist/components/floating-panel.d.ts +13 -4
  73. package/dist/components/floating-panel.js +7 -3
  74. package/dist/components/floating-panel.js.map +1 -1
  75. package/dist/components/form-actions.d.ts +53 -0
  76. package/dist/components/form-actions.js +98 -0
  77. package/dist/components/form-actions.js.map +1 -0
  78. package/dist/components/image-grid.d.ts +67 -0
  79. package/dist/components/image-grid.js +112 -0
  80. package/dist/components/image-grid.js.map +1 -0
  81. package/dist/components/lightbox.d.ts +104 -0
  82. package/dist/components/lightbox.js +229 -0
  83. package/dist/components/lightbox.js.map +1 -0
  84. package/dist/components/line-items.d.ts +114 -0
  85. package/dist/components/line-items.js +238 -0
  86. package/dist/components/line-items.js.map +1 -0
  87. package/dist/components/list.d.ts +23 -2
  88. package/dist/components/list.js +32 -13
  89. package/dist/components/list.js.map +1 -1
  90. package/dist/components/loading-state.d.ts +24 -0
  91. package/dist/components/loading-state.js +32 -0
  92. package/dist/components/loading-state.js.map +1 -0
  93. package/dist/components/menu-item.d.ts +2 -1
  94. package/dist/components/menu-item.js +5 -1
  95. package/dist/components/menu-item.js.map +1 -1
  96. package/dist/components/mini-calendar.d.ts +13 -1
  97. package/dist/components/mini-calendar.js +46 -2
  98. package/dist/components/mini-calendar.js.map +1 -1
  99. package/dist/components/modal.d.ts +20 -1
  100. package/dist/components/modal.js +26 -2
  101. package/dist/components/modal.js.map +1 -1
  102. package/dist/components/month-picker.d.ts +21 -1
  103. package/dist/components/month-picker.js +139 -26
  104. package/dist/components/month-picker.js.map +1 -1
  105. package/dist/components/multi-entity-combobox.d.ts +1 -1
  106. package/dist/components/multi-entity-combobox.js +19 -3
  107. package/dist/components/multi-entity-combobox.js.map +1 -1
  108. package/dist/components/nav-pills.d.ts +2 -1
  109. package/dist/components/nav-pills.js +5 -1
  110. package/dist/components/nav-pills.js.map +1 -1
  111. package/dist/components/number-field.d.ts +24 -5
  112. package/dist/components/number-field.js +5 -7
  113. package/dist/components/number-field.js.map +1 -1
  114. package/dist/components/number-input.d.ts +25 -5
  115. package/dist/components/number-input.js +7 -1
  116. package/dist/components/number-input.js.map +1 -1
  117. package/dist/components/numpad-sheet.d.ts +15 -4
  118. package/dist/components/passkeys-setting.d.ts +67 -0
  119. package/dist/components/passkeys-setting.js +214 -0
  120. package/dist/components/passkeys-setting.js.map +1 -0
  121. package/dist/components/picker-sheet.js +2 -1
  122. package/dist/components/picker-sheet.js.map +1 -1
  123. package/dist/components/pie-chart-labels.d.ts +20 -0
  124. package/dist/components/pie-chart-labels.js +12 -0
  125. package/dist/components/pie-chart-labels.js.map +1 -0
  126. package/dist/components/pie-chart.d.ts +101 -0
  127. package/dist/components/pie-chart.js +283 -0
  128. package/dist/components/pie-chart.js.map +1 -0
  129. package/dist/components/progress-bar.d.ts +44 -3
  130. package/dist/components/progress-bar.js +58 -22
  131. package/dist/components/progress-bar.js.map +1 -1
  132. package/dist/components/qr-code.d.ts +34 -0
  133. package/dist/components/qr-code.js +57 -0
  134. package/dist/components/qr-code.js.map +1 -0
  135. package/dist/components/search-field.d.ts +15 -3
  136. package/dist/components/search-field.js +5 -1
  137. package/dist/components/search-field.js.map +1 -1
  138. package/dist/components/settings-fields.d.ts +49 -1
  139. package/dist/components/signed-amount.d.ts +121 -0
  140. package/dist/components/signed-amount.js +152 -0
  141. package/dist/components/signed-amount.js.map +1 -0
  142. package/dist/components/skeleton.d.ts +10 -3
  143. package/dist/components/skeleton.js +12 -2
  144. package/dist/components/skeleton.js.map +1 -1
  145. package/dist/components/stat-tile.d.ts +2 -1
  146. package/dist/components/stat-tile.js +5 -2
  147. package/dist/components/stat-tile.js.map +1 -1
  148. package/dist/components/table.d.ts +39 -5
  149. package/dist/components/table.js +45 -11
  150. package/dist/components/table.js.map +1 -1
  151. package/dist/components/text-link.d.ts +176 -0
  152. package/dist/components/text-link.js +109 -0
  153. package/dist/components/text-link.js.map +1 -0
  154. package/dist/components/time-input.d.ts +49 -1
  155. package/dist/components/toggle-group.d.ts +13 -0
  156. package/dist/components/toggle-group.js +35 -6
  157. package/dist/components/toggle-group.js.map +1 -1
  158. package/dist/components/toggle-legend.d.ts +36 -3
  159. package/dist/components/toggle-legend.js +49 -0
  160. package/dist/components/toggle-legend.js.map +1 -1
  161. package/dist/components/ui.d.ts +51 -779
  162. package/dist/components/ui.js +215 -23
  163. package/dist/components/ui.js.map +1 -1
  164. package/dist/components/use-table-state.d.ts +3 -52
  165. package/dist/components/use-table-state.js +128 -30
  166. package/dist/components/use-table-state.js.map +1 -1
  167. package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
  168. package/dist/data-table.d.ts +2 -1
  169. package/dist/data-table.js +5 -1
  170. package/dist/data-table.js.map +1 -1
  171. package/dist/feedback/feedback-dialog.d.ts +26 -3
  172. package/dist/feedback/feedback-dialog.js +16 -2
  173. package/dist/feedback/feedback-dialog.js.map +1 -1
  174. package/dist/feedback/feedback-thread.d.ts +133 -0
  175. package/dist/feedback/feedback-thread.js +237 -0
  176. package/dist/feedback/feedback-thread.js.map +1 -0
  177. package/dist/feedback.d.ts +2 -1
  178. package/dist/feedback.js +1 -0
  179. package/dist/feedback.js.map +1 -1
  180. package/dist/hooks/use-authed-src.d.ts +71 -0
  181. package/dist/hooks/use-authed-src.js +63 -0
  182. package/dist/hooks/use-authed-src.js.map +1 -0
  183. package/dist/hooks/use-file-drop.d.ts +49 -2
  184. package/dist/hooks/use-hotkey.d.ts +59 -0
  185. package/dist/hooks/use-hotkey.js +105 -0
  186. package/dist/hooks/use-hotkey.js.map +1 -0
  187. package/dist/hooks/use-search-param-state.d.ts +83 -0
  188. package/dist/hooks/use-search-param-state.js +94 -0
  189. package/dist/hooks/use-search-param-state.js.map +1 -0
  190. package/dist/i18n/defaults.d.ts +15 -4
  191. package/dist/i18n/defaults.js +31 -1
  192. package/dist/i18n/defaults.js.map +1 -1
  193. package/dist/i18n/kit-labels.d.ts +46 -312
  194. package/dist/i18n/kit-labels.js +20 -5
  195. package/dist/i18n/kit-labels.js.map +1 -1
  196. package/dist/i18n/locales/de-CH-informal.d.ts +15 -4
  197. package/dist/i18n/locales/de-CH.d.ts +15 -4
  198. package/dist/i18n/locales/de-informal.d.ts +15 -4
  199. package/dist/i18n/locales/de.d.ts +15 -4
  200. package/dist/i18n/locales/de.js +156 -3
  201. package/dist/i18n/locales/de.js.map +1 -1
  202. package/dist/i18n/locales/es.d.ts +15 -4
  203. package/dist/i18n/locales/es.js +153 -3
  204. package/dist/i18n/locales/es.js.map +1 -1
  205. package/dist/i18n/locales/fr.d.ts +15 -4
  206. package/dist/i18n/locales/fr.js +153 -3
  207. package/dist/i18n/locales/fr.js.map +1 -1
  208. package/dist/i18n/locales/hu.d.ts +15 -4
  209. package/dist/i18n/locales/hu.js +153 -3
  210. package/dist/i18n/locales/hu.js.map +1 -1
  211. package/dist/i18n/locales/it.d.ts +15 -4
  212. package/dist/i18n/locales/it.js +153 -3
  213. package/dist/i18n/locales/it.js.map +1 -1
  214. package/dist/i18n/locales/zh.d.ts +15 -4
  215. package/dist/i18n/locales/zh.js +153 -3
  216. package/dist/i18n/locales/zh.js.map +1 -1
  217. package/dist/index.d.ts +37 -17
  218. package/dist/index.js +40 -1
  219. package/dist/index.js.map +1 -1
  220. package/dist/kit-labels-v3biUF1L.d.ts +1645 -0
  221. package/dist/lib/format.d.ts +144 -0
  222. package/dist/lib/format.js +149 -0
  223. package/dist/lib/format.js.map +1 -0
  224. package/dist/lib/qr-encode.d.ts +44 -0
  225. package/dist/lib/qr-encode.js +338 -0
  226. package/dist/lib/qr-encode.js.map +1 -0
  227. package/dist/rhf/fields.d.ts +241 -0
  228. package/dist/rhf/fields.js +601 -0
  229. package/dist/rhf/fields.js.map +1 -0
  230. package/dist/rhf/form.d.ts +49 -1
  231. package/dist/rhf/line-items.d.ts +31 -0
  232. package/dist/rhf/line-items.js +59 -0
  233. package/dist/rhf/line-items.js.map +1 -0
  234. package/dist/rhf.d.ts +61 -1
  235. package/dist/rhf.js +2 -0
  236. package/dist/rhf.js.map +1 -1
  237. package/dist/shell/app-shell.d.ts +99 -2
  238. package/dist/shell/app-shell.js +58 -3
  239. package/dist/shell/app-shell.js.map +1 -1
  240. package/dist/shell/auth-layout.d.ts +74 -0
  241. package/dist/shell/auth-layout.js +97 -0
  242. package/dist/shell/auth-layout.js.map +1 -0
  243. package/dist/shell/top-bar-brand.d.ts +81 -0
  244. package/dist/shell/top-bar-brand.js +47 -0
  245. package/dist/shell/top-bar-brand.js.map +1 -0
  246. package/dist/shell/topbar-action-menu.d.ts +12 -1
  247. package/dist/shell/topbar-action-menu.js +16 -1
  248. package/dist/shell/topbar-action-menu.js.map +1 -1
  249. package/dist/shell.d.ts +56 -1
  250. package/dist/shell.js +2 -0
  251. package/dist/shell.js.map +1 -1
  252. package/dist/wizard/stepper-nav.d.ts +48 -1
  253. package/dist/wizard/wizard-context.d.ts +24 -1
  254. package/dist/wizard/wizard-context.js +19 -2
  255. package/dist/wizard/wizard-context.js.map +1 -1
  256. package/dist/wizard/wizard-step.d.ts +20 -5
  257. package/dist/wizard/wizard-step.js +16 -3
  258. package/dist/wizard/wizard-step.js.map +1 -1
  259. package/dist/wizard.d.ts +50 -3
  260. package/package.json +1 -1
  261. package/src/chart.ts +4 -0
  262. package/src/components/account-settings-labels.ts +190 -0
  263. package/src/components/account-settings.tsx +110 -63
  264. package/src/components/amount-input.tsx +13 -1
  265. package/src/components/authed-image.tsx +190 -0
  266. package/src/components/breadcrumbs.tsx +8 -2
  267. package/src/components/button-group.tsx +7 -2
  268. package/src/components/calendar-heatmap.tsx +105 -10
  269. package/src/components/chip.tsx +8 -4
  270. package/src/components/choice-card.tsx +136 -25
  271. package/src/components/combobox-core.tsx +5 -0
  272. package/src/components/combobox.tsx +31 -9
  273. package/src/components/copy-button.tsx +3 -2
  274. package/src/components/data-table-cells.tsx +119 -0
  275. package/src/components/data-table-filters.ts +77 -0
  276. package/src/components/data-table-labels.ts +23 -0
  277. package/src/components/data-table.tsx +224 -13
  278. package/src/components/date-picker.tsx +19 -3
  279. package/src/components/description-list.tsx +52 -3
  280. package/src/components/entity-combobox.tsx +18 -3
  281. package/src/components/error-boundary.tsx +216 -0
  282. package/src/components/field.tsx +56 -5
  283. package/src/components/floating-panel.tsx +20 -7
  284. package/src/components/form-actions.tsx +191 -0
  285. package/src/components/image-grid.tsx +181 -0
  286. package/src/components/lightbox.tsx +374 -0
  287. package/src/components/line-items.tsx +385 -0
  288. package/src/components/list.tsx +65 -16
  289. package/src/components/loading-state.tsx +47 -0
  290. package/src/components/menu-item.tsx +7 -2
  291. package/src/components/mini-calendar.tsx +72 -3
  292. package/src/components/modal.tsx +61 -3
  293. package/src/components/month-picker.tsx +181 -26
  294. package/src/components/multi-entity-combobox.tsx +19 -3
  295. package/src/components/nav-pills.tsx +8 -1
  296. package/src/components/number-field.tsx +15 -10
  297. package/src/components/number-input.tsx +16 -1
  298. package/src/components/passkeys-setting.tsx +331 -0
  299. package/src/components/picker-sheet.tsx +7 -1
  300. package/src/components/pie-chart-labels.ts +32 -0
  301. package/src/components/pie-chart.tsx +479 -0
  302. package/src/components/progress-bar.tsx +129 -19
  303. package/src/components/qr-code.tsx +83 -0
  304. package/src/components/search-field.tsx +20 -4
  305. package/src/components/signed-amount.tsx +286 -0
  306. package/src/components/skeleton.tsx +21 -3
  307. package/src/components/stat-tile.tsx +8 -2
  308. package/src/components/table.tsx +102 -13
  309. package/src/components/text-link.tsx +248 -0
  310. package/src/components/toggle-group.tsx +58 -10
  311. package/src/components/toggle-legend.tsx +87 -2
  312. package/src/components/ui.tsx +426 -36
  313. package/src/components/use-table-state.ts +265 -33
  314. package/src/data-table.ts +10 -0
  315. package/src/feedback/feedback-dialog.tsx +42 -3
  316. package/src/feedback/feedback-thread.tsx +434 -0
  317. package/src/feedback.ts +1 -0
  318. package/src/hooks/use-authed-src.ts +161 -0
  319. package/src/hooks/use-hotkey.ts +172 -0
  320. package/src/hooks/use-search-param-state.ts +187 -0
  321. package/src/i18n/defaults.ts +30 -0
  322. package/src/i18n/kit-labels.tsx +102 -8
  323. package/src/i18n/locales/de.ts +153 -0
  324. package/src/i18n/locales/es.ts +150 -0
  325. package/src/i18n/locales/fr.ts +153 -0
  326. package/src/i18n/locales/hu.ts +151 -0
  327. package/src/i18n/locales/it.ts +151 -0
  328. package/src/i18n/locales/zh.ts +150 -0
  329. package/src/index.ts +59 -0
  330. package/src/lib/format.ts +325 -0
  331. package/src/lib/qr-encode.ts +420 -0
  332. package/src/rhf/fields.tsx +1054 -0
  333. package/src/rhf/line-items.tsx +130 -0
  334. package/src/rhf.ts +6 -0
  335. package/src/shell/app-shell.tsx +147 -2
  336. package/src/shell/auth-layout.tsx +190 -0
  337. package/src/shell/top-bar-brand.tsx +76 -0
  338. package/src/shell/topbar-action-menu.tsx +21 -0
  339. package/src/shell.ts +2 -0
  340. package/src/wizard/wizard-context.tsx +41 -1
  341. package/src/wizard/wizard-step.tsx +43 -6
  342. package/tokens.css +12 -0
@@ -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}
@@ -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>
@@ -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>}
@@ -1,8 +1,19 @@
1
1
  import { createContext, forwardRef, useCallback, useContext, useEffect, useId, useRef } from "react";
2
- import type { ButtonHTMLAttributes, ChangeEvent, ComponentType, InputHTMLAttributes, ReactNode } from "react";
2
+ import type {
3
+ ButtonHTMLAttributes,
4
+ ChangeEvent,
5
+ ComponentType,
6
+ InputHTMLAttributes,
7
+ MouseEvent,
8
+ ReactElement,
9
+ ReactNode,
10
+ Ref,
11
+ } from "react";
3
12
  import { Check, Minus } from "lucide-react";
4
13
  import { cn } from "../lib/cn";
5
14
  import { assignRef, hasMessage, mergeDescribedBy } from "./choice-parts";
15
+ import { useKitLink } from "../i18n/kit-labels";
16
+ import { pickLinkRenderer } from "./text-link";
6
17
 
7
18
  /**
8
19
  * A checkbox or a radio, presented as a bordered card: a title, a line of description,
@@ -231,6 +242,30 @@ const META_TONE: Record<ActionCardMetaTone, string> = {
231
242
  success: "text-[var(--success)]",
232
243
  };
233
244
 
245
+ /** The colour of {@link ActionCardProps.icon}. `default` is the secondary text colour
246
+ * the icon has always had; `brand` tints it (keksdose's privacy enrolment). */
247
+ export type ActionCardIconTone = "default" | "muted" | "brand" | "warning" | "danger" | "info" | "success";
248
+
249
+ const ICON_TONE: Record<ActionCardIconTone, string> = {
250
+ default: "text-[var(--text-secondary)]",
251
+ ...META_TONE,
252
+ brand: "text-[var(--brand)]",
253
+ };
254
+
255
+ /** What {@link ActionCardProps.renderLink} (and the provider's `linkComponent`) is
256
+ * handed. Spread it onto your router's link — `({ href, ...p }) => <Link to={href} {...p} />`. */
257
+ export interface ActionCardLinkProps {
258
+ href: string;
259
+ /** The card's whole look — keep it. */
260
+ className: string;
261
+ children: ReactNode;
262
+ ref?: Ref<HTMLAnchorElement>;
263
+ onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
264
+ id?: string;
265
+ [key: `aria-${string}`]: string | boolean | number | undefined;
266
+ [key: `data-${string}`]: unknown;
267
+ }
268
+
234
269
  export interface ActionCardProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "title"> {
235
270
  /** What the card does — the button's accessible name. */
236
271
  title: ReactNode;
@@ -247,6 +282,21 @@ export interface ActionCardProps extends Omit<ButtonHTMLAttributes<HTMLButtonEle
247
282
  metaTone?: ActionCardMetaTone;
248
283
  /** A Lucide icon (or any component taking a `className`), shown at the card's start. */
249
284
  icon?: ChoiceCardProps["icon"];
285
+ /** The icon's colour. Default `default` (the secondary text colour). */
286
+ iconTone?: ActionCardIconTone;
287
+ /** Classes on the icon, after {@link iconTone} — a size, a tinted tile behind it. */
288
+ iconClassName?: string;
289
+ /**
290
+ * Makes the card a LINK to another page rather than a button that acts: kastlan's
291
+ * section tiles on the group overview (group-overview-page), each an icon, a name and
292
+ * a line on what is there. A real `<a href>`, so a middle click opens it in a new
293
+ * tab. A `disabled` card renders the inert button instead — a link cannot be
294
+ * disabled. The ref then reaches the `<a>`.
295
+ */
296
+ href?: string;
297
+ /** Your router's link for an in-app `href`. Default: the `<UiKitProvider
298
+ * linkComponent>`, then a plain `<a>`. Ignored without `href`. */
299
+ renderLink?: (props: ActionCardLinkProps) => ReactElement;
250
300
  }
251
301
 
252
302
  /**
@@ -254,7 +304,8 @@ export interface ActionCardProps extends Omit<ButtonHTMLAttributes<HTMLButtonEle
254
304
  * card's icon, title and description, plus a `meta` line, that runs `onClick` the
255
305
  * moment it is pressed. keksdose's privacy enrolment (privacy-enroll-dialog:345,
256
306
  * `CustodyOption`) offers two custody modes this way — picking one IS the next step,
257
- * so there is no checked state to show and no "Continue" to press after it.
307
+ * so there is no checked state to show and no "Continue" to press after it. With
308
+ * `href` it is the same card as a link (kastlan's overview section tiles).
258
309
  *
259
310
  * A separate component rather than `ChoiceCard as="button"`: a ChoiceCard is an
260
311
  * `<input>` (its ref, its `checked`, its form value), and a button shares none of it.
@@ -262,9 +313,24 @@ export interface ActionCardProps extends Omit<ButtonHTMLAttributes<HTMLButtonEle
262
313
  * "Keep the key yourself, button" and then the explanation, not one long name.
263
314
  */
264
315
  export const ActionCard = forwardRef<HTMLButtonElement, ActionCardProps>(function ActionCard(
265
- { title, description, meta, metaTone = "muted", icon: Icon, className, id, type = "button", ...rest },
316
+ {
317
+ title,
318
+ description,
319
+ meta,
320
+ metaTone = "muted",
321
+ icon: Icon,
322
+ iconTone = "default",
323
+ iconClassName,
324
+ href,
325
+ renderLink,
326
+ className,
327
+ id,
328
+ type = "button",
329
+ ...rest
330
+ },
266
331
  ref,
267
332
  ) {
333
+ const kitLink = useKitLink();
268
334
  const generated = useId();
269
335
  const baseId = id ?? generated;
270
336
  const titleId = `${baseId}-title`;
@@ -272,28 +338,23 @@ export const ActionCard = forwardRef<HTMLButtonElement, ActionCardProps>(functio
272
338
  const metaId = `${baseId}-meta`;
273
339
  const showDescription = hasMessage(description);
274
340
  const showMeta = hasMessage(meta);
275
- return (
276
- <button
277
- ref={ref}
278
- id={id}
279
- type={type}
280
- {...rest}
281
- aria-labelledby={rest["aria-labelledby"] ?? titleId}
282
- aria-describedby={mergeDescribedBy(
283
- rest["aria-describedby"],
284
- showDescription && descriptionId,
285
- showMeta && metaId,
286
- )}
287
- className={cn(
288
- "flex w-full items-start gap-3 rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] p-3 text-start shadow-sm transition-colors",
289
- "hover:border-[var(--brand)] hover:bg-[var(--bg-hover)]",
290
- // The outline, as on ChoiceCard: the card is what the eye is on.
291
- "focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--brand)]",
292
- "disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:border-[var(--border)] disabled:hover:bg-[var(--bg-surface)]",
293
- className,
294
- )}
295
- >
296
- {Icon && <Icon aria-hidden className="mt-0.5 size-5 shrink-0 text-[var(--text-secondary)]" />}
341
+ const look = cn(
342
+ "flex w-full items-start gap-3 rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] p-3 text-start shadow-sm transition-colors",
343
+ "hover:border-[var(--brand)] hover:bg-[var(--bg-hover)]",
344
+ // The outline, as on ChoiceCard: the card is what the eye is on.
345
+ "focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--brand)]",
346
+ "disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:border-[var(--border)] disabled:hover:bg-[var(--bg-surface)]",
347
+ className,
348
+ );
349
+ const labelledBy = rest["aria-labelledby"] ?? titleId;
350
+ const describedBy = mergeDescribedBy(
351
+ rest["aria-describedby"],
352
+ showDescription && descriptionId,
353
+ showMeta && metaId,
354
+ );
355
+ const body = (
356
+ <>
357
+ {Icon && <Icon aria-hidden className={cn("mt-0.5 size-5 shrink-0", ICON_TONE[iconTone], iconClassName)} />}
297
358
  <span className="min-w-0 flex-1 space-y-1">
298
359
  <span id={titleId} className="block text-sm font-medium leading-5 text-[var(--text-primary)]">
299
360
  {title}
@@ -309,11 +370,61 @@ export const ActionCard = forwardRef<HTMLButtonElement, ActionCardProps>(functio
309
370
  </span>
310
371
  )}
311
372
  </span>
373
+ </>
374
+ );
375
+
376
+ if (href !== undefined && !rest.disabled) {
377
+ // The button-only attributes stay behind; `aria-*`, `data-*` and handlers ride along.
378
+ const {
379
+ disabled: _disabled,
380
+ form: _form,
381
+ formAction: _formAction,
382
+ name: _name,
383
+ value: _value,
384
+ onClick,
385
+ ...anchorRest
386
+ } = rest;
387
+ const linkProps: ActionCardLinkProps = {
388
+ ...(anchorRest as Partial<ActionCardLinkProps>),
389
+ ref: ref as unknown as Ref<HTMLAnchorElement>,
390
+ id,
391
+ href,
392
+ "aria-labelledby": labelledBy,
393
+ "aria-describedby": describedBy,
394
+ onClick: onClick as unknown as ActionCardLinkProps["onClick"],
395
+ className: look,
396
+ children: body,
397
+ };
398
+ const render = pickLinkRenderer(renderLink, kitLink, href);
399
+ if (render) return <RenderedActionLink render={render} {...linkProps} />;
400
+ const { children: content, ...anchor } = linkProps;
401
+ return <a {...anchor}>{content}</a>;
402
+ }
403
+
404
+ return (
405
+ <button
406
+ ref={ref}
407
+ id={id}
408
+ type={type}
409
+ {...rest}
410
+ aria-labelledby={labelledBy}
411
+ aria-describedby={describedBy}
412
+ className={look}
413
+ >
414
+ {body}
312
415
  </button>
313
416
  );
314
417
  });
315
418
  ActionCard.displayName = "ActionCard";
316
419
 
420
+ /** Calls the link renderer as a component, so a router link's hooks are its own. */
421
+ function RenderedActionLink({
422
+ render,
423
+ ...props
424
+ }: ActionCardLinkProps & { render: (props: ActionCardLinkProps) => ReactElement }) {
425
+ return render(props);
426
+ }
427
+
317
428
  /* ── ChoiceCardGroup ─────────────────────────────────────────────────────── */
318
429
 
319
430
  export interface ChoiceCardOption<T extends string> {
@@ -10,6 +10,7 @@ import type {
10
10
  } from "react";
11
11
  import { Check, Plus } from "lucide-react";
12
12
  import { cn } from "../lib/cn";
13
+ import { CLIPS_ATTRIBUTE } from "../lib/clipping";
13
14
  import { DropdownSearchHeader } from "./dropdown";
14
15
  import { PickerSheet, SHEET_ROW_CLASS } from "./picker-sheet";
15
16
  import { useMediaQuery } from "../hooks/use-media-query";
@@ -636,6 +637,10 @@ export function ComboboxPanel<V extends string | number>({
636
637
  // The rows are provisional while a lookup is in flight — whether or not the
637
638
  // previous query's are still showing.
638
639
  aria-busy={busy || undefined}
640
+ // It scrolls, and says so where jsdom can see it (see `CLIPS_ATTRIBUTE`): a
641
+ // Tooltip in a row then portals out in a test exactly as it does in the
642
+ // browser, instead of joining the option's accessible name (keksdose E8).
643
+ {...{ [CLIPS_ATTRIBUTE]: "" }}
639
644
  className={cn("min-h-0 flex-1 overflow-y-auto py-1", isPhone && "flex-none")}
640
645
  >
641
646
  {results.map((o, i) => {
@@ -113,7 +113,7 @@ function SuggestionList({
113
113
  dir={dir}
114
114
  style={{ top, left: rect.left, width: rect.width }}
115
115
  >
116
- <ul id={id} role="listbox" className={LIST_CLASS} style={{ maxHeight }}>
116
+ <ul id={id} role="listbox" data-clips="" className={LIST_CLASS} style={{ maxHeight }}>
117
117
  {children}
118
118
  </ul>
119
119
  </div>,
@@ -238,9 +238,19 @@ export function Combobox({
238
238
  onBlur,
239
239
  onSubmit,
240
240
  "aria-label": ariaLabel,
241
+ // Off `rest` and onto the <input>, which is the combobox a reader meets: `Field`'s
242
+ // render-prop spreads `{ id, aria-describedby, aria-invalid, aria-required }`, and
243
+ // on the wrapper div the hint and required state described nothing.
244
+ "aria-describedby": ariaDescribedBy,
245
+ "aria-invalid": ariaInvalid,
246
+ "aria-required": ariaRequired,
241
247
  ...rest
242
248
  }: ComboboxProps) {
243
- const field = useComboboxFieldError(error, invalid);
249
+ const field = useComboboxFieldError(
250
+ error,
251
+ invalid || ariaInvalid === true || ariaInvalid === "true",
252
+ ariaDescribedBy,
253
+ );
244
254
  const generated = useId();
245
255
  const fieldId = id ?? generated;
246
256
  // Derived from the GENERATED id, never from `id`: a caller's id is theirs to
@@ -366,9 +376,9 @@ export function Combobox({
366
376
  : null;
367
377
 
368
378
  return (
369
- // `rest` dresses the outer box — a `data-tour` anchor, a test id, a form-level
370
- // `aria-describedby`. Not the NAME: that belongs on the <input> below, which is
371
- // the combobox a reader meets. Spread FIRST, so the field's ARIA and the
379
+ // `rest` dresses the outer box — a `data-tour` anchor, a test id. Not the NAME,
380
+ // description, invalid or required state: those belong on the <input> below,
381
+ // which is the combobox a reader meets. Spread FIRST, so the field's ARIA and the
372
382
  // handlers carrying live #309 and dev#549 cannot be replaced from outside.
373
383
  <div {...rest} ref={wrapperRef} className={cn("relative", className)}>
374
384
  {/* A real <label for> since lenkbank's tests met a field `getByLabelText` could
@@ -407,6 +417,7 @@ export function Combobox({
407
417
  aria-autocomplete="list"
408
418
  aria-invalid={field.isInvalid || undefined}
409
419
  aria-describedby={field.describedBy}
420
+ aria-required={ariaRequired}
410
421
  autoComplete="off"
411
422
  disabled={disabled}
412
423
  autoFocus={autoFocus}
@@ -729,12 +740,22 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
729
740
  invalid,
730
741
  error,
731
742
  "aria-label": ariaLabel,
743
+ // Off `rest` and onto the <input>, which is the combobox a reader meets: `Field`'s
744
+ // render-prop spreads `{ id, aria-describedby, aria-invalid, aria-required }`, and
745
+ // on the wrapper div the hint and required state described nothing.
746
+ "aria-describedby": ariaDescribedBy,
747
+ "aria-invalid": ariaInvalid,
748
+ "aria-required": ariaRequired,
732
749
  ...rest
733
750
  }: InlineEntityComboboxProps<V, C>) {
734
751
  // The clear value reads as "nothing selected", whichever one the caller picked —
735
752
  // so from here down `value` is the id or `null`, as it always was.
736
753
  const value: V | null = rawValue == null || rawValue === clearValue ? null : (rawValue as V);
737
- const field = useComboboxFieldError(error, invalid);
754
+ const field = useComboboxFieldError(
755
+ error,
756
+ invalid || ariaInvalid === true || ariaInvalid === "true",
757
+ ariaDescribedBy,
758
+ );
738
759
  const generated = useId();
739
760
  const fieldId = id ?? generated;
740
761
  // See the twin above on why these hang off the generated id.
@@ -853,9 +874,9 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
853
874
  };
854
875
 
855
876
  return (
856
- // `rest` dresses the outer box — a `data-tour` anchor, a test id, a form-level
857
- // `aria-describedby`. Not the NAME: that belongs on the <input> below, which is
858
- // the combobox a reader meets. Spread FIRST, so the field's ARIA and the
877
+ // `rest` dresses the outer box — a `data-tour` anchor, a test id. Not the NAME,
878
+ // description, invalid or required state: those belong on the <input> below,
879
+ // which is the combobox a reader meets. Spread FIRST, so the field's ARIA and the
859
880
  // handlers carrying live #309 and dev#549 cannot be replaced from outside.
860
881
  <div {...rest} ref={wrapperRef} className={cn("relative", className)}>
861
882
  {/* A real <label for> — see {@link Combobox}. */}
@@ -889,6 +910,7 @@ export function InlineEntityCombobox<V extends string | number, C extends ComboC
889
910
  aria-autocomplete="list"
890
911
  aria-invalid={field.isInvalid || undefined}
891
912
  aria-describedby={field.describedBy}
913
+ aria-required={ariaRequired}
892
914
  autoComplete="off"
893
915
  disabled={disabled}
894
916
  autoFocus={autoFocus}
@@ -56,7 +56,8 @@ export interface CopyButtonProps
56
56
  stopPropagation?: boolean;
57
57
  /** Icon variant only: where the result tooltip opens. */
58
58
  tooltipSide?: TooltipSide;
59
- /** Pass through to the tooltip — needed inside a scroll container (see Tooltip). */
59
+ /** Pass through to the tooltip. Left unset, the Tooltip decides on its own — it
60
+ * portals inside a clipping container (a DataTable's `data-clips` scroller). */
60
61
  tooltipPortal?: boolean;
61
62
  /** ms until the button returns to idle. Default 2000. */
62
63
  resetAfter?: number;
@@ -93,7 +94,7 @@ export function CopyButton({
93
94
  tone,
94
95
  stopPropagation,
95
96
  tooltipSide = "top",
96
- tooltipPortal = false,
97
+ tooltipPortal,
97
98
  resetAfter = 2000,
98
99
  onCopied,
99
100
  labels,