@eifi1/ui-kit 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (397) hide show
  1. package/README.md +59 -34
  2. package/dist/chart.d.ts +3 -1
  3. package/dist/chart.js +2 -0
  4. package/dist/chart.js.map +1 -1
  5. package/dist/components/account-settings-labels.d.ts +105 -0
  6. package/dist/components/account-settings-labels.js +81 -0
  7. package/dist/components/account-settings-labels.js.map +1 -0
  8. package/dist/components/account-settings.d.ts +36 -52
  9. package/dist/components/account-settings.js +44 -8
  10. package/dist/components/account-settings.js.map +1 -1
  11. package/dist/components/amount-input.d.ts +40 -4
  12. package/dist/components/amount-input.js +50 -12
  13. package/dist/components/amount-input.js.map +1 -1
  14. package/dist/components/authed-image.d.ts +68 -0
  15. package/dist/components/authed-image.js +117 -0
  16. package/dist/components/authed-image.js.map +1 -0
  17. package/dist/components/breadcrumbs.d.ts +2 -1
  18. package/dist/components/breadcrumbs.js +5 -2
  19. package/dist/components/breadcrumbs.js.map +1 -1
  20. package/dist/components/bulk-action-bar.d.ts +10 -2
  21. package/dist/components/bulk-action-bar.js.map +1 -1
  22. package/dist/components/button-group.d.ts +51 -2
  23. package/dist/components/button-group.js +5 -1
  24. package/dist/components/button-group.js.map +1 -1
  25. package/dist/components/calculator.d.ts +15 -4
  26. package/dist/components/calendar-heatmap.d.ts +41 -3
  27. package/dist/components/calendar-heatmap.js +48 -7
  28. package/dist/components/calendar-heatmap.js.map +1 -1
  29. package/dist/components/chart.js +14 -8
  30. package/dist/components/chart.js.map +1 -1
  31. package/dist/components/chip.d.ts +2 -1
  32. package/dist/components/chip.js +26 -18
  33. package/dist/components/chip.js.map +1 -1
  34. package/dist/components/choice-card.d.ts +35 -3
  35. package/dist/components/choice-card.js +83 -25
  36. package/dist/components/choice-card.js.map +1 -1
  37. package/dist/components/combobox-core.d.ts +3 -1
  38. package/dist/components/combobox-core.js +18 -5
  39. package/dist/components/combobox-core.js.map +1 -1
  40. package/dist/components/combobox.d.ts +2 -2
  41. package/dist/components/combobox.js +31 -9
  42. package/dist/components/combobox.js.map +1 -1
  43. package/dist/components/copy-button.d.ts +51 -70
  44. package/dist/components/copy-button.js +1 -1
  45. package/dist/components/copy-button.js.map +1 -1
  46. package/dist/components/data-table-cells.d.ts +63 -0
  47. package/dist/components/data-table-cells.js +64 -0
  48. package/dist/components/data-table-cells.js.map +1 -0
  49. package/dist/components/data-table-filter-popover.d.ts +1 -1
  50. package/dist/components/data-table-filter-popover.js +8 -1
  51. package/dist/components/data-table-filter-popover.js.map +1 -1
  52. package/dist/components/data-table-filters.d.ts +1 -1
  53. package/dist/components/data-table-filters.js +31 -1
  54. package/dist/components/data-table-filters.js.map +1 -1
  55. package/dist/components/data-table-labels.d.ts +13 -0
  56. package/dist/components/data-table-labels.js +8 -1
  57. package/dist/components/data-table-labels.js.map +1 -1
  58. package/dist/components/data-table.d.ts +1 -1
  59. package/dist/components/data-table.js +125 -27
  60. package/dist/components/data-table.js.map +1 -1
  61. package/dist/components/date-picker.d.ts +3 -1
  62. package/dist/components/date-picker.js +15 -2
  63. package/dist/components/date-picker.js.map +1 -1
  64. package/dist/components/description-list.d.ts +23 -3
  65. package/dist/components/description-list.js +23 -3
  66. package/dist/components/description-list.js.map +1 -1
  67. package/dist/components/dialog-frame.d.ts +1 -0
  68. package/dist/components/entity-combobox.d.ts +1 -1
  69. package/dist/components/entity-combobox.js +40 -20
  70. package/dist/components/entity-combobox.js.map +1 -1
  71. package/dist/components/error-boundary.d.ts +281 -0
  72. package/dist/components/error-boundary.js +302 -0
  73. package/dist/components/error-boundary.js.map +1 -0
  74. package/dist/components/field-sync.js +90 -83
  75. package/dist/components/field-sync.js.map +1 -1
  76. package/dist/components/field.d.ts +30 -4
  77. package/dist/components/field.js +40 -17
  78. package/dist/components/field.js.map +1 -1
  79. package/dist/components/file-button.d.ts +50 -208
  80. package/dist/components/file-dropzone.d.ts +49 -2
  81. package/dist/components/floating-panel.d.ts +13 -4
  82. package/dist/components/floating-panel.js +7 -3
  83. package/dist/components/floating-panel.js.map +1 -1
  84. package/dist/components/form-actions.d.ts +53 -0
  85. package/dist/components/form-actions.js +98 -0
  86. package/dist/components/form-actions.js.map +1 -0
  87. package/dist/components/full-bleed-dialog.js +4 -0
  88. package/dist/components/full-bleed-dialog.js.map +1 -1
  89. package/dist/components/hover-menu.js +64 -56
  90. package/dist/components/hover-menu.js.map +1 -1
  91. package/dist/components/image-grid.d.ts +67 -0
  92. package/dist/components/image-grid.js +112 -0
  93. package/dist/components/image-grid.js.map +1 -0
  94. package/dist/components/lightbox.d.ts +104 -0
  95. package/dist/components/lightbox.js +229 -0
  96. package/dist/components/lightbox.js.map +1 -0
  97. package/dist/components/line-items.d.ts +114 -0
  98. package/dist/components/line-items.js +238 -0
  99. package/dist/components/line-items.js.map +1 -0
  100. package/dist/components/list.d.ts +37 -4
  101. package/dist/components/list.js +35 -14
  102. package/dist/components/list.js.map +1 -1
  103. package/dist/components/loading-state.d.ts +24 -0
  104. package/dist/components/loading-state.js +32 -0
  105. package/dist/components/loading-state.js.map +1 -0
  106. package/dist/components/menu-item.d.ts +2 -1
  107. package/dist/components/menu-item.js +5 -1
  108. package/dist/components/menu-item.js.map +1 -1
  109. package/dist/components/mini-calendar.d.ts +13 -1
  110. package/dist/components/mini-calendar.js +46 -2
  111. package/dist/components/mini-calendar.js.map +1 -1
  112. package/dist/components/modal.d.ts +20 -1
  113. package/dist/components/modal.js +26 -2
  114. package/dist/components/modal.js.map +1 -1
  115. package/dist/components/month-picker.d.ts +21 -1
  116. package/dist/components/month-picker.js +139 -26
  117. package/dist/components/month-picker.js.map +1 -1
  118. package/dist/components/multi-entity-combobox.d.ts +1 -1
  119. package/dist/components/multi-entity-combobox.js +42 -20
  120. package/dist/components/multi-entity-combobox.js.map +1 -1
  121. package/dist/components/nav-pills.d.ts +2 -1
  122. package/dist/components/nav-pills.js +5 -1
  123. package/dist/components/nav-pills.js.map +1 -1
  124. package/dist/components/number-field.d.ts +24 -5
  125. package/dist/components/number-field.js +5 -7
  126. package/dist/components/number-field.js.map +1 -1
  127. package/dist/components/number-input.d.ts +25 -5
  128. package/dist/components/number-input.js +7 -1
  129. package/dist/components/number-input.js.map +1 -1
  130. package/dist/components/numpad-sheet.d.ts +20 -5
  131. package/dist/components/numpad-sheet.js +3 -2
  132. package/dist/components/numpad-sheet.js.map +1 -1
  133. package/dist/components/page-header.d.ts +35 -6
  134. package/dist/components/page-header.js +7 -1
  135. package/dist/components/page-header.js.map +1 -1
  136. package/dist/components/passkeys-setting.d.ts +67 -0
  137. package/dist/components/passkeys-setting.js +214 -0
  138. package/dist/components/passkeys-setting.js.map +1 -0
  139. package/dist/components/picker-sheet.js +5 -1
  140. package/dist/components/picker-sheet.js.map +1 -1
  141. package/dist/components/pie-chart-labels.d.ts +20 -0
  142. package/dist/components/pie-chart-labels.js +12 -0
  143. package/dist/components/pie-chart-labels.js.map +1 -0
  144. package/dist/components/pie-chart.d.ts +101 -0
  145. package/dist/components/pie-chart.js +283 -0
  146. package/dist/components/pie-chart.js.map +1 -0
  147. package/dist/components/popover.js +3 -0
  148. package/dist/components/popover.js.map +1 -1
  149. package/dist/components/progress-bar.d.ts +44 -3
  150. package/dist/components/progress-bar.js +58 -22
  151. package/dist/components/progress-bar.js.map +1 -1
  152. package/dist/components/qr-code.d.ts +34 -0
  153. package/dist/components/qr-code.js +57 -0
  154. package/dist/components/qr-code.js.map +1 -0
  155. package/dist/components/search-field.d.ts +15 -3
  156. package/dist/components/search-field.js +5 -1
  157. package/dist/components/search-field.js.map +1 -1
  158. package/dist/components/settings-fields.d.ts +49 -1
  159. package/dist/components/signed-amount.d.ts +122 -0
  160. package/dist/components/signed-amount.js +152 -0
  161. package/dist/components/signed-amount.js.map +1 -0
  162. package/dist/components/skeleton.d.ts +10 -3
  163. package/dist/components/skeleton.js +12 -2
  164. package/dist/components/skeleton.js.map +1 -1
  165. package/dist/components/stat-tile.d.ts +2 -1
  166. package/dist/components/stat-tile.js +6 -3
  167. package/dist/components/stat-tile.js.map +1 -1
  168. package/dist/components/swipeable-row.js +4 -3
  169. package/dist/components/swipeable-row.js.map +1 -1
  170. package/dist/components/table.d.ts +39 -5
  171. package/dist/components/table.js +45 -11
  172. package/dist/components/table.js.map +1 -1
  173. package/dist/components/text-link.d.ts +238 -0
  174. package/dist/components/text-link.js +144 -0
  175. package/dist/components/text-link.js.map +1 -0
  176. package/dist/components/text.d.ts +26 -2
  177. package/dist/components/text.js +16 -2
  178. package/dist/components/text.js.map +1 -1
  179. package/dist/components/time-input.d.ts +49 -1
  180. package/dist/components/toggle-group.d.ts +35 -0
  181. package/dist/components/toggle-group.js +43 -14
  182. package/dist/components/toggle-group.js.map +1 -1
  183. package/dist/components/toggle-legend.d.ts +53 -3
  184. package/dist/components/toggle-legend.js +54 -1
  185. package/dist/components/toggle-legend.js.map +1 -1
  186. package/dist/components/tooltip.d.ts +51 -1
  187. package/dist/components/tooltip.js +59 -35
  188. package/dist/components/tooltip.js.map +1 -1
  189. package/dist/components/ui.d.ts +51 -779
  190. package/dist/components/ui.js +364 -34
  191. package/dist/components/ui.js.map +1 -1
  192. package/dist/components/use-table-state.d.ts +3 -52
  193. package/dist/components/use-table-state.js +128 -30
  194. package/dist/components/use-table-state.js.map +1 -1
  195. package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
  196. package/dist/data-table.d.ts +2 -1
  197. package/dist/data-table.js +5 -1
  198. package/dist/data-table.js.map +1 -1
  199. package/dist/feedback/feedback-attachment.js +4 -6
  200. package/dist/feedback/feedback-attachment.js.map +1 -1
  201. package/dist/feedback/feedback-dialog.d.ts +26 -3
  202. package/dist/feedback/feedback-dialog.js +21 -5
  203. package/dist/feedback/feedback-dialog.js.map +1 -1
  204. package/dist/feedback/feedback-inbox.js +86 -75
  205. package/dist/feedback/feedback-inbox.js.map +1 -1
  206. package/dist/feedback/feedback-thread.d.ts +173 -0
  207. package/dist/feedback/feedback-thread.js +264 -0
  208. package/dist/feedback/feedback-thread.js.map +1 -0
  209. package/dist/feedback.d.ts +2 -1
  210. package/dist/feedback.js +1 -0
  211. package/dist/feedback.js.map +1 -1
  212. package/dist/hooks/use-authed-src.d.ts +71 -0
  213. package/dist/hooks/use-authed-src.js +63 -0
  214. package/dist/hooks/use-authed-src.js.map +1 -0
  215. package/dist/hooks/use-file-drop.d.ts +49 -2
  216. package/dist/hooks/use-hotkey.d.ts +59 -0
  217. package/dist/hooks/use-hotkey.js +105 -0
  218. package/dist/hooks/use-hotkey.js.map +1 -0
  219. package/dist/hooks/use-search-param-state.d.ts +83 -0
  220. package/dist/hooks/use-search-param-state.js +94 -0
  221. package/dist/hooks/use-search-param-state.js.map +1 -0
  222. package/dist/i18n/defaults.d.ts +15 -4
  223. package/dist/i18n/defaults.js +31 -1
  224. package/dist/i18n/defaults.js.map +1 -1
  225. package/dist/i18n/kit-labels.d.ts +46 -312
  226. package/dist/i18n/kit-labels.js +20 -5
  227. package/dist/i18n/kit-labels.js.map +1 -1
  228. package/dist/i18n/locales/de-CH-informal.d.ts +15 -4
  229. package/dist/i18n/locales/de-CH.d.ts +15 -4
  230. package/dist/i18n/locales/de-informal.d.ts +15 -4
  231. package/dist/i18n/locales/de.d.ts +15 -4
  232. package/dist/i18n/locales/de.js +164 -3
  233. package/dist/i18n/locales/de.js.map +1 -1
  234. package/dist/i18n/locales/es.d.ts +15 -4
  235. package/dist/i18n/locales/es.js +161 -3
  236. package/dist/i18n/locales/es.js.map +1 -1
  237. package/dist/i18n/locales/fr.d.ts +15 -4
  238. package/dist/i18n/locales/fr.js +161 -3
  239. package/dist/i18n/locales/fr.js.map +1 -1
  240. package/dist/i18n/locales/hu.d.ts +15 -4
  241. package/dist/i18n/locales/hu.js +161 -3
  242. package/dist/i18n/locales/hu.js.map +1 -1
  243. package/dist/i18n/locales/it.d.ts +15 -4
  244. package/dist/i18n/locales/it.js +161 -3
  245. package/dist/i18n/locales/it.js.map +1 -1
  246. package/dist/i18n/locales/zh.d.ts +15 -4
  247. package/dist/i18n/locales/zh.js +161 -3
  248. package/dist/i18n/locales/zh.js.map +1 -1
  249. package/dist/index.d.ts +39 -19
  250. package/dist/index.js +40 -1
  251. package/dist/index.js.map +1 -1
  252. package/dist/kit-labels-Mz8zZHIF.d.ts +1744 -0
  253. package/dist/lib/format.d.ts +162 -0
  254. package/dist/lib/format.js +151 -0
  255. package/dist/lib/format.js.map +1 -0
  256. package/dist/lib/qr-encode.d.ts +44 -0
  257. package/dist/lib/qr-encode.js +338 -0
  258. package/dist/lib/qr-encode.js.map +1 -0
  259. package/dist/rhf/fields.d.ts +283 -0
  260. package/dist/rhf/fields.js +631 -0
  261. package/dist/rhf/fields.js.map +1 -0
  262. package/dist/rhf/form.d.ts +49 -1
  263. package/dist/rhf/line-items.d.ts +31 -0
  264. package/dist/rhf/line-items.js +59 -0
  265. package/dist/rhf/line-items.js.map +1 -0
  266. package/dist/rhf.d.ts +61 -1
  267. package/dist/rhf.js +2 -0
  268. package/dist/rhf.js.map +1 -1
  269. package/dist/search/command-palette.js +20 -14
  270. package/dist/search/command-palette.js.map +1 -1
  271. package/dist/shell/app-shell.d.ts +99 -2
  272. package/dist/shell/app-shell.js +120 -57
  273. package/dist/shell/app-shell.js.map +1 -1
  274. package/dist/shell/auth-layout.d.ts +74 -0
  275. package/dist/shell/auth-layout.js +97 -0
  276. package/dist/shell/auth-layout.js.map +1 -0
  277. package/dist/shell/top-bar-brand.d.ts +81 -0
  278. package/dist/shell/top-bar-brand.js +47 -0
  279. package/dist/shell/top-bar-brand.js.map +1 -0
  280. package/dist/shell/topbar-action-menu.d.ts +21 -4
  281. package/dist/shell/topbar-action-menu.js +18 -2
  282. package/dist/shell/topbar-action-menu.js.map +1 -1
  283. package/dist/shell.d.ts +56 -1
  284. package/dist/shell.js +2 -0
  285. package/dist/shell.js.map +1 -1
  286. package/dist/tour/tour.js +10 -4
  287. package/dist/tour/tour.js.map +1 -1
  288. package/dist/wizard/stepper-nav.d.ts +48 -1
  289. package/dist/wizard/use-wizard.js +4 -2
  290. package/dist/wizard/use-wizard.js.map +1 -1
  291. package/dist/wizard/wizard-context.d.ts +24 -1
  292. package/dist/wizard/wizard-context.js +19 -2
  293. package/dist/wizard/wizard-context.js.map +1 -1
  294. package/dist/wizard/wizard-step.d.ts +30 -5
  295. package/dist/wizard/wizard-step.js +34 -3
  296. package/dist/wizard/wizard-step.js.map +1 -1
  297. package/dist/wizard.d.ts +50 -3
  298. package/package.json +6 -6
  299. package/src/chart.ts +4 -0
  300. package/src/components/account-settings-labels.ts +190 -0
  301. package/src/components/account-settings.tsx +110 -63
  302. package/src/components/amount-input.tsx +109 -10
  303. package/src/components/authed-image.tsx +190 -0
  304. package/src/components/breadcrumbs.tsx +8 -2
  305. package/src/components/bulk-action-bar.tsx +16 -3
  306. package/src/components/button-group.tsx +7 -2
  307. package/src/components/calendar-heatmap.tsx +105 -10
  308. package/src/components/chart.tsx +19 -8
  309. package/src/components/chip.tsx +14 -4
  310. package/src/components/choice-card.tsx +136 -25
  311. package/src/components/combobox-core.tsx +33 -9
  312. package/src/components/combobox.tsx +33 -9
  313. package/src/components/copy-button.tsx +3 -2
  314. package/src/components/data-table-cells.tsx +119 -0
  315. package/src/components/data-table-filter-popover.tsx +13 -5
  316. package/src/components/data-table-filters.ts +77 -0
  317. package/src/components/data-table-labels.ts +23 -0
  318. package/src/components/data-table.tsx +230 -13
  319. package/src/components/date-picker.tsx +19 -3
  320. package/src/components/description-list.tsx +52 -3
  321. package/src/components/entity-combobox.tsx +25 -7
  322. package/src/components/error-boundary.tsx +640 -0
  323. package/src/components/field-sync.tsx +48 -40
  324. package/src/components/field.tsx +56 -5
  325. package/src/components/floating-panel.tsx +20 -7
  326. package/src/components/form-actions.tsx +191 -0
  327. package/src/components/full-bleed-dialog.tsx +4 -0
  328. package/src/components/hover-menu.tsx +26 -17
  329. package/src/components/image-grid.tsx +181 -0
  330. package/src/components/lightbox.tsx +374 -0
  331. package/src/components/line-items.tsx +385 -0
  332. package/src/components/list.tsx +82 -19
  333. package/src/components/loading-state.tsx +47 -0
  334. package/src/components/menu-item.tsx +7 -2
  335. package/src/components/mini-calendar.tsx +72 -3
  336. package/src/components/modal.tsx +65 -3
  337. package/src/components/month-picker.tsx +181 -26
  338. package/src/components/multi-entity-combobox.tsx +27 -7
  339. package/src/components/nav-pills.tsx +8 -1
  340. package/src/components/number-field.tsx +15 -10
  341. package/src/components/number-input.tsx +17 -1
  342. package/src/components/numpad-sheet.tsx +6 -1
  343. package/src/components/page-header.tsx +42 -5
  344. package/src/components/passkeys-setting.tsx +331 -0
  345. package/src/components/picker-sheet.tsx +10 -1
  346. package/src/components/pie-chart-labels.ts +32 -0
  347. package/src/components/pie-chart.tsx +479 -0
  348. package/src/components/popover.tsx +3 -0
  349. package/src/components/progress-bar.tsx +129 -19
  350. package/src/components/qr-code.tsx +83 -0
  351. package/src/components/search-field.tsx +20 -4
  352. package/src/components/signed-amount.tsx +295 -0
  353. package/src/components/skeleton.tsx +21 -3
  354. package/src/components/stat-tile.tsx +11 -3
  355. package/src/components/swipeable-row.tsx +7 -7
  356. package/src/components/table.tsx +102 -13
  357. package/src/components/text-link.tsx +356 -0
  358. package/src/components/text.tsx +46 -2
  359. package/src/components/toggle-group.tsx +93 -20
  360. package/src/components/toggle-legend.tsx +108 -2
  361. package/src/components/tooltip.tsx +79 -8
  362. package/src/components/ui.tsx +707 -49
  363. package/src/components/use-table-state.ts +265 -33
  364. package/src/data-table.ts +10 -0
  365. package/src/feedback/feedback-attachment.tsx +10 -7
  366. package/src/feedback/feedback-dialog.tsx +49 -7
  367. package/src/feedback/feedback-inbox.tsx +7 -0
  368. package/src/feedback/feedback-thread.tsx +510 -0
  369. package/src/feedback.ts +1 -0
  370. package/src/hooks/use-authed-src.ts +161 -0
  371. package/src/hooks/use-hotkey.ts +172 -0
  372. package/src/hooks/use-search-param-state.ts +187 -0
  373. package/src/i18n/defaults.ts +30 -0
  374. package/src/i18n/kit-labels.tsx +118 -8
  375. package/src/i18n/locales/de.ts +161 -0
  376. package/src/i18n/locales/es.ts +158 -0
  377. package/src/i18n/locales/fr.ts +161 -0
  378. package/src/i18n/locales/hu.ts +159 -0
  379. package/src/i18n/locales/it.ts +159 -0
  380. package/src/i18n/locales/zh.ts +158 -0
  381. package/src/index.ts +59 -0
  382. package/src/lib/format.ts +348 -0
  383. package/src/lib/qr-encode.ts +420 -0
  384. package/src/rhf/fields.tsx +1135 -0
  385. package/src/rhf/line-items.tsx +130 -0
  386. package/src/rhf.ts +6 -0
  387. package/src/search/command-palette.tsx +29 -19
  388. package/src/shell/app-shell.tsx +153 -3
  389. package/src/shell/auth-layout.tsx +190 -0
  390. package/src/shell/top-bar-brand.tsx +76 -0
  391. package/src/shell/topbar-action-menu.tsx +32 -4
  392. package/src/shell.ts +2 -0
  393. package/src/tour/tour.tsx +16 -5
  394. package/src/wizard/use-wizard.ts +8 -4
  395. package/src/wizard/wizard-context.tsx +41 -1
  396. package/src/wizard/wizard-step.tsx +75 -6
  397. package/tokens.css +12 -0
@@ -175,6 +175,18 @@ export interface MiniCalendarProps extends Omit<ComponentPropsWithoutRef<"div">,
175
175
  * it is what names the grid.
176
176
  */
177
177
  hideNavigation?: boolean;
178
+ /**
179
+ * Fill the leading and trailing cells with the neighbouring months' days, muted —
180
+ * the wall-calendar look, where the first row does not start on a hole.
181
+ *
182
+ * They are real days, not decoration: each is named with its whole date, honours
183
+ * `min`/`max`, and a click selects it and moves the grid to its month (focus goes
184
+ * with it). They are NOT stops of the roving tabindex — the keyboard already reaches
185
+ * them by arrowing off the month's edge, which moves the grid the same way — and
186
+ * `renderDay` is not called for them, so a day's events are drawn once, in its own
187
+ * month. Default `false`: the cells stay empty.
188
+ */
189
+ showOutsideDays?: boolean;
178
190
  /** Extra classes for the calendar's root. */
179
191
  className?: string;
180
192
  }
@@ -256,6 +268,7 @@ export function MiniCalendar({
256
268
  month,
257
269
  onMonthChange,
258
270
  hideNavigation,
271
+ showOutsideDays = false,
259
272
  className,
260
273
  ...rest
261
274
  }: MiniCalendarProps) {
@@ -335,12 +348,19 @@ export function MiniCalendar({
335
348
 
336
349
  const firstOfMonth = new Date(view.year, view.month, 1);
337
350
  const lastOfMonth = new Date(view.year, view.month + 1, 0);
338
- // Leading blanks up to the first of the month, counted from the week's first day.
351
+ // Leading blanks up to the first of the month, counted from the week's first day —
352
+ // or the previous month's last days, with `showOutsideDays` (and the next month's
353
+ // first ones at the end). `new Date(y, m, 0)` is the day before the 1st, so the
354
+ // offsets below roll into the neighbouring month by themselves.
339
355
  const startWeekday = weekdayIndex(firstOfMonth, weekStart);
340
356
  const cells: (Date | null)[] = [];
341
- for (let i = 0; i < startWeekday; i++) cells.push(null);
357
+ for (let i = 0; i < startWeekday; i++) {
358
+ cells.push(showOutsideDays ? new Date(view.year, view.month, i - startWeekday + 1) : null);
359
+ }
342
360
  for (let d = 1; d <= lastOfMonth.getDate(); d++) cells.push(new Date(view.year, view.month, d));
343
- while (cells.length % 7 !== 0) cells.push(null);
361
+ for (let extra = 1; cells.length % 7 !== 0; extra++) {
362
+ cells.push(showOutsideDays ? new Date(view.year, view.month + 1, extra) : null);
363
+ }
344
364
  const weeks: (Date | null)[][] = [];
345
365
  for (let i = 0; i < cells.length; i += 7) weeks.push(cells.slice(i, i + 7));
346
366
 
@@ -588,6 +608,55 @@ export function MiniCalendar({
588
608
  }
589
609
  const iso = toLocalIso(d);
590
610
  const disabled = isDisabled(d);
611
+ // A neighbouring month's day (`showOutsideDays`): a muted, selectable
612
+ // cell outside the tab order — see the prop.
613
+ if (d.getMonth() !== view.month) {
614
+ return (
615
+ <div
616
+ key={di}
617
+ role="none"
618
+ className={cn(
619
+ "relative flex",
620
+ lg ? cn("min-w-0", LG_CELL_HEIGHT) : "h-8 items-center justify-center",
621
+ )}
622
+ >
623
+ <button
624
+ type="button"
625
+ role="gridcell"
626
+ data-iso={iso}
627
+ data-outside=""
628
+ tabIndex={-1}
629
+ aria-label={labels.day(formatDay(d))}
630
+ aria-selected={!disabled && inRange(d)}
631
+ aria-disabled={disabled || undefined}
632
+ onClick={() => {
633
+ if (disabled) return;
634
+ // Focus follows into the month the click moves the grid to:
635
+ // this button is gone after the next commit.
636
+ pendingFocus.current = iso;
637
+ handleClick(d);
638
+ }}
639
+ onKeyDown={onDayKeyDown}
640
+ className={cn(
641
+ "text-xs tabular-nums text-[var(--text-placeholder)] transition-colors",
642
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]",
643
+ disabled ? "cursor-not-allowed opacity-60" : "hover:bg-[var(--bg-hover)]",
644
+ lg
645
+ ? cn("flex w-full min-w-0 items-start bg-[var(--bg-surface-2)] p-1.5", LG_CELL_HEIGHT)
646
+ : "size-8 rounded-full",
647
+ )}
648
+ >
649
+ {lg ? (
650
+ <span className="inline-flex size-6 items-center justify-center">
651
+ {dayNumber.format(d.getDate())}
652
+ </span>
653
+ ) : (
654
+ dayNumber.format(d.getDate())
655
+ )}
656
+ </button>
657
+ </div>
658
+ );
659
+ }
591
660
  const isStart = !disabled && fromDate && sameYmd(d, fromDate);
592
661
  const isEnd = !disabled && toDate && sameYmd(d, toDate);
593
662
  const isToday = sameYmd(d, today);
@@ -11,6 +11,7 @@ import { cn } from "../lib/cn";
11
11
  import { useFocusTrap } from "../hooks/use-focus-trap";
12
12
  import { useBodyScrollLock } from "../hooks/use-body-scroll-lock";
13
13
  import { useOverlayHistory } from "../hooks/use-overlay-history";
14
+ import { useDialogParam, type DialogParam } from "../hooks/use-search-param-state";
14
15
  import { OVERLAY_EXIT_MS, prefersReducedMotion, useCloseTransition } from "../hooks/use-close-transition";
15
16
 
16
17
  /**
@@ -196,6 +197,50 @@ export interface ModalProps extends Omit<ComponentPropsWithoutRef<"div">, "role"
196
197
  * suppressed there — there is nothing beside it to see.
197
198
  */
198
199
  draggable?: boolean;
200
+ /**
201
+ * Bind the open state to a URL search param (0.12.0): `"edit"` is open while
202
+ * `?edit` is in the query, `{ key: "dialog", value: "create-lease" }` while
203
+ * `?dialog=create-lease` is. The URL is then the only state — `open` is ignored —
204
+ * and the dialog opens from anywhere that sets the param: {@link useDialogParam}'s
205
+ * setter, a `<Link>`, a shared link, a reload.
206
+ *
207
+ * kastlan's `useDialogParam` did this beside the dialog, so its open state could be
208
+ * deep-linked and captured in a feedback report. Here it also owns the history:
209
+ * opening through `useDialogParam` pushes an entry, so the Back gesture closes the
210
+ * dialog by leaving it, and a close (Escape, the backdrop, the X) goes back over
211
+ * that entry rather than stacking a second one. The dialog's own Back handling
212
+ * ({@link useOverlayHistory}'s throwaway entry) stays off in this mode — the param's
213
+ * entry already is one, and two would take two Back presses to close one dialog.
214
+ *
215
+ * `onClose` still runs, after the param is cleared. Requires a react-router Router.
216
+ */
217
+ urlParam?: DialogParam;
218
+ }
219
+
220
+ /**
221
+ * The URL-bound dialog: the param is the open state, and closing clears it. A
222
+ * component of its own so the router hook is only called by a `Modal` that asked for
223
+ * it — a plain `Modal` must keep working with no Router above it.
224
+ */
225
+ function UrlParamModal({ urlParam, onClose, ...rest }: ModalProps & { urlParam: DialogParam }) {
226
+ const [open, setOpen] = useDialogParam(urlParam);
227
+ // Closed from here the moment the dismissal is decided. Going back is a traversal
228
+ // the browser lands a task later, and between the two the param still says "open":
229
+ // without this the panel, already lowered, would pop back up for that task.
230
+ const [leaving, setLeaving] = useState(false);
231
+ if (leaving && !open) setLeaving(false);
232
+ return (
233
+ <ModalPanel
234
+ {...rest}
235
+ open={open && !leaving}
236
+ ownHistoryEntry={false}
237
+ onClose={() => {
238
+ setLeaving(true);
239
+ setOpen(false);
240
+ onClose();
241
+ }}
242
+ />
243
+ );
199
244
  }
200
245
 
201
246
  /**
@@ -206,7 +251,15 @@ export interface ModalProps extends Omit<ComponentPropsWithoutRef<"div">, "role"
206
251
  * into the panel on open and back to the trigger on close, a Tab focus trap, and
207
252
  * `role="dialog"`/`aria-modal`.
208
253
  */
209
- export function Modal({
254
+ export function Modal(props: ModalProps) {
255
+ return props.urlParam !== undefined ? (
256
+ <UrlParamModal {...props} urlParam={props.urlParam} />
257
+ ) : (
258
+ <ModalPanel {...props} />
259
+ );
260
+ }
261
+
262
+ function ModalPanel({
210
263
  onClose,
211
264
  open,
212
265
  children,
@@ -218,8 +271,13 @@ export function Modal({
218
271
  draggable,
219
272
  style,
220
273
  role = "dialog",
274
+ urlParam: _urlParam,
275
+ ownHistoryEntry = true,
221
276
  ...rest
222
- }: ModalProps) {
277
+ }: ModalProps & {
278
+ /** False when the URL already holds an entry for this dialog — see `urlParam`. */
279
+ ownHistoryEntry?: boolean;
280
+ }) {
223
281
  const panelRef = useRef<HTMLDivElement>(null);
224
282
  const drag = useDragOffset(Boolean(draggable));
225
283
  // Without `open`, mounted means open — the constant `true` every hook below used to
@@ -262,7 +320,7 @@ export function Modal({
262
320
  // this the only way out of a dialog is finding its close button — and Back, the
263
321
  // gesture everyone reaches for, navigated the page underneath instead (#172).
264
322
  // Held only while open: a closed `open={false}` dialog must not own a history entry.
265
- useOverlayHistory(active, requestClose);
323
+ useOverlayHistory(active && ownHistoryEntry, requestClose);
266
324
 
267
325
  // Through the shared hook and not by hand: this component's own save/restore copy
268
326
  // was one half of the pair that left the page permanently unscrollable when a dialog
@@ -312,6 +370,10 @@ export function Modal({
312
370
  )}
313
371
  {...backdropClose}
314
372
  >
373
+ {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- always
374
+ role="dialog" or "alertdialog" (the ternary hides that from the rule): `onKeyDown`
375
+ is Escape-to-close per the APG dialog pattern, and the drag handlers are
376
+ swipe-to-dismiss, whose keyboard path is that same Escape. */}
315
377
  <div
316
378
  // `...rest` FIRST, everything the dialog needs to be a dialog after it. A
317
379
  // wrapper that spreads its own props through to this one cannot then take away
@@ -1,10 +1,10 @@
1
1
  import { useEffect, useId, useMemo, useRef, useState } from "react";
2
2
  import type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, RefObject } from "react";
3
- import { Calendar, ChevronLeft, ChevronRight } from "lucide-react";
3
+ import { Calendar, ChevronDown, ChevronLeft, ChevronRight } from "lucide-react";
4
4
  import { cn } from "../lib/cn";
5
5
  import { dirOf, horizontalStep, type Direction } from "../lib/direction";
6
6
  import { monthKey, pad } from "../lib/dates";
7
- import { FieldLabel, FIELD_FLOATING_PAD, FIELD_INVALID, FIELD_TRIGGER } from "./ui";
7
+ import { Button, FieldLabel, FIELD_FLOATING_PAD, FIELD_INVALID, FIELD_TRIGGER, IconButton } from "./ui";
8
8
  import { Popover } from "./popover";
9
9
  import { splitTriggerAria } from "./trigger-aria";
10
10
  import type { TriggerAria } from "./trigger-aria";
@@ -27,6 +27,11 @@ export interface MonthPickerLabels {
27
27
  * "Aug" read out on its own does not say which year the grid is on.
28
28
  */
29
29
  month: (monthYear: string) => string;
30
+ /** `variant="stepper"`: the icon-only arrows that step the value by one month. */
31
+ previousMonth: string;
32
+ nextMonth: string;
33
+ /** `variant="stepper"`: the button that jumps to the current month. Visible text. */
34
+ today: string;
30
35
  }
31
36
 
32
37
  /** The English starting point every override is merged onto. Exported (unlike the
@@ -37,6 +42,9 @@ export const DEFAULT_MONTH_PICKER_LABELS: MonthPickerLabels = {
37
42
  nextYear: "Next year",
38
43
  panel: "Choose a month",
39
44
  month: (monthYear) => monthYear,
45
+ previousMonth: "Previous month",
46
+ nextMonth: "Next month",
47
+ today: "Today",
40
48
  };
41
49
 
42
50
  /** Merge caller overrides onto the English defaults. */
@@ -95,6 +103,21 @@ export interface MonthPickerProps extends Omit<ComponentPropsWithoutRef<"div">,
95
103
  /** Extra classes for the trigger button — e.g. a compact `h-9 w-auto` trigger
96
104
  * sitting between two icon buttons in a toolbar. */
97
105
  triggerClassName?: string;
106
+ /**
107
+ * `"field"` (default): the field-shaped trigger, for a form.
108
+ *
109
+ * `"stepper"`: the header of a month PAGE — the month as a heading that opens the
110
+ * grid, then Today and ‹ › stepping the value by one month. What a calendar page
111
+ * assembled by hand around the field (overriding its trigger to look like a
112
+ * heading). `label`, `placeholder`, `invalid` and `triggerClassName` are the field's
113
+ * and are not drawn here; with no `value` the arrows step from the current month.
114
+ */
115
+ variant?: "field" | "stepper";
116
+ /** `variant="stepper"`: the heading level the month is set in (`<h2>` by default), or
117
+ * `false` for no heading element — the page already has one for the month. */
118
+ headingLevel?: 1 | 2 | 3 | 4 | 5 | 6 | false;
119
+ /** `variant="stepper"`: draw the Today button. Default `true`. */
120
+ showToday?: boolean;
98
121
  }
99
122
 
100
123
  // ── Month keys ────────────────────────────────────────────────────────────
@@ -140,6 +163,74 @@ const MONTH_NAME_FORMAT: Intl.DateTimeFormatOptions = { month: "long", year: "nu
140
163
 
141
164
  // ── Trigger ───────────────────────────────────────────────────────────────
142
165
 
166
+ /** Hand focus back to the trigger when the panel closes with focus still inside it. */
167
+ function useFocusBackOnClose(open: boolean, triggerRef: RefObject<HTMLButtonElement | null>) {
168
+ const wasOpen = useRef(false);
169
+ useEffect(() => {
170
+ const justClosed = wasOpen.current && !open;
171
+ wasOpen.current = open;
172
+ if (!justClosed) return;
173
+ // Only when focus went down with the panel. An outside click has already put it
174
+ // somewhere real, and dragging it back here would be worse than leaving it.
175
+ const active = document.activeElement;
176
+ if (!active || active === document.body) triggerRef.current?.focus();
177
+ }, [open, triggerRef]);
178
+ }
179
+
180
+ /**
181
+ * The stepper's trigger: the month as a heading-sized button that opens the grid.
182
+ *
183
+ * Its own text is its name ("September 2026"), so it needs no label of its own, and
184
+ * it is `aria-live` for the reason `MiniCalendar`'s caption is: pressing ‹ or › leaves
185
+ * focus on the arrow, and the heading is the only thing that says where the arrow
186
+ * went. A plain button with `aria-haspopup`, not the field's `combobox` — it does not
187
+ * look or behave like a field, and a heading reading "combobox" is a surprise.
188
+ */
189
+ function MonthHeadingTrigger({
190
+ open,
191
+ toggle,
192
+ triggerRef,
193
+ text,
194
+ panelId,
195
+ disabled,
196
+ aria,
197
+ }: {
198
+ open: boolean;
199
+ toggle: () => void;
200
+ triggerRef: RefObject<HTMLButtonElement | null>;
201
+ text: string;
202
+ panelId: string;
203
+ disabled?: boolean;
204
+ aria: TriggerAria;
205
+ }) {
206
+ useFocusBackOnClose(open, triggerRef);
207
+ return (
208
+ <button
209
+ ref={triggerRef}
210
+ type="button"
211
+ disabled={disabled}
212
+ onClick={toggle}
213
+ aria-haspopup="dialog"
214
+ aria-controls={panelId}
215
+ aria-expanded={open}
216
+ id={aria.id}
217
+ aria-label={aria["aria-label"]}
218
+ aria-labelledby={aria["aria-labelledby"]}
219
+ aria-describedby={aria["aria-describedby"]}
220
+ className={cn(
221
+ "-ms-1.5 inline-flex h-9 min-w-0 items-center gap-1 rounded-md px-1.5 text-lg font-semibold text-[var(--text-primary)] transition-colors",
222
+ "hover:bg-[var(--bg-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--brand)]",
223
+ disabled && "cursor-not-allowed opacity-50",
224
+ )}
225
+ >
226
+ <span aria-live="polite" className="truncate">
227
+ {text || "\u00a0"}
228
+ </span>
229
+ <ChevronDown aria-hidden className="size-4 shrink-0 text-[var(--text-muted)]" />
230
+ </button>
231
+ );
232
+ }
233
+
143
234
  /**
144
235
  * The field-shaped button that opens the grid. The same shape as `DatePicker`'s
145
236
  * trigger, for the same reasons written down there: `role="combobox"` so it may carry
@@ -176,16 +267,7 @@ function MonthFieldTrigger({
176
267
  invalid?: boolean;
177
268
  className?: string;
178
269
  }) {
179
- const wasOpen = useRef(false);
180
- useEffect(() => {
181
- const justClosed = wasOpen.current && !open;
182
- wasOpen.current = open;
183
- if (!justClosed) return;
184
- // Only when focus went down with the panel. An outside click has already put it
185
- // somewhere real, and dragging it back here would be worse than leaving it.
186
- const active = document.activeElement;
187
- if (!active || active === document.body) triggerRef.current?.focus();
188
- }, [open, triggerRef]);
270
+ useFocusBackOnClose(open, triggerRef);
189
271
 
190
272
  return (
191
273
  <button
@@ -475,6 +557,9 @@ export function MonthPicker({
475
557
  labels: labelsProp,
476
558
  className,
477
559
  triggerClassName,
560
+ variant = "field",
561
+ headingLevel = 2,
562
+ showToday = true,
478
563
  ...rest
479
564
  }: MonthPickerProps) {
480
565
  const labels = useMonthPickerLabels(labelsProp);
@@ -506,6 +591,89 @@ export function MonthPicker({
506
591
  const rootRef = useRef<HTMLDivElement>(null);
507
592
  const [dir, setDir] = useState<Direction>("ltr");
508
593
 
594
+ const grid = (close: () => void) => (
595
+ <MonthGrid
596
+ value={selected}
597
+ locale={locale}
598
+ minKey={minKey}
599
+ maxKey={maxKey}
600
+ currentKey={currentKey}
601
+ labels={labels}
602
+ onPick={(key) => {
603
+ onChange(key);
604
+ close();
605
+ }}
606
+ />
607
+ );
608
+
609
+ if (variant === "stepper") {
610
+ // The arrows step from the value, or from "now" when there is none; each is dead
611
+ // once the month it leads to is out of bounds, and Today once "now" is.
612
+ const base = selected ?? parseMonth(currentKey)!;
613
+ const prevKey = toKey(shiftMonth(base, -1));
614
+ const nextKey = toKey(shiftMonth(base, 1));
615
+ const outside = (key: string) => Boolean((minKey && key < minKey) || (maxKey && key > maxKey));
616
+ const Heading = headingLevel ? (`h${headingLevel}` as const) : null;
617
+ const trigger = (
618
+ <Popover
619
+ width={256}
620
+ panelId={panelId}
621
+ labels={{ panel: labels.panel }}
622
+ dir={dir}
623
+ trigger={({ open, toggle, ref }) => (
624
+ <MonthHeadingTrigger
625
+ open={open}
626
+ toggle={() => {
627
+ setDir(dirOf(rootRef.current));
628
+ toggle();
629
+ }}
630
+ triggerRef={ref}
631
+ text={triggerText}
632
+ panelId={panelId}
633
+ disabled={disabled}
634
+ aria={aria}
635
+ />
636
+ )}
637
+ >
638
+ {grid}
639
+ </Popover>
640
+ );
641
+ return (
642
+ <div {...wrapperRest} ref={rootRef} className={cn("flex min-w-0 items-center gap-2", className)}>
643
+ {Heading ? <Heading className="m-0 min-w-0 text-lg font-semibold">{trigger}</Heading> : trigger}
644
+ <div className="ms-auto flex shrink-0 items-center gap-1">
645
+ {showToday && (
646
+ <Button
647
+ variant="secondary"
648
+ size="sm"
649
+ disabled={disabled || outside(currentKey)}
650
+ onClick={() => {
651
+ if (currentKey !== value) onChange(currentKey);
652
+ }}
653
+ >
654
+ {labels.today}
655
+ </Button>
656
+ )}
657
+ <IconButton
658
+ aria-label={labels.previousMonth}
659
+ disabled={disabled || outside(prevKey)}
660
+ onClick={() => onChange(prevKey)}
661
+ >
662
+ {/* Mirrored in RTL, where "previous" points the other way. */}
663
+ <ChevronLeft className="rtl:-scale-x-100" />
664
+ </IconButton>
665
+ <IconButton
666
+ aria-label={labels.nextMonth}
667
+ disabled={disabled || outside(nextKey)}
668
+ onClick={() => onChange(nextKey)}
669
+ >
670
+ <ChevronRight className="rtl:-scale-x-100" />
671
+ </IconButton>
672
+ </div>
673
+ </div>
674
+ );
675
+ }
676
+
509
677
  return (
510
678
  <div {...wrapperRest} ref={rootRef} className={cn("relative", className)}>
511
679
  {label !== undefined && <FieldLabel>{label}</FieldLabel>}
@@ -550,20 +718,7 @@ export function MonthPicker({
550
718
  />
551
719
  )}
552
720
  >
553
- {(close) => (
554
- <MonthGrid
555
- value={selected}
556
- locale={locale}
557
- minKey={minKey}
558
- maxKey={maxKey}
559
- currentKey={currentKey}
560
- labels={labels}
561
- onPick={(key) => {
562
- onChange(key);
563
- close();
564
- }}
565
- />
566
- )}
721
+ {grid}
567
722
  </Popover>
568
723
  <Calendar
569
724
  aria-hidden
@@ -94,6 +94,15 @@ export function MultiEntityCombobox<V extends string | number>({
94
94
  debounceMs,
95
95
  loadErrorLabel,
96
96
  "aria-label": ariaLabel,
97
+ // The control's own wiring, taken off `rest` so it lands on the TRIGGER rather than
98
+ // the wrapper: `Field`'s render-prop spreads `{ id, aria-describedby, aria-invalid,
99
+ // aria-required }`, and on the role-less wrapper div the label's `htmlFor` named
100
+ // nothing and the hint and required state described nothing — kastlan's
101
+ // communication-page To/Cc/Bcc and lease-tenants-step.tsx read as unnamed pickers.
102
+ id,
103
+ "aria-describedby": ariaDescribedBy,
104
+ "aria-invalid": ariaInvalid,
105
+ "aria-required": ariaRequired,
97
106
  ...rest
98
107
  }: MultiEntityComboboxProps<V>) {
99
108
  const core = useComboboxCore<V>({
@@ -104,7 +113,11 @@ export function MultiEntityCombobox<V extends string | number>({
104
113
  minChars,
105
114
  debounceMs,
106
115
  });
107
- const field = useComboboxFieldError(error, invalid);
116
+ const field = useComboboxFieldError(
117
+ error,
118
+ invalid || ariaInvalid === true || ariaInvalid === "true",
119
+ ariaDescribedBy,
120
+ );
108
121
  // The props are the per-instance overrides, the provider the app-wide ones; a
109
122
  // prop left `undefined` falls through to the provider rather than masking it.
110
123
  const labels = useKitLabels("combobox", DEFAULT_COMBOBOX_LABELS, {
@@ -114,7 +127,7 @@ export function MultiEntityCombobox<V extends string | number>({
114
127
  create: createLabel,
115
128
  });
116
129
  const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
117
- const { open, results, resolve, setOpen } = core;
130
+ const { open, results, resolve, setOpen, query, triggerRef } = core;
118
131
  // One id per instance, generated here rather than in the core: `aria-controls` on
119
132
  // the trigger has to name the list while the list is still closed, so the id
120
133
  // belongs to whoever renders both ends of it.
@@ -141,26 +154,28 @@ export function MultiEntityCombobox<V extends string | number>({
141
154
  : labels.selectedCount(value.length);
142
155
  })();
143
156
 
144
- const q = core.query.trim();
157
+ const q = query.trim();
145
158
  const showCreate =
146
159
  Boolean(onCreate) && q.length > 0 && !results.some((o) => o.label.toLowerCase() === q.toLowerCase());
147
160
  const showClear = Boolean(clearable && value.length > 0 && !disabled);
148
161
 
149
162
  const toggle = (o: ComboOption<V>) => {
150
- core.cacheRef.current.set(o.value, o);
163
+ core.rememberOption(o);
151
164
  if (valueSet.has(o.value)) onChange(value.filter((v) => v !== o.value));
152
165
  else onChange([...value, o.value]);
153
166
  // Stay open — multi-select keeps adding.
154
167
  };
155
168
 
156
169
  return (
157
- // `rest` dresses the wrapper, which has no role; the accessible NAME goes on
158
- // the trigger, which has one. Spread FIRST so the trigger's ARIA and the
170
+ // `rest` dresses the wrapper, which has no role; the accessible NAME — and the
171
+ // id, description, invalid and required state above — go on the trigger, which
172
+ // has one. Spread FIRST so the trigger's ARIA and the
159
173
  // handlers that open the panel cannot be clobbered from outside.
160
174
  <div {...rest} className={cn("relative", className)}>
161
175
  {label !== undefined && <FieldLabel>{label}</FieldLabel>}
162
176
  <button
163
- ref={core.triggerRef}
177
+ ref={triggerRef}
178
+ id={id}
164
179
  type="button"
165
180
  // A combobox, not a button. The distinction is not pedantry: this control
166
181
  // carried `aria-invalid`, which `button` does not support, so a required
@@ -188,6 +203,7 @@ export function MultiEntityCombobox<V extends string | number>({
188
203
  disabled={disabled}
189
204
  aria-invalid={field.isInvalid || undefined}
190
205
  aria-describedby={field.describedBy}
206
+ aria-required={ariaRequired}
191
207
  onClick={() => !disabled && setOpen((o) => !o)}
192
208
  // Down/Up opens the list from the closed trigger, per the APG. Enter and
193
209
  // Space already do it through the button's own click.
@@ -218,6 +234,10 @@ export function MultiEntityCombobox<V extends string | number>({
218
234
  {summary}
219
235
  </span>
220
236
  {showClear ? (
237
+ /* eslint-disable-next-line jsx-a11y/click-events-have-key-events -- tabIndex -1 inside
238
+ the trigger <button>, so it never holds focus and a key handler here could
239
+ never fire. A pointer shortcut; the keyboard path is unticking the options
240
+ in the open list. */
221
241
  <span
222
242
  role="button"
223
243
  tabIndex={-1}
@@ -1,5 +1,7 @@
1
1
  import type { ComponentPropsWithoutRef, ComponentType, MouseEvent, ReactElement, ReactNode } from "react";
2
2
  import { cn } from "../lib/cn";
3
+ import { useKitLink } from "../i18n/kit-labels";
4
+ import { pickLinkRenderer, RenderedKitLink } from "./text-link";
3
5
 
4
6
  export interface NavPillItem<T extends string> {
5
7
  value: T;
@@ -33,7 +35,8 @@ export interface NavPillsProps<T extends string> extends Omit<ComponentPropsWith
33
35
  /** Called with the pill's value on a click — of a button, and of a link too (before
34
36
  * the router follows it), for a caller that keeps the choice in state as well. */
35
37
  onSelect?: (value: T) => void;
36
- /** Your router's link. Default `<a>`. The API `Breadcrumbs`, `Chip` and `ListItem` share. */
38
+ /** Your router's link. Default: the `<UiKitProvider linkComponent>` (for an in-app
39
+ * `href`), then `<a>`. The API `Breadcrumbs`, `Chip` and `ListItem` share. */
37
40
  renderLink?: (props: NavPillLinkProps) => ReactElement;
38
41
  /** What `aria-current` says on the current pill. Default `page` where the items
39
42
  * are links and `true` where they are buttons (keksdose's swipe-surface picker
@@ -84,6 +87,7 @@ export function NavPills<T extends string>({
84
87
  className,
85
88
  ...rest
86
89
  }: NavPillsProps<T>) {
90
+ const kitLink = useKitLink();
87
91
  const Root = landmark ? "nav" : "div";
88
92
  return (
89
93
  <Root {...rest} role={landmark ? undefined : "group"} className={cn("min-w-0", className)}>
@@ -113,8 +117,11 @@ export function NavPills<T extends string>({
113
117
  "aria-current": isCurrent ? (currentType ?? "page") : undefined,
114
118
  onClick: () => onSelect?.(item.value),
115
119
  };
120
+ const kit = pickLinkRenderer<NavPillLinkProps>(undefined, kitLink, item.href);
116
121
  pill = renderLink ? (
117
122
  renderLink(props)
123
+ ) : kit ? (
124
+ <RenderedKitLink render={kit} props={props} />
118
125
  ) : (
119
126
  <a href={props.href} className={props.className} aria-current={props["aria-current"]} onClick={props.onClick}>
120
127
  {props.children}
@@ -1,4 +1,4 @@
1
- import { useEffect, useId, useState } from "react";
1
+ import { useId, useState } from "react";
2
2
  import type { ComponentProps, ReactNode } from "react";
3
3
  import { NumberInput } from "./number-input";
4
4
  import { evaluateExpression, formatResult } from "../lib/calc";
@@ -62,6 +62,14 @@ export interface NumberFieldProps
62
62
  * injects `id` / `aria-describedby` / `aria-invalid`) can wrap this directly. */
63
63
  "aria-describedby"?: string;
64
64
  "aria-invalid"?: boolean | "true" | "false";
65
+ /**
66
+ * Set on the `<input>`. Declared, with `id` (from NumberInput) and the two above, so
67
+ * `Field`'s render-prop spread — `{(ids) => <NumberField {...ids} … />}`, kastlan's
68
+ * wizard steps — is typed and reaches the input rather than being dropped: before
69
+ * this, `aria-required` was not in the type and a required number was announced as
70
+ * optional.
71
+ */
72
+ "aria-required"?: boolean | "true" | "false";
65
73
  }
66
74
 
67
75
  /** The locale's decimal mark, as far as this field can type it: "," or ".". A mark
@@ -163,6 +171,7 @@ export function NumberField({
163
171
  id,
164
172
  "aria-describedby": ariaDescribedBy,
165
173
  "aria-invalid": ariaInvalid,
174
+ "aria-required": ariaRequired,
166
175
  ...rest
167
176
  }: NumberFieldProps) {
168
177
  const locale = useKitLocale(localeProp);
@@ -250,15 +259,9 @@ export function NumberField({
250
259
  const hasError = error !== undefined && error !== null && error !== false && error !== "";
251
260
  const describedBy = [ariaDescribedBy, hasError ? errorId : undefined].filter(Boolean).join(" ");
252
261
 
253
- // NumberInput takes no `aria-describedby` of its own, so the reference is set on
254
- // its <input> directly, found by the id this field hands it. React never renders
255
- // that attribute there, so nothing reconciles it away again.
256
- useEffect(() => {
257
- const input = document.getElementById(fieldId);
258
- if (!input) return;
259
- if (describedBy) input.setAttribute("aria-describedby", describedBy);
260
- else input.removeAttribute("aria-describedby");
261
- }, [fieldId, describedBy]);
262
+ // Straight onto NumberInput's <input>, which declares both now. They used to be set
263
+ // on the DOM node from an effect, because NumberInput had no props for them.
264
+ const required = ariaRequired === true || ariaRequired === "true";
262
265
 
263
266
  const labelWithUnit =
264
267
  unit !== undefined && unitPlacement === "label"
@@ -277,6 +280,8 @@ export function NumberField({
277
280
  <NumberInput
278
281
  {...rest}
279
282
  id={fieldId}
283
+ aria-describedby={describedBy || undefined}
284
+ aria-required={required || undefined}
280
285
  label={labelWithUnit}
281
286
  value={draft}
282
287
  onChange={onText}