@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
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/list.tsx"],"sourcesContent":["import { createContext, forwardRef, useContext } from \"react\";\nimport type { ComponentPropsWithoutRef, MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { ExternalLink } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { Spinner } from \"./ui\";\nimport { StatusDot } from \"./status-dot\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\n\n/* ── Labels ───────────────────────────────────────────────────────────────── */\n\nexport interface ListLabels {\n /** Read after an unread row's title — the dot alone says nothing to a screen reader. */\n unread: string;\n /** Read after an external row's title: the row leaves the app in a new tab. */\n opensInNewTab: string;\n}\n\nexport const DEFAULT_LIST_LABELS: ListLabels = {\n unread: \"Unread\",\n opensInNewTab: \"opens in a new tab\",\n};\n\n/* ── List ─────────────────────────────────────────────────────────────────── */\n\n/**\n * `compact` 36px-ish rows for a dense panel (an inbox in a menu), `default` the ~44px\n * row every site draws with `py-2.5`, `comfortable` for a list that is the page.\n */\nexport type ListDensity = \"compact\" | \"default\" | \"comfortable\";\n/** `divider`: a rule between rows (kastlan's `divide-y` lists); `gap`: a small space\n * between rows (lenkbank's `space-y-2`, kastlan's `space-y-1`); `none`: flush. */\nexport type ListSeparator = \"divider\" | \"gap\" | \"none\";\n\nconst ListContext = createContext<{ density: ListDensity; separator: ListSeparator }>({\n density: \"default\",\n separator: \"gap\",\n});\n\nexport interface ListProps extends Omit<ComponentPropsWithoutRef<\"ul\">, \"role\"> {\n /** `ol` when the order means something (a ranked list, numbered segments). */\n as?: \"ul\" | \"ol\";\n /** Every row's density, unless a row says otherwise. Default `default`. */\n density?: ListDensity;\n /** Default `gap`. */\n separator?: ListSeparator;\n}\n\n/**\n * The list of {@link ListItem}s.\n *\n * A `<ul>` with an explicit `role=\"list\"`: Safari drops a list's semantics once its\n * bullets are styled away, and \"list, 7 items\" is exactly what a screen-reader user\n * wants to hear before the first row.\n */\nexport const List = forwardRef<HTMLUListElement, ListProps>(function List(\n { as = \"ul\", density = \"default\", separator = \"gap\", className, children, ...rest },\n ref,\n) {\n const Tag = as;\n return (\n <ListContext.Provider value={{ density, separator }}>\n <Tag\n {...rest}\n ref={ref as Ref<HTMLUListElement & HTMLOListElement>}\n role=\"list\"\n className={cn(\n \"m-0 flex list-none flex-col p-0\",\n separator === \"gap\" && (density === \"compact\" ? \"gap-0.5\" : \"gap-1\"),\n className,\n )}\n >\n {children}\n </Tag>\n </ListContext.Provider>\n );\n});\n\n/* ── ListItem ─────────────────────────────────────────────────────────────── */\n\n/** What {@link ListItemProps.renderLink} is handed. Spread it onto your router's link,\n * mapping `href` to what it calls it — `({ href, ...p }) => <Link to={href} {...p} />`. */\nexport interface ListItemLinkProps {\n href: string;\n /** The row's target look — keep it, or the row's padding and focus ring go. */\n className: string;\n children: ReactNode;\n ref?: Ref<HTMLAnchorElement>;\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n onAuxClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n \"aria-current\"?: \"true\";\n \"aria-busy\"?: true;\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\nconst PAD: Record<ListDensity, { target: string; actions: string; gap: string }> = {\n compact: { target: \"gap-2 px-2 py-1.5\", actions: \"pe-1.5\", gap: \"gap-0.5\" },\n default: { target: \"gap-3 px-3 py-2.5\", actions: \"pe-2\", gap: \"gap-1\" },\n comfortable: { target: \"gap-3 px-4 py-3\", actions: \"pe-3\", gap: \"gap-1\" },\n};\n\nconst TARGET_RING =\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]\";\n\ninterface ListItemBaseProps {\n /** The row's name: one line, truncated. */\n title: ReactNode;\n /** The line under it: one line, truncated — or two, with `subtitleLines={2}`. */\n subtitle?: ReactNode;\n /** `2` for a line that is a body rather than a label (keksdose's notification text). */\n subtitleLines?: 1 | 2;\n /** A third line in the caption type: a timestamp, \"Updated …\" (lenkbank's projects). */\n meta?: ReactNode;\n /** A Lucide icon before the text, muted, at 16px. */\n icon?: LucideIcon;\n /** Anything else before the text — an avatar, a file-type badge, a drag handle. */\n leading?: ReactNode;\n /**\n * Inside the main target, at its end: a status chip, a file size, a count. It is part\n * of what the row's click does and part of its accessible name — so never a control.\n * Controls go in {@link actions}.\n */\n trailing?: ReactNode;\n /**\n * Controls BESIDE the main target, never inside it: copy, delete, \"Open\".\n *\n * A button inside a button (or a link) is invalid HTML, and a screen reader folds the\n * inner one into the outer one's name where it cannot be reached. lenkbank found this\n * three times (profile-bar.tsx:110, projects-page.tsx:61, segment-list.tsx:165) and\n * solved it the same way each time: the row's target and its actions are siblings in\n * one bordered box. That is this slot — and being siblings is also what stops a click\n * on \"Copy\" from selecting the row.\n */\n actions?: ReactNode;\n /**\n * The unread mark: a brand dot at the row's end, the title in semibold, and \"Unread\"\n * read after the title (keksdose notification-inbox.tsx:215). For another status,\n * pass your own mark in {@link status}.\n */\n unread?: boolean;\n /** A mark in the unread dot's place — a `StatusDot` of another tone, a count. */\n status?: ReactNode;\n /**\n * The row is the chosen one — a brand border on the raised surface (lenkbank's\n * profile bar and segment list). `aria-current=\"true\"` on the target: the row is the\n * current one of a set, which is what those lists mean, not a pressed toggle.\n */\n selected?: boolean;\n disabled?: boolean;\n /**\n * Work under way on the row — keksdose's attachment download (support-attachments\n * .tsx:91). A spinner takes the trailing edge, the target is `aria-busy`, and a\n * second click is ignored. The row keeps focus: `aria-disabled`, not `disabled`, so\n * a keyboard user is not dropped back to the top of the page mid-download.\n */\n loading?: boolean;\n /** Overrides the {@link List}'s density for this row. */\n density?: ListDensity;\n /** `start` for rows whose text runs to two or three lines, so the icon sits by the\n * title rather than the middle of the paragraph. Default `center`. */\n align?: \"center\" | \"start\";\n /** Classes on the visible row (the bordered box). */\n className?: string;\n /** Classes on the main target (the button, link or static block). */\n targetClassName?: string;\n /** `div` for a single row outside a {@link List}; an `<li>` must sit in a list. */\n as?: \"li\" | \"div\";\n /** Reaches the main target — the element with the row's name and action. */\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\n/**\n * The row is ONE thing to activate, so the types allow one: a button, an in-app link,\n * an external link, or nothing (a static row, like kastlan's passkey and user lists).\n */\nexport type ListItemProps = ListItemBaseProps &\n (\n | {\n /** Makes the row a button — select, open, mark read. */\n onClick: (event: MouseEvent<HTMLButtonElement>) => void;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n }\n | {\n /**\n * Makes the row a link. A real `<a href>`, so a middle click opens it in a\n * background tab (keksdose feedback #451) — which a button calling `navigate`\n * cannot do.\n */\n href: string;\n /** Your router's link for an in-app `href`. Default `<a>`. The API `Chip`,\n * `StatTile` and `MenuItem` share. Ignored for an `external` row. */\n renderLink?: (props: ListItemLinkProps) => ReactElement;\n /**\n * The link leaves the app: a plain `<a target=\"_blank\" rel=\"noopener\n * noreferrer\">` with an external-link mark, and \"opens in a new tab\" read\n * after the title. keksdose's inbox rows whose URL is absolute.\n */\n external?: boolean;\n /** Runs on a click, before the navigation (mark the row read). */\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n /** Runs on a middle click, which fires no `click` — keksdose marks a\n * notification read here too, so a row opened into a background tab does not\n * stay unread. */\n onAuxClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n }\n | {\n onClick?: never;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n }\n );\n\n/**\n * One row of a {@link List}: leading icon or avatar, title and subtitle (both\n * truncating), a trailing slot, and — beside the row, not inside it — its actions.\n *\n * All three apps draw this row by hand, a dozen times: kastlan's units\n * (building-detail-page.tsx:402), open tickets (group-overview-page.tsx:160), activity\n * feed (activity-feed.tsx:53), company users (platform-company-detail-page.tsx:140)\n * and passkeys (passkeys-card.tsx:75); keksdose's notification inbox and attachment\n * downloads; lenkbank's profiles, projects and segments. The copies differ in the\n * details that are easy to get wrong — which of them truncate, whether the actions\n * are nested in the button, whether a link row is a link at all.\n */\nexport const ListItem = forwardRef<HTMLElement, ListItemProps>(function ListItem(\n {\n title,\n subtitle,\n subtitleLines = 1,\n meta,\n icon: Icon,\n leading,\n trailing,\n actions,\n unread = false,\n status,\n selected = false,\n disabled = false,\n loading = false,\n density: densityProp,\n align = \"center\",\n className,\n targetClassName,\n as = \"li\",\n href,\n renderLink,\n external = false,\n onClick,\n onAuxClick,\n ...rest\n },\n ref,\n) {\n const list = useContext(ListContext);\n const labels = useKitLabels(\"list\", DEFAULT_LIST_LABELS);\n const density = densityProp ?? list.density;\n const pad = PAD[density];\n const isLink = href !== undefined && !disabled;\n const isButton = !isLink && onClick !== undefined && href === undefined;\n const interactive = isLink || isButton;\n const inert = disabled || loading;\n\n const mark =\n status ??\n (unread ? <StatusDot tone=\"brand\" size=\"sm\" className={align === \"start\" ? \"mt-1.5\" : undefined} /> : null);\n\n const body = (\n <>\n {Icon && (\n <Icon\n className={cn(\"size-4 shrink-0 text-[var(--text-muted)]\", align === \"start\" && \"mt-0.5\")}\n aria-hidden\n />\n )}\n {leading}\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span\n className={cn(\n \"block truncate text-sm text-[var(--text-primary)]\",\n unread ? \"font-semibold\" : \"font-medium\",\n )}\n >\n {title}\n {unread && <span className=\"sr-only\"> ({labels.unread})</span>}\n {external && href !== undefined && <span className=\"sr-only\"> ({labels.opensInNewTab})</span>}\n </span>\n {subtitle != null && (\n <span\n className={cn(\n \"block text-xs text-[var(--text-muted)]\",\n subtitleLines === 2 ? \"line-clamp-2\" : \"truncate\",\n )}\n >\n {subtitle}\n </span>\n )}\n {meta != null && <span className=\"mt-0.5 block truncate text-[11px] leading-snug text-[var(--text-muted)]\">{meta}</span>}\n </span>\n {trailing != null && <span className=\"flex shrink-0 items-center gap-2 text-xs text-[var(--text-muted)]\">{trailing}</span>}\n {loading ? (\n <Spinner label={null} className=\"size-3.5 shrink-0\" />\n ) : (\n external && href !== undefined && (\n <ExternalLink className=\"size-3.5 shrink-0 text-[var(--text-muted)] rtl:-scale-x-100\" aria-hidden />\n )\n )}\n {mark}\n </>\n );\n\n const target = cn(\n \"flex min-w-0 flex-1 text-start\",\n align === \"start\" ? \"items-start\" : \"items-center\",\n pad.target,\n \"rounded-[inherit]\",\n interactive && TARGET_RING,\n interactive && !inert && \"cursor-pointer\",\n loading && \"cursor-progress\",\n targetClassName,\n );\n\n let main: ReactNode;\n if (isLink) {\n const linkProps: ListItemLinkProps = {\n ...rest,\n ref: ref as Ref<HTMLAnchorElement>,\n href,\n className: target,\n \"aria-current\": selected ? \"true\" : undefined,\n \"aria-busy\": loading || undefined,\n onClick: loading\n ? (e) => e.preventDefault()\n : (onClick as ListItemLinkProps[\"onClick\"]),\n onAuxClick,\n children: body,\n };\n const { children: linkBody, ...anchorProps } = linkProps;\n main = renderLink && !external ? (\n <RenderedLink render={renderLink} {...linkProps} />\n ) : (\n <a {...anchorProps} {...(external ? { target: \"_blank\", rel: \"noopener noreferrer\" } : null)}>\n {linkBody}\n </a>\n );\n } else if (isButton) {\n main = (\n <button\n {...rest}\n ref={ref as Ref<HTMLButtonElement>}\n type=\"button\"\n // A disabled row is `disabled`: nothing to do there, and nothing to reach. A\n // loading row is only `aria-disabled` — see `loading`.\n disabled={disabled}\n aria-disabled={loading || undefined}\n aria-busy={loading || undefined}\n aria-current={selected ? \"true\" : undefined}\n onClick={loading ? undefined : (onClick as (event: MouseEvent<HTMLButtonElement>) => void)}\n className={target}\n >\n {body}\n </button>\n );\n } else {\n main = (\n <div\n {...rest}\n ref={ref as Ref<HTMLDivElement>}\n aria-disabled={disabled || undefined}\n aria-busy={loading || undefined}\n className={target}\n >\n {body}\n </div>\n );\n }\n\n const Tag = as;\n return (\n <Tag\n className={cn(\n list.separator === \"divider\" && \"border-b border-[var(--border)] last:border-b-0\",\n list.separator === \"divider\" && as === \"li\" && \"py-0.5\",\n )}\n >\n <div\n className={cn(\n \"flex min-w-0 items-center rounded-md border transition-colors\",\n selected ? \"border-[var(--brand)] bg-[var(--bg-surface-2)]\" : \"border-transparent\",\n interactive && !inert && !selected && \"hover:bg-[var(--bg-hover)]\",\n disabled && \"opacity-50\",\n className,\n )}\n >\n {main}\n {actions != null && (\n <div className={cn(\"flex shrink-0 items-center\", pad.gap, pad.actions)}>{actions}</div>\n )}\n </div>\n </Tag>\n );\n});\n\nfunction RenderedLink({\n render,\n ...props\n}: ListItemLinkProps & { render: (props: ListItemLinkProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":";AA8DM,SAsNF,UAtNE,KAsOe,YAtOf;AA9DN,SAAS,eAAe,YAAY,kBAAkB;AAEtD,SAAS,oBAAoB;AAE7B,SAAS,UAAU;AACnB,SAAS,eAAe;AACxB,SAAS,iBAAiB;AAC1B,SAAS,oBAAoB;AAWtB,MAAM,sBAAkC;AAAA,EAC7C,QAAQ;AAAA,EACR,eAAe;AACjB;AAaA,MAAM,cAAc,cAAkE;AAAA,EACpF,SAAS;AAAA,EACT,WAAW;AACb,CAAC;AAkBM,MAAM,OAAO,WAAwC,SAASA,MACnE,EAAE,KAAK,MAAM,UAAU,WAAW,YAAY,OAAO,WAAW,UAAU,GAAG,KAAK,GAClF,KACA;AACA,QAAM,MAAM;AACZ,SACE,oBAAC,YAAY,UAAZ,EAAqB,OAAO,EAAE,SAAS,UAAU,GAChD;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ;AAAA,MACA,MAAK;AAAA,MACL,WAAW;AAAA,QACT;AAAA,QACA,cAAc,UAAU,YAAY,YAAY,YAAY;AAAA,QAC5D;AAAA,MACF;AAAA,MAEC;AAAA;AAAA,EACH,GACF;AAEJ,CAAC;AAqBD,MAAM,MAA6E;AAAA,EACjF,SAAS,EAAE,QAAQ,qBAAqB,SAAS,UAAU,KAAK,UAAU;AAAA,EAC1E,SAAS,EAAE,QAAQ,qBAAqB,SAAS,QAAQ,KAAK,QAAQ;AAAA,EACtE,aAAa,EAAE,QAAQ,mBAAmB,SAAS,QAAQ,KAAK,QAAQ;AAC1E;AAEA,MAAM,cACJ;AAiIK,MAAM,WAAW,WAAuC,SAASC,UACtE;AAAA,EACE;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB;AAAA,EACA,MAAM;AAAA,EACN;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA,WAAW;AAAA,EACX,WAAW;AAAA,EACX,UAAU;AAAA,EACV,SAAS;AAAA,EACT,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,KAAK;AAAA,EACL;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,GAAG;AACL,GACA,KACA;AACA,QAAM,OAAO,WAAW,WAAW;AACnC,QAAM,SAAS,aAAa,QAAQ,mBAAmB;AACvD,QAAM,UAAU,eAAe,KAAK;AACpC,QAAM,MAAM,IAAI,OAAO;AACvB,QAAM,SAAS,SAAS,UAAa,CAAC;AACtC,QAAM,WAAW,CAAC,UAAU,YAAY,UAAa,SAAS;AAC9D,QAAM,cAAc,UAAU;AAC9B,QAAM,QAAQ,YAAY;AAE1B,QAAM,OACJ,WACC,SAAS,oBAAC,aAAU,MAAK,SAAQ,MAAK,MAAK,WAAW,UAAU,UAAU,WAAW,QAAW,IAAK;AAExG,QAAM,OACJ,iCACG;AAAA,YACC;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,GAAG,4CAA4C,UAAU,WAAW,QAAQ;AAAA,QACvF,eAAW;AAAA;AAAA,IACb;AAAA,IAED;AAAA,IACD,qBAAC,UAAK,WAAU,gCACd;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,WAAW;AAAA,YACT;AAAA,YACA,SAAS,kBAAkB;AAAA,UAC7B;AAAA,UAEC;AAAA;AAAA,YACA,UAAU,qBAAC,UAAK,WAAU,WAAU;AAAA;AAAA,cAAG,OAAO;AAAA,cAAO;AAAA,eAAC;AAAA,YACtD,YAAY,SAAS,UAAa,qBAAC,UAAK,WAAU,WAAU;AAAA;AAAA,cAAG,OAAO;AAAA,cAAc;AAAA,eAAC;AAAA;AAAA;AAAA,MACxF;AAAA,MACC,YAAY,QACX;AAAA,QAAC;AAAA;AAAA,UACC,WAAW;AAAA,YACT;AAAA,YACA,kBAAkB,IAAI,iBAAiB;AAAA,UACzC;AAAA,UAEC;AAAA;AAAA,MACH;AAAA,MAED,QAAQ,QAAQ,oBAAC,UAAK,WAAU,2EAA2E,gBAAK;AAAA,OACnH;AAAA,IACC,YAAY,QAAQ,oBAAC,UAAK,WAAU,qEAAqE,oBAAS;AAAA,IAClH,UACC,oBAAC,WAAQ,OAAO,MAAM,WAAU,qBAAoB,IAEpD,YAAY,SAAS,UACnB,oBAAC,gBAAa,WAAU,+DAA8D,eAAW,MAAC;AAAA,IAGrG;AAAA,KACH;AAGF,QAAM,SAAS;AAAA,IACb;AAAA,IACA,UAAU,UAAU,gBAAgB;AAAA,IACpC,IAAI;AAAA,IACJ;AAAA,IACA,eAAe;AAAA,IACf,eAAe,CAAC,SAAS;AAAA,IACzB,WAAW;AAAA,IACX;AAAA,EACF;AAEA,MAAI;AACJ,MAAI,QAAQ;AACV,UAAM,YAA+B;AAAA,MACnC,GAAG;AAAA,MACH;AAAA,MACA;AAAA,MACA,WAAW;AAAA,MACX,gBAAgB,WAAW,SAAS;AAAA,MACpC,aAAa,WAAW;AAAA,MACxB,SAAS,UACL,CAAC,MAAM,EAAE,eAAe,IACvB;AAAA,MACL;AAAA,MACA,UAAU;AAAA,IACZ;AACA,UAAM,EAAE,UAAU,UAAU,GAAG,YAAY,IAAI;AAC/C,WAAO,cAAc,CAAC,WACpB,oBAAC,gBAAa,QAAQ,YAAa,GAAG,WAAW,IAEjD,oBAAC,OAAG,GAAG,aAAc,GAAI,WAAW,EAAE,QAAQ,UAAU,KAAK,sBAAsB,IAAI,MACpF,oBACH;AAAA,EAEJ,WAAW,UAAU;AACnB,WACE;AAAA,MAAC;AAAA;AAAA,QACE,GAAG;AAAA,QACJ;AAAA,QACA,MAAK;AAAA,QAGL;AAAA,QACA,iBAAe,WAAW;AAAA,QAC1B,aAAW,WAAW;AAAA,QACtB,gBAAc,WAAW,SAAS;AAAA,QAClC,SAAS,UAAU,SAAa;AAAA,QAChC,WAAW;AAAA,QAEV;AAAA;AAAA,IACH;AAAA,EAEJ,OAAO;AACL,WACE;AAAA,MAAC;AAAA;AAAA,QACE,GAAG;AAAA,QACJ;AAAA,QACA,iBAAe,YAAY;AAAA,QAC3B,aAAW,WAAW;AAAA,QACtB,WAAW;AAAA,QAEV;AAAA;AAAA,IACH;AAAA,EAEJ;AAEA,QAAM,MAAM;AACZ,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT,KAAK,cAAc,aAAa;AAAA,QAChC,KAAK,cAAc,aAAa,OAAO,QAAQ;AAAA,MACjD;AAAA,MAEA;AAAA,QAAC;AAAA;AAAA,UACC,WAAW;AAAA,YACT;AAAA,YACA,WAAW,mDAAmD;AAAA,YAC9D,eAAe,CAAC,SAAS,CAAC,YAAY;AAAA,YACtC,YAAY;AAAA,YACZ;AAAA,UACF;AAAA,UAEC;AAAA;AAAA,YACA,WAAW,QACV,oBAAC,SAAI,WAAW,GAAG,8BAA8B,IAAI,KAAK,IAAI,OAAO,GAAI,mBAAQ;AAAA;AAAA;AAAA,MAErF;AAAA;AAAA,EACF;AAEJ,CAAC;AAED,SAAS,aAAa;AAAA,EACpB;AAAA,EACA,GAAG;AACL,GAA+E;AAC7E,SAAO,OAAO,KAAK;AACrB;","names":["List","ListItem"]}
1
+ {"version":3,"sources":["../../src/components/list.tsx"],"sourcesContent":["import { createContext, forwardRef, useContext, useId } from \"react\";\nimport type { ComponentPropsWithoutRef, HTMLAttributes, MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { ExternalLink } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { Spinner } from \"./ui\";\nimport { StatusDot } from \"./status-dot\";\nimport { useKitLabels, useKitLink } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer } from \"./text-link\";\n\n/* ── Labels ───────────────────────────────────────────────────────────────── */\n\nexport interface ListLabels {\n /** Read after an unread row's title — the dot alone says nothing to a screen reader. */\n unread: string;\n /** Read after an external row's title: the row leaves the app in a new tab. */\n opensInNewTab: string;\n}\n\nexport const DEFAULT_LIST_LABELS: ListLabels = {\n unread: \"Unread\",\n opensInNewTab: \"opens in a new tab\",\n};\n\n/* ── List ─────────────────────────────────────────────────────────────────── */\n\n/**\n * `compact` 36px-ish rows for a dense panel (an inbox in a menu), `default` the ~44px\n * row every site draws with `py-2.5`, `comfortable` for a list that is the page.\n */\nexport type ListDensity = \"compact\" | \"default\" | \"comfortable\";\n/** `divider`: a rule between rows (kastlan's `divide-y` lists); `gap`: a small space\n * between rows (lenkbank's `space-y-2`, kastlan's `space-y-1`); `none`: flush. */\nexport type ListSeparator = \"divider\" | \"gap\" | \"none\";\n\nconst ListContext = createContext<{ density: ListDensity; separator: ListSeparator }>({\n density: \"default\",\n separator: \"gap\",\n});\n\nexport interface ListProps extends Omit<ComponentPropsWithoutRef<\"ul\">, \"role\"> {\n /** `ol` when the order means something (a ranked list, numbered segments). */\n as?: \"ul\" | \"ol\";\n /** Every row's density, unless a row says otherwise. Default `default`. */\n density?: ListDensity;\n /** Default `gap`. */\n separator?: ListSeparator;\n}\n\n/**\n * The list of {@link ListItem}s.\n *\n * A `<ul>` with an explicit `role=\"list\"`: Safari drops a list's semantics once its\n * bullets are styled away, and \"list, 7 items\" is exactly what a screen-reader user\n * wants to hear before the first row.\n */\nexport const List = forwardRef<HTMLUListElement, ListProps>(function List(\n { as = \"ul\", density = \"default\", separator = \"gap\", className, children, ...rest },\n ref,\n) {\n const Tag = as;\n return (\n <ListContext.Provider value={{ density, separator }}>\n <Tag\n {...rest}\n ref={ref as Ref<HTMLUListElement & HTMLOListElement>}\n role=\"list\"\n className={cn(\n \"m-0 flex list-none flex-col p-0\",\n separator === \"gap\" && (density === \"compact\" ? \"gap-0.5\" : \"gap-1\"),\n className,\n )}\n >\n {children}\n </Tag>\n </ListContext.Provider>\n );\n});\n\n/* ── ListItem ─────────────────────────────────────────────────────────────── */\n\n/** What {@link ListItemProps.renderLink} is handed. Spread it onto your router's link,\n * mapping `href` to what it calls it — `({ href, ...p }) => <Link to={href} {...p} />`. */\nexport interface ListItemLinkProps {\n href: string;\n /** The row's target look — keep it, or the row's padding and focus ring go. */\n className: string;\n children: ReactNode;\n ref?: Ref<HTMLAnchorElement>;\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n onAuxClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n \"aria-current\"?: \"true\";\n \"aria-busy\"?: true;\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\nconst PAD: Record<ListDensity, { target: string; actions: string; gap: string; body: string }> = {\n compact: { target: \"gap-2 px-2 py-1.5\", actions: \"pe-1.5\", gap: \"gap-0.5\", body: \"px-2 pb-1.5\" },\n default: { target: \"gap-3 px-3 py-2.5\", actions: \"pe-2\", gap: \"gap-1\", body: \"px-3 pb-2.5\" },\n comfortable: { target: \"gap-3 px-4 py-3\", actions: \"pe-3\", gap: \"gap-1\", body: \"px-4 pb-3\" },\n};\n\nconst TARGET_RING =\n \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]\";\n\ninterface ListItemBaseProps {\n /** The row's name: one line, truncated. */\n title: ReactNode;\n /** The line under it: one line, truncated — or two, with `subtitleLines={2}`. */\n subtitle?: ReactNode;\n /** `2` for a line that is a body rather than a label (keksdose's notification text). */\n subtitleLines?: 1 | 2;\n /** A third line in the caption type: a timestamp, \"Updated …\" (lenkbank's projects). */\n meta?: ReactNode;\n /** A Lucide icon before the text, muted, at 16px. */\n icon?: LucideIcon;\n /** Anything else before the text — an avatar, a file-type badge, a drag handle. */\n leading?: ReactNode;\n /**\n * Inside the main target, at its end: a status chip, a file size, a count. It is part\n * of what the row's click does and part of its accessible name — so never a control.\n * Controls go in {@link actions}.\n */\n trailing?: ReactNode;\n /**\n * Controls BESIDE the main target, never inside it: copy, delete, \"Open\".\n *\n * A button inside a button (or a link) is invalid HTML, and a screen reader folds the\n * inner one into the outer one's name where it cannot be reached. lenkbank found this\n * three times (profile-bar.tsx:110, projects-page.tsx:61, segment-list.tsx:165) and\n * solved it the same way each time: the row's target and its actions are siblings in\n * one bordered box. That is this slot — and being siblings is also what stops a click\n * on \"Copy\" from selecting the row.\n */\n actions?: ReactNode;\n /**\n * Rich content UNDER the title row and outside its target: photos, per-row buttons,\n * form fields — kastlan's handover room inspector (room-inspector.tsx), the handover\n * detail page and the wizard's defects step (wizard/defects-step.tsx), which draw\n * each room or defect as a `Card variant=\"inset\"` because a row could not hold a\n * photo strip with its own buttons.\n *\n * The same rule as {@link actions}, one row down: the title area stays ONE named\n * target (the button or link, with the title and subtitle as its name), and the body\n * is its sibling, never nested in it — a thumbnail's \"Remove\" inside the row's button\n * would be invalid HTML and unreachable by a screen reader. The body is a\n * `role=\"group\"` named by the row's title (`aria-labelledby`), so tabbing into the\n * third photo's button of the second room is heard as \"Kitchen, group\" first rather\n * than as a button floating free of any row. Inside the row's box, so a `selected`\n * border and a `divider` hold both; the hover fill stays on the target alone, since\n * the body is not what a click on the row does.\n */\n children?: ReactNode;\n /** Classes on the {@link children} body. */\n bodyClassName?: string;\n /**\n * The unread mark: a brand dot at the row's end, the title in semibold, and \"Unread\"\n * read after the title (keksdose notification-inbox.tsx:215). For another status,\n * pass your own mark in {@link status}.\n */\n unread?: boolean;\n /** A mark in the unread dot's place — a `StatusDot` of another tone, a count. */\n status?: ReactNode;\n /**\n * The row is the chosen one — a brand border on the raised surface (lenkbank's\n * profile bar and segment list). `aria-current=\"true\"` on the target: the row is the\n * current one of a set, which is what those lists mean, not a pressed toggle.\n */\n selected?: boolean;\n disabled?: boolean;\n /**\n * Work under way on the row — keksdose's attachment download (support-attachments\n * .tsx:91). A spinner takes the trailing edge, the target is `aria-busy`, and a\n * second click is ignored. The row keeps focus: `aria-disabled`, not `disabled`, so\n * a keyboard user is not dropped back to the top of the page mid-download.\n */\n loading?: boolean;\n /** Overrides the {@link List}'s density for this row. */\n density?: ListDensity;\n /** `start` for rows whose text runs to two or three lines, so the icon sits by the\n * title rather than the middle of the paragraph. Default `center`. */\n align?: \"center\" | \"start\";\n /** Classes on the visible row (the bordered box). */\n className?: string;\n /** Classes on the main target (the button, link or static block). */\n targetClassName?: string;\n /**\n * Extra attributes and handlers for the main target — e.g. HTML5 drag-and-drop\n * (`draggable`, `onDragStart`, `onDragEnd`): lenkbank's segment list drags the WHOLE\n * 44px row, and a grip inside the button cannot start a drag in Firefox. Spread first,\n * so the row's own role, name, state and click handling always win.\n */\n targetProps?: HTMLAttributes<HTMLElement>;\n /**\n * A visible border while unselected, instead of the transparent one. For rows laid out\n * as a wrapping strip (lenkbank's profile bar), where borderless rows read as loose\n * text. The selected row's brand border is unchanged.\n */\n bordered?: boolean;\n /** `div` for a single row outside a {@link List}; an `<li>` must sit in a list. */\n as?: \"li\" | \"div\";\n /** Reaches the main target — the element with the row's name and action. */\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\n/**\n * The row is ONE thing to activate, so the types allow one: a button, an in-app link,\n * an external link, or nothing (a static row, like kastlan's passkey and user lists).\n */\nexport type ListItemProps = ListItemBaseProps &\n (\n | {\n /** Makes the row a button — select, open, mark read. */\n onClick: (event: MouseEvent<HTMLButtonElement>) => void;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n }\n | {\n /**\n * Makes the row a link. A real `<a href>`, so a middle click opens it in a\n * background tab (keksdose feedback #451) — which a button calling `navigate`\n * cannot do.\n */\n href: string;\n /** Your router's link for an in-app `href`. Default: the `<UiKitProvider\n * linkComponent>`, then `<a>`. The API `Chip`, `StatTile` and `MenuItem`\n * share. Ignored for an `external` row. */\n renderLink?: (props: ListItemLinkProps) => ReactElement;\n /**\n * The link leaves the app: a plain `<a target=\"_blank\" rel=\"noopener\n * noreferrer\">` with an external-link mark, and \"opens in a new tab\" read\n * after the title. keksdose's inbox rows whose URL is absolute.\n */\n external?: boolean;\n /** Runs on a click, before the navigation (mark the row read). */\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n /** Runs on a middle click, which fires no `click` — keksdose marks a\n * notification read here too, so a row opened into a background tab does not\n * stay unread. */\n onAuxClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n }\n | {\n onClick?: never;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n }\n );\n\n/**\n * One row of a {@link List}: leading icon or avatar, title and subtitle (both\n * truncating), a trailing slot, and — beside the row, not inside it — its actions.\n *\n * All three apps draw this row by hand, a dozen times: kastlan's units\n * (building-detail-page.tsx:402), open tickets (group-overview-page.tsx:160), activity\n * feed (activity-feed.tsx:53), company users (platform-company-detail-page.tsx:140)\n * and passkeys (passkeys-card.tsx:75); keksdose's notification inbox and attachment\n * downloads; lenkbank's profiles, projects and segments. The copies differ in the\n * details that are easy to get wrong — which of them truncate, whether the actions\n * are nested in the button, whether a link row is a link at all.\n */\nexport const ListItem = forwardRef<HTMLElement, ListItemProps>(function ListItem(\n {\n title,\n subtitle,\n subtitleLines = 1,\n meta,\n icon: Icon,\n leading,\n trailing,\n actions,\n unread = false,\n status,\n selected = false,\n disabled = false,\n loading = false,\n density: densityProp,\n align = \"center\",\n className,\n targetClassName,\n targetProps,\n bordered = false,\n as = \"li\",\n children,\n bodyClassName,\n href,\n renderLink,\n external = false,\n onClick,\n onAuxClick,\n ...rest\n },\n ref,\n) {\n const list = useContext(ListContext);\n const labels = useKitLabels(\"list\", DEFAULT_LIST_LABELS);\n const kitLink = useKitLink();\n const density = densityProp ?? list.density;\n const pad = PAD[density];\n const isLink = href !== undefined && !disabled;\n const isButton = !isLink && onClick !== undefined && href === undefined;\n const interactive = isLink || isButton;\n const inert = disabled || loading;\n const hasBody = children !== undefined && children !== null && children !== false;\n const titleId = useId();\n\n const mark =\n status ??\n (unread ? <StatusDot tone=\"brand\" size=\"sm\" className={align === \"start\" ? \"mt-1.5\" : undefined} /> : null);\n\n const body = (\n <>\n {Icon && (\n <Icon\n className={cn(\"size-4 shrink-0 text-[var(--text-muted)]\", align === \"start\" && \"mt-0.5\")}\n aria-hidden\n />\n )}\n {leading}\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span\n // Names the body's group; harmless without one.\n id={hasBody ? titleId : undefined}\n className={cn(\n \"block truncate text-sm text-[var(--text-primary)]\",\n unread ? \"font-semibold\" : \"font-medium\",\n )}\n >\n {title}\n {unread && <span className=\"sr-only\"> ({labels.unread})</span>}\n {external && href !== undefined && <span className=\"sr-only\"> ({labels.opensInNewTab})</span>}\n </span>\n {subtitle != null && (\n <span\n className={cn(\n \"block text-xs text-[var(--text-muted)]\",\n subtitleLines === 2 ? \"line-clamp-2\" : \"truncate\",\n )}\n >\n {subtitle}\n </span>\n )}\n {meta != null && <span className=\"mt-0.5 block truncate text-[11px] leading-snug text-[var(--text-muted)]\">{meta}</span>}\n </span>\n {trailing != null && <span className=\"flex shrink-0 items-center gap-2 text-xs text-[var(--text-muted)]\">{trailing}</span>}\n {loading ? (\n <Spinner label={null} className=\"size-3.5 shrink-0\" />\n ) : (\n external && href !== undefined && (\n <ExternalLink className=\"size-3.5 shrink-0 text-[var(--text-muted)] rtl:-scale-x-100\" aria-hidden />\n )\n )}\n {mark}\n </>\n );\n\n const target = cn(\n \"flex min-w-0 flex-1 text-start\",\n align === \"start\" ? \"items-start\" : \"items-center\",\n pad.target,\n // With a body the target is the top of the box, not all of it, so it takes its\n // own corners and its own hover fill (see `children`).\n hasBody ? \"rounded-md\" : \"rounded-[inherit]\",\n hasBody && interactive && !inert && !selected && \"transition-colors hover:bg-[var(--bg-hover)]\",\n interactive && TARGET_RING,\n interactive && !inert && \"cursor-pointer\",\n loading && \"cursor-progress\",\n targetClassName,\n );\n\n // `role` is the row's own (a button, a link, or nothing on a static block): a\n // targetProps role would silently turn the row into something else.\n const { role: _ignoredRole, ...targetExtras } = targetProps ?? {};\n\n let main: ReactNode;\n if (isLink) {\n const linkProps: ListItemLinkProps = {\n ...(targetExtras as Partial<ListItemLinkProps>),\n ...rest,\n ref: ref as Ref<HTMLAnchorElement>,\n href,\n className: target,\n \"aria-current\": selected ? \"true\" : undefined,\n \"aria-busy\": loading || undefined,\n onClick: loading\n ? (e) => e.preventDefault()\n : (onClick as ListItemLinkProps[\"onClick\"]),\n onAuxClick,\n children: body,\n };\n const { children: linkBody, ...anchorProps } = linkProps;\n const render = external ? undefined : pickLinkRenderer(renderLink, kitLink, href);\n main = render ? (\n <RenderedLink render={render} {...linkProps} />\n ) : (\n <a {...anchorProps} {...(external ? { target: \"_blank\", rel: \"noopener noreferrer\" } : null)}>\n {linkBody}\n </a>\n );\n } else if (isButton) {\n main = (\n <button\n {...(targetExtras as HTMLAttributes<HTMLButtonElement>)}\n {...rest}\n ref={ref as Ref<HTMLButtonElement>}\n type=\"button\"\n // A disabled row is `disabled`: nothing to do there, and nothing to reach. A\n // loading row is only `aria-disabled` — see `loading`.\n disabled={disabled}\n aria-disabled={loading || undefined}\n aria-busy={loading || undefined}\n aria-current={selected ? \"true\" : undefined}\n onClick={loading ? undefined : (onClick as (event: MouseEvent<HTMLButtonElement>) => void)}\n className={target}\n >\n {body}\n </button>\n );\n } else {\n main = (\n <div\n {...(targetExtras as HTMLAttributes<HTMLDivElement>)}\n {...rest}\n ref={ref as Ref<HTMLDivElement>}\n aria-disabled={disabled || undefined}\n aria-busy={loading || undefined}\n className={target}\n >\n {body}\n </div>\n );\n }\n\n const actionsEl =\n actions != null ? <div className={cn(\"flex shrink-0 items-center\", pad.gap, pad.actions)}>{actions}</div> : null;\n\n const Tag = as;\n return (\n <Tag\n className={cn(\n list.separator === \"divider\" && \"border-b border-[var(--border)] last:border-b-0\",\n list.separator === \"divider\" && as === \"li\" && \"py-0.5\",\n )}\n >\n <div\n className={cn(\n \"flex min-w-0 rounded-md border transition-colors\",\n hasBody ? \"flex-col\" : \"items-center\",\n selected\n ? \"border-[var(--brand)] bg-[var(--bg-surface-2)]\"\n : bordered\n ? \"border-[var(--border)]\"\n : \"border-transparent\",\n !hasBody && interactive && !inert && !selected && \"hover:bg-[var(--bg-hover)]\",\n disabled && \"opacity-50\",\n className,\n )}\n >\n {hasBody ? (\n <>\n <div className=\"flex min-w-0 items-center\">\n {main}\n {actionsEl}\n </div>\n <div role=\"group\" aria-labelledby={titleId} className={cn(\"min-w-0\", pad.body, bodyClassName)}>\n {children}\n </div>\n </>\n ) : (\n <>\n {main}\n {actionsEl}\n </>\n )}\n </div>\n </Tag>\n );\n});\n\nfunction RenderedLink({\n render,\n ...props\n}: ListItemLinkProps & { render: (props: ListItemLinkProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":";AA+DM,SA+PF,UA/PE,KAiRe,YAjRf;AA/DN,SAAS,eAAe,YAAY,YAAY,aAAa;AAE7D,SAAS,oBAAoB;AAE7B,SAAS,UAAU;AACnB,SAAS,eAAe;AACxB,SAAS,iBAAiB;AAC1B,SAAS,cAAc,kBAAkB;AACzC,SAAS,wBAAwB;AAW1B,MAAM,sBAAkC;AAAA,EAC7C,QAAQ;AAAA,EACR,eAAe;AACjB;AAaA,MAAM,cAAc,cAAkE;AAAA,EACpF,SAAS;AAAA,EACT,WAAW;AACb,CAAC;AAkBM,MAAM,OAAO,WAAwC,SAASA,MACnE,EAAE,KAAK,MAAM,UAAU,WAAW,YAAY,OAAO,WAAW,UAAU,GAAG,KAAK,GAClF,KACA;AACA,QAAM,MAAM;AACZ,SACE,oBAAC,YAAY,UAAZ,EAAqB,OAAO,EAAE,SAAS,UAAU,GAChD;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ;AAAA,MACA,MAAK;AAAA,MACL,WAAW;AAAA,QACT;AAAA,QACA,cAAc,UAAU,YAAY,YAAY,YAAY;AAAA,QAC5D;AAAA,MACF;AAAA,MAEC;AAAA;AAAA,EACH,GACF;AAEJ,CAAC;AAqBD,MAAM,MAA2F;AAAA,EAC/F,SAAS,EAAE,QAAQ,qBAAqB,SAAS,UAAU,KAAK,WAAW,MAAM,cAAc;AAAA,EAC/F,SAAS,EAAE,QAAQ,qBAAqB,SAAS,QAAQ,KAAK,SAAS,MAAM,cAAc;AAAA,EAC3F,aAAa,EAAE,QAAQ,mBAAmB,SAAS,QAAQ,KAAK,SAAS,MAAM,YAAY;AAC7F;AAEA,MAAM,cACJ;AAmKK,MAAM,WAAW,WAAuC,SAASC,UACtE;AAAA,EACE;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB;AAAA,EACA,MAAM;AAAA,EACN;AAAA,EACA;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA,WAAW;AAAA,EACX,WAAW;AAAA,EACX,UAAU;AAAA,EACV,SAAS;AAAA,EACT,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,KAAK;AAAA,EACL;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,GAAG;AACL,GACA,KACA;AACA,QAAM,OAAO,WAAW,WAAW;AACnC,QAAM,SAAS,aAAa,QAAQ,mBAAmB;AACvD,QAAM,UAAU,WAAW;AAC3B,QAAM,UAAU,eAAe,KAAK;AACpC,QAAM,MAAM,IAAI,OAAO;AACvB,QAAM,SAAS,SAAS,UAAa,CAAC;AACtC,QAAM,WAAW,CAAC,UAAU,YAAY,UAAa,SAAS;AAC9D,QAAM,cAAc,UAAU;AAC9B,QAAM,QAAQ,YAAY;AAC1B,QAAM,UAAU,aAAa,UAAa,aAAa,QAAQ,aAAa;AAC5E,QAAM,UAAU,MAAM;AAEtB,QAAM,OACJ,WACC,SAAS,oBAAC,aAAU,MAAK,SAAQ,MAAK,MAAK,WAAW,UAAU,UAAU,WAAW,QAAW,IAAK;AAExG,QAAM,OACJ,iCACG;AAAA,YACC;AAAA,MAAC;AAAA;AAAA,QACC,WAAW,GAAG,4CAA4C,UAAU,WAAW,QAAQ;AAAA,QACvF,eAAW;AAAA;AAAA,IACb;AAAA,IAED;AAAA,IACD,qBAAC,UAAK,WAAU,gCACd;AAAA;AAAA,QAAC;AAAA;AAAA,UAEC,IAAI,UAAU,UAAU;AAAA,UACxB,WAAW;AAAA,YACT;AAAA,YACA,SAAS,kBAAkB;AAAA,UAC7B;AAAA,UAEC;AAAA;AAAA,YACA,UAAU,qBAAC,UAAK,WAAU,WAAU;AAAA;AAAA,cAAG,OAAO;AAAA,cAAO;AAAA,eAAC;AAAA,YACtD,YAAY,SAAS,UAAa,qBAAC,UAAK,WAAU,WAAU;AAAA;AAAA,cAAG,OAAO;AAAA,cAAc;AAAA,eAAC;AAAA;AAAA;AAAA,MACxF;AAAA,MACC,YAAY,QACX;AAAA,QAAC;AAAA;AAAA,UACC,WAAW;AAAA,YACT;AAAA,YACA,kBAAkB,IAAI,iBAAiB;AAAA,UACzC;AAAA,UAEC;AAAA;AAAA,MACH;AAAA,MAED,QAAQ,QAAQ,oBAAC,UAAK,WAAU,2EAA2E,gBAAK;AAAA,OACnH;AAAA,IACC,YAAY,QAAQ,oBAAC,UAAK,WAAU,qEAAqE,oBAAS;AAAA,IAClH,UACC,oBAAC,WAAQ,OAAO,MAAM,WAAU,qBAAoB,IAEpD,YAAY,SAAS,UACnB,oBAAC,gBAAa,WAAU,+DAA8D,eAAW,MAAC;AAAA,IAGrG;AAAA,KACH;AAGF,QAAM,SAAS;AAAA,IACb;AAAA,IACA,UAAU,UAAU,gBAAgB;AAAA,IACpC,IAAI;AAAA;AAAA;AAAA,IAGJ,UAAU,eAAe;AAAA,IACzB,WAAW,eAAe,CAAC,SAAS,CAAC,YAAY;AAAA,IACjD,eAAe;AAAA,IACf,eAAe,CAAC,SAAS;AAAA,IACzB,WAAW;AAAA,IACX;AAAA,EACF;AAIA,QAAM,EAAE,MAAM,cAAc,GAAG,aAAa,IAAI,eAAe,CAAC;AAEhE,MAAI;AACJ,MAAI,QAAQ;AACV,UAAM,YAA+B;AAAA,MACnC,GAAI;AAAA,MACJ,GAAG;AAAA,MACH;AAAA,MACA;AAAA,MACA,WAAW;AAAA,MACX,gBAAgB,WAAW,SAAS;AAAA,MACpC,aAAa,WAAW;AAAA,MACxB,SAAS,UACL,CAAC,MAAM,EAAE,eAAe,IACvB;AAAA,MACL;AAAA,MACA,UAAU;AAAA,IACZ;AACA,UAAM,EAAE,UAAU,UAAU,GAAG,YAAY,IAAI;AAC/C,UAAM,SAAS,WAAW,SAAY,iBAAiB,YAAY,SAAS,IAAI;AAChF,WAAO,SACL,oBAAC,gBAAa,QAAiB,GAAG,WAAW,IAE7C,oBAAC,OAAG,GAAG,aAAc,GAAI,WAAW,EAAE,QAAQ,UAAU,KAAK,sBAAsB,IAAI,MACpF,oBACH;AAAA,EAEJ,WAAW,UAAU;AACnB,WACE;AAAA,MAAC;AAAA;AAAA,QACE,GAAI;AAAA,QACJ,GAAG;AAAA,QACJ;AAAA,QACA,MAAK;AAAA,QAGL;AAAA,QACA,iBAAe,WAAW;AAAA,QAC1B,aAAW,WAAW;AAAA,QACtB,gBAAc,WAAW,SAAS;AAAA,QAClC,SAAS,UAAU,SAAa;AAAA,QAChC,WAAW;AAAA,QAEV;AAAA;AAAA,IACH;AAAA,EAEJ,OAAO;AACL,WACE;AAAA,MAAC;AAAA;AAAA,QACE,GAAI;AAAA,QACJ,GAAG;AAAA,QACJ;AAAA,QACA,iBAAe,YAAY;AAAA,QAC3B,aAAW,WAAW;AAAA,QACtB,WAAW;AAAA,QAEV;AAAA;AAAA,IACH;AAAA,EAEJ;AAEA,QAAM,YACJ,WAAW,OAAO,oBAAC,SAAI,WAAW,GAAG,8BAA8B,IAAI,KAAK,IAAI,OAAO,GAAI,mBAAQ,IAAS;AAE9G,QAAM,MAAM;AACZ,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW;AAAA,QACT,KAAK,cAAc,aAAa;AAAA,QAChC,KAAK,cAAc,aAAa,OAAO,QAAQ;AAAA,MACjD;AAAA,MAEA;AAAA,QAAC;AAAA;AAAA,UACC,WAAW;AAAA,YACT;AAAA,YACA,UAAU,aAAa;AAAA,YACvB,WACI,mDACA,WACE,2BACA;AAAA,YACN,CAAC,WAAW,eAAe,CAAC,SAAS,CAAC,YAAY;AAAA,YAClD,YAAY;AAAA,YACZ;AAAA,UACF;AAAA,UAEC,oBACC,iCACE;AAAA,iCAAC,SAAI,WAAU,6BACZ;AAAA;AAAA,cACA;AAAA,eACH;AAAA,YACA,oBAAC,SAAI,MAAK,SAAQ,mBAAiB,SAAS,WAAW,GAAG,WAAW,IAAI,MAAM,aAAa,GACzF,UACH;AAAA,aACF,IAEA,iCACG;AAAA;AAAA,YACA;AAAA,aACH;AAAA;AAAA,MAEJ;AAAA;AAAA,EACF;AAEJ,CAAC;AAED,SAAS,aAAa;AAAA,EACpB;AAAA,EACA,GAAG;AACL,GAA+E;AAC7E,SAAO,OAAO,KAAK;AACrB;","names":["List","ListItem"]}
@@ -0,0 +1,24 @@
1
+ import * as react from 'react';
2
+ import { ComponentPropsWithoutRef, ReactNode } from 'react';
3
+
4
+ type LoadingStateSize = "sm" | "md" | "lg";
5
+ interface LoadingStateProps extends Omit<ComponentPropsWithoutRef<"div">, "children" | "role"> {
6
+ /** The visible words under the spinner, and what the status region announces.
7
+ * Default: `common.loading` ("Loading…"). */
8
+ label?: ReactNode;
9
+ /** `sm` for a panel or a table body, `md` (default) for a section, `lg` for a page. */
10
+ size?: LoadingStateSize;
11
+ /** Spinner beside the text on one line, rather than above it. */
12
+ inline?: boolean;
13
+ }
14
+ /**
15
+ * A spinner and the words "Loading…", centred where the content will be.
16
+ *
17
+ * kastlan's `LoadingState` (feedback/loading-state.tsx:14), which got the reading
18
+ * right the second time: the WRAPPER is the `role="status"` region and the words are
19
+ * visible text inside it, so the spinner is decorative (`label={null}`) — otherwise
20
+ * the reader hears "Loading… Loading…", one from each.
21
+ */
22
+ declare function LoadingState({ label, size, inline, className, ...rest }: LoadingStateProps): react.JSX.Element;
23
+
24
+ export { LoadingState, type LoadingStateProps, type LoadingStateSize };
@@ -0,0 +1,32 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { cn } from "../lib/cn.js";
4
+ import { DEFAULT_COMMON_LABELS, useKitLabels } from "../i18n/kit-labels.js";
5
+ import { Spinner } from "./ui.js";
6
+ const SPINNER = { sm: "h-4 w-4", md: "h-8 w-8", lg: "h-10 w-10" };
7
+ const TEXT = { sm: "text-xs", md: "text-sm", lg: "text-base" };
8
+ const PAD = { sm: "py-4", md: "py-12", lg: "py-20" };
9
+ function LoadingState({ label, size = "md", inline = false, className, ...rest }) {
10
+ const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
11
+ return /* @__PURE__ */ jsxs(
12
+ "div",
13
+ {
14
+ ...rest,
15
+ role: "status",
16
+ className: cn(
17
+ "flex items-center justify-center text-center text-[var(--text-muted)]",
18
+ inline ? "gap-2" : "flex-col gap-2",
19
+ PAD[size],
20
+ className
21
+ ),
22
+ children: [
23
+ /* @__PURE__ */ jsx(Spinner, { label: null, className: SPINNER[size] }),
24
+ /* @__PURE__ */ jsx("span", { className: TEXT[size], children: label ?? common.loading })
25
+ ]
26
+ }
27
+ );
28
+ }
29
+ export {
30
+ LoadingState
31
+ };
32
+ //# sourceMappingURL=loading-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/loading-state.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { DEFAULT_COMMON_LABELS, useKitLabels } from \"../i18n/kit-labels\";\nimport { Spinner } from \"./ui\";\n\nexport type LoadingStateSize = \"sm\" | \"md\" | \"lg\";\n\nexport interface LoadingStateProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\" | \"role\"> {\n /** The visible words under the spinner, and what the status region announces.\n * Default: `common.loading` (\"Loading…\"). */\n label?: ReactNode;\n /** `sm` for a panel or a table body, `md` (default) for a section, `lg` for a page. */\n size?: LoadingStateSize;\n /** Spinner beside the text on one line, rather than above it. */\n inline?: boolean;\n}\n\nconst SPINNER: Record<LoadingStateSize, string> = { sm: \"h-4 w-4\", md: \"h-8 w-8\", lg: \"h-10 w-10\" };\nconst TEXT: Record<LoadingStateSize, string> = { sm: \"text-xs\", md: \"text-sm\", lg: \"text-base\" };\nconst PAD: Record<LoadingStateSize, string> = { sm: \"py-4\", md: \"py-12\", lg: \"py-20\" };\n\n/**\n * A spinner and the words \"Loading…\", centred where the content will be.\n *\n * kastlan's `LoadingState` (feedback/loading-state.tsx:14), which got the reading\n * right the second time: the WRAPPER is the `role=\"status\"` region and the words are\n * visible text inside it, so the spinner is decorative (`label={null}`) — otherwise\n * the reader hears \"Loading… Loading…\", one from each.\n */\nexport function LoadingState({ label, size = \"md\", inline = false, className, ...rest }: LoadingStateProps) {\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n return (\n <div\n {...rest}\n role=\"status\"\n className={cn(\n \"flex items-center justify-center text-center text-[var(--text-muted)]\",\n inline ? \"gap-2\" : \"flex-col gap-2\",\n PAD[size],\n className,\n )}\n >\n <Spinner label={null} className={SPINNER[size]} />\n <span className={TEXT[size]}>{label ?? common.loading}</span>\n </div>\n );\n}\n"],"mappings":";AAgCI,SAUE,KAVF;AA/BJ,SAAS,UAAU;AACnB,SAAS,uBAAuB,oBAAoB;AACpD,SAAS,eAAe;AAcxB,MAAM,UAA4C,EAAE,IAAI,WAAW,IAAI,WAAW,IAAI,YAAY;AAClG,MAAM,OAAyC,EAAE,IAAI,WAAW,IAAI,WAAW,IAAI,YAAY;AAC/F,MAAM,MAAwC,EAAE,IAAI,QAAQ,IAAI,SAAS,IAAI,QAAQ;AAU9E,SAAS,aAAa,EAAE,OAAO,OAAO,MAAM,SAAS,OAAO,WAAW,GAAG,KAAK,GAAsB;AAC1G,QAAM,SAAS,aAAa,UAAU,qBAAqB;AAC3D,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,MAAK;AAAA,MACL,WAAW;AAAA,QACT;AAAA,QACA,SAAS,UAAU;AAAA,QACnB,IAAI,IAAI;AAAA,QACR;AAAA,MACF;AAAA,MAEA;AAAA,4BAAC,WAAQ,OAAO,MAAM,WAAW,QAAQ,IAAI,GAAG;AAAA,QAChD,oBAAC,UAAK,WAAW,KAAK,IAAI,GAAI,mBAAS,OAAO,SAAQ;AAAA;AAAA;AAAA,EACxD;AAEJ;","names":[]}
@@ -25,7 +25,17 @@ interface MenuItemBaseProps {
25
25
  icon?: LucideIcon;
26
26
  /** Anything else before the label — an avatar, a flag — when a Lucide icon will not do. */
27
27
  leading?: ReactNode;
28
- /** After the label, at the row's end: a shortcut, a count, a badge. */
28
+ /**
29
+ * Right after the label text, before the gap that pushes {@link trailing} to the
30
+ * row's end: keksdose's budget switcher tags a budget "Guest" or "Shared" with a
31
+ * `Chip` that belongs to the NAME (budget-switcher.tsx:55/59), not to the row's end
32
+ * where a count or a check mark sits. Hand-placed inside `children` it was truncated
33
+ * with the name — a long budget name ate the chip first. Here the label truncates
34
+ * and the badge never shrinks, so it stays visible beside what is left of the name.
35
+ * It is part of the row's accessible name ("Household Shared"), so keep it text.
36
+ */
37
+ badge?: ReactNode;
38
+ /** After the label, at the row's end: a shortcut, a count. (A tag that belongs to the name is {@link badge}.) */
29
39
  trailing?: ReactNode;
30
40
  /** `danger` for the row that destroys or signs out. Colour is not the whole message:
31
41
  * the label must still say what the row does. */
@@ -70,7 +80,8 @@ type MenuItemProps = MenuItemBaseProps & ({
70
80
  /** Makes the row a link — keksdose's "Manage budgets" and account-menu rows. */
71
81
  href: string;
72
82
  /** Renders the link — pass your router's `<Link>`, since a plain `<a>` reloads
73
- * a single-page app. Default `<a>`. The same API as `Chip` and `StatTile`. */
83
+ * a single-page app. Default: the `<UiKitProvider linkComponent>`, then `<a>`.
84
+ * The same API as `Chip` and `StatTile`. */
74
85
  renderLink?: (props: MenuItemLinkProps) => ReactElement;
75
86
  /** Runs on the way (close the menu). */
76
87
  onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
@@ -3,12 +3,15 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { forwardRef } from "react";
4
4
  import { Check } from "lucide-react";
5
5
  import { cn } from "../lib/cn.js";
6
+ import { useKitLink } from "../i18n/kit-labels.js";
7
+ import { pickLinkRenderer } from "./text-link.js";
6
8
  const MENU_ITEM_ROW = "flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-start text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]";
7
9
  const MENU_ITEM_FOCUS = "focus-visible:outline-none focus-visible:bg-[var(--bg-hover)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]";
8
10
  const MenuItem = forwardRef(function MenuItem2({
9
11
  children,
10
12
  icon: Icon,
11
13
  leading,
14
+ badge,
12
15
  trailing,
13
16
  tone = "default",
14
17
  disabled = false,
@@ -21,6 +24,7 @@ const MenuItem = forwardRef(function MenuItem2({
21
24
  current,
22
25
  ...rest
23
26
  }, ref) {
27
+ const kitLink = useKitLink();
24
28
  const on = href !== void 0 ? !!current : !!checked;
25
29
  const look = cn(
26
30
  MENU_ITEM_ROW,
@@ -33,7 +37,11 @@ const MenuItem = forwardRef(function MenuItem2({
33
37
  /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [
34
38
  Icon && /* @__PURE__ */ jsx(Icon, { className: "size-4 shrink-0", "aria-hidden": true }),
35
39
  leading,
36
- /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children })
40
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children }),
41
+ badge != null && badge !== false && /* @__PURE__ */ jsxs(Fragment, { children: [
42
+ " ",
43
+ /* @__PURE__ */ jsx("span", { "data-menu-item-badge": "", className: "flex shrink-0 items-center", children: badge })
44
+ ] })
37
45
  ] }),
38
46
  (trailing != null || on) && /* @__PURE__ */ jsxs("span", { className: "flex shrink-0 items-center gap-2", children: [
39
47
  trailing,
@@ -51,7 +59,8 @@ const MenuItem = forwardRef(function MenuItem2({
51
59
  "aria-current": current ? "page" : void 0,
52
60
  children: body
53
61
  };
54
- if (renderLink) return /* @__PURE__ */ jsx(RenderedLink, { render: renderLink, ...linkProps });
62
+ const render = pickLinkRenderer(renderLink, kitLink, href);
63
+ if (render) return /* @__PURE__ */ jsx(RenderedLink, { render, ...linkProps });
55
64
  const { children: linkBody, ...anchorProps } = linkProps;
56
65
  return /* @__PURE__ */ jsx("a", { ...anchorProps, children: linkBody });
57
66
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/menu-item.tsx"],"sourcesContent":["import { forwardRef } from \"react\";\nimport type { MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { Check } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\n\n/**\n * `TOPBAR_MENU_ITEM_CLASS`'s row, spelled out rather than imported: a component does\n * not reach up into the shell (the same call `Disclosure`'s menu variant made). A test\n * holds the two to the same classes, so a menu mixing `MenuItem`s with the shell's own\n * rows (`TopBarActionMenu`, `OptionSwitcherMenu`) cannot drift into two looks.\n */\nconst MENU_ITEM_ROW =\n \"flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-start text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]\";\n\n/** HoverMenu moves focus onto items with the arrow keys; a pointer user never sees the\n * ring, a keyboard user always does. Inset, because the panel clips its children. */\nconst MENU_ITEM_FOCUS =\n \"focus-visible:outline-none focus-visible:bg-[var(--bg-hover)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]\";\n\nexport type MenuItemTone = \"default\" | \"danger\";\n\n/** What {@link MenuItemProps.renderLink} is handed. Spread it onto your router's link,\n * mapping `href` to what the link calls it —\n * `renderLink={({ href, ...p }) => <Link to={href} {...p} />}`. */\nexport interface MenuItemLinkProps {\n href: string;\n className: string;\n children: ReactNode;\n role: \"menuitem\";\n ref?: Ref<HTMLAnchorElement>;\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n \"aria-current\"?: \"page\";\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\ninterface MenuItemBaseProps {\n /** The row's text. */\n children: ReactNode;\n /** An icon before the label, drawn at the menu's 16px. */\n icon?: LucideIcon;\n /** Anything else before the label — an avatar, a flag — when a Lucide icon will not do. */\n leading?: ReactNode;\n /** After the label, at the row's end: a shortcut, a count, a badge. */\n trailing?: ReactNode;\n /** `danger` for the row that destroys or signs out. Colour is not the whole message:\n * the label must still say what the row does. */\n tone?: MenuItemTone;\n disabled?: boolean;\n className?: string;\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\n/**\n * A menu row is a button OR a link, and only the button can be CHECKED: a checked link\n * is a link to where you already are, which is `current`, not a selection.\n */\nexport type MenuItemProps = MenuItemBaseProps &\n (\n | {\n href?: never;\n renderLink?: never;\n current?: never;\n /** Runs the row. Close the menu from here — `HoverMenu` hands its `close` to\n * the render prop that draws the rows. */\n onClick?: (event: MouseEvent<HTMLButtonElement>) => void;\n /**\n * Makes the row a CHOICE and says whether it is chosen. Leave it undefined for a\n * plain command; `false` is a choice that is not chosen, which is different.\n * A check mark is drawn at the end when it is on.\n */\n checked?: boolean;\n /**\n * What kind of choice, when `checked` is given:\n *\n * - `radio` (default): one of a set — keksdose's budget switcher\n * (budget-switcher.tsx:51), the theme and language pickers. `menuitemradio`.\n * - `checkbox`: an independent on/off — \"Show archived\". `menuitemcheckbox`.\n *\n * Not `aria-pressed`: that belongs to a toggle BUTTON, and inside a\n * `role=\"menu\"` a screen reader expects the menu roles and reads \"checked\"\n * only from them. A button with `aria-pressed` in a menu is announced as a\n * button in a menu, which is two widgets in one.\n */\n checkable?: \"radio\" | \"checkbox\";\n }\n | {\n /** Makes the row a link — keksdose's \"Manage budgets\" and account-menu rows. */\n href: string;\n /** Renders the link — pass your router's `<Link>`, since a plain `<a>` reloads\n * a single-page app. Default `<a>`. The same API as `Chip` and `StatTile`. */\n renderLink?: (props: MenuItemLinkProps) => ReactElement;\n /** Runs on the way (close the menu). */\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n /** The link to the page already open: `aria-current=\"page\"` and a check mark. */\n current?: boolean;\n checked?: never;\n checkable?: never;\n }\n );\n\n/**\n * One row of a `HoverMenu` / `TopBarActionMenu` panel.\n *\n * keksdose's account menu and budget switcher each rebuild this row by hand — the\n * shell's class, an icon, a label, a badge, a check mark on the active budget — and the\n * check mark was the only thing saying which budget was active, so a screen reader\n * heard four identical buttons. This row says it (`menuitemradio` + `aria-checked`).\n *\n * The role is set HERE rather than left to `HoverMenu`, which promotes only role-less\n * buttons and links to `menuitem`: a checked row must be `menuitemradio` or\n * `menuitemcheckbox` from its first render, and `HoverMenu` leaves an element that\n * already has a role alone — so its roving focus (↑/↓, Home/End) picks these rows up\n * with the rest. Render it inside an `<li>` like the shell's own rows; `HoverMenu`\n * turns the list markup into `role=\"none\"`.\n */\nexport const MenuItem = forwardRef<HTMLElement, MenuItemProps>(function MenuItem(\n {\n children,\n icon: Icon,\n leading,\n trailing,\n tone = \"default\",\n disabled = false,\n className,\n href,\n renderLink,\n onClick,\n checked,\n checkable,\n current,\n ...rest\n },\n ref,\n) {\n const on = href !== undefined ? !!current : !!checked;\n const look = cn(\n MENU_ITEM_ROW,\n MENU_ITEM_FOCUS,\n tone === \"danger\" && \"text-[var(--danger)] hover:bg-[var(--danger-bg)] focus-visible:bg-[var(--danger-bg)]\",\n disabled && \"cursor-default opacity-50 hover:bg-transparent\",\n className,\n );\n const body = (\n <>\n <span className=\"flex min-w-0 items-center gap-2\">\n {Icon && <Icon className=\"size-4 shrink-0\" aria-hidden />}\n {leading}\n <span className=\"min-w-0 truncate\">{children}</span>\n </span>\n {(trailing != null || on) && (\n <span className=\"flex shrink-0 items-center gap-2\">\n {trailing}\n {on && <Check className=\"size-4 text-[var(--text-secondary)]\" aria-hidden />}\n </span>\n )}\n </>\n );\n\n if (href !== undefined && !disabled) {\n const linkProps: MenuItemLinkProps = {\n ...rest,\n ref: ref as Ref<HTMLAnchorElement>,\n href,\n role: \"menuitem\",\n className: look,\n onClick: onClick as MenuItemLinkProps[\"onClick\"],\n \"aria-current\": current ? \"page\" : undefined,\n children: body,\n };\n if (renderLink) return <RenderedLink render={renderLink} {...linkProps} />;\n const { children: linkBody, ...anchorProps } = linkProps;\n return <a {...anchorProps}>{linkBody}</a>;\n }\n\n const role =\n href === undefined && checked !== undefined\n ? checkable === \"checkbox\"\n ? \"menuitemcheckbox\"\n : \"menuitemradio\"\n : \"menuitem\";\n return (\n <button\n {...rest}\n ref={ref as Ref<HTMLButtonElement>}\n type=\"button\"\n role={role}\n aria-checked={role === \"menuitem\" ? undefined : !!checked}\n // `aria-disabled` rather than `disabled`: HoverMenu's roving focus skips either,\n // and the ARIA menu pattern keeps an unavailable item perceivable (\"Delete,\n // dimmed\") rather than greyed to the point some browsers stop exposing it.\n aria-disabled={disabled || undefined}\n onClick={\n disabled\n ? (e) => e.preventDefault()\n : (onClick as ((event: MouseEvent<HTMLButtonElement>) => void) | undefined)\n }\n className={look}\n >\n {body}\n </button>\n );\n});\n\nfunction RenderedLink({\n render,\n ...props\n}: MenuItemLinkProps & { render: (props: MenuItemLinkProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":";AAoJI,mBAEa,KADX,YADF;AApJJ,SAAS,kBAAkB;AAE3B,SAAS,aAAa;AAEtB,SAAS,UAAU;AAQnB,MAAM,gBACJ;AAIF,MAAM,kBACJ;AAsGK,MAAM,WAAW,WAAuC,SAASA,UACtE;AAAA,EACE;AAAA,EACA,MAAM;AAAA,EACN;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GACA,KACA;AACA,QAAM,KAAK,SAAS,SAAY,CAAC,CAAC,UAAU,CAAC,CAAC;AAC9C,QAAM,OAAO;AAAA,IACX;AAAA,IACA;AAAA,IACA,SAAS,YAAY;AAAA,IACrB,YAAY;AAAA,IACZ;AAAA,EACF;AACA,QAAM,OACJ,iCACE;AAAA,yBAAC,UAAK,WAAU,mCACb;AAAA,cAAQ,oBAAC,QAAK,WAAU,mBAAkB,eAAW,MAAC;AAAA,MACtD;AAAA,MACD,oBAAC,UAAK,WAAU,oBAAoB,UAAS;AAAA,OAC/C;AAAA,KACE,YAAY,QAAQ,OACpB,qBAAC,UAAK,WAAU,oCACb;AAAA;AAAA,MACA,MAAM,oBAAC,SAAM,WAAU,uCAAsC,eAAW,MAAC;AAAA,OAC5E;AAAA,KAEJ;AAGF,MAAI,SAAS,UAAa,CAAC,UAAU;AACnC,UAAM,YAA+B;AAAA,MACnC,GAAG;AAAA,MACH;AAAA,MACA;AAAA,MACA,MAAM;AAAA,MACN,WAAW;AAAA,MACX;AAAA,MACA,gBAAgB,UAAU,SAAS;AAAA,MACnC,UAAU;AAAA,IACZ;AACA,QAAI,WAAY,QAAO,oBAAC,gBAAa,QAAQ,YAAa,GAAG,WAAW;AACxE,UAAM,EAAE,UAAU,UAAU,GAAG,YAAY,IAAI;AAC/C,WAAO,oBAAC,OAAG,GAAG,aAAc,oBAAS;AAAA,EACvC;AAEA,QAAM,OACJ,SAAS,UAAa,YAAY,SAC9B,cAAc,aACZ,qBACA,kBACF;AACN,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ;AAAA,MACA,MAAK;AAAA,MACL;AAAA,MACA,gBAAc,SAAS,aAAa,SAAY,CAAC,CAAC;AAAA,MAIlD,iBAAe,YAAY;AAAA,MAC3B,SACE,WACI,CAAC,MAAM,EAAE,eAAe,IACvB;AAAA,MAEP,WAAW;AAAA,MAEV;AAAA;AAAA,EACH;AAEJ,CAAC;AAED,SAAS,aAAa;AAAA,EACpB;AAAA,EACA,GAAG;AACL,GAA+E;AAC7E,SAAO,OAAO,KAAK;AACrB;","names":["MenuItem"]}
1
+ {"version":3,"sources":["../../src/components/menu-item.tsx"],"sourcesContent":["import { forwardRef } from \"react\";\nimport type { MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { Check } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLink } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer } from \"./text-link\";\n\n/**\n * `TOPBAR_MENU_ITEM_CLASS`'s row, spelled out rather than imported: a component does\n * not reach up into the shell (the same call `Disclosure`'s menu variant made). A test\n * holds the two to the same classes, so a menu mixing `MenuItem`s with the shell's own\n * rows (`TopBarActionMenu`, `OptionSwitcherMenu`) cannot drift into two looks.\n */\nconst MENU_ITEM_ROW =\n \"flex w-full items-center justify-between gap-3 px-3 py-2 text-sm text-start text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]\";\n\n/** HoverMenu moves focus onto items with the arrow keys; a pointer user never sees the\n * ring, a keyboard user always does. Inset, because the panel clips its children. */\nconst MENU_ITEM_FOCUS =\n \"focus-visible:outline-none focus-visible:bg-[var(--bg-hover)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]\";\n\nexport type MenuItemTone = \"default\" | \"danger\";\n\n/** What {@link MenuItemProps.renderLink} is handed. Spread it onto your router's link,\n * mapping `href` to what the link calls it —\n * `renderLink={({ href, ...p }) => <Link to={href} {...p} />}`. */\nexport interface MenuItemLinkProps {\n href: string;\n className: string;\n children: ReactNode;\n role: \"menuitem\";\n ref?: Ref<HTMLAnchorElement>;\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n \"aria-current\"?: \"page\";\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\ninterface MenuItemBaseProps {\n /** The row's text. */\n children: ReactNode;\n /** An icon before the label, drawn at the menu's 16px. */\n icon?: LucideIcon;\n /** Anything else before the label — an avatar, a flag — when a Lucide icon will not do. */\n leading?: ReactNode;\n /**\n * Right after the label text, before the gap that pushes {@link trailing} to the\n * row's end: keksdose's budget switcher tags a budget \"Guest\" or \"Shared\" with a\n * `Chip` that belongs to the NAME (budget-switcher.tsx:55/59), not to the row's end\n * where a count or a check mark sits. Hand-placed inside `children` it was truncated\n * with the name — a long budget name ate the chip first. Here the label truncates\n * and the badge never shrinks, so it stays visible beside what is left of the name.\n * It is part of the row's accessible name (\"Household Shared\"), so keep it text.\n */\n badge?: ReactNode;\n /** After the label, at the row's end: a shortcut, a count. (A tag that belongs to the name is {@link badge}.) */\n trailing?: ReactNode;\n /** `danger` for the row that destroys or signs out. Colour is not the whole message:\n * the label must still say what the row does. */\n tone?: MenuItemTone;\n disabled?: boolean;\n className?: string;\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\n/**\n * A menu row is a button OR a link, and only the button can be CHECKED: a checked link\n * is a link to where you already are, which is `current`, not a selection.\n */\nexport type MenuItemProps = MenuItemBaseProps &\n (\n | {\n href?: never;\n renderLink?: never;\n current?: never;\n /** Runs the row. Close the menu from here — `HoverMenu` hands its `close` to\n * the render prop that draws the rows. */\n onClick?: (event: MouseEvent<HTMLButtonElement>) => void;\n /**\n * Makes the row a CHOICE and says whether it is chosen. Leave it undefined for a\n * plain command; `false` is a choice that is not chosen, which is different.\n * A check mark is drawn at the end when it is on.\n */\n checked?: boolean;\n /**\n * What kind of choice, when `checked` is given:\n *\n * - `radio` (default): one of a set — keksdose's budget switcher\n * (budget-switcher.tsx:51), the theme and language pickers. `menuitemradio`.\n * - `checkbox`: an independent on/off — \"Show archived\". `menuitemcheckbox`.\n *\n * Not `aria-pressed`: that belongs to a toggle BUTTON, and inside a\n * `role=\"menu\"` a screen reader expects the menu roles and reads \"checked\"\n * only from them. A button with `aria-pressed` in a menu is announced as a\n * button in a menu, which is two widgets in one.\n */\n checkable?: \"radio\" | \"checkbox\";\n }\n | {\n /** Makes the row a link — keksdose's \"Manage budgets\" and account-menu rows. */\n href: string;\n /** Renders the link — pass your router's `<Link>`, since a plain `<a>` reloads\n * a single-page app. Default: the `<UiKitProvider linkComponent>`, then `<a>`.\n * The same API as `Chip` and `StatTile`. */\n renderLink?: (props: MenuItemLinkProps) => ReactElement;\n /** Runs on the way (close the menu). */\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n /** The link to the page already open: `aria-current=\"page\"` and a check mark. */\n current?: boolean;\n checked?: never;\n checkable?: never;\n }\n );\n\n/**\n * One row of a `HoverMenu` / `TopBarActionMenu` panel.\n *\n * keksdose's account menu and budget switcher each rebuild this row by hand — the\n * shell's class, an icon, a label, a badge, a check mark on the active budget — and the\n * check mark was the only thing saying which budget was active, so a screen reader\n * heard four identical buttons. This row says it (`menuitemradio` + `aria-checked`).\n *\n * The role is set HERE rather than left to `HoverMenu`, which promotes only role-less\n * buttons and links to `menuitem`: a checked row must be `menuitemradio` or\n * `menuitemcheckbox` from its first render, and `HoverMenu` leaves an element that\n * already has a role alone — so its roving focus (↑/↓, Home/End) picks these rows up\n * with the rest. Render it inside an `<li>` like the shell's own rows; `HoverMenu`\n * turns the list markup into `role=\"none\"`.\n */\nexport const MenuItem = forwardRef<HTMLElement, MenuItemProps>(function MenuItem(\n {\n children,\n icon: Icon,\n leading,\n badge,\n trailing,\n tone = \"default\",\n disabled = false,\n className,\n href,\n renderLink,\n onClick,\n checked,\n checkable,\n current,\n ...rest\n },\n ref,\n) {\n const kitLink = useKitLink();\n const on = href !== undefined ? !!current : !!checked;\n const look = cn(\n MENU_ITEM_ROW,\n MENU_ITEM_FOCUS,\n tone === \"danger\" && \"text-[var(--danger)] hover:bg-[var(--danger-bg)] focus-visible:bg-[var(--danger-bg)]\",\n disabled && \"cursor-default opacity-50 hover:bg-transparent\",\n className,\n );\n const body = (\n <>\n <span className=\"flex min-w-0 items-center gap-2\">\n {Icon && <Icon className=\"size-4 shrink-0\" aria-hidden />}\n {leading}\n <span className=\"min-w-0 truncate\">{children}</span>\n {badge != null && badge !== false && (\n <>\n {/* A space between the name and the badge in the accessible name (\"Household\n Shared\", not \"HouseholdShared\"); whitespace between flex items is not\n laid out, so it costs nothing on screen. */}{\" \"}\n <span data-menu-item-badge=\"\" className=\"flex shrink-0 items-center\">\n {badge}\n </span>\n </>\n )}\n </span>\n {(trailing != null || on) && (\n <span className=\"flex shrink-0 items-center gap-2\">\n {trailing}\n {on && <Check className=\"size-4 text-[var(--text-secondary)]\" aria-hidden />}\n </span>\n )}\n </>\n );\n\n if (href !== undefined && !disabled) {\n const linkProps: MenuItemLinkProps = {\n ...rest,\n ref: ref as Ref<HTMLAnchorElement>,\n href,\n role: \"menuitem\",\n className: look,\n onClick: onClick as MenuItemLinkProps[\"onClick\"],\n \"aria-current\": current ? \"page\" : undefined,\n children: body,\n };\n const render = pickLinkRenderer(renderLink, kitLink, href);\n if (render) return <RenderedLink render={render} {...linkProps} />;\n const { children: linkBody, ...anchorProps } = linkProps;\n return <a {...anchorProps}>{linkBody}</a>;\n }\n\n const role =\n href === undefined && checked !== undefined\n ? checkable === \"checkbox\"\n ? \"menuitemcheckbox\"\n : \"menuitemradio\"\n : \"menuitem\";\n return (\n <button\n {...rest}\n ref={ref as Ref<HTMLButtonElement>}\n type=\"button\"\n role={role}\n aria-checked={role === \"menuitem\" ? undefined : !!checked}\n // `aria-disabled` rather than `disabled`: HoverMenu's roving focus skips either,\n // and the ARIA menu pattern keeps an unavailable item perceivable (\"Delete,\n // dimmed\") rather than greyed to the point some browsers stop exposing it.\n aria-disabled={disabled || undefined}\n onClick={\n disabled\n ? (e) => e.preventDefault()\n : (onClick as ((event: MouseEvent<HTMLButtonElement>) => void) | undefined)\n }\n className={look}\n >\n {body}\n </button>\n );\n});\n\nfunction RenderedLink({\n render,\n ...props\n}: MenuItemLinkProps & { render: (props: MenuItemLinkProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":";AAqKiB,SAIP,UAJO,KAIP,YAJO;AArKjB,SAAS,kBAAkB;AAE3B,SAAS,aAAa;AAEtB,SAAS,UAAU;AACnB,SAAS,kBAAkB;AAC3B,SAAS,wBAAwB;AAQjC,MAAM,gBACJ;AAIF,MAAM,kBACJ;AAiHK,MAAM,WAAW,WAAuC,SAASA,UACtE;AAAA,EACE;AAAA,EACA,MAAM;AAAA,EACN;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GACA,KACA;AACA,QAAM,UAAU,WAAW;AAC3B,QAAM,KAAK,SAAS,SAAY,CAAC,CAAC,UAAU,CAAC,CAAC;AAC9C,QAAM,OAAO;AAAA,IACX;AAAA,IACA;AAAA,IACA,SAAS,YAAY;AAAA,IACrB,YAAY;AAAA,IACZ;AAAA,EACF;AACA,QAAM,OACJ,iCACE;AAAA,yBAAC,UAAK,WAAU,mCACb;AAAA,cAAQ,oBAAC,QAAK,WAAU,mBAAkB,eAAW,MAAC;AAAA,MACtD;AAAA,MACD,oBAAC,UAAK,WAAU,oBAAoB,UAAS;AAAA,MAC5C,SAAS,QAAQ,UAAU,SAC1B,iCAGmD;AAAA;AAAA,QACjD,oBAAC,UAAK,wBAAqB,IAAG,WAAU,8BACrC,iBACH;AAAA,SACF;AAAA,OAEJ;AAAA,KACE,YAAY,QAAQ,OACpB,qBAAC,UAAK,WAAU,oCACb;AAAA;AAAA,MACA,MAAM,oBAAC,SAAM,WAAU,uCAAsC,eAAW,MAAC;AAAA,OAC5E;AAAA,KAEJ;AAGF,MAAI,SAAS,UAAa,CAAC,UAAU;AACnC,UAAM,YAA+B;AAAA,MACnC,GAAG;AAAA,MACH;AAAA,MACA;AAAA,MACA,MAAM;AAAA,MACN,WAAW;AAAA,MACX;AAAA,MACA,gBAAgB,UAAU,SAAS;AAAA,MACnC,UAAU;AAAA,IACZ;AACA,UAAM,SAAS,iBAAiB,YAAY,SAAS,IAAI;AACzD,QAAI,OAAQ,QAAO,oBAAC,gBAAa,QAAiB,GAAG,WAAW;AAChE,UAAM,EAAE,UAAU,UAAU,GAAG,YAAY,IAAI;AAC/C,WAAO,oBAAC,OAAG,GAAG,aAAc,oBAAS;AAAA,EACvC;AAEA,QAAM,OACJ,SAAS,UAAa,YAAY,SAC9B,cAAc,aACZ,qBACA,kBACF;AACN,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ;AAAA,MACA,MAAK;AAAA,MACL;AAAA,MACA,gBAAc,SAAS,aAAa,SAAY,CAAC,CAAC;AAAA,MAIlD,iBAAe,YAAY;AAAA,MAC3B,SACE,WACI,CAAC,MAAM,EAAE,eAAe,IACvB;AAAA,MAEP,WAAW;AAAA,MAEV;AAAA;AAAA,EACH;AAEJ,CAAC;AAED,SAAS,aAAa;AAAA,EACpB;AAAA,EACA,GAAG;AACL,GAA+E;AAC7E,SAAO,OAAO,KAAK;AACrB;","names":["MenuItem"]}
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { ComponentPropsWithoutRef } from 'react';
2
+ import { ComponentPropsWithoutRef, ReactNode } from 'react';
3
3
 
4
4
  /**
5
5
  * Every string this calendar can speak — the `miniCalendar` namespace of
@@ -52,8 +52,10 @@ declare const DEFAULT_MINI_CALENDAR_LABELS: MiniCalendarLabels;
52
52
  * before this, a closed prop list dropped both (audit §api-design).
53
53
  */
54
54
  interface MiniCalendarProps extends Omit<ComponentPropsWithoutRef<"div">, "onSelect"> {
55
- from: string;
56
- to: string;
55
+ /** The selection's start as "YYYY-MM-DD"; `""` (the default) for none. */
56
+ from?: string;
57
+ /** The selection's end; `""` (the default) for none. */
58
+ to?: string;
57
59
  /**
58
60
  * BCP 47 tag for the month caption, the weekday heads, the day numbers and the
59
61
  * spoken dates. Optional since the kit grew `<UiKitProvider locale>`: a calendar
@@ -94,9 +96,84 @@ interface MiniCalendarProps extends Omit<ComponentPropsWithoutRef<"div">, "onSel
94
96
  * merely by existing.
95
97
  */
96
98
  focusOnOpen?: boolean;
99
+ /**
100
+ * `"sm"` (default) is the compact picker: 32px round days, the size the date
101
+ * pickers and the data-table filter draw. `"lg"` is a MONTH VIEW for a page — tall
102
+ * cells in a ruled grid, the day number in the top corner and {@link renderDay}'s
103
+ * content under it — the calendar a planner page hand-built before this existed.
104
+ * Both are the same grid underneath: the same roles, roving tabindex, keys, RTL
105
+ * mirroring and week start.
106
+ */
107
+ size?: "sm" | "lg";
108
+ /**
109
+ * Custom content for one day: events, an amount, a dot. Called for every day of the
110
+ * month on show; return `null` for none.
111
+ *
112
+ * ⚠️ **It renders INSIDE the day's button** — a dot under the number at `"sm"`, a
113
+ * column under it at `"lg"` — so it must not be interactive: no links, no buttons,
114
+ * no inputs. A control inside a control is invalid HTML, and a screen reader lands
115
+ * on the day and never reaches what is inside it. Events that are themselves
116
+ * clickable are shown here as plain text, and the DAY is what is clicked: pass
117
+ * `mode="single"` and let `onSelect` open a day panel (a drawer, a popover) that
118
+ * lists that day's events as real links.
119
+ *
120
+ * The day's accessible name stays the full date; this content is attached to it as
121
+ * its DESCRIPTION (`aria-describedby`), so "Monday, 14 September 2026" is read
122
+ * first and "Rent due · Plumber" after it. Anything decorative (a coloured dot)
123
+ * should be `aria-hidden` or carry an `sr-only` word for what it means.
124
+ */
125
+ renderDay?: (day: Date, state: MiniCalendarDayState) => ReactNode;
126
+ /**
127
+ * The month on show, as "YYYY-MM". FOLLOWED rather than controlled, exactly like
128
+ * `from`/`to`: when it changes the grid moves there, and in between the user pages
129
+ * freely — every move to another month is reported through {@link onMonthChange}.
130
+ * For a month page whose own header (a month picker, a Today button) drives the
131
+ * calendar. Left out, the grid opens on the selection or today.
132
+ */
133
+ month?: string;
134
+ /** Called with "YYYY-MM" whenever the USER moves the grid to another month (the
135
+ * arrows, PageUp/PageDown, arrowing off the edge). */
136
+ onMonthChange?: (month: string) => void;
137
+ /**
138
+ * Hide the built-in previous/next arrows and month caption, for a page that draws
139
+ * its own. The caption is kept for assistive technology (visually hidden), because
140
+ * it is what names the grid.
141
+ */
142
+ hideNavigation?: boolean;
143
+ /**
144
+ * Fill the leading and trailing cells with the neighbouring months' days, muted —
145
+ * the wall-calendar look, where the first row does not start on a hole.
146
+ *
147
+ * They are real days, not decoration: each is named with its whole date, honours
148
+ * `min`/`max`, and a click selects it and moves the grid to its month (focus goes
149
+ * with it). They are NOT stops of the roving tabindex — the keyboard already reaches
150
+ * them by arrowing off the month's edge, which moves the grid the same way — and
151
+ * `renderDay` is not called for them, so a day's events are drawn once, in its own
152
+ * month. Default `false`: the cells stay empty.
153
+ */
154
+ showOutsideDays?: boolean;
97
155
  /** Extra classes for the calendar's root. */
98
156
  className?: string;
99
157
  }
158
+ /** What {@link MiniCalendarProps.renderDay} is told about the day it draws. */
159
+ interface MiniCalendarDayState {
160
+ /** The day as "YYYY-MM-DD". */
161
+ iso: string;
162
+ /** The day number as drawn, in the calendar's locale. */
163
+ label: string;
164
+ today: boolean;
165
+ /** Outside `min`/`max`. */
166
+ disabled: boolean;
167
+ /** Part of the selection — an end or a day between. */
168
+ selected: boolean;
169
+ rangeStart: boolean;
170
+ rangeEnd: boolean;
171
+ /** Strictly between the two ends of a range. */
172
+ inRange: boolean;
173
+ /** The day that holds the tab stop. */
174
+ active: boolean;
175
+ size: "sm" | "lg";
176
+ }
100
177
  type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6;
101
178
  /**
102
179
  * Month-grid range picker used by the data-table date filter. Click once to set
@@ -119,6 +196,6 @@ type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6;
119
196
  * month would have holes that the roving tabindex falls into. They stay reachable and
120
197
  * readable, and the click handler is what refuses them.
121
198
  */
122
- declare function MiniCalendar({ from, to, locale: localeProp, weekStartsOn, onSelect, mode, min, max, labels: labelsProp, focusOnOpen, className, ...rest }: MiniCalendarProps): react.JSX.Element;
199
+ declare function MiniCalendar({ from, to, locale: localeProp, weekStartsOn, onSelect, mode, min, max, labels: labelsProp, focusOnOpen, size, renderDay, month, onMonthChange, hideNavigation, showOutsideDays, className, ...rest }: MiniCalendarProps): react.JSX.Element;
123
200
 
124
- export { DEFAULT_MINI_CALENDAR_LABELS, MiniCalendar, type MiniCalendarLabels, type MiniCalendarProps, type WeekDay };
201
+ export { DEFAULT_MINI_CALENDAR_LABELS, MiniCalendar, type MiniCalendarDayState, type MiniCalendarLabels, type MiniCalendarProps, type WeekDay };