@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
@@ -0,0 +1,114 @@
1
+ import * as react from 'react';
2
+ import { ComponentPropsWithoutRef, ReactNode, Key } from 'react';
3
+
4
+ /** The words {@link LineItems} renders on its own behalf. Row numbers are 1-based. */
5
+ interface LineItemsLabels {
6
+ /** The dashed add button. */
7
+ add: string;
8
+ /** The remove button's name for one row. */
9
+ remove: (row: number) => string;
10
+ /** Its name while it waits for the second press (`confirmRemove`). */
11
+ confirmRemove: (row: number) => string;
12
+ /** A row's group name, when the caller gives no `rowLabel`. */
13
+ row: (row: number) => string;
14
+ /** The name a cell's field should carry: the column, and which row. */
15
+ cell: (column: string, row: number) => string;
16
+ /** The totals row's label, and its group name. */
17
+ totals: string;
18
+ }
19
+ declare const DEFAULT_LINE_ITEMS_LABELS: LineItemsLabels;
20
+ /** What a column's `render` receives for one row. */
21
+ interface LineItemCellContext<T> {
22
+ item: T;
23
+ /** 0-based. */
24
+ index: number;
25
+ /** The accessible name for the cell's field — "Debit, row 2". The header is
26
+ * hidden from assistive tech (each row repeats it), so the field has to carry it:
27
+ * `<Input aria-label={label} … />`. */
28
+ label: string;
29
+ }
30
+ interface LineItemsColumn<T> {
31
+ /** Stable id: the React key, and the key into `totals`. */
32
+ key: string;
33
+ /** The column header (and the small label over the cell once the rows stack). */
34
+ header: ReactNode;
35
+ /** The column's name for {@link LineItemCellContext.label}, when `header` is not a
36
+ * string. */
37
+ label?: string;
38
+ /** The grid track once the rows are wide enough to be a table. Default
39
+ * `minmax(0,1fr)`; `"7rem"` for an amount. */
40
+ width?: string;
41
+ /** `end` for figures: the header and the totals cell end-align with the field. */
42
+ align?: "start" | "end";
43
+ render: (ctx: LineItemCellContext<T>) => ReactNode;
44
+ }
45
+ interface LineItemsProps<T> extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
46
+ items: readonly T[];
47
+ columns: readonly LineItemsColumn<T>[];
48
+ /** The row's React key. Default: the item's `id` (react-hook-form's `useFieldArray`
49
+ * `fields` carry one), else its index. */
50
+ getKey?: (item: T, index: number) => Key;
51
+ /** Append a row. Omitted: no add button. The first field of the new row is focused. */
52
+ onAdd?: () => void;
53
+ /** Remove the row at `index`. Omitted: no remove buttons. */
54
+ onRemove?: (index: number) => void;
55
+ /** Remove is disabled at or below this many rows (a journal entry needs two).
56
+ * Default 0. */
57
+ minItems?: number;
58
+ /** Add is disabled at this many rows. */
59
+ maxItems?: number;
60
+ /**
61
+ * Ask before a row goes:
62
+ * - `true`: the remove button asks for a second press — it fills red and its name
63
+ * becomes `lineItems.confirmRemove`; Escape, or leaving it, calls it off.
64
+ * - a function: called with the row, and the row goes only if it answers `true`
65
+ * (`(item, i) => confirm({ title: … , tone: "danger" })` with `useConfirm`).
66
+ */
67
+ confirmRemove?: boolean | ((item: T, index: number) => boolean | Promise<boolean>);
68
+ /** The add button's text. Default `lineItems.add`. */
69
+ addLabel?: ReactNode;
70
+ /** A row's group name ("Line 2"). Default `lineItems.row`. */
71
+ rowLabel?: (index: number, item: T) => string;
72
+ /** A totals row under the lines, one entry per column `key`. The first column
73
+ * without an entry carries {@link totalsLabel}. Omitted: no totals row. */
74
+ totals?: Partial<Record<string, ReactNode>>;
75
+ /** Default `lineItems.totals`, "Total". */
76
+ totalsLabel?: ReactNode;
77
+ /** Shown in place of the rows when there are none. */
78
+ empty?: ReactNode;
79
+ /** A message about the list as a whole ("The entry does not balance"). */
80
+ error?: ReactNode;
81
+ /** Disables add and remove (the fields are the caller's to disable). */
82
+ disabled?: boolean;
83
+ labels?: Partial<LineItemsLabels>;
84
+ }
85
+ /**
86
+ * An editable list of lines — journal entry lines, lease components, invoice
87
+ * positions, budget rows: column headers, a row of fields per item, a remove button
88
+ * per row, a dashed "add" button under them, and an optional totals row.
89
+ *
90
+ * Presentation only: the rows are the caller's `items` and the fields are the
91
+ * caller's, rendered per column. So it works over plain state —
92
+ *
93
+ * ```tsx
94
+ * <LineItems items={lines} columns={cols} onAdd={() => setLines([...lines, blank()])}
95
+ * onRemove={(i) => setLines(lines.filter((_, j) => j !== i))} minItems={2} />
96
+ * ```
97
+ *
98
+ * — and over react-hook-form's `useFieldArray` alike (`items={fields}`, `onAdd={() =>
99
+ * append(blank())}`, `onRemove={remove}`), for which `@eifi1/ui-kit/rhf` has
100
+ * `RhfLineItems`.
101
+ *
102
+ * On a wide pane the rows are a grid under the headers; on a narrow one (a container
103
+ * query, so a dialog stacks where it is cramped) each row stacks its fields, each
104
+ * under a small copy of its header. The headers are `aria-hidden`: every row is a
105
+ * named group and every field is named by `render`'s `label` ("Debit, row 2"), which
106
+ * is what a screen reader needs — a header row it would read once, out of context.
107
+ *
108
+ * Keyboard: after Add, focus lands in the new row's first field; after a remove, on
109
+ * the row that took its place (else the add button). Ctrl/⌘+Enter in any row adds a
110
+ * row.
111
+ */
112
+ declare function LineItems<T>({ items, columns, getKey, onAdd, onRemove, minItems, maxItems, confirmRemove, addLabel, rowLabel, totals, totalsLabel, empty, error, disabled, labels: labelsProp, className, style, ...rest }: LineItemsProps<T>): react.JSX.Element;
113
+
114
+ export { DEFAULT_LINE_ITEMS_LABELS, type LineItemCellContext, LineItems, type LineItemsColumn, type LineItemsLabels, type LineItemsProps };
@@ -0,0 +1,238 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useLayoutEffect, useRef, useState } from "react";
4
+ import { Plus, Trash2 } from "lucide-react";
5
+ import { cn } from "../lib/cn.js";
6
+ import { useKitLabels } from "../i18n/kit-labels.js";
7
+ import { Button, IconButton } from "./ui.js";
8
+ const DEFAULT_LINE_ITEMS_LABELS = {
9
+ add: "Add row",
10
+ remove: (row) => `Remove row ${row}`,
11
+ confirmRemove: (row) => `Remove row ${row}? Press again to confirm`,
12
+ row: (row) => `Row ${row}`,
13
+ cell: (column, row) => `${column}, row ${row}`,
14
+ totals: "Total"
15
+ };
16
+ const REMOVE_TRACK = "2.25rem";
17
+ const FOCUSABLE = "input:not([disabled]):not([type=hidden]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex='-1'])";
18
+ function defaultKey(item, index) {
19
+ if (typeof item === "object" && item !== null && "id" in item) {
20
+ const id = item.id;
21
+ if (typeof id === "string" || typeof id === "number") return id;
22
+ }
23
+ return index;
24
+ }
25
+ function hasContent(node) {
26
+ return node !== void 0 && node !== null && node !== false && node !== "";
27
+ }
28
+ function LineItems({
29
+ items,
30
+ columns,
31
+ getKey = defaultKey,
32
+ onAdd,
33
+ onRemove,
34
+ minItems = 0,
35
+ maxItems,
36
+ confirmRemove,
37
+ addLabel,
38
+ rowLabel,
39
+ totals,
40
+ totalsLabel,
41
+ empty,
42
+ error,
43
+ disabled = false,
44
+ labels: labelsProp,
45
+ className,
46
+ style,
47
+ ...rest
48
+ }) {
49
+ const labels = useKitLabels("lineItems", DEFAULT_LINE_ITEMS_LABELS, labelsProp);
50
+ const rowRefs = useRef([]);
51
+ const addRef = useRef(null);
52
+ const pendingFocus = useRef(null);
53
+ const [confirmingAt, setConfirming] = useState(null);
54
+ const confirming = confirmingAt !== null && confirmingAt < items.length ? confirmingAt : null;
55
+ const tracks = [...columns.map((c) => c.width ?? "minmax(0,1fr)"), ...onRemove ? [REMOVE_TRACK] : []];
56
+ const gridStyle = { "--line-items-cols": tracks.join(" ") };
57
+ const canAdd = Boolean(onAdd) && !disabled && (maxItems === void 0 || items.length < maxItems);
58
+ const canRemove = Boolean(onRemove) && !disabled && items.length > minItems;
59
+ const hasError = hasContent(error);
60
+ useLayoutEffect(() => {
61
+ const pending = pendingFocus.current;
62
+ if (!pending) return;
63
+ pendingFocus.current = null;
64
+ if (pending.kind === "added") {
65
+ const row2 = rowRefs.current[items.length - 1];
66
+ row2?.querySelector(FOCUSABLE)?.focus();
67
+ return;
68
+ }
69
+ const row = rowRefs.current[Math.min(pending.index, items.length - 1)];
70
+ const target = row?.querySelector("[data-line-items-remove]:not([disabled])") ?? row?.querySelector(FOCUSABLE) ?? addRef.current;
71
+ target?.focus();
72
+ }, [items.length]);
73
+ const add = useCallback(() => {
74
+ if (!canAdd || !onAdd) return;
75
+ pendingFocus.current = { kind: "added" };
76
+ onAdd();
77
+ }, [canAdd, onAdd]);
78
+ const removeAt = async (index) => {
79
+ if (!canRemove || !onRemove) return;
80
+ if (confirmRemove === true && confirming !== index) {
81
+ setConfirming(index);
82
+ return;
83
+ }
84
+ if (typeof confirmRemove === "function") {
85
+ const ok = await confirmRemove(items[index], index);
86
+ if (!ok) return;
87
+ }
88
+ setConfirming(null);
89
+ pendingFocus.current = { kind: "removed", index };
90
+ onRemove(index);
91
+ };
92
+ const onRowKeyDown = (e) => {
93
+ if (e.key === "Enter" && (e.ctrlKey || e.metaKey) && canAdd) {
94
+ e.preventDefault();
95
+ add();
96
+ }
97
+ };
98
+ const columnName = (column) => column.label ?? (typeof column.header === "string" ? column.header : column.key);
99
+ const headerCell = (column) => /* @__PURE__ */ jsx(
100
+ "span",
101
+ {
102
+ className: cn(
103
+ "min-w-0 truncate text-xs font-medium text-[var(--text-muted)]",
104
+ column.align === "end" && "text-end"
105
+ ),
106
+ children: column.header
107
+ },
108
+ column.key
109
+ );
110
+ const showTotals = totals !== void 0;
111
+ const labelColumn = showTotals ? columns.find((c) => !hasContent(totals[c.key]))?.key : void 0;
112
+ return /* @__PURE__ */ jsxs(
113
+ "div",
114
+ {
115
+ "data-slot": "line-items",
116
+ ...rest,
117
+ style: { ...gridStyle, ...style },
118
+ className: cn("@container space-y-2", className),
119
+ children: [
120
+ /* @__PURE__ */ jsxs("div", { "aria-hidden": true, className: "hidden gap-2 @lg:grid @lg:grid-cols-[var(--line-items-cols)]", children: [
121
+ columns.map(headerCell),
122
+ onRemove && /* @__PURE__ */ jsx("span", {})
123
+ ] }),
124
+ items.length === 0 && hasContent(empty) && /* @__PURE__ */ jsx("div", { className: "rounded-md border border-dashed border-[var(--border)] px-3 py-4 text-center text-sm text-[var(--text-muted)]", children: empty }),
125
+ items.map((item, index) => {
126
+ const row = index + 1;
127
+ const isConfirming = confirming === index;
128
+ return (
129
+ // The row's Ctrl/⌘+Enter is a shortcut for the add button, which is itself
130
+ // reachable by Tab; the group only listens for it bubbling from its fields.
131
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
132
+ /* @__PURE__ */ jsxs(
133
+ "div",
134
+ {
135
+ ref: (el) => {
136
+ rowRefs.current[index] = el;
137
+ },
138
+ role: "group",
139
+ "aria-label": rowLabel ? rowLabel(index, item) : labels.row(row),
140
+ "data-line-items-row": "",
141
+ onKeyDown: onRowKeyDown,
142
+ className: cn(
143
+ "grid grid-cols-1 gap-2 @lg:grid-cols-[var(--line-items-cols)] @lg:items-start",
144
+ // Stacked, each row is a card of its own so the rows stay apart.
145
+ "@max-lg:rounded-md @max-lg:border @max-lg:border-[var(--border)] @max-lg:p-3"
146
+ ),
147
+ children: [
148
+ columns.map((column) => /* @__PURE__ */ jsxs("div", { className: "min-w-0 space-y-1 @lg:space-y-0", children: [
149
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "block text-xs font-medium text-[var(--text-muted)] @lg:hidden", children: column.header }),
150
+ column.render({ item, index, label: labels.cell(columnName(column), row) })
151
+ ] }, column.key)),
152
+ onRemove && /* @__PURE__ */ jsx("div", { className: "flex justify-end @lg:justify-center", children: /* @__PURE__ */ jsx(
153
+ IconButton,
154
+ {
155
+ type: "button",
156
+ "data-line-items-remove": "",
157
+ variant: isConfirming ? "danger" : "ghost",
158
+ tone: isConfirming ? void 0 : "danger",
159
+ label: isConfirming ? labels.confirmRemove(row) : labels.remove(row),
160
+ disabled: !canRemove,
161
+ onClick: () => void removeAt(index),
162
+ onBlur: () => {
163
+ if (isConfirming) setConfirming(null);
164
+ },
165
+ onKeyDown: (e) => {
166
+ if (e.key === "Escape" && isConfirming) {
167
+ e.stopPropagation();
168
+ setConfirming(null);
169
+ }
170
+ },
171
+ children: /* @__PURE__ */ jsx(Trash2, {})
172
+ }
173
+ ) })
174
+ ]
175
+ },
176
+ getKey(item, index)
177
+ )
178
+ );
179
+ }),
180
+ showTotals && /* @__PURE__ */ jsxs(
181
+ "div",
182
+ {
183
+ role: "group",
184
+ "aria-label": typeof totalsLabel === "string" ? totalsLabel : labels.totals,
185
+ "data-line-items-totals": "",
186
+ className: "grid grid-cols-1 gap-2 border-t border-[var(--border)] pt-2 text-sm font-semibold tabular-nums text-[var(--text-primary)] @lg:grid-cols-[var(--line-items-cols)]",
187
+ children: [
188
+ columns.map((column) => {
189
+ const value = totals[column.key];
190
+ if (column.key === labelColumn) {
191
+ return /* @__PURE__ */ jsx("span", { className: "min-w-0", children: totalsLabel ?? labels.totals }, column.key);
192
+ }
193
+ return /* @__PURE__ */ jsx(
194
+ "span",
195
+ {
196
+ className: cn(
197
+ "min-w-0",
198
+ column.align === "end" && "@lg:text-end",
199
+ // Stacked, a column with no total takes no line.
200
+ !hasContent(value) && "@max-lg:hidden"
201
+ ),
202
+ children: hasContent(value) && /* @__PURE__ */ jsxs("span", { className: "flex justify-between gap-2 @lg:block", children: [
203
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "font-normal text-[var(--text-muted)] @lg:hidden", children: column.header }),
204
+ value
205
+ ] })
206
+ },
207
+ column.key
208
+ );
209
+ }),
210
+ onRemove && /* @__PURE__ */ jsx("span", { className: "@max-lg:hidden" })
211
+ ]
212
+ }
213
+ ),
214
+ hasError && /* @__PURE__ */ jsx("p", { className: "text-[11px] leading-tight text-[var(--danger)]", children: error }),
215
+ onAdd && /* @__PURE__ */ jsxs(
216
+ Button,
217
+ {
218
+ ref: addRef,
219
+ type: "button",
220
+ variant: "secondary",
221
+ className: "w-full border-dashed",
222
+ disabled: !canAdd,
223
+ onClick: add,
224
+ children: [
225
+ /* @__PURE__ */ jsx(Plus, { "aria-hidden": true, className: "size-4" }),
226
+ addLabel ?? labels.add
227
+ ]
228
+ }
229
+ )
230
+ ]
231
+ }
232
+ );
233
+ }
234
+ export {
235
+ DEFAULT_LINE_ITEMS_LABELS,
236
+ LineItems
237
+ };
238
+ //# sourceMappingURL=line-items.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/line-items.tsx"],"sourcesContent":["import { useCallback, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, CSSProperties, KeyboardEvent, Key, ReactNode } from \"react\";\nimport { Plus, Trash2 } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { Button, IconButton } from \"./ui\";\n\n/** The words {@link LineItems} renders on its own behalf. Row numbers are 1-based. */\nexport interface LineItemsLabels {\n /** The dashed add button. */\n add: string;\n /** The remove button's name for one row. */\n remove: (row: number) => string;\n /** Its name while it waits for the second press (`confirmRemove`). */\n confirmRemove: (row: number) => string;\n /** A row's group name, when the caller gives no `rowLabel`. */\n row: (row: number) => string;\n /** The name a cell's field should carry: the column, and which row. */\n cell: (column: string, row: number) => string;\n /** The totals row's label, and its group name. */\n totals: string;\n}\n\nexport const DEFAULT_LINE_ITEMS_LABELS: LineItemsLabels = {\n add: \"Add row\",\n remove: (row) => `Remove row ${row}`,\n confirmRemove: (row) => `Remove row ${row}? Press again to confirm`,\n row: (row) => `Row ${row}`,\n cell: (column, row) => `${column}, row ${row}`,\n totals: \"Total\",\n};\n\n/** What a column's `render` receives for one row. */\nexport interface LineItemCellContext<T> {\n item: T;\n /** 0-based. */\n index: number;\n /** The accessible name for the cell's field — \"Debit, row 2\". The header is\n * hidden from assistive tech (each row repeats it), so the field has to carry it:\n * `<Input aria-label={label} … />`. */\n label: string;\n}\n\nexport interface LineItemsColumn<T> {\n /** Stable id: the React key, and the key into `totals`. */\n key: string;\n /** The column header (and the small label over the cell once the rows stack). */\n header: ReactNode;\n /** The column's name for {@link LineItemCellContext.label}, when `header` is not a\n * string. */\n label?: string;\n /** The grid track once the rows are wide enough to be a table. Default\n * `minmax(0,1fr)`; `\"7rem\"` for an amount. */\n width?: string;\n /** `end` for figures: the header and the totals cell end-align with the field. */\n align?: \"start\" | \"end\";\n render: (ctx: LineItemCellContext<T>) => ReactNode;\n}\n\nexport interface LineItemsProps<T> extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n items: readonly T[];\n columns: readonly LineItemsColumn<T>[];\n /** The row's React key. Default: the item's `id` (react-hook-form's `useFieldArray`\n * `fields` carry one), else its index. */\n getKey?: (item: T, index: number) => Key;\n /** Append a row. Omitted: no add button. The first field of the new row is focused. */\n onAdd?: () => void;\n /** Remove the row at `index`. Omitted: no remove buttons. */\n onRemove?: (index: number) => void;\n /** Remove is disabled at or below this many rows (a journal entry needs two).\n * Default 0. */\n minItems?: number;\n /** Add is disabled at this many rows. */\n maxItems?: number;\n /**\n * Ask before a row goes:\n * - `true`: the remove button asks for a second press — it fills red and its name\n * becomes `lineItems.confirmRemove`; Escape, or leaving it, calls it off.\n * - a function: called with the row, and the row goes only if it answers `true`\n * (`(item, i) => confirm({ title: … , tone: \"danger\" })` with `useConfirm`).\n */\n confirmRemove?: boolean | ((item: T, index: number) => boolean | Promise<boolean>);\n /** The add button's text. Default `lineItems.add`. */\n addLabel?: ReactNode;\n /** A row's group name (\"Line 2\"). Default `lineItems.row`. */\n rowLabel?: (index: number, item: T) => string;\n /** A totals row under the lines, one entry per column `key`. The first column\n * without an entry carries {@link totalsLabel}. Omitted: no totals row. */\n totals?: Partial<Record<string, ReactNode>>;\n /** Default `lineItems.totals`, \"Total\". */\n totalsLabel?: ReactNode;\n /** Shown in place of the rows when there are none. */\n empty?: ReactNode;\n /** A message about the list as a whole (\"The entry does not balance\"). */\n error?: ReactNode;\n /** Disables add and remove (the fields are the caller's to disable). */\n disabled?: boolean;\n labels?: Partial<LineItemsLabels>;\n}\n\nconst REMOVE_TRACK = \"2.25rem\";\nconst FOCUSABLE =\n \"input:not([disabled]):not([type=hidden]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex='-1'])\";\n\nfunction defaultKey(item: unknown, index: number): Key {\n if (typeof item === \"object\" && item !== null && \"id\" in item) {\n const id = (item as { id: unknown }).id;\n if (typeof id === \"string\" || typeof id === \"number\") return id;\n }\n return index;\n}\n\nfunction hasContent(node: ReactNode): boolean {\n return node !== undefined && node !== null && node !== false && node !== \"\";\n}\n\ntype PendingFocus = { kind: \"added\" } | { kind: \"removed\"; index: number };\n\n/**\n * An editable list of lines — journal entry lines, lease components, invoice\n * positions, budget rows: column headers, a row of fields per item, a remove button\n * per row, a dashed \"add\" button under them, and an optional totals row.\n *\n * Presentation only: the rows are the caller's `items` and the fields are the\n * caller's, rendered per column. So it works over plain state —\n *\n * ```tsx\n * <LineItems items={lines} columns={cols} onAdd={() => setLines([...lines, blank()])}\n * onRemove={(i) => setLines(lines.filter((_, j) => j !== i))} minItems={2} />\n * ```\n *\n * — and over react-hook-form's `useFieldArray` alike (`items={fields}`, `onAdd={() =>\n * append(blank())}`, `onRemove={remove}`), for which `@eifi1/ui-kit/rhf` has\n * `RhfLineItems`.\n *\n * On a wide pane the rows are a grid under the headers; on a narrow one (a container\n * query, so a dialog stacks where it is cramped) each row stacks its fields, each\n * under a small copy of its header. The headers are `aria-hidden`: every row is a\n * named group and every field is named by `render`'s `label` (\"Debit, row 2\"), which\n * is what a screen reader needs — a header row it would read once, out of context.\n *\n * Keyboard: after Add, focus lands in the new row's first field; after a remove, on\n * the row that took its place (else the add button). Ctrl/⌘+Enter in any row adds a\n * row.\n */\nexport function LineItems<T>({\n items,\n columns,\n getKey = defaultKey,\n onAdd,\n onRemove,\n minItems = 0,\n maxItems,\n confirmRemove,\n addLabel,\n rowLabel,\n totals,\n totalsLabel,\n empty,\n error,\n disabled = false,\n labels: labelsProp,\n className,\n style,\n ...rest\n}: LineItemsProps<T>) {\n const labels = useKitLabels(\"lineItems\", DEFAULT_LINE_ITEMS_LABELS, labelsProp);\n const rowRefs = useRef<Array<HTMLDivElement | null>>([]);\n const addRef = useRef<HTMLButtonElement>(null);\n const pendingFocus = useRef<PendingFocus | null>(null);\n const [confirmingAt, setConfirming] = useState<number | null>(null);\n // A confirm waiting on a row that has since gone is void.\n const confirming = confirmingAt !== null && confirmingAt < items.length ? confirmingAt : null;\n\n const tracks = [...columns.map((c) => c.width ?? \"minmax(0,1fr)\"), ...(onRemove ? [REMOVE_TRACK] : [])];\n const gridStyle = { \"--line-items-cols\": tracks.join(\" \") } as CSSProperties;\n const canAdd = Boolean(onAdd) && !disabled && (maxItems === undefined || items.length < maxItems);\n const canRemove = Boolean(onRemove) && !disabled && items.length > minItems;\n const hasError = hasContent(error);\n\n\n // Focus after the list has re-rendered with the new rows — a layout effect, so the\n // caret is where it belongs before paint.\n useLayoutEffect(() => {\n const pending = pendingFocus.current;\n if (!pending) return;\n pendingFocus.current = null;\n if (pending.kind === \"added\") {\n const row = rowRefs.current[items.length - 1];\n row?.querySelector<HTMLElement>(FOCUSABLE)?.focus();\n return;\n }\n const row = rowRefs.current[Math.min(pending.index, items.length - 1)];\n const target =\n row?.querySelector<HTMLElement>(\"[data-line-items-remove]:not([disabled])\") ??\n row?.querySelector<HTMLElement>(FOCUSABLE) ??\n addRef.current;\n target?.focus();\n }, [items.length]);\n\n const add = useCallback(() => {\n if (!canAdd || !onAdd) return;\n pendingFocus.current = { kind: \"added\" };\n onAdd();\n }, [canAdd, onAdd]);\n\n const removeAt = async (index: number) => {\n if (!canRemove || !onRemove) return;\n if (confirmRemove === true && confirming !== index) {\n setConfirming(index);\n return;\n }\n if (typeof confirmRemove === \"function\") {\n const ok = await confirmRemove(items[index], index);\n if (!ok) return;\n }\n setConfirming(null);\n pendingFocus.current = { kind: \"removed\", index };\n onRemove(index);\n };\n\n const onRowKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n if (e.key === \"Enter\" && (e.ctrlKey || e.metaKey) && canAdd) {\n e.preventDefault();\n add();\n }\n };\n\n const columnName = (column: LineItemsColumn<T>) =>\n column.label ?? (typeof column.header === \"string\" ? column.header : column.key);\n\n const headerCell = (column: LineItemsColumn<T>) => (\n <span\n key={column.key}\n className={cn(\n \"min-w-0 truncate text-xs font-medium text-[var(--text-muted)]\",\n column.align === \"end\" && \"text-end\",\n )}\n >\n {column.header}\n </span>\n );\n\n const showTotals = totals !== undefined;\n const labelColumn = showTotals ? columns.find((c) => !hasContent(totals[c.key]))?.key : undefined;\n\n return (\n <div\n data-slot=\"line-items\"\n {...rest}\n style={{ ...gridStyle, ...style }}\n className={cn(\"@container space-y-2\", className)}\n >\n <div aria-hidden className=\"hidden gap-2 @lg:grid @lg:grid-cols-[var(--line-items-cols)]\">\n {columns.map(headerCell)}\n {onRemove && <span />}\n </div>\n\n {items.length === 0 && hasContent(empty) && (\n <div className=\"rounded-md border border-dashed border-[var(--border)] px-3 py-4 text-center text-sm text-[var(--text-muted)]\">\n {empty}\n </div>\n )}\n\n {items.map((item, index) => {\n const row = index + 1;\n const isConfirming = confirming === index;\n return (\n // The row's Ctrl/⌘+Enter is a shortcut for the add button, which is itself\n // reachable by Tab; the group only listens for it bubbling from its fields.\n // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions\n <div\n key={getKey(item, index)}\n ref={(el) => {\n rowRefs.current[index] = el;\n }}\n role=\"group\"\n aria-label={rowLabel ? rowLabel(index, item) : labels.row(row)}\n data-line-items-row=\"\"\n onKeyDown={onRowKeyDown}\n className={cn(\n \"grid grid-cols-1 gap-2 @lg:grid-cols-[var(--line-items-cols)] @lg:items-start\",\n // Stacked, each row is a card of its own so the rows stay apart.\n \"@max-lg:rounded-md @max-lg:border @max-lg:border-[var(--border)] @max-lg:p-3\",\n )}\n >\n {columns.map((column) => (\n <div key={column.key} className=\"min-w-0 space-y-1 @lg:space-y-0\">\n <span aria-hidden className=\"block text-xs font-medium text-[var(--text-muted)] @lg:hidden\">\n {column.header}\n </span>\n {column.render({ item, index, label: labels.cell(columnName(column), row) })}\n </div>\n ))}\n {onRemove && (\n <div className=\"flex justify-end @lg:justify-center\">\n <IconButton\n type=\"button\"\n data-line-items-remove=\"\"\n variant={isConfirming ? \"danger\" : \"ghost\"}\n tone={isConfirming ? undefined : \"danger\"}\n label={isConfirming ? labels.confirmRemove(row) : labels.remove(row)}\n disabled={!canRemove}\n onClick={() => void removeAt(index)}\n onBlur={() => {\n if (isConfirming) setConfirming(null);\n }}\n onKeyDown={(e) => {\n if (e.key === \"Escape\" && isConfirming) {\n e.stopPropagation();\n setConfirming(null);\n }\n }}\n >\n <Trash2 />\n </IconButton>\n </div>\n )}\n </div>\n );\n })}\n\n {showTotals && (\n <div\n role=\"group\"\n aria-label={typeof totalsLabel === \"string\" ? totalsLabel : labels.totals}\n data-line-items-totals=\"\"\n className=\"grid grid-cols-1 gap-2 border-t border-[var(--border)] pt-2 text-sm font-semibold tabular-nums text-[var(--text-primary)] @lg:grid-cols-[var(--line-items-cols)]\"\n >\n {columns.map((column) => {\n const value = totals[column.key];\n if (column.key === labelColumn) {\n return (\n <span key={column.key} className=\"min-w-0\">\n {totalsLabel ?? labels.totals}\n </span>\n );\n }\n return (\n <span\n key={column.key}\n className={cn(\n \"min-w-0\",\n column.align === \"end\" && \"@lg:text-end\",\n // Stacked, a column with no total takes no line.\n !hasContent(value) && \"@max-lg:hidden\",\n )}\n >\n {hasContent(value) && (\n <span className=\"flex justify-between gap-2 @lg:block\">\n <span aria-hidden className=\"font-normal text-[var(--text-muted)] @lg:hidden\">\n {column.header}\n </span>\n {value}\n </span>\n )}\n </span>\n );\n })}\n {onRemove && <span className=\"@max-lg:hidden\" />}\n </div>\n )}\n\n {hasError && (\n <p className=\"text-[11px] leading-tight text-[var(--danger)]\">\n {error}\n </p>\n )}\n\n {onAdd && (\n <Button\n ref={addRef}\n type=\"button\"\n variant=\"secondary\"\n className=\"w-full border-dashed\"\n disabled={!canAdd}\n onClick={add}\n >\n <Plus aria-hidden className=\"size-4\" />\n {addLabel ?? labels.add}\n </Button>\n )}\n </div>\n );\n}\n"],"mappings":";AAwOI,cAqBE,YArBF;AAxOJ,SAAS,aAAa,iBAAiB,QAAQ,gBAAgB;AAE/D,SAAS,MAAM,cAAc;AAC7B,SAAS,UAAU;AACnB,SAAS,oBAAoB;AAC7B,SAAS,QAAQ,kBAAkB;AAkB5B,MAAM,4BAA6C;AAAA,EACxD,KAAK;AAAA,EACL,QAAQ,CAAC,QAAQ,cAAc,GAAG;AAAA,EAClC,eAAe,CAAC,QAAQ,cAAc,GAAG;AAAA,EACzC,KAAK,CAAC,QAAQ,OAAO,GAAG;AAAA,EACxB,MAAM,CAAC,QAAQ,QAAQ,GAAG,MAAM,SAAS,GAAG;AAAA,EAC5C,QAAQ;AACV;AAsEA,MAAM,eAAe;AACrB,MAAM,YACJ;AAEF,SAAS,WAAW,MAAe,OAAoB;AACrD,MAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,QAAQ,MAAM;AAC7D,UAAM,KAAM,KAAyB;AACrC,QAAI,OAAO,OAAO,YAAY,OAAO,OAAO,SAAU,QAAO;AAAA,EAC/D;AACA,SAAO;AACT;AAEA,SAAS,WAAW,MAA0B;AAC5C,SAAO,SAAS,UAAa,SAAS,QAAQ,SAAS,SAAS,SAAS;AAC3E;AA+BO,SAAS,UAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA,SAAS;AAAA,EACT;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAsB;AACpB,QAAM,SAAS,aAAa,aAAa,2BAA2B,UAAU;AAC9E,QAAM,UAAU,OAAqC,CAAC,CAAC;AACvD,QAAM,SAAS,OAA0B,IAAI;AAC7C,QAAM,eAAe,OAA4B,IAAI;AACrD,QAAM,CAAC,cAAc,aAAa,IAAI,SAAwB,IAAI;AAElE,QAAM,aAAa,iBAAiB,QAAQ,eAAe,MAAM,SAAS,eAAe;AAEzF,QAAM,SAAS,CAAC,GAAG,QAAQ,IAAI,CAAC,MAAM,EAAE,SAAS,eAAe,GAAG,GAAI,WAAW,CAAC,YAAY,IAAI,CAAC,CAAE;AACtG,QAAM,YAAY,EAAE,qBAAqB,OAAO,KAAK,GAAG,EAAE;AAC1D,QAAM,SAAS,QAAQ,KAAK,KAAK,CAAC,aAAa,aAAa,UAAa,MAAM,SAAS;AACxF,QAAM,YAAY,QAAQ,QAAQ,KAAK,CAAC,YAAY,MAAM,SAAS;AACnE,QAAM,WAAW,WAAW,KAAK;AAKjC,kBAAgB,MAAM;AACpB,UAAM,UAAU,aAAa;AAC7B,QAAI,CAAC,QAAS;AACd,iBAAa,UAAU;AACvB,QAAI,QAAQ,SAAS,SAAS;AAC5B,YAAMA,OAAM,QAAQ,QAAQ,MAAM,SAAS,CAAC;AAC5C,MAAAA,MAAK,cAA2B,SAAS,GAAG,MAAM;AAClD;AAAA,IACF;AACA,UAAM,MAAM,QAAQ,QAAQ,KAAK,IAAI,QAAQ,OAAO,MAAM,SAAS,CAAC,CAAC;AACrE,UAAM,SACJ,KAAK,cAA2B,0CAA0C,KAC1E,KAAK,cAA2B,SAAS,KACzC,OAAO;AACT,YAAQ,MAAM;AAAA,EAChB,GAAG,CAAC,MAAM,MAAM,CAAC;AAEjB,QAAM,MAAM,YAAY,MAAM;AAC5B,QAAI,CAAC,UAAU,CAAC,MAAO;AACvB,iBAAa,UAAU,EAAE,MAAM,QAAQ;AACvC,UAAM;AAAA,EACR,GAAG,CAAC,QAAQ,KAAK,CAAC;AAElB,QAAM,WAAW,OAAO,UAAkB;AACxC,QAAI,CAAC,aAAa,CAAC,SAAU;AAC7B,QAAI,kBAAkB,QAAQ,eAAe,OAAO;AAClD,oBAAc,KAAK;AACnB;AAAA,IACF;AACA,QAAI,OAAO,kBAAkB,YAAY;AACvC,YAAM,KAAK,MAAM,cAAc,MAAM,KAAK,GAAG,KAAK;AAClD,UAAI,CAAC,GAAI;AAAA,IACX;AACA,kBAAc,IAAI;AAClB,iBAAa,UAAU,EAAE,MAAM,WAAW,MAAM;AAChD,aAAS,KAAK;AAAA,EAChB;AAEA,QAAM,eAAe,CAAC,MAAqC;AACzD,QAAI,EAAE,QAAQ,YAAY,EAAE,WAAW,EAAE,YAAY,QAAQ;AAC3D,QAAE,eAAe;AACjB,UAAI;AAAA,IACN;AAAA,EACF;AAEA,QAAM,aAAa,CAAC,WAClB,OAAO,UAAU,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS,OAAO;AAE9E,QAAM,aAAa,CAAC,WAClB;AAAA,IAAC;AAAA;AAAA,MAEC,WAAW;AAAA,QACT;AAAA,QACA,OAAO,UAAU,SAAS;AAAA,MAC5B;AAAA,MAEC,iBAAO;AAAA;AAAA,IANH,OAAO;AAAA,EAOd;AAGF,QAAM,aAAa,WAAW;AAC9B,QAAM,cAAc,aAAa,QAAQ,KAAK,CAAC,MAAM,CAAC,WAAW,OAAO,EAAE,GAAG,CAAC,CAAC,GAAG,MAAM;AAExF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,aAAU;AAAA,MACT,GAAG;AAAA,MACJ,OAAO,EAAE,GAAG,WAAW,GAAG,MAAM;AAAA,MAChC,WAAW,GAAG,wBAAwB,SAAS;AAAA,MAE/C;AAAA,6BAAC,SAAI,eAAW,MAAC,WAAU,gEACxB;AAAA,kBAAQ,IAAI,UAAU;AAAA,UACtB,YAAY,oBAAC,UAAK;AAAA,WACrB;AAAA,QAEC,MAAM,WAAW,KAAK,WAAW,KAAK,KACrC,oBAAC,SAAI,WAAU,iHACZ,iBACH;AAAA,QAGD,MAAM,IAAI,CAAC,MAAM,UAAU;AAC1B,gBAAM,MAAM,QAAQ;AACpB,gBAAM,eAAe,eAAe;AACpC;AAAA;AAAA;AAAA;AAAA,YAIE;AAAA,cAAC;AAAA;AAAA,gBAEC,KAAK,CAAC,OAAO;AACX,0BAAQ,QAAQ,KAAK,IAAI;AAAA,gBAC3B;AAAA,gBACA,MAAK;AAAA,gBACL,cAAY,WAAW,SAAS,OAAO,IAAI,IAAI,OAAO,IAAI,GAAG;AAAA,gBAC7D,uBAAoB;AAAA,gBACpB,WAAW;AAAA,gBACX,WAAW;AAAA,kBACT;AAAA;AAAA,kBAEA;AAAA,gBACF;AAAA,gBAEC;AAAA,0BAAQ,IAAI,CAAC,WACZ,qBAAC,SAAqB,WAAU,mCAC9B;AAAA,wCAAC,UAAK,eAAW,MAAC,WAAU,iEACzB,iBAAO,QACV;AAAA,oBACC,OAAO,OAAO,EAAE,MAAM,OAAO,OAAO,OAAO,KAAK,WAAW,MAAM,GAAG,GAAG,EAAE,CAAC;AAAA,uBAJnE,OAAO,GAKjB,CACD;AAAA,kBACA,YACC,oBAAC,SAAI,WAAU,uCACb;AAAA,oBAAC;AAAA;AAAA,sBACC,MAAK;AAAA,sBACL,0BAAuB;AAAA,sBACvB,SAAS,eAAe,WAAW;AAAA,sBACnC,MAAM,eAAe,SAAY;AAAA,sBACjC,OAAO,eAAe,OAAO,cAAc,GAAG,IAAI,OAAO,OAAO,GAAG;AAAA,sBACnE,UAAU,CAAC;AAAA,sBACX,SAAS,MAAM,KAAK,SAAS,KAAK;AAAA,sBAClC,QAAQ,MAAM;AACZ,4BAAI,aAAc,eAAc,IAAI;AAAA,sBACtC;AAAA,sBACA,WAAW,CAAC,MAAM;AAChB,4BAAI,EAAE,QAAQ,YAAY,cAAc;AACtC,4BAAE,gBAAgB;AAClB,wCAAc,IAAI;AAAA,wBACpB;AAAA,sBACF;AAAA,sBAEA,8BAAC,UAAO;AAAA;AAAA,kBACV,GACF;AAAA;AAAA;AAAA,cA5CG,OAAO,MAAM,KAAK;AAAA,YA8CzB;AAAA;AAAA,QAEJ,CAAC;AAAA,QAEA,cACC;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,cAAY,OAAO,gBAAgB,WAAW,cAAc,OAAO;AAAA,YACnE,0BAAuB;AAAA,YACvB,WAAU;AAAA,YAET;AAAA,sBAAQ,IAAI,CAAC,WAAW;AACvB,sBAAM,QAAQ,OAAO,OAAO,GAAG;AAC/B,oBAAI,OAAO,QAAQ,aAAa;AAC9B,yBACE,oBAAC,UAAsB,WAAU,WAC9B,yBAAe,OAAO,UADd,OAAO,GAElB;AAAA,gBAEJ;AACA,uBACE;AAAA,kBAAC;AAAA;AAAA,oBAEC,WAAW;AAAA,sBACT;AAAA,sBACA,OAAO,UAAU,SAAS;AAAA;AAAA,sBAE1B,CAAC,WAAW,KAAK,KAAK;AAAA,oBACxB;AAAA,oBAEC,qBAAW,KAAK,KACf,qBAAC,UAAK,WAAU,wCACd;AAAA,0CAAC,UAAK,eAAW,MAAC,WAAU,mDACzB,iBAAO,QACV;AAAA,sBACC;AAAA,uBACH;AAAA;AAAA,kBAdG,OAAO;AAAA,gBAgBd;AAAA,cAEJ,CAAC;AAAA,cACA,YAAY,oBAAC,UAAK,WAAU,kBAAiB;AAAA;AAAA;AAAA,QAChD;AAAA,QAGD,YACC,oBAAC,OAAE,WAAU,kDACV,iBACH;AAAA,QAGD,SACC;AAAA,UAAC;AAAA;AAAA,YACC,KAAK;AAAA,YACL,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,WAAU;AAAA,YACV,UAAU,CAAC;AAAA,YACX,SAAS;AAAA,YAET;AAAA,kCAAC,QAAK,eAAW,MAAC,WAAU,UAAS;AAAA,cACpC,YAAY,OAAO;AAAA;AAAA;AAAA,QACtB;AAAA;AAAA;AAAA,EAEJ;AAEJ;","names":["row"]}
@@ -1,5 +1,5 @@
1
1
  import * as react from 'react';
2
- import { ComponentPropsWithoutRef, ReactNode, MouseEvent, Ref, ReactElement } from 'react';
2
+ import { ComponentPropsWithoutRef, ReactNode, HTMLAttributes, MouseEvent, Ref, ReactElement } from 'react';
3
3
  import { LucideIcon } from 'lucide-react';
4
4
 
5
5
  interface ListLabels {
@@ -79,6 +79,26 @@ interface ListItemBaseProps {
79
79
  * on "Copy" from selecting the row.
80
80
  */
81
81
  actions?: ReactNode;
82
+ /**
83
+ * Rich content UNDER the title row and outside its target: photos, per-row buttons,
84
+ * form fields — kastlan's handover room inspector (room-inspector.tsx), the handover
85
+ * detail page and the wizard's defects step (wizard/defects-step.tsx), which draw
86
+ * each room or defect as a `Card variant="inset"` because a row could not hold a
87
+ * photo strip with its own buttons.
88
+ *
89
+ * The same rule as {@link actions}, one row down: the title area stays ONE named
90
+ * target (the button or link, with the title and subtitle as its name), and the body
91
+ * is its sibling, never nested in it — a thumbnail's "Remove" inside the row's button
92
+ * would be invalid HTML and unreachable by a screen reader. The body is a
93
+ * `role="group"` named by the row's title (`aria-labelledby`), so tabbing into the
94
+ * third photo's button of the second room is heard as "Kitchen, group" first rather
95
+ * than as a button floating free of any row. Inside the row's box, so a `selected`
96
+ * border and a `divider` hold both; the hover fill stays on the target alone, since
97
+ * the body is not what a click on the row does.
98
+ */
99
+ children?: ReactNode;
100
+ /** Classes on the {@link children} body. */
101
+ bodyClassName?: string;
82
102
  /**
83
103
  * The unread mark: a brand dot at the row's end, the title in semibold, and "Unread"
84
104
  * read after the title (keksdose notification-inbox.tsx:215). For another status,
@@ -110,6 +130,19 @@ interface ListItemBaseProps {
110
130
  className?: string;
111
131
  /** Classes on the main target (the button, link or static block). */
112
132
  targetClassName?: string;
133
+ /**
134
+ * Extra attributes and handlers for the main target — e.g. HTML5 drag-and-drop
135
+ * (`draggable`, `onDragStart`, `onDragEnd`): lenkbank's segment list drags the WHOLE
136
+ * 44px row, and a grip inside the button cannot start a drag in Firefox. Spread first,
137
+ * so the row's own role, name, state and click handling always win.
138
+ */
139
+ targetProps?: HTMLAttributes<HTMLElement>;
140
+ /**
141
+ * A visible border while unselected, instead of the transparent one. For rows laid out
142
+ * as a wrapping strip (lenkbank's profile bar), where borderless rows read as loose
143
+ * text. The selected row's brand border is unchanged.
144
+ */
145
+ bordered?: boolean;
113
146
  /** `div` for a single row outside a {@link List}; an `<li>` must sit in a list. */
114
147
  as?: "li" | "div";
115
148
  /** Reaches the main target — the element with the row's name and action. */
@@ -135,8 +168,9 @@ type ListItemProps = ListItemBaseProps & ({
135
168
  * cannot do.
136
169
  */
137
170
  href: string;
138
- /** Your router's link for an in-app `href`. Default `<a>`. The API `Chip`,
139
- * `StatTile` and `MenuItem` share. Ignored for an `external` row. */
171
+ /** Your router's link for an in-app `href`. Default: the `<UiKitProvider
172
+ * linkComponent>`, then `<a>`. The API `Chip`, `StatTile` and `MenuItem`
173
+ * share. Ignored for an `external` row. */
140
174
  renderLink?: (props: ListItemLinkProps) => ReactElement;
141
175
  /**
142
176
  * The link leaves the app: a plain `<a target="_blank" rel="noopener
@@ -1,11 +1,12 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
- import { createContext, forwardRef, useContext } from "react";
3
+ import { createContext, forwardRef, useContext, useId } from "react";
4
4
  import { ExternalLink } from "lucide-react";
5
5
  import { cn } from "../lib/cn.js";
6
6
  import { Spinner } from "./ui.js";
7
7
  import { StatusDot } from "./status-dot.js";
8
- import { useKitLabels } from "../i18n/kit-labels.js";
8
+ import { useKitLabels, useKitLink } from "../i18n/kit-labels.js";
9
+ import { pickLinkRenderer } from "./text-link.js";
9
10
  const DEFAULT_LIST_LABELS = {
10
11
  unread: "Unread",
11
12
  opensInNewTab: "opens in a new tab"
@@ -32,9 +33,9 @@ const List = forwardRef(function List2({ as = "ul", density = "default", separat
32
33
  ) });
33
34
  });
34
35
  const PAD = {
35
- compact: { target: "gap-2 px-2 py-1.5", actions: "pe-1.5", gap: "gap-0.5" },
36
- default: { target: "gap-3 px-3 py-2.5", actions: "pe-2", gap: "gap-1" },
37
- comfortable: { target: "gap-3 px-4 py-3", actions: "pe-3", gap: "gap-1" }
36
+ compact: { target: "gap-2 px-2 py-1.5", actions: "pe-1.5", gap: "gap-0.5", body: "px-2 pb-1.5" },
37
+ default: { target: "gap-3 px-3 py-2.5", actions: "pe-2", gap: "gap-1", body: "px-3 pb-2.5" },
38
+ comfortable: { target: "gap-3 px-4 py-3", actions: "pe-3", gap: "gap-1", body: "px-4 pb-3" }
38
39
  };
39
40
  const TARGET_RING = "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]";
40
41
  const ListItem = forwardRef(function ListItem2({
@@ -55,7 +56,11 @@ const ListItem = forwardRef(function ListItem2({
55
56
  align = "center",
56
57
  className,
57
58
  targetClassName,
59
+ targetProps,
60
+ bordered = false,
58
61
  as = "li",
62
+ children,
63
+ bodyClassName,
59
64
  href,
60
65
  renderLink,
61
66
  external = false,
@@ -65,12 +70,15 @@ const ListItem = forwardRef(function ListItem2({
65
70
  }, ref) {
66
71
  const list = useContext(ListContext);
67
72
  const labels = useKitLabels("list", DEFAULT_LIST_LABELS);
73
+ const kitLink = useKitLink();
68
74
  const density = densityProp ?? list.density;
69
75
  const pad = PAD[density];
70
76
  const isLink = href !== void 0 && !disabled;
71
77
  const isButton = !isLink && onClick !== void 0 && href === void 0;
72
78
  const interactive = isLink || isButton;
73
79
  const inert = disabled || loading;
80
+ const hasBody = children !== void 0 && children !== null && children !== false;
81
+ const titleId = useId();
74
82
  const mark = status ?? (unread ? /* @__PURE__ */ jsx(StatusDot, { tone: "brand", size: "sm", className: align === "start" ? "mt-1.5" : void 0 }) : null);
75
83
  const body = /* @__PURE__ */ jsxs(Fragment, { children: [
76
84
  Icon && /* @__PURE__ */ jsx(
@@ -85,6 +93,7 @@ const ListItem = forwardRef(function ListItem2({
85
93
  /* @__PURE__ */ jsxs(
86
94
  "span",
87
95
  {
96
+ id: hasBody ? titleId : void 0,
88
97
  className: cn(
89
98
  "block truncate text-sm text-[var(--text-primary)]",
90
99
  unread ? "font-semibold" : "font-medium"
@@ -124,15 +133,20 @@ const ListItem = forwardRef(function ListItem2({
124
133
  "flex min-w-0 flex-1 text-start",
125
134
  align === "start" ? "items-start" : "items-center",
126
135
  pad.target,
127
- "rounded-[inherit]",
136
+ // With a body the target is the top of the box, not all of it, so it takes its
137
+ // own corners and its own hover fill (see `children`).
138
+ hasBody ? "rounded-md" : "rounded-[inherit]",
139
+ hasBody && interactive && !inert && !selected && "transition-colors hover:bg-[var(--bg-hover)]",
128
140
  interactive && TARGET_RING,
129
141
  interactive && !inert && "cursor-pointer",
130
142
  loading && "cursor-progress",
131
143
  targetClassName
132
144
  );
145
+ const { role: _ignoredRole, ...targetExtras } = targetProps ?? {};
133
146
  let main;
134
147
  if (isLink) {
135
148
  const linkProps = {
149
+ ...targetExtras,
136
150
  ...rest,
137
151
  ref,
138
152
  href,
@@ -144,11 +158,13 @@ const ListItem = forwardRef(function ListItem2({
144
158
  children: body
145
159
  };
146
160
  const { children: linkBody, ...anchorProps } = linkProps;
147
- main = renderLink && !external ? /* @__PURE__ */ jsx(RenderedLink, { render: renderLink, ...linkProps }) : /* @__PURE__ */ jsx("a", { ...anchorProps, ...external ? { target: "_blank", rel: "noopener noreferrer" } : null, children: linkBody });
161
+ const render = external ? void 0 : pickLinkRenderer(renderLink, kitLink, href);
162
+ main = render ? /* @__PURE__ */ jsx(RenderedLink, { render, ...linkProps }) : /* @__PURE__ */ jsx("a", { ...anchorProps, ...external ? { target: "_blank", rel: "noopener noreferrer" } : null, children: linkBody });
148
163
  } else if (isButton) {
149
164
  main = /* @__PURE__ */ jsx(
150
165
  "button",
151
166
  {
167
+ ...targetExtras,
152
168
  ...rest,
153
169
  ref,
154
170
  type: "button",
@@ -165,6 +181,7 @@ const ListItem = forwardRef(function ListItem2({
165
181
  main = /* @__PURE__ */ jsx(
166
182
  "div",
167
183
  {
184
+ ...targetExtras,
168
185
  ...rest,
169
186
  ref,
170
187
  "aria-disabled": disabled || void 0,
@@ -174,6 +191,7 @@ const ListItem = forwardRef(function ListItem2({
174
191
  }
175
192
  );
176
193
  }
194
+ const actionsEl = actions != null ? /* @__PURE__ */ jsx("div", { className: cn("flex shrink-0 items-center", pad.gap, pad.actions), children: actions }) : null;
177
195
  const Tag = as;
178
196
  return /* @__PURE__ */ jsx(
179
197
  Tag,
@@ -182,20 +200,27 @@ const ListItem = forwardRef(function ListItem2({
182
200
  list.separator === "divider" && "border-b border-[var(--border)] last:border-b-0",
183
201
  list.separator === "divider" && as === "li" && "py-0.5"
184
202
  ),
185
- children: /* @__PURE__ */ jsxs(
203
+ children: /* @__PURE__ */ jsx(
186
204
  "div",
187
205
  {
188
206
  className: cn(
189
- "flex min-w-0 items-center rounded-md border transition-colors",
190
- selected ? "border-[var(--brand)] bg-[var(--bg-surface-2)]" : "border-transparent",
191
- interactive && !inert && !selected && "hover:bg-[var(--bg-hover)]",
207
+ "flex min-w-0 rounded-md border transition-colors",
208
+ hasBody ? "flex-col" : "items-center",
209
+ selected ? "border-[var(--brand)] bg-[var(--bg-surface-2)]" : bordered ? "border-[var(--border)]" : "border-transparent",
210
+ !hasBody && interactive && !inert && !selected && "hover:bg-[var(--bg-hover)]",
192
211
  disabled && "opacity-50",
193
212
  className
194
213
  ),
195
- children: [
214
+ children: hasBody ? /* @__PURE__ */ jsxs(Fragment, { children: [
215
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center", children: [
216
+ main,
217
+ actionsEl
218
+ ] }),
219
+ /* @__PURE__ */ jsx("div", { role: "group", "aria-labelledby": titleId, className: cn("min-w-0", pad.body, bodyClassName), children })
220
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
196
221
  main,
197
- actions != null && /* @__PURE__ */ jsx("div", { className: cn("flex shrink-0 items-center", pad.gap, pad.actions), children: actions })
198
- ]
222
+ actionsEl
223
+ ] })
199
224
  }
200
225
  )
201
226
  }