@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
@@ -91,6 +91,8 @@ export interface DateTriggerAttributes {
91
91
  "aria-label"?: string;
92
92
  "aria-describedby"?: string;
93
93
  "aria-invalid"?: true;
94
+ /** The field's required state — `Field required` hands it down with the id pair. */
95
+ "aria-required"?: boolean | "true" | "false";
94
96
  disabled?: boolean;
95
97
  onClick: () => void;
96
98
  className: string;
@@ -132,6 +134,7 @@ function DateFieldTrigger({
132
134
  padded,
133
135
  disabled,
134
136
  invalid,
137
+ required,
135
138
  panelId,
136
139
  aria,
137
140
  renderTrigger,
@@ -153,6 +156,8 @@ function DateFieldTrigger({
153
156
  padded: boolean;
154
157
  disabled?: boolean;
155
158
  invalid?: boolean;
159
+ /** `aria-required`, on the trigger with the rest of the field's ARIA. */
160
+ required?: boolean | "true" | "false";
156
161
  }) {
157
162
  const wasOpen = useRef(false);
158
163
  useEffect(() => {
@@ -177,6 +182,7 @@ function DateFieldTrigger({
177
182
  // One attribute object, worn either by the kit's own button below or by a caller's
178
183
  // (`renderTrigger`) — so a custom trigger cannot quietly fall out of step with the
179
184
  // naming and state this one announces.
185
+ const isInvalid = Boolean(invalid) || aria["aria-invalid"] === true || aria["aria-invalid"] === "true";
180
186
  const attrs: DateTriggerAttributes = {
181
187
  ref: triggerRef,
182
188
  type: "button",
@@ -209,14 +215,15 @@ function DateFieldTrigger({
209
215
  "aria-haspopup": "dialog",
210
216
  "aria-controls": panelId,
211
217
  "aria-expanded": open,
212
- "aria-invalid":
213
- invalid || aria["aria-invalid"] === true || aria["aria-invalid"] === "true" || undefined,
218
+ "aria-invalid": isInvalid || undefined,
219
+ "aria-required": required,
214
220
  className: cn(
215
221
  FIELD_TRIGGER,
216
222
  "pe-9",
217
223
  padded && FIELD_FLOATING_PAD,
218
224
  disabled && "cursor-not-allowed opacity-50",
219
- invalid && FIELD_INVALID,
225
+ // An `aria-invalid` from a Field spread paints too, as on Input and AmountInput.
226
+ isInvalid && FIELD_INVALID,
220
227
  ),
221
228
  };
222
229
  if (renderTrigger) return <CustomTrigger render={renderTrigger} attrs={attrs} valueId={valueId} />;
@@ -279,6 +286,10 @@ function DateField({
279
286
  sheetBackCloses = true,
280
287
  renderTrigger,
281
288
  children,
289
+ // The trigger's, not the wrapper's: `Field`'s render-prop spreads it with the id
290
+ // pair, and a required date read as optional while it sat on the role-less div.
291
+ // Taken here rather than in `splitTriggerAria`, which MonthPicker shares.
292
+ "aria-required": ariaRequired,
282
293
  ...rest
283
294
  }: Omit<ComponentPropsWithoutRef<"div">, "children"> & {
284
295
  triggerText: string;
@@ -393,6 +404,7 @@ function DateField({
393
404
  padded={label !== undefined}
394
405
  disabled={disabled}
395
406
  invalid={invalid}
407
+ required={ariaRequired}
396
408
  renderTrigger={renderTrigger}
397
409
  />
398
410
  );
@@ -577,6 +589,9 @@ export function DatePicker({
577
589
  clearLabel,
578
590
  disabled,
579
591
  invalid,
592
+ // Named so it reaches the field (and so its trigger) when a step/today row is the
593
+ // root and takes the rest of the caller's props.
594
+ "aria-required": ariaRequired,
580
595
  ...rest
581
596
  }: DatePickerProps) {
582
597
  const locale = useKitLocale(localeProp);
@@ -593,6 +608,7 @@ export function DatePicker({
593
608
  // With step/today buttons the flex row is the root and takes the caller's props —
594
609
  // except the ones that name the field, which still belong on its trigger.
595
610
  {...(wrapped ? triggerAria : rest)}
611
+ aria-required={ariaRequired}
596
612
  label={label}
597
613
  clearable={clearable}
598
614
  clearLabel={text.clear}
@@ -1,8 +1,20 @@
1
1
  import { createContext, useContext } from "react";
2
2
  import type { ComponentPropsWithoutRef, CSSProperties, ReactNode } from "react";
3
3
  import { cn } from "../lib/cn";
4
+ import { useKitLabels } from "../i18n/kit-labels";
4
5
  import { FieldHint } from "./ui";
5
6
 
7
+ /** The words a {@link DescriptionList} renders on its own behalf. */
8
+ export interface DescriptionListLabels {
9
+ /** What an item with no value shows (see {@link DescriptionItemProps.placeholder}).
10
+ * Default "—", an em dash. */
11
+ empty: string;
12
+ }
13
+
14
+ export const DEFAULT_DESCRIPTION_LIST_LABELS: DescriptionListLabels = {
15
+ empty: "—",
16
+ };
17
+
6
18
  export type DescriptionListLayout = "rows" | "cards" | "stacked";
7
19
  export type DescriptionListDensity = "comfortable" | "compact" | "tight";
8
20
  /** How many columns a `stacked` or `cards` list may use. */
@@ -14,6 +26,7 @@ interface ListContextValue {
14
26
  prose: boolean;
15
27
  density: DescriptionListDensity;
16
28
  columns: DescriptionListColumns;
29
+ placeholder?: ReactNode;
17
30
  }
18
31
 
19
32
  const ListContext = createContext<ListContextValue>({
@@ -103,6 +116,9 @@ export interface DescriptionListProps extends ComponentPropsWithoutRef<"dl"> {
103
116
  * all would not be an improvement. Per item: {@link DescriptionItemProps.prose}.
104
117
  */
105
118
  prose?: boolean;
119
+ /** What an item with no value shows, for every item that does not set its own —
120
+ * see {@link DescriptionItemProps.placeholder}. */
121
+ placeholder?: ReactNode;
106
122
  }
107
123
 
108
124
  /**
@@ -129,6 +145,7 @@ export function DescriptionList({
129
145
  density = "comfortable",
130
146
  minCardWidth = "10rem",
131
147
  columns,
148
+ placeholder,
132
149
  className,
133
150
  style,
134
151
  ...rest
@@ -147,7 +164,7 @@ export function DescriptionList({
147
164
  const grid: CSSProperties | undefined = cards
148
165
  ? { gridTemplateColumns: `repeat(auto-fill, minmax(min(100%, ${track}), 1fr))` }
149
166
  : undefined;
150
- const ctx: ListContextValue = { layout, numeric, prose, density, columns: columns ?? 2 };
167
+ const ctx: ListContextValue = { layout, numeric, prose, density, columns: columns ?? 2, placeholder };
151
168
  const text = density === "tight" ? "text-[11px] leading-4" : density === "compact" ? "text-xs" : "text-sm";
152
169
 
153
170
  if (stacked) {
@@ -215,6 +232,20 @@ export interface DescriptionItemProps extends Omit<ComponentPropsWithoutRef<"div
215
232
  * because a card grid's column count is not known to CSS ahead of layout.
216
233
  */
217
234
  span?: number | "full";
235
+ /**
236
+ * Shown in place of a value that is not there — `children` of `null`, `undefined`,
237
+ * `false` or `""` (a `0` is a value and is shown). Default: the list's own
238
+ * `placeholder`, else `descriptionList.empty` from the provider, else "—". `null`
239
+ * shows nothing, as an empty detail did before. kastlan's DetailField put its
240
+ * `EMPTY_VALUE` into every one of its call sites' values by hand.
241
+ *
242
+ * Drawn in the muted ink, so an absent value does not read as a value of "—".
243
+ */
244
+ placeholder?: ReactNode;
245
+ }
246
+
247
+ function isEmptyValue(node: ReactNode): boolean {
248
+ return node === undefined || node === null || node === false || node === "";
218
249
  }
219
250
 
220
251
  const ROW_PAD: Record<DescriptionListDensity, string> = {
@@ -236,11 +267,23 @@ export function DescriptionItem({
236
267
  numeric: numericProp,
237
268
  prose: proseProp,
238
269
  span,
270
+ placeholder: placeholderProp,
239
271
  detailClassName,
240
272
  className,
241
273
  ...rest
242
274
  }: DescriptionItemProps) {
243
- const { layout, numeric: listNumeric, prose: listProse, density, columns } = useContext(ListContext);
275
+ const {
276
+ layout,
277
+ numeric: listNumeric,
278
+ prose: listProse,
279
+ density,
280
+ columns,
281
+ placeholder: listPlaceholder,
282
+ } = useContext(ListContext);
283
+ const labels = useKitLabels("descriptionList", DEFAULT_DESCRIPTION_LIST_LABELS);
284
+ const empty = isEmptyValue(children);
285
+ const placeholder =
286
+ placeholderProp !== undefined ? placeholderProp : listPlaceholder !== undefined ? listPlaceholder : labels.empty;
244
287
  const numeric = numericProp ?? listNumeric;
245
288
  const prose = proseProp ?? listProse;
246
289
  const cards = layout === "cards";
@@ -277,7 +320,13 @@ export function DescriptionItem({
277
320
  detailClassName,
278
321
  )}
279
322
  >
280
- {children}
323
+ {empty && !isEmptyValue(placeholder) ? (
324
+ <span data-slot="description-empty" className="font-normal text-[var(--text-muted)]">
325
+ {placeholder}
326
+ </span>
327
+ ) : (
328
+ children
329
+ )}
281
330
  </dd>
282
331
  );
283
332
 
@@ -105,6 +105,14 @@ export function EntityCombobox<V extends string | number, C extends ComboClearVa
105
105
  debounceMs,
106
106
  loadErrorLabel,
107
107
  "aria-label": ariaLabel,
108
+ // The control's own wiring, taken off `rest` so it lands on the TRIGGER rather than
109
+ // the wrapper — see MultiEntityCombobox: `Field`'s render-prop spreads `{ id,
110
+ // aria-describedby, aria-invalid, aria-required }`, and on the role-less wrapper
111
+ // div the label's `htmlFor` named nothing and the hint described nothing.
112
+ id,
113
+ "aria-describedby": ariaDescribedBy,
114
+ "aria-invalid": ariaInvalid,
115
+ "aria-required": ariaRequired,
108
116
  ...rest
109
117
  }: EntityComboboxProps<V, C>) {
110
118
  const core = useComboboxCore<V>({
@@ -115,7 +123,11 @@ export function EntityCombobox<V extends string | number, C extends ComboClearVa
115
123
  minChars,
116
124
  debounceMs,
117
125
  });
118
- const field = useComboboxFieldError(error, invalid);
126
+ const field = useComboboxFieldError(
127
+ error,
128
+ invalid || ariaInvalid === true || ariaInvalid === "true",
129
+ ariaDescribedBy,
130
+ );
119
131
  // The props are the per-instance overrides, the provider the app-wide ones; a
120
132
  // prop left `undefined` falls through to the provider rather than masking it.
121
133
  const labels = useKitLabels("combobox", DEFAULT_COMBOBOX_LABELS, {
@@ -151,13 +163,15 @@ export function EntityCombobox<V extends string | number, C extends ComboClearVa
151
163
  };
152
164
 
153
165
  return (
154
- // `rest` dresses the wrapper, which has no role; the accessible NAME goes on
155
- // the trigger, which has one. Spread FIRST so the trigger's ARIA and the
166
+ // `rest` dresses the wrapper, which has no role; the accessible NAME — and the
167
+ // id, description, invalid and required state above — go on the trigger, which
168
+ // has one. Spread FIRST so the trigger's ARIA and the
156
169
  // handlers that open the panel cannot be clobbered from outside.
157
170
  <div {...rest} className={cn("relative", className)}>
158
171
  {label !== undefined && <FieldLabel>{label}</FieldLabel>}
159
172
  <button
160
173
  ref={core.triggerRef}
174
+ id={id}
161
175
  type="button"
162
176
  // A combobox, not a button. The distinction is not pedantry: this control
163
177
  // carried `aria-invalid`, which `button` does not support, so a required
@@ -185,6 +199,7 @@ export function EntityCombobox<V extends string | number, C extends ComboClearVa
185
199
  disabled={disabled}
186
200
  aria-invalid={field.isInvalid || undefined}
187
201
  aria-describedby={field.describedBy}
202
+ aria-required={ariaRequired}
188
203
  onClick={() => !disabled && setOpen((o) => !o)}
189
204
  // Down/Up opens the list from the closed trigger, per the APG. Enter and
190
205
  // Space already do it through the button's own click.
@@ -0,0 +1,216 @@
1
+ import { Component } from "react";
2
+ import type { ErrorInfo, ReactNode } from "react";
3
+ import { AlertTriangle } from "lucide-react";
4
+ import { useKitLabels } from "../i18n/kit-labels";
5
+ import { Button, EmptyState } from "./ui";
6
+ import { Disclosure } from "./disclosure";
7
+
8
+ /** The words of the default fallback. */
9
+ export interface ErrorBoundaryLabels {
10
+ /** The fallback's heading. */
11
+ title: string;
12
+ /** The line under it. */
13
+ message: string;
14
+ /** The button that clears the error and renders the children again. */
15
+ retry: string;
16
+ /** The disclosure that opens the error's own text (`showDetails`). */
17
+ details: string;
18
+ }
19
+
20
+ export const DEFAULT_ERROR_BOUNDARY_LABELS: ErrorBoundaryLabels = {
21
+ title: "Something went wrong",
22
+ message: "This part of the page could not be shown. Try again, or reload the page.",
23
+ retry: "Try again",
24
+ details: "Error details",
25
+ };
26
+
27
+ /**
28
+ * What was thrown, as flat strings, read ONCE when it arrives.
29
+ *
30
+ * React stores whatever was thrown, hostile or not — `throw { get message() { throw … } }`,
31
+ * `throw { name: Symbol() }`, `throw undefined` — and reading one of those during the
32
+ * fallback's own render would throw INSIDE the boundary, escalating to the next one up
33
+ * (keksdose's error-boundary.tsx learned this as feedback #160: at the app level there is
34
+ * none, and the result is the white screen the boundary exists to prevent).
35
+ */
36
+ export interface ErrorBoundaryDetails {
37
+ name: string;
38
+ message: string;
39
+ stack?: string;
40
+ }
41
+
42
+ function read(get: () => unknown): string | undefined {
43
+ try {
44
+ const v = get();
45
+ return typeof v === "string" ? v : v === undefined || v === null ? undefined : String(v);
46
+ } catch {
47
+ return undefined;
48
+ }
49
+ }
50
+
51
+ /** {@link ErrorBoundaryDetails} of any thrown value, without ever throwing. */
52
+ export function describeThrown(error: unknown): ErrorBoundaryDetails {
53
+ if (typeof error === "object" && error !== null) {
54
+ const e = error as { name?: unknown; message?: unknown; stack?: unknown };
55
+ return {
56
+ name: read(() => e.name) ?? "Error",
57
+ message: read(() => e.message) ?? "",
58
+ stack: read(() => e.stack),
59
+ };
60
+ }
61
+ return { name: "Error", message: read(() => error) ?? "" };
62
+ }
63
+
64
+ /** What a render-prop `fallback` is handed. */
65
+ export interface ErrorBoundaryFallbackProps {
66
+ /** The thrown value, as it was thrown. Read it with care — see {@link describeThrown}. */
67
+ error: unknown;
68
+ details: ErrorBoundaryDetails;
69
+ /** Clear the error and render the children again. */
70
+ reset: () => void;
71
+ }
72
+
73
+ export interface ErrorBoundaryProps {
74
+ children?: ReactNode;
75
+ /** Replaces the default fallback: a node, or a render function handed the error and
76
+ * a `reset`. */
77
+ fallback?: ReactNode | ((props: ErrorBoundaryFallbackProps) => ReactNode);
78
+ /** Called once per caught error — log it, file a crash report. */
79
+ onError?: (error: unknown, info: ErrorInfo) => void;
80
+ /** Called when the error is cleared, by Retry, `reset` or a `resetKeys` change —
81
+ * drop a cache, refetch. */
82
+ onReset?: () => void;
83
+ /**
84
+ * When any of these changes (compared with `Object.is`) AFTER an error was caught, the
85
+ * error is cleared and the children render again — a change in the same update that
86
+ * threw does not count (it is what the error was shown under) — pass the route path so leaving a broken page does not
87
+ * carry its fallback to the next one (keksdose's `resetKey`).
88
+ */
89
+ resetKeys?: ReadonlyArray<unknown>;
90
+ /** A disclosure under the message with the error's name, message and stack — for an
91
+ * internal tool, or while developing. Off by default: a stack means nothing to a user. */
92
+ showDetails?: boolean;
93
+ /** The default fallback's heading level. Default `h2`: the fallback often IS the page. */
94
+ headingAs?: "h2" | "h3" | "h4" | "h5" | "h6";
95
+ /** Classes for the default fallback's box (`m-8 min-h-[400px]`, kastlan). */
96
+ className?: string;
97
+ labels?: Partial<ErrorBoundaryLabels>;
98
+ }
99
+
100
+ interface ErrorBoundaryState {
101
+ /** A flag of its own, so `throw undefined` is still an error shown. */
102
+ failed: boolean;
103
+ error: unknown;
104
+ details: ErrorBoundaryDetails | null;
105
+ }
106
+
107
+ const CLEAR: ErrorBoundaryState = { failed: false, error: null, details: null };
108
+
109
+ function keysChanged(a: ReadonlyArray<unknown> | undefined, b: ReadonlyArray<unknown> | undefined): boolean {
110
+ if (a === b) return false;
111
+ if (!a || !b || a.length !== b.length) return true;
112
+ return a.some((v, i) => !Object.is(v, b[i]));
113
+ }
114
+
115
+ /**
116
+ * Catch a render error below it and show a fallback instead of an empty page.
117
+ *
118
+ * kastlan (shared/components/error-boundary.tsx) and keksdose (app/error-boundary.tsx)
119
+ * each built one on `EmptyState tone="danger"`: a warning glyph, a title, a line of
120
+ * explanation and a Retry. This is that fallback, translated through the provider
121
+ * (`errorBoundary.*`), with the hooks an app hangs its own behaviour on — `onError`
122
+ * for the crash report, `resetKeys` for the route change, `fallback` for anything else.
123
+ *
124
+ * The fallback is `role="alert"`: a page replaced by an error is news, and a reader
125
+ * that was on it is told.
126
+ */
127
+ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
128
+ state: ErrorBoundaryState = CLEAR;
129
+
130
+ static getDerivedStateFromError(error: unknown): Partial<ErrorBoundaryState> {
131
+ return { failed: true, error, details: describeThrown(error) };
132
+ }
133
+
134
+ componentDidCatch(error: unknown, info: ErrorInfo): void {
135
+ this.props.onError?.(error, info);
136
+ }
137
+
138
+ componentDidUpdate(prev: ErrorBoundaryProps, prevState: ErrorBoundaryState): void {
139
+ // Only a change AFTER the error was caught clears it (react-error-boundary's rule).
140
+ // `prevState.failed` is false in the update that caught it: that update may well be
141
+ // the one that changed the keys too — a navigation to a page that throws, with
142
+ // `resetKeys={[pathname]}` — and resetting then would fire `onReset` for nothing and
143
+ // render the failing child a second time. From then on `prev.resetKeys` are the
144
+ // keys the error was shown under, so any change clears it.
145
+ if (this.state.failed && prevState.failed && keysChanged(prev.resetKeys, this.props.resetKeys)) {
146
+ this.reset();
147
+ }
148
+ }
149
+
150
+ reset = (): void => {
151
+ this.props.onReset?.();
152
+ this.setState(CLEAR);
153
+ };
154
+
155
+ render(): ReactNode {
156
+ const { failed, error, details } = this.state;
157
+ if (!failed || !details) return this.props.children;
158
+ const { fallback } = this.props;
159
+ if (typeof fallback === "function") return fallback({ error, details, reset: this.reset });
160
+ if (fallback !== undefined) return fallback;
161
+ return (
162
+ <ErrorFallback
163
+ details={details}
164
+ reset={this.reset}
165
+ showDetails={this.props.showDetails ?? false}
166
+ headingAs={this.props.headingAs ?? "h2"}
167
+ className={this.props.className}
168
+ labels={this.props.labels}
169
+ />
170
+ );
171
+ }
172
+ }
173
+
174
+ function ErrorFallback({
175
+ details,
176
+ reset,
177
+ showDetails,
178
+ headingAs,
179
+ className,
180
+ labels,
181
+ }: {
182
+ details: ErrorBoundaryDetails;
183
+ reset: () => void;
184
+ showDetails: boolean;
185
+ headingAs: NonNullable<ErrorBoundaryProps["headingAs"]>;
186
+ className?: string;
187
+ labels?: Partial<ErrorBoundaryLabels>;
188
+ }) {
189
+ const text = useKitLabels("errorBoundary", DEFAULT_ERROR_BOUNDARY_LABELS, labels);
190
+ return (
191
+ <EmptyState
192
+ role="alert"
193
+ tone="danger"
194
+ headingAs={headingAs}
195
+ className={className}
196
+ icon={<AlertTriangle />}
197
+ title={text.title}
198
+ hint={text.message}
199
+ action={
200
+ <>
201
+ <Button variant="secondary" onClick={reset}>
202
+ {text.retry}
203
+ </Button>
204
+ {showDetails && (
205
+ <Disclosure variant="bare" title={text.details} className="basis-full text-start">
206
+ <pre className="max-h-60 overflow-auto whitespace-pre-wrap break-words text-xs text-[var(--text-secondary)]">
207
+ {details.message ? `${details.name}: ${details.message}` : details.name}
208
+ {details.stack ? `\n\n${details.stack}` : null}
209
+ </pre>
210
+ </Disclosure>
211
+ )}
212
+ </>
213
+ }
214
+ />
215
+ );
216
+ }
@@ -55,6 +55,40 @@ export interface FieldControlProps {
55
55
  "aria-required": true | undefined;
56
56
  }
57
57
 
58
+ /**
59
+ * The second argument of a `Field`'s render-prop children: what a GROUP needs that
60
+ * a single control does not. `htmlFor` names an `<input>`, but a `<label>` pointing
61
+ * at a `role="radiogroup"` div names nothing — so a group (a {@link ToggleGroup})
62
+ * takes `aria-labelledby={labelId}` instead. Kept out of {@link FieldControlProps}
63
+ * so the spread every existing control takes is unchanged.
64
+ */
65
+ export interface FieldRenderMeta {
66
+ /** The label element's id, or `undefined` when the field has no label. */
67
+ labelId: string | undefined;
68
+ }
69
+
70
+ /**
71
+ * Where the label is visible. `"visible"` (default); `"sr-only"`: never drawn, still
72
+ * the control's name; `"below-sm"` / `"below-md"` / `"below-lg"`: drawn only under
73
+ * that breakpoint and screen-reader-only from it up.
74
+ *
75
+ * For table-like editors — kastlan's journal entry lines
76
+ * (accounting/components/wizard/journal-entry-lines-step.tsx) — where a desktop
77
+ * row sits under column headers that already say "Account" and "Amount", but the
78
+ * phone layout stacks each line as a card and needs the labels back. The name stays
79
+ * on the control at every width; only the ink changes.
80
+ */
81
+ export type FieldLabelVisibility = "visible" | "sr-only" | "below-sm" | "below-md" | "below-lg";
82
+
83
+ // Literal class strings so Tailwind's scanner sees each one.
84
+ const LABEL_VISIBILITY: Record<FieldLabelVisibility, string | undefined> = {
85
+ visible: undefined,
86
+ "sr-only": "sr-only",
87
+ "below-sm": "sm:sr-only",
88
+ "below-md": "md:sr-only",
89
+ "below-lg": "lg:sr-only",
90
+ };
91
+
58
92
  export interface FieldProps {
59
93
  /** The label above the control. Omit it for a group whose control is labelled
60
94
  * some other way; the hint and error still render. */
@@ -74,10 +108,13 @@ export interface FieldProps {
74
108
  disabled?: boolean;
75
109
  /** The label's size: `sm` for a dense row, `md` otherwise. */
76
110
  labelSize?: "sm" | "md";
111
+ /** See {@link FieldLabelVisibility}. */
112
+ labelVisibility?: FieldLabelVisibility;
77
113
  /** Wrapper className. */
78
114
  className?: string;
79
- /** The control. A function receives {@link FieldControlProps} to spread on it. */
80
- children: ReactNode | ((control: FieldControlProps) => ReactNode);
115
+ /** The control. A function receives {@link FieldControlProps} to spread on it, and
116
+ * {@link FieldRenderMeta} (the label's id, for a group). */
117
+ children: ReactNode | ((control: FieldControlProps, meta: FieldRenderMeta) => ReactNode);
81
118
  }
82
119
 
83
120
  const isShown = (node: ReactNode) =>
@@ -91,6 +128,7 @@ export function Field({
91
128
  htmlFor,
92
129
  disabled,
93
130
  labelSize,
131
+ labelVisibility = "visible",
94
132
  className,
95
133
  children,
96
134
  }: FieldProps) {
@@ -98,6 +136,7 @@ export function Field({
98
136
  const id = htmlFor ?? `${generated}-control`;
99
137
  const hintId = `${generated}-hint`;
100
138
  const errorId = `${generated}-error`;
139
+ const labelId = `${generated}-label`;
101
140
  const hasHint = isShown(hint);
102
141
  const hasError = isShown(error);
103
142
  const describedBy = [hasHint && hintId, hasError && errorId].filter(Boolean).join(" ");
@@ -108,20 +147,32 @@ export function Field({
108
147
  "aria-required": required || undefined,
109
148
  };
110
149
  return (
111
- <div data-slot="field" className={cn("grid gap-1.5", className)}>
150
+ <div
151
+ data-slot="field"
152
+ className={cn(
153
+ "grid gap-1.5",
154
+ // A hidden label is `position: absolute`; `relative` keeps its containing
155
+ // block here rather than the page's (see sr-only-containment.test).
156
+ labelVisibility !== "visible" && "relative",
157
+ className,
158
+ )}
159
+ >
112
160
  {label !== undefined && (
113
161
  <Label
162
+ id={labelId}
114
163
  htmlFor={id}
115
164
  required={required}
116
165
  disabled={disabled}
117
166
  size={labelSize}
118
167
  data-error={hasError || undefined}
119
- className="data-[error=true]:text-[var(--danger)]"
168
+ className={cn("data-[error=true]:text-[var(--danger)]", LABEL_VISIBILITY[labelVisibility])}
120
169
  >
121
170
  {label}
122
171
  </Label>
123
172
  )}
124
- {typeof children === "function" ? children(control) : children}
173
+ {typeof children === "function"
174
+ ? children(control, { labelId: label !== undefined ? labelId : undefined })
175
+ : children}
125
176
  {hasHint && (
126
177
  <p id={hintId} className="text-[11px] leading-tight text-[var(--text-muted)]">
127
178
  {hint}
@@ -15,7 +15,8 @@ import { X } from "lucide-react";
15
15
 
16
16
  import { cn } from "../lib/cn";
17
17
  import { dirOf, type Direction } from "../lib/direction";
18
- import { useKitLabels } from "../i18n/kit-labels";
18
+ import { useKitLabels, useKitLink } from "../i18n/kit-labels";
19
+ import { pickLinkRenderer } from "./text-link";
19
20
  import { Tooltip, type TooltipSide } from "./tooltip";
20
21
 
21
22
  export interface FloatingPanelLabels {
@@ -588,8 +589,17 @@ export interface FloatingActionProps extends Omit<ButtonHTMLAttributes<HTMLButto
588
589
  label: string;
589
590
  icon: ReactNode;
590
591
  /** Make it a toggle: `aria-pressed` and the brand "on" look (the brand glyph on the
591
- * quiet brand fill), as {@link FloatingActionButtonProps.pressed}. Ignored on a link. */
592
+ * quiet brand fill), as {@link FloatingActionButtonProps.pressed}. Ignored on a link.
593
+ * For the state without the tint, see {@link pressedStyle}. */
592
594
  pressed?: boolean;
595
+ /**
596
+ * How `pressed` looks. `tint` (default): the brand "on" look. `plain`: the member
597
+ * keeps its quiet look in both states and only `aria-pressed` changes — for a toggle
598
+ * whose icon already says which state it is in (keksdose's /transactions "Upcoming"
599
+ * swaps two icons; a tint on top said it twice). The same as passing `aria-pressed`
600
+ * without `pressed`, which also rides through untinted.
601
+ */
602
+ pressedStyle?: "tint" | "plain";
593
603
  /**
594
604
  * `primary`: the brand-filled member, for the group's main action (the /transactions
595
605
  * "+"). Default: the quiet one.
@@ -611,8 +621,8 @@ export interface FloatingActionProps extends Omit<ButtonHTMLAttributes<HTMLButto
611
621
  tooltip?: boolean;
612
622
  /** Where the tooltip opens. Default `top` — the group sits at the bottom of the screen. */
613
623
  tooltipSide?: TooltipSide;
614
- /** Make it a link — the /transactions invoices member. A plain `<a>` unless
615
- * `renderLink` hands it to your router. */
624
+ /** Make it a link — the /transactions invoices member. Drawn by `renderLink`, else
625
+ * the `<UiKitProvider linkComponent>` (an in-app `href`), else a plain `<a>`. */
616
626
  href?: string;
617
627
  /** Render the link through your router's `Link` — see {@link FloatingActionLinkProps}. */
618
628
  renderLink?: (props: FloatingActionLinkProps) => ReactElement;
@@ -637,6 +647,7 @@ export function FloatingAction({
637
647
  label,
638
648
  icon,
639
649
  pressed,
650
+ pressedStyle = "tint",
640
651
  variant = "default",
641
652
  badge,
642
653
  badgeLabel,
@@ -652,6 +663,7 @@ export function FloatingAction({
652
663
  ...rest
653
664
  }: FloatingActionProps) {
654
665
  const labels = useKitLabels("floatingPanel", DEFAULT_FLOATING_PANEL_LABELS);
666
+ const kitLink = useKitLink();
655
667
  const counted = badge !== undefined && badge > 0;
656
668
  const name = counted ? `${label}, ${badgeLabel ?? labels.badge(badge)}` : label;
657
669
  const isLink = href !== undefined;
@@ -661,7 +673,7 @@ export function FloatingAction({
661
673
  "disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:size-5",
662
674
  variant === "primary"
663
675
  ? "bg-[var(--brand)] text-[var(--brand-contrast)] hover:bg-[var(--brand-hover)]"
664
- : pressed && !isLink
676
+ : pressed && !isLink && pressedStyle === "tint"
665
677
  ? "bg-[var(--brand-bg)] text-[var(--brand)] hover:bg-[var(--brand-bg-hover)]"
666
678
  : "text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]",
667
679
  className,
@@ -697,8 +709,9 @@ export function FloatingAction({
697
709
  children: body,
698
710
  };
699
711
  const { children: content, ...anchor } = linkProps;
700
- member = renderLink ? (
701
- <RenderedFloatingLink render={renderLink} {...linkProps} />
712
+ const render = pickLinkRenderer(renderLink, kitLink, href);
713
+ member = render ? (
714
+ <RenderedFloatingLink render={render} {...linkProps} />
702
715
  ) : (
703
716
  <a {...anchor}>{content}</a>
704
717
  );