@eifi1/ui-kit 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (382) hide show
  1. package/README.md +66 -37
  2. package/dist/chart.d.ts +3 -1
  3. package/dist/chart.js +2 -0
  4. package/dist/chart.js.map +1 -1
  5. package/dist/components/account-settings-labels.d.ts +105 -0
  6. package/dist/components/account-settings-labels.js +81 -0
  7. package/dist/components/account-settings-labels.js.map +1 -0
  8. package/dist/components/account-settings.d.ts +36 -52
  9. package/dist/components/account-settings.js +44 -8
  10. package/dist/components/account-settings.js.map +1 -1
  11. package/dist/components/alert-banner.d.ts +26 -1
  12. package/dist/components/alert-banner.js +10 -7
  13. package/dist/components/alert-banner.js.map +1 -1
  14. package/dist/components/amount-input.d.ts +26 -4
  15. package/dist/components/amount-input.js +5 -2
  16. package/dist/components/amount-input.js.map +1 -1
  17. package/dist/components/authed-image.d.ts +68 -0
  18. package/dist/components/authed-image.js +117 -0
  19. package/dist/components/authed-image.js.map +1 -0
  20. package/dist/components/breadcrumbs.d.ts +2 -1
  21. package/dist/components/breadcrumbs.js +5 -2
  22. package/dist/components/breadcrumbs.js.map +1 -1
  23. package/dist/components/bulk-action-bar.d.ts +51 -5
  24. package/dist/components/bulk-action-bar.js +88 -41
  25. package/dist/components/bulk-action-bar.js.map +1 -1
  26. package/dist/components/button-group.d.ts +107 -3
  27. package/dist/components/button-group.js +60 -5
  28. package/dist/components/button-group.js.map +1 -1
  29. package/dist/components/calculator.d.ts +17 -4
  30. package/dist/components/calendar-heatmap.d.ts +162 -0
  31. package/dist/components/calendar-heatmap.js +336 -0
  32. package/dist/components/calendar-heatmap.js.map +1 -0
  33. package/dist/components/chip.d.ts +2 -1
  34. package/dist/components/chip.js +6 -3
  35. package/dist/components/chip.js.map +1 -1
  36. package/dist/components/choice-card.d.ts +66 -2
  37. package/dist/components/choice-card.js +108 -1
  38. package/dist/components/choice-card.js.map +1 -1
  39. package/dist/components/combobox-core.js +2 -0
  40. package/dist/components/combobox-core.js.map +1 -1
  41. package/dist/components/combobox.d.ts +2 -2
  42. package/dist/components/combobox.js +31 -9
  43. package/dist/components/combobox.js.map +1 -1
  44. package/dist/components/copy-button.d.ts +52 -70
  45. package/dist/components/copy-button.js +1 -1
  46. package/dist/components/copy-button.js.map +1 -1
  47. package/dist/components/data-table-cells.d.ts +63 -0
  48. package/dist/components/data-table-cells.js +64 -0
  49. package/dist/components/data-table-cells.js.map +1 -0
  50. package/dist/components/data-table-filter-popover.d.ts +1 -1
  51. package/dist/components/data-table-filters.d.ts +1 -1
  52. package/dist/components/data-table-filters.js +31 -1
  53. package/dist/components/data-table-filters.js.map +1 -1
  54. package/dist/components/data-table-labels.d.ts +13 -0
  55. package/dist/components/data-table-labels.js +8 -1
  56. package/dist/components/data-table-labels.js.map +1 -1
  57. package/dist/components/data-table.d.ts +1 -1
  58. package/dist/components/data-table.js +104 -12
  59. package/dist/components/data-table.js.map +1 -1
  60. package/dist/components/date-picker.d.ts +3 -1
  61. package/dist/components/date-picker.js +15 -2
  62. package/dist/components/date-picker.js.map +1 -1
  63. package/dist/components/description-list.d.ts +23 -3
  64. package/dist/components/description-list.js +23 -3
  65. package/dist/components/description-list.js.map +1 -1
  66. package/dist/components/dialog-frame.d.ts +1 -0
  67. package/dist/components/entity-combobox.d.ts +1 -1
  68. package/dist/components/entity-combobox.js +18 -3
  69. package/dist/components/entity-combobox.js.map +1 -1
  70. package/dist/components/error-boundary.d.ts +92 -0
  71. package/dist/components/error-boundary.js +111 -0
  72. package/dist/components/error-boundary.js.map +1 -0
  73. package/dist/components/field.d.ts +69 -0
  74. package/dist/components/field.js +75 -0
  75. package/dist/components/field.js.map +1 -0
  76. package/dist/components/file-button.d.ts +51 -208
  77. package/dist/components/file-dropzone.d.ts +50 -2
  78. package/dist/components/floating-panel.d.ts +156 -4
  79. package/dist/components/floating-panel.js +193 -29
  80. package/dist/components/floating-panel.js.map +1 -1
  81. package/dist/components/form-actions.d.ts +53 -0
  82. package/dist/components/form-actions.js +98 -0
  83. package/dist/components/form-actions.js.map +1 -0
  84. package/dist/components/image-grid.d.ts +67 -0
  85. package/dist/components/image-grid.js +112 -0
  86. package/dist/components/image-grid.js.map +1 -0
  87. package/dist/components/lightbox.d.ts +104 -0
  88. package/dist/components/lightbox.js +229 -0
  89. package/dist/components/lightbox.js.map +1 -0
  90. package/dist/components/line-items.d.ts +114 -0
  91. package/dist/components/line-items.js +238 -0
  92. package/dist/components/line-items.js.map +1 -0
  93. package/dist/components/list.d.ts +37 -3
  94. package/dist/components/list.js +39 -14
  95. package/dist/components/list.js.map +1 -1
  96. package/dist/components/loading-state.d.ts +24 -0
  97. package/dist/components/loading-state.js +32 -0
  98. package/dist/components/loading-state.js.map +1 -0
  99. package/dist/components/menu-item.d.ts +13 -2
  100. package/dist/components/menu-item.js +11 -2
  101. package/dist/components/menu-item.js.map +1 -1
  102. package/dist/components/mini-calendar.d.ts +82 -5
  103. package/dist/components/mini-calendar.js +212 -64
  104. package/dist/components/mini-calendar.js.map +1 -1
  105. package/dist/components/modal.d.ts +20 -1
  106. package/dist/components/modal.js +26 -2
  107. package/dist/components/modal.js.map +1 -1
  108. package/dist/components/month-picker.d.ts +21 -1
  109. package/dist/components/month-picker.js +139 -26
  110. package/dist/components/month-picker.js.map +1 -1
  111. package/dist/components/multi-entity-combobox.d.ts +1 -1
  112. package/dist/components/multi-entity-combobox.js +19 -3
  113. package/dist/components/multi-entity-combobox.js.map +1 -1
  114. package/dist/components/nav-pills.d.ts +67 -0
  115. package/dist/components/nav-pills.js +69 -0
  116. package/dist/components/nav-pills.js.map +1 -0
  117. package/dist/components/number-field.d.ts +26 -5
  118. package/dist/components/number-field.js +5 -7
  119. package/dist/components/number-field.js.map +1 -1
  120. package/dist/components/number-input.d.ts +27 -5
  121. package/dist/components/number-input.js +7 -1
  122. package/dist/components/number-input.js.map +1 -1
  123. package/dist/components/numpad-sheet.d.ts +17 -4
  124. package/dist/components/passkeys-setting.d.ts +67 -0
  125. package/dist/components/passkeys-setting.js +214 -0
  126. package/dist/components/passkeys-setting.js.map +1 -0
  127. package/dist/components/picker-sheet.js +2 -1
  128. package/dist/components/picker-sheet.js.map +1 -1
  129. package/dist/components/pie-chart-labels.d.ts +20 -0
  130. package/dist/components/pie-chart-labels.js +12 -0
  131. package/dist/components/pie-chart-labels.js.map +1 -0
  132. package/dist/components/pie-chart.d.ts +101 -0
  133. package/dist/components/pie-chart.js +283 -0
  134. package/dist/components/pie-chart.js.map +1 -0
  135. package/dist/components/progress-bar.d.ts +73 -3
  136. package/dist/components/progress-bar.js +110 -8
  137. package/dist/components/progress-bar.js.map +1 -1
  138. package/dist/components/qr-code.d.ts +34 -0
  139. package/dist/components/qr-code.js +57 -0
  140. package/dist/components/qr-code.js.map +1 -0
  141. package/dist/components/search-field.d.ts +15 -3
  142. package/dist/components/search-field.js +5 -1
  143. package/dist/components/search-field.js.map +1 -1
  144. package/dist/components/series-chart.d.ts +16 -0
  145. package/dist/components/series-chart.js +4 -1
  146. package/dist/components/series-chart.js.map +1 -1
  147. package/dist/components/settings-fields.d.ts +50 -1
  148. package/dist/components/signed-amount.d.ts +121 -0
  149. package/dist/components/signed-amount.js +152 -0
  150. package/dist/components/signed-amount.js.map +1 -0
  151. package/dist/components/skeleton.d.ts +10 -3
  152. package/dist/components/skeleton.js +12 -2
  153. package/dist/components/skeleton.js.map +1 -1
  154. package/dist/components/stat-tile.d.ts +2 -1
  155. package/dist/components/stat-tile.js +5 -2
  156. package/dist/components/stat-tile.js.map +1 -1
  157. package/dist/components/status-dot.d.ts +6 -2
  158. package/dist/components/status-dot.js +6 -1
  159. package/dist/components/status-dot.js.map +1 -1
  160. package/dist/components/table.d.ts +77 -11
  161. package/dist/components/table.js +85 -17
  162. package/dist/components/table.js.map +1 -1
  163. package/dist/components/text-link.d.ts +176 -0
  164. package/dist/components/text-link.js +109 -0
  165. package/dist/components/text-link.js.map +1 -0
  166. package/dist/components/text.d.ts +8 -1
  167. package/dist/components/text.js +1 -0
  168. package/dist/components/text.js.map +1 -1
  169. package/dist/components/time-input.d.ts +50 -1
  170. package/dist/components/toggle-group.d.ts +28 -0
  171. package/dist/components/toggle-group.js +58 -9
  172. package/dist/components/toggle-group.js.map +1 -1
  173. package/dist/components/toggle-legend.d.ts +36 -3
  174. package/dist/components/toggle-legend.js +49 -0
  175. package/dist/components/toggle-legend.js.map +1 -1
  176. package/dist/components/tooltip.d.ts +11 -3
  177. package/dist/components/tooltip.js +2 -0
  178. package/dist/components/tooltip.js.map +1 -1
  179. package/dist/components/ui.d.ts +52 -729
  180. package/dist/components/ui.js +296 -27
  181. package/dist/components/ui.js.map +1 -1
  182. package/dist/components/use-table-state.d.ts +3 -52
  183. package/dist/components/use-table-state.js +128 -30
  184. package/dist/components/use-table-state.js.map +1 -1
  185. package/dist/components/user-avatar.d.ts +25 -3
  186. package/dist/components/user-avatar.js +32 -3
  187. package/dist/components/user-avatar.js.map +1 -1
  188. package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
  189. package/dist/data-table.d.ts +2 -1
  190. package/dist/data-table.js +5 -1
  191. package/dist/data-table.js.map +1 -1
  192. package/dist/feedback/feedback-dialog.d.ts +26 -3
  193. package/dist/feedback/feedback-dialog.js +16 -2
  194. package/dist/feedback/feedback-dialog.js.map +1 -1
  195. package/dist/feedback/feedback-thread.d.ts +133 -0
  196. package/dist/feedback/feedback-thread.js +237 -0
  197. package/dist/feedback/feedback-thread.js.map +1 -0
  198. package/dist/feedback.d.ts +2 -1
  199. package/dist/feedback.js +1 -0
  200. package/dist/feedback.js.map +1 -1
  201. package/dist/hooks/use-authed-src.d.ts +71 -0
  202. package/dist/hooks/use-authed-src.js +63 -0
  203. package/dist/hooks/use-authed-src.js.map +1 -0
  204. package/dist/hooks/use-file-drop.d.ts +50 -2
  205. package/dist/hooks/use-hotkey.d.ts +59 -0
  206. package/dist/hooks/use-hotkey.js +105 -0
  207. package/dist/hooks/use-hotkey.js.map +1 -0
  208. package/dist/hooks/use-search-param-state.d.ts +83 -0
  209. package/dist/hooks/use-search-param-state.js +94 -0
  210. package/dist/hooks/use-search-param-state.js.map +1 -0
  211. package/dist/i18n/defaults.d.ts +17 -4
  212. package/dist/i18n/defaults.js +33 -1
  213. package/dist/i18n/defaults.js.map +1 -1
  214. package/dist/i18n/kit-labels.d.ts +47 -310
  215. package/dist/i18n/kit-labels.js +20 -5
  216. package/dist/i18n/kit-labels.js.map +1 -1
  217. package/dist/i18n/locales/de-CH-informal.d.ts +17 -4
  218. package/dist/i18n/locales/de-CH.d.ts +17 -4
  219. package/dist/i18n/locales/de-informal.d.ts +17 -4
  220. package/dist/i18n/locales/de.d.ts +17 -4
  221. package/dist/i18n/locales/de.js +165 -4
  222. package/dist/i18n/locales/de.js.map +1 -1
  223. package/dist/i18n/locales/es.d.ts +17 -4
  224. package/dist/i18n/locales/es.js +162 -4
  225. package/dist/i18n/locales/es.js.map +1 -1
  226. package/dist/i18n/locales/fr.d.ts +17 -4
  227. package/dist/i18n/locales/fr.js +162 -4
  228. package/dist/i18n/locales/fr.js.map +1 -1
  229. package/dist/i18n/locales/hu.d.ts +17 -4
  230. package/dist/i18n/locales/hu.js +162 -4
  231. package/dist/i18n/locales/hu.js.map +1 -1
  232. package/dist/i18n/locales/it.d.ts +17 -4
  233. package/dist/i18n/locales/it.js +162 -4
  234. package/dist/i18n/locales/it.js.map +1 -1
  235. package/dist/i18n/locales/zh.d.ts +17 -4
  236. package/dist/i18n/locales/zh.js +162 -4
  237. package/dist/i18n/locales/zh.js.map +1 -1
  238. package/dist/index.d.ts +45 -21
  239. package/dist/index.js +43 -1
  240. package/dist/index.js.map +1 -1
  241. package/dist/kit-labels-v3biUF1L.d.ts +1645 -0
  242. package/dist/lib/clipping.d.ts +21 -5
  243. package/dist/lib/clipping.js +3 -0
  244. package/dist/lib/clipping.js.map +1 -1
  245. package/dist/lib/dates.d.ts +17 -1
  246. package/dist/lib/dates.js +19 -0
  247. package/dist/lib/dates.js.map +1 -1
  248. package/dist/lib/format.d.ts +144 -0
  249. package/dist/lib/format.js +149 -0
  250. package/dist/lib/format.js.map +1 -0
  251. package/dist/lib/qr-encode.d.ts +44 -0
  252. package/dist/lib/qr-encode.js +338 -0
  253. package/dist/lib/qr-encode.js.map +1 -0
  254. package/dist/rhf/fields.d.ts +241 -0
  255. package/dist/rhf/fields.js +601 -0
  256. package/dist/rhf/fields.js.map +1 -0
  257. package/dist/rhf/form.d.ts +50 -1
  258. package/dist/rhf/line-items.d.ts +31 -0
  259. package/dist/rhf/line-items.js +59 -0
  260. package/dist/rhf/line-items.js.map +1 -0
  261. package/dist/rhf/use-rhf-wizard-step.d.ts +22 -0
  262. package/dist/rhf/use-rhf-wizard-step.js +38 -0
  263. package/dist/rhf/use-rhf-wizard-step.js.map +1 -0
  264. package/dist/rhf.d.ts +63 -1
  265. package/dist/rhf.js +3 -0
  266. package/dist/rhf.js.map +1 -1
  267. package/dist/shell/app-shell.d.ts +99 -2
  268. package/dist/shell/app-shell.js +58 -3
  269. package/dist/shell/app-shell.js.map +1 -1
  270. package/dist/shell/auth-layout.d.ts +74 -0
  271. package/dist/shell/auth-layout.js +97 -0
  272. package/dist/shell/auth-layout.js.map +1 -0
  273. package/dist/shell/top-bar-brand.d.ts +81 -0
  274. package/dist/shell/top-bar-brand.js +47 -0
  275. package/dist/shell/top-bar-brand.js.map +1 -0
  276. package/dist/shell/topbar-action-menu.d.ts +84 -3
  277. package/dist/shell/topbar-action-menu.js +115 -27
  278. package/dist/shell/topbar-action-menu.js.map +1 -1
  279. package/dist/shell.d.ts +58 -2
  280. package/dist/shell.js +2 -0
  281. package/dist/shell.js.map +1 -1
  282. package/dist/wizard/stepper-nav.d.ts +49 -1
  283. package/dist/wizard/wizard-context.d.ts +30 -1
  284. package/dist/wizard/wizard-context.js +23 -2
  285. package/dist/wizard/wizard-context.js.map +1 -1
  286. package/dist/wizard/wizard-step.d.ts +20 -5
  287. package/dist/wizard/wizard-step.js +16 -3
  288. package/dist/wizard/wizard-step.js.map +1 -1
  289. package/dist/wizard.d.ts +51 -3
  290. package/package.json +1 -1
  291. package/src/chart.ts +4 -0
  292. package/src/components/account-settings-labels.ts +190 -0
  293. package/src/components/account-settings.tsx +110 -63
  294. package/src/components/alert-banner.tsx +36 -7
  295. package/src/components/amount-input.tsx +13 -1
  296. package/src/components/authed-image.tsx +190 -0
  297. package/src/components/breadcrumbs.tsx +8 -2
  298. package/src/components/bulk-action-bar.tsx +163 -57
  299. package/src/components/button-group.tsx +129 -4
  300. package/src/components/calendar-heatmap.tsx +599 -0
  301. package/src/components/chip.tsx +8 -4
  302. package/src/components/choice-card.tsx +208 -1
  303. package/src/components/combobox-core.tsx +5 -0
  304. package/src/components/combobox.tsx +31 -9
  305. package/src/components/copy-button.tsx +3 -2
  306. package/src/components/data-table-cells.tsx +119 -0
  307. package/src/components/data-table-filters.ts +77 -0
  308. package/src/components/data-table-labels.ts +23 -0
  309. package/src/components/data-table.tsx +231 -13
  310. package/src/components/date-picker.tsx +19 -3
  311. package/src/components/description-list.tsx +52 -3
  312. package/src/components/entity-combobox.tsx +18 -3
  313. package/src/components/error-boundary.tsx +216 -0
  314. package/src/components/field.tsx +188 -0
  315. package/src/components/floating-panel.tsx +380 -16
  316. package/src/components/form-actions.tsx +191 -0
  317. package/src/components/image-grid.tsx +181 -0
  318. package/src/components/lightbox.tsx +374 -0
  319. package/src/components/line-items.tsx +385 -0
  320. package/src/components/list.tsx +93 -18
  321. package/src/components/loading-state.tsx +47 -0
  322. package/src/components/menu-item.tsx +29 -3
  323. package/src/components/mini-calendar.tsx +317 -57
  324. package/src/components/modal.tsx +61 -3
  325. package/src/components/month-picker.tsx +181 -26
  326. package/src/components/multi-entity-combobox.tsx +19 -3
  327. package/src/components/nav-pills.tsx +152 -0
  328. package/src/components/number-field.tsx +15 -10
  329. package/src/components/number-input.tsx +16 -1
  330. package/src/components/passkeys-setting.tsx +331 -0
  331. package/src/components/picker-sheet.tsx +7 -1
  332. package/src/components/pie-chart-labels.ts +32 -0
  333. package/src/components/pie-chart.tsx +479 -0
  334. package/src/components/progress-bar.tsx +241 -13
  335. package/src/components/qr-code.tsx +83 -0
  336. package/src/components/search-field.tsx +20 -4
  337. package/src/components/series-chart.tsx +21 -0
  338. package/src/components/signed-amount.tsx +286 -0
  339. package/src/components/skeleton.tsx +21 -3
  340. package/src/components/stat-tile.tsx +8 -2
  341. package/src/components/status-dot.tsx +10 -2
  342. package/src/components/table.tsx +186 -23
  343. package/src/components/text-link.tsx +248 -0
  344. package/src/components/text.tsx +9 -1
  345. package/src/components/toggle-group.tsx +106 -13
  346. package/src/components/toggle-legend.tsx +87 -2
  347. package/src/components/tooltip.tsx +17 -3
  348. package/src/components/ui.tsx +572 -44
  349. package/src/components/use-table-state.ts +265 -33
  350. package/src/components/user-avatar.tsx +57 -3
  351. package/src/data-table.ts +10 -0
  352. package/src/feedback/feedback-dialog.tsx +42 -3
  353. package/src/feedback/feedback-thread.tsx +434 -0
  354. package/src/feedback.ts +1 -0
  355. package/src/hooks/use-authed-src.ts +161 -0
  356. package/src/hooks/use-hotkey.ts +172 -0
  357. package/src/hooks/use-search-param-state.ts +187 -0
  358. package/src/i18n/defaults.ts +32 -0
  359. package/src/i18n/kit-labels.tsx +104 -8
  360. package/src/i18n/locales/de.ts +162 -0
  361. package/src/i18n/locales/es.ts +159 -0
  362. package/src/i18n/locales/fr.ts +162 -0
  363. package/src/i18n/locales/hu.ts +160 -0
  364. package/src/i18n/locales/it.ts +160 -0
  365. package/src/i18n/locales/zh.ts +159 -0
  366. package/src/index.ts +64 -0
  367. package/src/lib/clipping.ts +22 -4
  368. package/src/lib/dates.ts +37 -0
  369. package/src/lib/format.ts +325 -0
  370. package/src/lib/qr-encode.ts +420 -0
  371. package/src/rhf/fields.tsx +1054 -0
  372. package/src/rhf/line-items.tsx +130 -0
  373. package/src/rhf/use-rhf-wizard-step.ts +113 -0
  374. package/src/rhf.ts +8 -0
  375. package/src/shell/app-shell.tsx +147 -2
  376. package/src/shell/auth-layout.tsx +190 -0
  377. package/src/shell/top-bar-brand.tsx +76 -0
  378. package/src/shell/topbar-action-menu.tsx +260 -49
  379. package/src/shell.ts +2 -0
  380. package/src/wizard/wizard-context.tsx +50 -1
  381. package/src/wizard/wizard-step.tsx +43 -6
  382. package/tokens.css +12 -0
@@ -3,6 +3,8 @@ import type { MouseEvent, ReactElement, ReactNode, Ref } from "react";
3
3
  import { Check } from "lucide-react";
4
4
  import type { LucideIcon } from "lucide-react";
5
5
  import { cn } from "../lib/cn";
6
+ import { useKitLink } from "../i18n/kit-labels";
7
+ import { pickLinkRenderer } from "./text-link";
6
8
 
7
9
  /**
8
10
  * `TOPBAR_MENU_ITEM_CLASS`'s row, spelled out rather than imported: a component does
@@ -43,7 +45,17 @@ interface MenuItemBaseProps {
43
45
  icon?: LucideIcon;
44
46
  /** Anything else before the label — an avatar, a flag — when a Lucide icon will not do. */
45
47
  leading?: ReactNode;
46
- /** After the label, at the row's end: a shortcut, a count, a badge. */
48
+ /**
49
+ * Right after the label text, before the gap that pushes {@link trailing} to the
50
+ * row's end: keksdose's budget switcher tags a budget "Guest" or "Shared" with a
51
+ * `Chip` that belongs to the NAME (budget-switcher.tsx:55/59), not to the row's end
52
+ * where a count or a check mark sits. Hand-placed inside `children` it was truncated
53
+ * with the name — a long budget name ate the chip first. Here the label truncates
54
+ * and the badge never shrinks, so it stays visible beside what is left of the name.
55
+ * It is part of the row's accessible name ("Household Shared"), so keep it text.
56
+ */
57
+ badge?: ReactNode;
58
+ /** After the label, at the row's end: a shortcut, a count. (A tag that belongs to the name is {@link badge}.) */
47
59
  trailing?: ReactNode;
48
60
  /** `danger` for the row that destroys or signs out. Colour is not the whole message:
49
61
  * the label must still say what the row does. */
@@ -92,7 +104,8 @@ export type MenuItemProps = MenuItemBaseProps &
92
104
  /** Makes the row a link — keksdose's "Manage budgets" and account-menu rows. */
93
105
  href: string;
94
106
  /** Renders the link — pass your router's `<Link>`, since a plain `<a>` reloads
95
- * a single-page app. Default `<a>`. The same API as `Chip` and `StatTile`. */
107
+ * a single-page app. Default: the `<UiKitProvider linkComponent>`, then `<a>`.
108
+ * The same API as `Chip` and `StatTile`. */
96
109
  renderLink?: (props: MenuItemLinkProps) => ReactElement;
97
110
  /** Runs on the way (close the menu). */
98
111
  onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
@@ -123,6 +136,7 @@ export const MenuItem = forwardRef<HTMLElement, MenuItemProps>(function MenuItem
123
136
  children,
124
137
  icon: Icon,
125
138
  leading,
139
+ badge,
126
140
  trailing,
127
141
  tone = "default",
128
142
  disabled = false,
@@ -137,6 +151,7 @@ export const MenuItem = forwardRef<HTMLElement, MenuItemProps>(function MenuItem
137
151
  },
138
152
  ref,
139
153
  ) {
154
+ const kitLink = useKitLink();
140
155
  const on = href !== undefined ? !!current : !!checked;
141
156
  const look = cn(
142
157
  MENU_ITEM_ROW,
@@ -151,6 +166,16 @@ export const MenuItem = forwardRef<HTMLElement, MenuItemProps>(function MenuItem
151
166
  {Icon && <Icon className="size-4 shrink-0" aria-hidden />}
152
167
  {leading}
153
168
  <span className="min-w-0 truncate">{children}</span>
169
+ {badge != null && badge !== false && (
170
+ <>
171
+ {/* A space between the name and the badge in the accessible name ("Household
172
+ Shared", not "HouseholdShared"); whitespace between flex items is not
173
+ laid out, so it costs nothing on screen. */}{" "}
174
+ <span data-menu-item-badge="" className="flex shrink-0 items-center">
175
+ {badge}
176
+ </span>
177
+ </>
178
+ )}
154
179
  </span>
155
180
  {(trailing != null || on) && (
156
181
  <span className="flex shrink-0 items-center gap-2">
@@ -172,7 +197,8 @@ export const MenuItem = forwardRef<HTMLElement, MenuItemProps>(function MenuItem
172
197
  "aria-current": current ? "page" : undefined,
173
198
  children: body,
174
199
  };
175
- if (renderLink) return <RenderedLink render={renderLink} {...linkProps} />;
200
+ const render = pickLinkRenderer(renderLink, kitLink, href);
201
+ if (render) return <RenderedLink render={render} {...linkProps} />;
176
202
  const { children: linkBody, ...anchorProps } = linkProps;
177
203
  return <a {...anchorProps}>{linkBody}</a>;
178
204
  }
@@ -1,10 +1,17 @@
1
1
  import { useEffect, useId, useMemo, useRef, useState } from "react";
2
- import type { ComponentPropsWithoutRef, KeyboardEvent } from "react";
2
+ import type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode } from "react";
3
3
  import { ChevronLeft, ChevronRight } from "lucide-react";
4
4
  import { cn } from "../lib/cn";
5
5
  import { horizontalStep } from "../lib/direction";
6
6
  import { useAnnounce } from "../hooks/use-announce";
7
- import { parseIsoDate, sameYmd, toLocalIso } from "../lib/dates";
7
+ import {
8
+ addMonthsClamped,
9
+ localeWeekStart,
10
+ monthKey,
11
+ parseIsoDate,
12
+ sameYmd,
13
+ toLocalIso,
14
+ } from "../lib/dates";
8
15
  import { useKitLabels, useKitLocale, useKitWeekStart } from "../i18n/kit-labels";
9
16
 
10
17
  /**
@@ -80,8 +87,10 @@ const DAY_NAME_FORMAT: Intl.DateTimeFormatOptions = {
80
87
  * before this, a closed prop list dropped both (audit §api-design).
81
88
  */
82
89
  export interface MiniCalendarProps extends Omit<ComponentPropsWithoutRef<"div">, "onSelect"> {
83
- from: string;
84
- to: string;
90
+ /** The selection's start as "YYYY-MM-DD"; `""` (the default) for none. */
91
+ from?: string;
92
+ /** The selection's end; `""` (the default) for none. */
93
+ to?: string;
85
94
  /**
86
95
  * BCP 47 tag for the month caption, the weekday heads, the day numbers and the
87
96
  * spoken dates. Optional since the kit grew `<UiKitProvider locale>`: a calendar
@@ -122,47 +131,104 @@ export interface MiniCalendarProps extends Omit<ComponentPropsWithoutRef<"div">,
122
131
  * merely by existing.
123
132
  */
124
133
  focusOnOpen?: boolean;
134
+ /**
135
+ * `"sm"` (default) is the compact picker: 32px round days, the size the date
136
+ * pickers and the data-table filter draw. `"lg"` is a MONTH VIEW for a page — tall
137
+ * cells in a ruled grid, the day number in the top corner and {@link renderDay}'s
138
+ * content under it — the calendar a planner page hand-built before this existed.
139
+ * Both are the same grid underneath: the same roles, roving tabindex, keys, RTL
140
+ * mirroring and week start.
141
+ */
142
+ size?: "sm" | "lg";
143
+ /**
144
+ * Custom content for one day: events, an amount, a dot. Called for every day of the
145
+ * month on show; return `null` for none.
146
+ *
147
+ * ⚠️ **It renders INSIDE the day's button** — a dot under the number at `"sm"`, a
148
+ * column under it at `"lg"` — so it must not be interactive: no links, no buttons,
149
+ * no inputs. A control inside a control is invalid HTML, and a screen reader lands
150
+ * on the day and never reaches what is inside it. Events that are themselves
151
+ * clickable are shown here as plain text, and the DAY is what is clicked: pass
152
+ * `mode="single"` and let `onSelect` open a day panel (a drawer, a popover) that
153
+ * lists that day's events as real links.
154
+ *
155
+ * The day's accessible name stays the full date; this content is attached to it as
156
+ * its DESCRIPTION (`aria-describedby`), so "Monday, 14 September 2026" is read
157
+ * first and "Rent due · Plumber" after it. Anything decorative (a coloured dot)
158
+ * should be `aria-hidden` or carry an `sr-only` word for what it means.
159
+ */
160
+ renderDay?: (day: Date, state: MiniCalendarDayState) => ReactNode;
161
+ /**
162
+ * The month on show, as "YYYY-MM". FOLLOWED rather than controlled, exactly like
163
+ * `from`/`to`: when it changes the grid moves there, and in between the user pages
164
+ * freely — every move to another month is reported through {@link onMonthChange}.
165
+ * For a month page whose own header (a month picker, a Today button) drives the
166
+ * calendar. Left out, the grid opens on the selection or today.
167
+ */
168
+ month?: string;
169
+ /** Called with "YYYY-MM" whenever the USER moves the grid to another month (the
170
+ * arrows, PageUp/PageDown, arrowing off the edge). */
171
+ onMonthChange?: (month: string) => void;
172
+ /**
173
+ * Hide the built-in previous/next arrows and month caption, for a page that draws
174
+ * its own. The caption is kept for assistive technology (visually hidden), because
175
+ * it is what names the grid.
176
+ */
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;
125
190
  /** Extra classes for the calendar's root. */
126
191
  className?: string;
127
192
  }
128
193
 
129
- export type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6;
130
-
131
- /**
132
- * The first day of the week in `locale`, as a `Date#getDay` index.
133
- *
134
- * `Intl.Locale#getWeekInfo()` is the standard spelling; V8 shipped it first as the
135
- * `weekInfo` accessor, and Firefox has neither yet — hence both reads and a Monday
136
- * fallback, which is ISO 8601 and what this grid did unconditionally before. Its
137
- * `firstDay` counts 1 = Monday … 7 = Sunday, so `% 7` maps it onto `getDay`.
138
- */
139
- function localeWeekStart(locale: string | undefined): WeekDay {
140
- try {
141
- const tag = locale ?? new Intl.DateTimeFormat().resolvedOptions().locale;
142
- const loc = new Intl.Locale(tag) as Intl.Locale & {
143
- getWeekInfo?: () => { firstDay: number };
144
- weekInfo?: { firstDay: number };
145
- };
146
- const firstDay = (loc.getWeekInfo?.() ?? loc.weekInfo)?.firstDay;
147
- if (typeof firstDay === "number") return (firstDay % 7) as WeekDay;
148
- } catch {
149
- // A malformed tag throws from `Intl.Locale`; the calendar still has to render.
150
- }
151
- return 1;
194
+ /** What {@link MiniCalendarProps.renderDay} is told about the day it draws. */
195
+ export interface MiniCalendarDayState {
196
+ /** The day as "YYYY-MM-DD". */
197
+ iso: string;
198
+ /** The day number as drawn, in the calendar's locale. */
199
+ label: string;
200
+ today: boolean;
201
+ /** Outside `min`/`max`. */
202
+ disabled: boolean;
203
+ /** Part of the selection — an end or a day between. */
204
+ selected: boolean;
205
+ rangeStart: boolean;
206
+ rangeEnd: boolean;
207
+ /** Strictly between the two ends of a range. */
208
+ inRange: boolean;
209
+ /** The day that holds the tab stop. */
210
+ active: boolean;
211
+ size: "sm" | "lg";
152
212
  }
153
213
 
214
+ export type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6;
215
+
154
216
  /** Where `d` sits in a week that starts on `weekStart` (the first column = 0). */
155
217
  const weekdayIndex = (d: Date, weekStart: WeekDay) => (d.getDay() - weekStart + 7) % 7;
156
218
 
157
- /** Move `d` by whole months, keeping the day of the month where the target has one.
158
- * `setMonth` alone ROLLS OVER — 31 January + 1 month is 3 March — which would skip
159
- * February entirely for anyone paging through the year with PageDown. */
160
- function stepMonths(d: Date, months: number): Date {
161
- const day = d.getDate();
162
- const out = new Date(d.getFullYear(), d.getMonth() + months, 1);
163
- const lastOfTarget = new Date(out.getFullYear(), out.getMonth() + 1, 0).getDate();
164
- out.setDate(Math.min(day, lastOfTarget));
165
- return out;
219
+ /** Move `d` by whole months without rolling over (see {@link addMonthsClamped}). */
220
+ const stepMonths = addMonthsClamped;
221
+
222
+ /** A month-view cell: tall enough for two or three event lines, shorter on a phone. */
223
+ const LG_CELL_HEIGHT = "min-h-16 md:min-h-24";
224
+
225
+ /** The 1st of a "YYYY-MM" month, or null when absent or malformed. */
226
+ const monthStart = (key: string | undefined) => (key ? parseIsoDate(`${key}-01`) : null);
227
+
228
+ /** `day`'s day of the month in `first`'s month, clamped to that month's length. */
229
+ function sameDayIn(first: Date, day: Date): Date {
230
+ const last = new Date(first.getFullYear(), first.getMonth() + 1, 0).getDate();
231
+ return new Date(first.getFullYear(), first.getMonth(), Math.min(day.getDate(), last));
166
232
  }
167
233
 
168
234
  /**
@@ -187,8 +253,8 @@ function stepMonths(d: Date, months: number): Date {
187
253
  * readable, and the click handler is what refuses them.
188
254
  */
189
255
  export function MiniCalendar({
190
- from,
191
- to,
256
+ from = "",
257
+ to = "",
192
258
  locale: localeProp,
193
259
  weekStartsOn,
194
260
  onSelect,
@@ -197,9 +263,16 @@ export function MiniCalendar({
197
263
  max,
198
264
  labels: labelsProp,
199
265
  focusOnOpen,
266
+ size = "sm",
267
+ renderDay,
268
+ month,
269
+ onMonthChange,
270
+ hideNavigation,
271
+ showOutsideDays = false,
200
272
  className,
201
273
  ...rest
202
274
  }: MiniCalendarProps) {
275
+ const lg = size === "lg";
203
276
  const labels = useKitLabels("miniCalendar", DEFAULT_MINI_CALENDAR_LABELS, labelsProp);
204
277
  const locale = useKitLocale(localeProp);
205
278
  const providerWeekStart = useKitWeekStart();
@@ -232,7 +305,11 @@ export function MiniCalendar({
232
305
  * so a bounded calendar opens on a day it will actually let you pick.
233
306
  */
234
307
  const [activeIso, setActiveIso] = useState(() => {
235
- const start = fromDate ?? toDate ?? new Date();
308
+ let start = fromDate ?? toDate ?? new Date();
309
+ // A `month` wins over the selection's month: the page asked for THAT month. The
310
+ // selection (or today) is kept when it falls inside it, else the 1st.
311
+ const first = monthStart(month);
312
+ if (first && monthKey(start) !== month) start = first;
236
313
  if (minDate && start < minDate) return toLocalIso(minDate);
237
314
  if (maxDate && start > maxDate) return toLocalIso(maxDate);
238
315
  return toLocalIso(start);
@@ -253,17 +330,37 @@ export function MiniCalendar({
253
330
  if (next && toLocalIso(next) !== activeIso) setActiveIso(toLocalIso(next));
254
331
  }
255
332
 
333
+ // …and the caller's month, the same way. Only a CHANGE moves the grid, so paging
334
+ // with the arrows is not snapped back by a parent that ignores `onMonthChange`.
335
+ const [lastMonth, setLastMonth] = useState(month);
336
+ if (lastMonth !== month) {
337
+ setLastMonth(month);
338
+ const first = monthStart(month);
339
+ const current = parseIsoDate(activeIso) ?? new Date();
340
+ if (first && monthKey(current) !== month) {
341
+ // Keep the day of the month, clamped: the 31st in a 30-day month is the 30th.
342
+ setActiveIso(toLocalIso(sameDayIn(first, current)));
343
+ }
344
+ }
345
+
256
346
  const activeDate = parseIsoDate(activeIso) ?? new Date();
257
347
  const view = { year: activeDate.getFullYear(), month: activeDate.getMonth() };
258
348
 
259
349
  const firstOfMonth = new Date(view.year, view.month, 1);
260
350
  const lastOfMonth = new Date(view.year, view.month + 1, 0);
261
- // 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.
262
355
  const startWeekday = weekdayIndex(firstOfMonth, weekStart);
263
356
  const cells: (Date | null)[] = [];
264
- 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
+ }
265
360
  for (let d = 1; d <= lastOfMonth.getDate(); d++) cells.push(new Date(view.year, view.month, d));
266
- 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
+ }
267
364
  const weeks: (Date | null)[][] = [];
268
365
  for (let i = 0; i < cells.length; i += 7) weeks.push(cells.slice(i, i + 7));
269
366
 
@@ -281,6 +378,7 @@ export function MiniCalendar({
281
378
  d.setDate(ref.getDate() + i);
282
379
  return {
283
380
  narrow: d.toLocaleDateString(locale, { weekday: "narrow" }),
381
+ short: d.toLocaleDateString(locale, { weekday: "short" }),
284
382
  long: d.toLocaleDateString(locale, { weekday: "long" }),
285
383
  };
286
384
  });
@@ -312,6 +410,8 @@ export function MiniCalendar({
312
410
  if (iso === activeIso) return;
313
411
  if (focus) pendingFocus.current = iso;
314
412
  setActiveIso(iso);
413
+ const key = monthKey(d);
414
+ if (key !== monthKey(activeDate)) onMonthChange?.(key);
315
415
  };
316
416
 
317
417
  /**
@@ -414,7 +514,13 @@ export function MiniCalendar({
414
514
  // `className` does everywhere else in the kit; `...rest` first, as in the rest of
415
515
  // this wave, so nothing from outside can take the grid's own wiring away.
416
516
  <div {...rest} className={cn("select-none", className)}>
417
- <div className="flex items-center justify-between pb-1">
517
+ {hideNavigation ? (
518
+ // Still in the tree: the caption is what names the grid.
519
+ <div id={monthId} className="sr-only-fixed">
520
+ {monthLabel}
521
+ </div>
522
+ ) : (
523
+ <div className={cn("flex items-center justify-between", lg ? "pb-2" : "pb-1")}>
418
524
  <button
419
525
  type="button"
420
526
  onClick={() => moveMonth(-1)}
@@ -431,7 +537,12 @@ export function MiniCalendar({
431
537
  <div
432
538
  id={monthId}
433
539
  aria-live="polite"
434
- className="text-xs font-medium capitalize text-[var(--text-secondary)]"
540
+ className={cn(
541
+ "capitalize",
542
+ lg
543
+ ? "text-base font-semibold text-[var(--text-primary)]"
544
+ : "text-xs font-medium text-[var(--text-secondary)]",
545
+ )}
435
546
  >
436
547
  {monthLabel}
437
548
  </div>
@@ -444,44 +555,136 @@ export function MiniCalendar({
444
555
  <ChevronRight className="size-4 rtl:-scale-x-100" aria-hidden />
445
556
  </button>
446
557
  </div>
558
+ )}
447
559
  <div
448
560
  ref={gridRef}
449
561
  role="grid"
450
562
  aria-labelledby={monthId}
451
563
  aria-describedby={mode === "range" ? hintId : undefined}
452
- className="grid gap-y-0.5"
564
+ className={cn(
565
+ "grid",
566
+ // The month view is RULED: a 1px gap over a border-coloured ground draws the
567
+ // lines between the cells, the way a wall calendar does.
568
+ lg
569
+ ? "gap-px overflow-hidden rounded-lg border border-[var(--border)] bg-[var(--border)]"
570
+ : "gap-y-0.5",
571
+ )}
453
572
  >
454
573
  <div
455
574
  role="row"
456
- className="grid grid-cols-7 text-center text-[10px] font-medium uppercase text-[var(--text-placeholder)]"
575
+ className={cn(
576
+ "grid grid-cols-7 text-center font-medium",
577
+ lg
578
+ ? "gap-px text-xs text-[var(--text-muted)]"
579
+ : "text-[10px] uppercase text-[var(--text-placeholder)]",
580
+ )}
457
581
  >
458
582
  {weekdayLabels.map((w, i) => (
459
- <div key={i} role="columnheader" aria-label={w.long} className="py-0.5">
460
- {w.narrow}
583
+ <div
584
+ key={i}
585
+ role="columnheader"
586
+ aria-label={w.long}
587
+ className={lg ? "bg-[var(--bg-surface)] py-2" : "py-0.5"}
588
+ >
589
+ {lg ? w.short : w.narrow}
461
590
  </div>
462
591
  ))}
463
592
  </div>
464
593
  {weeks.map((week, wi) => (
465
594
  // No column gap: the days of a range join into ONE band. With a gap each day
466
595
  // was its own tinted box, and a selected fortnight read as fourteen chips.
467
- <div key={wi} role="row" className="grid grid-cols-7">
596
+ <div key={wi} role="row" className={cn("grid grid-cols-7", lg && "gap-px")}>
468
597
  {week.map((d, di) => {
469
598
  // The padding cells still have to BE cells, or the rows are ragged and
470
599
  // a screen reader's "column 3" stops meaning Wednesday.
471
- if (!d) return <div key={di} role="gridcell" className="h-8" />;
600
+ if (!d) {
601
+ return (
602
+ <div
603
+ key={di}
604
+ role="gridcell"
605
+ className={lg ? cn(LG_CELL_HEIGHT, "bg-[var(--bg-surface-2)]") : "h-8"}
606
+ />
607
+ );
608
+ }
472
609
  const iso = toLocalIso(d);
473
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
+ }
474
660
  const isStart = !disabled && fromDate && sameYmd(d, fromDate);
475
661
  const isEnd = !disabled && toDate && sameYmd(d, toDate);
476
662
  const isToday = sameYmd(d, today);
477
663
  const isInRange = !disabled && inRange(d) && !isStart && !isEnd;
478
664
  const isActive = iso === activeIso;
665
+ const isSelected = Boolean(isStart || isEnd || isInRange);
666
+ const label = dayNumber.format(d.getDate());
667
+ const content = renderDay?.(d, {
668
+ iso,
669
+ label,
670
+ today: isToday,
671
+ disabled,
672
+ selected: isSelected,
673
+ rangeStart: Boolean(isStart),
674
+ rangeEnd: Boolean(isEnd),
675
+ inRange: isInRange,
676
+ active: isActive,
677
+ size,
678
+ });
679
+ const hasContent = content != null && content !== false && content !== "";
680
+ const contentId = `${id}-day-${iso}`;
479
681
  // The band behind the day: full width between the ends, half width at an
480
682
  // end (so the round endpoint sits ON the band's tip), rounded where a row
481
683
  // or the month breaks it. Only when both ends exist and differ — a lone
482
- // start is a dot, not a band of one.
684
+ // start is a dot, not a band of one. The month view tints whole cells
685
+ // instead, so it has no band.
483
686
  const banded =
484
- hasBand && !disabled && (isStart || isEnd || isInRange);
687
+ !lg && hasBand && !disabled && (isStart || isEnd || isInRange);
485
688
  const lastOfMonth = d.getDate() === daysInMonth(d);
486
689
  const band = banded
487
690
  ? cn(
@@ -494,7 +697,14 @@ export function MiniCalendar({
494
697
  )
495
698
  : null;
496
699
  return (
497
- <div key={di} role="none" className="relative flex h-8 items-center justify-center">
700
+ <div
701
+ key={di}
702
+ role="none"
703
+ className={cn(
704
+ "relative flex",
705
+ lg ? "min-w-0" : "h-8 items-center justify-center",
706
+ )}
707
+ >
498
708
  {band && <span aria-hidden className={band} />}
499
709
  <button
500
710
  key={di}
@@ -506,12 +716,28 @@ export function MiniCalendar({
506
716
  // control, which is the same reason the pattern exists.
507
717
  tabIndex={isActive ? 0 : -1}
508
718
  aria-label={labels.day(formatDay(d))}
509
- aria-selected={Boolean(isStart || isEnd || isInRange)}
719
+ // The custom content is the day's DESCRIPTION: `aria-label` replaces
720
+ // the button's text as its name, so without this link a screen
721
+ // reader would hear the date and never the events drawn under it.
722
+ aria-describedby={hasContent ? contentId : undefined}
723
+ aria-selected={isSelected}
510
724
  aria-disabled={disabled || undefined}
511
725
  aria-current={isToday ? "date" : undefined}
512
726
  onClick={() => handleClick(d)}
513
727
  onKeyDown={onDayKeyDown}
514
- className={cn(
728
+ className={
729
+ lg
730
+ ? cn(
731
+ "flex w-full min-w-0 flex-col items-stretch gap-1 overflow-hidden p-1.5 text-start text-xs transition-colors",
732
+ LG_CELL_HEIGHT,
733
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]",
734
+ disabled
735
+ ? "cursor-not-allowed bg-[var(--bg-surface)] text-[var(--text-placeholder)]"
736
+ : isSelected
737
+ ? "bg-[var(--brand-bg)] text-[var(--text-primary)] hover:bg-[var(--brand-bg-hover)]"
738
+ : "bg-[var(--bg-surface)] text-[var(--text-primary)] hover:bg-[var(--bg-hover)]",
739
+ )
740
+ : cn(
515
741
  "relative size-8 rounded-full text-xs tabular-nums transition-colors",
516
742
  // A grid you can walk with the keyboard needs to show where the
517
743
  // keyboard IS. Inset because the cells sit half a pixel apart and
@@ -537,9 +763,43 @@ export function MiniCalendar({
537
763
  ? "font-medium text-[var(--brand-muted)] hover:bg-[var(--brand-bg-hover)]"
538
764
  : "text-[var(--text-primary)] hover:bg-[var(--bg-hover)]",
539
765
  isToday && !isStart && !isEnd && !isInRange && !disabled && "ring-1 ring-inset ring-[var(--border-strong)]",
540
- )}
766
+ )
767
+ }
541
768
  >
542
- {dayNumber.format(d.getDate())}
769
+ {lg ? (
770
+ <>
771
+ <span
772
+ className={cn(
773
+ "inline-flex size-6 shrink-0 items-center justify-center self-start rounded-full tabular-nums",
774
+ isStart || isEnd
775
+ ? "bg-[var(--brand)] font-semibold text-[var(--brand-contrast)]"
776
+ : isToday && !disabled
777
+ ? "font-semibold text-[var(--brand)] ring-1 ring-inset ring-[var(--brand)]"
778
+ : isInRange && "font-medium text-[var(--brand-muted)]",
779
+ )}
780
+ >
781
+ {label}
782
+ </span>
783
+ {hasContent && (
784
+ <span id={contentId} className="flex min-w-0 flex-col gap-0.5">
785
+ {content}
786
+ </span>
787
+ )}
788
+ </>
789
+ ) : (
790
+ <>
791
+ {label}
792
+ {/* Under the number, inside the circle: room for a dot or two. */}
793
+ {hasContent && (
794
+ <span
795
+ id={contentId}
796
+ className="pointer-events-none absolute inset-x-0 bottom-0.5 flex justify-center gap-px leading-none"
797
+ >
798
+ {content}
799
+ </span>
800
+ )}
801
+ </>
802
+ )}
543
803
  </button>
544
804
  </div>
545
805
  );