@eifi1/ui-kit 0.31.1 → 0.32.1

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 (565) hide show
  1. package/README.md +51 -28
  2. package/dist/account/delete-account-setting.d.ts +8 -3
  3. package/dist/admin/admin-action-confirm.d.ts +8 -3
  4. package/dist/admin/admin-action-log.d.ts +1 -1
  5. package/dist/admin/admin-action-log.js +4 -2
  6. package/dist/admin/admin-action-log.js.map +1 -1
  7. package/dist/admin/invitations-panel.d.ts +8 -3
  8. package/dist/admin/invitations-panel.js +29 -26
  9. package/dist/admin/invitations-panel.js.map +1 -1
  10. package/dist/admin/reviewer-scope-editor.d.ts +8 -3
  11. package/dist/admin/role-select.d.ts +10 -5
  12. package/dist/admin/transfer-ownership-dialog.d.ts +8 -3
  13. package/dist/admin/user-roster.d.ts +10 -5
  14. package/dist/admin/user-roster.js +8 -47
  15. package/dist/admin/user-roster.js.map +1 -1
  16. package/dist/auth/auth-errors.d.ts +43 -8
  17. package/dist/auth/auth-errors.js +24 -1
  18. package/dist/auth/auth-errors.js.map +1 -1
  19. package/dist/auth/register-form.d.ts +8 -3
  20. package/dist/billing/billing-banner.d.ts +184 -0
  21. package/dist/billing/billing-banner.js +98 -0
  22. package/dist/billing/billing-banner.js.map +1 -0
  23. package/dist/billing/billing-labels.d.ts +100 -0
  24. package/dist/billing/billing-labels.js +76 -0
  25. package/dist/billing/billing-labels.js.map +1 -0
  26. package/dist/billing/billing-lock.d.ts +158 -0
  27. package/dist/billing/billing-lock.js +17 -0
  28. package/dist/billing/billing-lock.js.map +1 -0
  29. package/dist/billing/billing-standing.d.ts +149 -0
  30. package/dist/billing/billing-standing.js +28 -0
  31. package/dist/billing/billing-standing.js.map +1 -0
  32. package/dist/billing/plan-card.d.ts +248 -0
  33. package/dist/billing/plan-card.js +248 -0
  34. package/dist/billing/plan-card.js.map +1 -0
  35. package/dist/billing/plan-limit-notice.d.ts +156 -0
  36. package/dist/billing/plan-limit-notice.js +76 -0
  37. package/dist/billing/plan-limit-notice.js.map +1 -0
  38. package/dist/billing/plan-limit.d.ts +34 -0
  39. package/dist/billing/plan-limit.js +20 -0
  40. package/dist/billing/plan-limit.js.map +1 -0
  41. package/dist/billing/plan-price.d.ts +61 -0
  42. package/dist/billing/plan-price.js +45 -0
  43. package/dist/billing/plan-price.js.map +1 -0
  44. package/dist/billing/subscription-actions.d.ts +133 -0
  45. package/dist/billing/subscription-actions.js +28 -0
  46. package/dist/billing/subscription-actions.js.map +1 -0
  47. package/dist/billing/subscription-status-chip.d.ts +149 -0
  48. package/dist/billing/subscription-status-chip.js +48 -0
  49. package/dist/billing/subscription-status-chip.js.map +1 -0
  50. package/dist/chart.d.ts +9 -3
  51. package/dist/chart.js.map +1 -1
  52. package/dist/components/account-chips.d.ts +16 -4
  53. package/dist/components/account-chips.js +21 -3
  54. package/dist/components/account-chips.js.map +1 -1
  55. package/dist/components/alert-banner.d.ts +6 -1
  56. package/dist/components/alert-banner.js +5 -4
  57. package/dist/components/alert-banner.js.map +1 -1
  58. package/dist/components/amount-input.d.ts +8 -3
  59. package/dist/components/amount-input.js +5 -5
  60. package/dist/components/amount-input.js.map +1 -1
  61. package/dist/components/appearance-labels.d.ts +38 -0
  62. package/dist/components/appearance-labels.js +19 -0
  63. package/dist/components/appearance-labels.js.map +1 -0
  64. package/dist/components/autocomplete.js +2 -2
  65. package/dist/components/autocomplete.js.map +1 -1
  66. package/dist/components/breadcrumbs.js +3 -2
  67. package/dist/components/breadcrumbs.js.map +1 -1
  68. package/dist/components/bulk-action-bar.d.ts +2 -2
  69. package/dist/components/bulk-action-bar.js +7 -12
  70. package/dist/components/bulk-action-bar.js.map +1 -1
  71. package/dist/components/button-group.d.ts +8 -3
  72. package/dist/components/button-group.js +7 -1
  73. package/dist/components/button-group.js.map +1 -1
  74. package/dist/components/calculator.d.ts +8 -3
  75. package/dist/components/calculator.js +4 -3
  76. package/dist/components/calculator.js.map +1 -1
  77. package/dist/components/calendar-heatmap.js +5 -5
  78. package/dist/components/calendar-heatmap.js.map +1 -1
  79. package/dist/components/chart-height.d.ts +23 -0
  80. package/dist/components/chart-height.js +15 -0
  81. package/dist/components/chart-height.js.map +1 -0
  82. package/dist/components/chart-zoom.js +6 -1
  83. package/dist/components/chart-zoom.js.map +1 -1
  84. package/dist/components/checkbox-group.js +2 -2
  85. package/dist/components/checkbox-group.js.map +1 -1
  86. package/dist/components/checkbox.d.ts +10 -0
  87. package/dist/components/checkbox.js +25 -10
  88. package/dist/components/checkbox.js.map +1 -1
  89. package/dist/components/chip.d.ts +19 -3
  90. package/dist/components/chip.js +54 -23
  91. package/dist/components/chip.js.map +1 -1
  92. package/dist/components/choice-card.d.ts +1 -1
  93. package/dist/components/choice-card.js +9 -8
  94. package/dist/components/choice-card.js.map +1 -1
  95. package/dist/components/column-mapper.js +1 -1
  96. package/dist/components/column-mapper.js.map +1 -1
  97. package/dist/components/combobox-core.js +8 -7
  98. package/dist/components/combobox-core.js.map +1 -1
  99. package/dist/components/combobox.js +8 -8
  100. package/dist/components/combobox.js.map +1 -1
  101. package/dist/components/confirm-dialog.d.ts +8 -3
  102. package/dist/components/copy-button.d.ts +10 -5
  103. package/dist/components/country-select.js +1 -1
  104. package/dist/components/country-select.js.map +1 -1
  105. package/dist/components/currency-select.js +3 -3
  106. package/dist/components/currency-select.js.map +1 -1
  107. package/dist/components/danger-confirm.d.ts +8 -3
  108. package/dist/components/data-table-cells.d.ts +1 -1
  109. package/dist/components/data-table-filter-popover.d.ts +1 -1
  110. package/dist/components/data-table-filter-popover.js +4 -1
  111. package/dist/components/data-table-filter-popover.js.map +1 -1
  112. package/dist/components/data-table-filters.d.ts +1 -1
  113. package/dist/components/data-table-labels.d.ts +1 -1
  114. package/dist/components/data-table-pagination.d.ts +3 -2
  115. package/dist/components/data-table-pagination.js +15 -6
  116. package/dist/components/data-table-pagination.js.map +1 -1
  117. package/dist/components/data-table.d.ts +1 -1
  118. package/dist/components/data-table.js +123 -52
  119. package/dist/components/data-table.js.map +1 -1
  120. package/dist/components/date-picker.d.ts +1 -1
  121. package/dist/components/date-picker.js +18 -6
  122. package/dist/components/date-picker.js.map +1 -1
  123. package/dist/components/description-list.js +2 -2
  124. package/dist/components/description-list.js.map +1 -1
  125. package/dist/components/dialog-frame.js +2 -1
  126. package/dist/components/dialog-frame.js.map +1 -1
  127. package/dist/components/disclosure.js +3 -2
  128. package/dist/components/disclosure.js.map +1 -1
  129. package/dist/components/dropdown.d.ts +1 -1
  130. package/dist/components/dropdown.js +7 -1
  131. package/dist/components/dropdown.js.map +1 -1
  132. package/dist/components/facing-pair.d.ts +19 -6
  133. package/dist/components/facing-pair.js +3 -2
  134. package/dist/components/facing-pair.js.map +1 -1
  135. package/dist/components/field-parts.d.ts +102 -6
  136. package/dist/components/field-parts.js +81 -6
  137. package/dist/components/field-parts.js.map +1 -1
  138. package/dist/components/field-sync.d.ts +4 -1
  139. package/dist/components/field-sync.js +12 -2
  140. package/dist/components/field-sync.js.map +1 -1
  141. package/dist/components/field.js +2 -2
  142. package/dist/components/field.js.map +1 -1
  143. package/dist/components/file-button.d.ts +8 -3
  144. package/dist/components/file-dropzone.d.ts +8 -3
  145. package/dist/components/floating-panel.js +7 -4
  146. package/dist/components/floating-panel.js.map +1 -1
  147. package/dist/components/focus-ring.d.ts +12 -0
  148. package/dist/components/focus-ring.js +8 -0
  149. package/dist/components/focus-ring.js.map +1 -0
  150. package/dist/components/form-actions.d.ts +8 -3
  151. package/dist/components/form-actions.js +33 -19
  152. package/dist/components/form-actions.js.map +1 -1
  153. package/dist/components/full-bleed-dialog.d.ts +11 -1
  154. package/dist/components/full-bleed-dialog.js +3 -1
  155. package/dist/components/full-bleed-dialog.js.map +1 -1
  156. package/dist/components/iban-input.d.ts +8 -3
  157. package/dist/components/image-grid.js +1 -1
  158. package/dist/components/image-grid.js.map +1 -1
  159. package/dist/components/inline-edit-field.js +10 -4
  160. package/dist/components/inline-edit-field.js.map +1 -1
  161. package/dist/components/language-select.d.ts +8 -3
  162. package/dist/components/legal-page.d.ts +26 -5
  163. package/dist/components/legal-page.js +3 -1
  164. package/dist/components/legal-page.js.map +1 -1
  165. package/dist/components/legal.d.ts +8 -3
  166. package/dist/components/legal.js +2 -1
  167. package/dist/components/legal.js.map +1 -1
  168. package/dist/components/lightbox.js +24 -2
  169. package/dist/components/lightbox.js.map +1 -1
  170. package/dist/components/line-items.js +2 -1
  171. package/dist/components/line-items.js.map +1 -1
  172. package/dist/components/list.d.ts +5 -0
  173. package/dist/components/list.js +13 -9
  174. package/dist/components/list.js.map +1 -1
  175. package/dist/components/measured-grid.d.ts +3 -1
  176. package/dist/components/measured-grid.js +38 -20
  177. package/dist/components/measured-grid.js.map +1 -1
  178. package/dist/components/menu-item.js +2 -1
  179. package/dist/components/menu-item.js.map +1 -1
  180. package/dist/components/mini-calendar.js +8 -4
  181. package/dist/components/mini-calendar.js.map +1 -1
  182. package/dist/components/money-field.d.ts +8 -3
  183. package/dist/components/month-picker.js +50 -39
  184. package/dist/components/month-picker.js.map +1 -1
  185. package/dist/components/nav-pills.js +1 -1
  186. package/dist/components/nav-pills.js.map +1 -1
  187. package/dist/components/number-field.d.ts +8 -3
  188. package/dist/components/number-input.d.ts +11 -4
  189. package/dist/components/number-input.js +8 -6
  190. package/dist/components/number-input.js.map +1 -1
  191. package/dist/components/numpad-sheet.d.ts +8 -3
  192. package/dist/components/numpad-sheet.js +2 -1
  193. package/dist/components/numpad-sheet.js.map +1 -1
  194. package/dist/components/one-time-code-input.js +2 -2
  195. package/dist/components/one-time-code-input.js.map +1 -1
  196. package/dist/components/page-contents.js +2 -2
  197. package/dist/components/page-contents.js.map +1 -1
  198. package/dist/components/phone-input.d.ts +8 -3
  199. package/dist/components/phone-input.js +4 -4
  200. package/dist/components/phone-input.js.map +1 -1
  201. package/dist/components/pie-chart.d.ts +8 -2
  202. package/dist/components/pie-chart.js +11 -7
  203. package/dist/components/pie-chart.js.map +1 -1
  204. package/dist/components/popover.d.ts +9 -1
  205. package/dist/components/popover.js +8 -4
  206. package/dist/components/popover.js.map +1 -1
  207. package/dist/components/row-actions.d.ts +100 -0
  208. package/dist/components/row-actions.js +231 -0
  209. package/dist/components/row-actions.js.map +1 -0
  210. package/dist/components/scroll-area.js +1 -1
  211. package/dist/components/scroll-area.js.map +1 -1
  212. package/dist/components/search-field.js +3 -2
  213. package/dist/components/search-field.js.map +1 -1
  214. package/dist/components/series-chart-budget.d.ts +30 -7
  215. package/dist/components/series-chart-budget.js +12 -4
  216. package/dist/components/series-chart-budget.js.map +1 -1
  217. package/dist/components/series-chart-ticks.d.ts +7 -1
  218. package/dist/components/series-chart-ticks.js +6 -0
  219. package/dist/components/series-chart-ticks.js.map +1 -1
  220. package/dist/components/series-chart.d.ts +58 -28
  221. package/dist/components/series-chart.js +42 -32
  222. package/dist/components/series-chart.js.map +1 -1
  223. package/dist/components/settings-fields.d.ts +57 -8
  224. package/dist/components/settings-fields.js +39 -1
  225. package/dist/components/settings-fields.js.map +1 -1
  226. package/dist/components/share-card.d.ts +11 -6
  227. package/dist/components/share-card.js +8 -2
  228. package/dist/components/share-card.js.map +1 -1
  229. package/dist/components/sign-chip.d.ts +4 -0
  230. package/dist/components/sign-chip.js +17 -4
  231. package/dist/components/sign-chip.js.map +1 -1
  232. package/dist/components/signature-pad.d.ts +3 -1
  233. package/dist/components/signature-pad.js +8 -5
  234. package/dist/components/signature-pad.js.map +1 -1
  235. package/dist/components/slider.js +7 -6
  236. package/dist/components/slider.js.map +1 -1
  237. package/dist/components/sparkline.d.ts +5 -1
  238. package/dist/components/sparkline.js +4 -2
  239. package/dist/components/sparkline.js.map +1 -1
  240. package/dist/components/stat-tile.d.ts +17 -1
  241. package/dist/components/stat-tile.js +48 -20
  242. package/dist/components/stat-tile.js.map +1 -1
  243. package/dist/components/status-dot.d.ts +4 -0
  244. package/dist/components/status-dot.js +9 -2
  245. package/dist/components/status-dot.js.map +1 -1
  246. package/dist/components/swipeable-row.js +10 -2
  247. package/dist/components/swipeable-row.js.map +1 -1
  248. package/dist/components/switch.d.ts +21 -7
  249. package/dist/components/switch.js +24 -11
  250. package/dist/components/switch.js.map +1 -1
  251. package/dist/components/table.js +2 -2
  252. package/dist/components/table.js.map +1 -1
  253. package/dist/components/text-link.d.ts +8 -3
  254. package/dist/components/text-link.js +1 -1
  255. package/dist/components/text-link.js.map +1 -1
  256. package/dist/components/text.d.ts +7 -2
  257. package/dist/components/text.js +5 -5
  258. package/dist/components/text.js.map +1 -1
  259. package/dist/components/tile-radio.js +2 -1
  260. package/dist/components/tile-radio.js.map +1 -1
  261. package/dist/components/time-input.d.ts +8 -3
  262. package/dist/components/toast.d.ts +18 -2
  263. package/dist/components/toast.js +8 -5
  264. package/dist/components/toast.js.map +1 -1
  265. package/dist/components/toggle-group.d.ts +25 -3
  266. package/dist/components/toggle-group.js +40 -11
  267. package/dist/components/toggle-group.js.map +1 -1
  268. package/dist/components/toggle-legend.d.ts +10 -1
  269. package/dist/components/toggle-legend.js +21 -7
  270. package/dist/components/toggle-legend.js.map +1 -1
  271. package/dist/components/translation-review-editor.js +1 -1
  272. package/dist/components/translation-review-editor.js.map +1 -1
  273. package/dist/components/translation-review-parts.d.ts +4 -0
  274. package/dist/components/translation-review.d.ts +4 -0
  275. package/dist/components/translation-review.js +3 -3
  276. package/dist/components/translation-review.js.map +1 -1
  277. package/dist/components/tree-view.js +3 -2
  278. package/dist/components/tree-view.js.map +1 -1
  279. package/dist/components/treemap.d.ts +5 -2
  280. package/dist/components/treemap.js +33 -16
  281. package/dist/components/treemap.js.map +1 -1
  282. package/dist/components/ui.d.ts +8 -3
  283. package/dist/components/ui.js +207 -119
  284. package/dist/components/ui.js.map +1 -1
  285. package/dist/components/use-table-state.d.ts +1 -1
  286. package/dist/components/user-avatar.d.ts +5 -1
  287. package/dist/components/user-avatar.js +1 -1
  288. package/dist/components/user-avatar.js.map +1 -1
  289. package/dist/components/wizard-stepper.js +20 -14
  290. package/dist/components/wizard-stepper.js.map +1 -1
  291. package/dist/{data-table-labels-DUELeXpa.d.ts → data-table-labels-kpAAVi6g.d.ts} +26 -2
  292. package/dist/data-table.d.ts +1 -1
  293. package/dist/{delete-account-setting-CpIhzxxZ.d.ts → delete-account-setting-FLyJf-kZ.d.ts} +642 -60
  294. package/dist/demo/demo-banner.js +1 -0
  295. package/dist/demo/demo-banner.js.map +1 -1
  296. package/dist/feedback/feedback-attachment.d.ts +8 -3
  297. package/dist/feedback/feedback-context.d.ts +8 -3
  298. package/dist/feedback/feedback-crash.d.ts +8 -3
  299. package/dist/feedback/feedback-dialog.d.ts +8 -3
  300. package/dist/feedback/feedback-dialog.js +3 -3
  301. package/dist/feedback/feedback-dialog.js.map +1 -1
  302. package/dist/feedback/feedback-inbox.d.ts +8 -3
  303. package/dist/feedback/feedback-inbox.js +2 -2
  304. package/dist/feedback/feedback-inbox.js.map +1 -1
  305. package/dist/feedback/feedback-labels.d.ts +8 -3
  306. package/dist/feedback/feedback-menu.d.ts +9 -4
  307. package/dist/feedback/feedback-record.d.ts +8 -3
  308. package/dist/feedback/feedback-row-detail.d.ts +8 -3
  309. package/dist/feedback/feedback-status-undo.d.ts +8 -3
  310. package/dist/feedback/feedback-submit.d.ts +8 -3
  311. package/dist/feedback/feedback-swipe.d.ts +8 -3
  312. package/dist/feedback/feedback-table.d.ts +8 -3
  313. package/dist/feedback/feedback-thread.d.ts +8 -3
  314. package/dist/feedback/feedback-thread.js +3 -3
  315. package/dist/feedback/feedback-thread.js.map +1 -1
  316. package/dist/feedback.d.ts +8 -3
  317. package/dist/hooks/use-anchored-panel.d.ts +32 -3
  318. package/dist/hooks/use-anchored-panel.js +21 -3
  319. package/dist/hooks/use-anchored-panel.js.map +1 -1
  320. package/dist/hooks/use-breakpoint.d.ts +88 -0
  321. package/dist/hooks/use-breakpoint.js +50 -0
  322. package/dist/hooks/use-breakpoint.js.map +1 -0
  323. package/dist/hooks/use-file-drop.d.ts +8 -3
  324. package/dist/hooks/use-large-text.d.ts +29 -0
  325. package/dist/hooks/use-large-text.js +20 -0
  326. package/dist/hooks/use-large-text.js.map +1 -0
  327. package/dist/hooks/use-windowed-rows.d.ts +52 -8
  328. package/dist/hooks/use-windowed-rows.js +94 -5
  329. package/dist/hooks/use-windowed-rows.js.map +1 -1
  330. package/dist/i18n/defaults.d.ts +8 -3
  331. package/dist/i18n/defaults.js +8 -0
  332. package/dist/i18n/defaults.js.map +1 -1
  333. package/dist/i18n/german.d.ts +8 -3
  334. package/dist/i18n/german.js +94 -3
  335. package/dist/i18n/german.js.map +1 -1
  336. package/dist/i18n/kit-labels.d.ts +8 -3
  337. package/dist/i18n/kit-labels.js.map +1 -1
  338. package/dist/i18n/languages.d.ts +10 -5
  339. package/dist/i18n/locales/de-CH.d.ts +8 -3
  340. package/dist/i18n/locales/en.d.ts +8 -3
  341. package/dist/i18n/locales/es.d.ts +8 -3
  342. package/dist/i18n/locales/es.js +89 -3
  343. package/dist/i18n/locales/es.js.map +1 -1
  344. package/dist/i18n/locales/fr.d.ts +8 -3
  345. package/dist/i18n/locales/fr.js +91 -3
  346. package/dist/i18n/locales/fr.js.map +1 -1
  347. package/dist/i18n/locales/hu.d.ts +8 -3
  348. package/dist/i18n/locales/hu.js +91 -3
  349. package/dist/i18n/locales/hu.js.map +1 -1
  350. package/dist/i18n/locales/it.d.ts +8 -3
  351. package/dist/i18n/locales/it.js +92 -3
  352. package/dist/i18n/locales/it.js.map +1 -1
  353. package/dist/i18n/locales/zh.d.ts +8 -3
  354. package/dist/i18n/locales/zh.js +89 -3
  355. package/dist/i18n/locales/zh.js.map +1 -1
  356. package/dist/i18n/review.d.ts +8 -3
  357. package/dist/i18n/review.js +15 -1
  358. package/dist/i18n/review.js.map +1 -1
  359. package/dist/index.d.ts +27 -11
  360. package/dist/index.js +77 -4
  361. package/dist/index.js.map +1 -1
  362. package/dist/landing/landing-actions.d.ts +8 -3
  363. package/dist/landing/landing-sections.d.ts +8 -3
  364. package/dist/landing/landing-sections.js +7 -3
  365. package/dist/landing/landing-sections.js.map +1 -1
  366. package/dist/landing/public-header.d.ts +9 -4
  367. package/dist/landing/public-header.js +3 -3
  368. package/dist/landing/public-header.js.map +1 -1
  369. package/dist/landing/routing.d.ts +8 -3
  370. package/dist/lib/cn.js +3 -2
  371. package/dist/lib/cn.js.map +1 -1
  372. package/dist/rhf/fields.d.ts +8 -3
  373. package/dist/rhf/fields.js +1 -1
  374. package/dist/rhf/fields.js.map +1 -1
  375. package/dist/rhf/form.d.ts +8 -3
  376. package/dist/rhf/form.js +2 -2
  377. package/dist/rhf/form.js.map +1 -1
  378. package/dist/rhf.d.ts +8 -3
  379. package/dist/search/command-palette.d.ts +1 -1
  380. package/dist/search/command-palette.js +9 -8
  381. package/dist/search/command-palette.js.map +1 -1
  382. package/dist/settings/settings-layout.js +2 -1
  383. package/dist/settings/settings-layout.js.map +1 -1
  384. package/dist/settings/settings-section.d.ts +8 -3
  385. package/dist/settings/use-account-appearance.d.ts +92 -0
  386. package/dist/settings/use-account-appearance.js +63 -0
  387. package/dist/settings/use-account-appearance.js.map +1 -0
  388. package/dist/settings/use-settings-route.d.ts +1 -1
  389. package/dist/settings/use-settings-route.js +2 -3
  390. package/dist/settings/use-settings-route.js.map +1 -1
  391. package/dist/shell/app-shell.d.ts +12 -171
  392. package/dist/shell/app-shell.js +144 -28
  393. package/dist/shell/app-shell.js.map +1 -1
  394. package/dist/shell/auth-layout.js +1 -1
  395. package/dist/shell/auth-layout.js.map +1 -1
  396. package/dist/shell/option-switcher-menu.js +1 -1
  397. package/dist/shell/option-switcher-menu.js.map +1 -1
  398. package/dist/shell/top-bar-brand.d.ts +8 -3
  399. package/dist/shell/top-bar-brand.js +3 -1
  400. package/dist/shell/top-bar-brand.js.map +1 -1
  401. package/dist/shell/topbar-action-menu.d.ts +4 -0
  402. package/dist/shell/topbar-action-menu.js +3 -3
  403. package/dist/shell/topbar-action-menu.js.map +1 -1
  404. package/dist/shell/topbar-controls.js +4 -4
  405. package/dist/shell/topbar-controls.js.map +1 -1
  406. package/dist/shell.d.ts +9 -5
  407. package/dist/theme/contrast-tokens.d.ts +41 -0
  408. package/dist/theme/contrast-tokens.js +59 -0
  409. package/dist/theme/contrast-tokens.js.map +1 -0
  410. package/dist/theme/contrast.d.ts +110 -0
  411. package/dist/theme/contrast.js +128 -0
  412. package/dist/theme/contrast.js.map +1 -0
  413. package/dist/theme/palette-presets.d.ts +12 -1
  414. package/dist/theme/palette-presets.js +9 -4
  415. package/dist/theme/palette-presets.js.map +1 -1
  416. package/dist/theme/palette-store.js.map +1 -1
  417. package/dist/theme/text-size.d.ts +173 -0
  418. package/dist/theme/text-size.js +121 -0
  419. package/dist/theme/text-size.js.map +1 -0
  420. package/dist/wizard/stepper-nav.d.ts +8 -3
  421. package/dist/wizard/wizard-summary.js +1 -1
  422. package/dist/wizard/wizard-summary.js.map +1 -1
  423. package/dist/wizard.d.ts +8 -3
  424. package/package.json +1 -1
  425. package/src/admin/admin-action-log.tsx +6 -1
  426. package/src/admin/invitations-panel.tsx +36 -29
  427. package/src/admin/user-roster.tsx +17 -72
  428. package/src/auth/auth-errors.ts +68 -7
  429. package/src/billing/billing-banner.tsx +205 -0
  430. package/src/billing/billing-labels.ts +240 -0
  431. package/src/billing/billing-lock.ts +75 -0
  432. package/src/billing/billing-standing.ts +72 -0
  433. package/src/billing/plan-card.tsx +458 -0
  434. package/src/billing/plan-limit-notice.tsx +145 -0
  435. package/src/billing/plan-limit.ts +53 -0
  436. package/src/billing/plan-price.ts +101 -0
  437. package/src/billing/subscription-actions.tsx +76 -0
  438. package/src/billing/subscription-status-chip.tsx +95 -0
  439. package/src/chart.ts +2 -0
  440. package/src/components/account-chips.tsx +36 -10
  441. package/src/components/alert-banner.tsx +16 -7
  442. package/src/components/amount-input.tsx +5 -5
  443. package/src/components/appearance-labels.ts +47 -0
  444. package/src/components/autocomplete.tsx +2 -2
  445. package/src/components/breadcrumbs.tsx +3 -3
  446. package/src/components/bulk-action-bar.tsx +11 -14
  447. package/src/components/button-group.tsx +21 -0
  448. package/src/components/calculator.tsx +4 -3
  449. package/src/components/calendar-heatmap.tsx +5 -5
  450. package/src/components/chart-height.ts +30 -0
  451. package/src/components/chart-zoom.tsx +6 -1
  452. package/src/components/checkbox-group.tsx +2 -2
  453. package/src/components/checkbox.tsx +62 -20
  454. package/src/components/chip.tsx +139 -30
  455. package/src/components/choice-card.tsx +16 -11
  456. package/src/components/column-mapper.tsx +1 -1
  457. package/src/components/combobox-core.tsx +8 -7
  458. package/src/components/combobox.tsx +8 -8
  459. package/src/components/country-select.tsx +1 -1
  460. package/src/components/currency-select.tsx +3 -3
  461. package/src/components/data-table-filter-popover.tsx +7 -1
  462. package/src/components/data-table-pagination.tsx +60 -31
  463. package/src/components/data-table.tsx +164 -43
  464. package/src/components/date-picker.tsx +44 -10
  465. package/src/components/description-list.tsx +2 -2
  466. package/src/components/dialog-frame.tsx +2 -1
  467. package/src/components/disclosure.tsx +3 -2
  468. package/src/components/dropdown.tsx +10 -2
  469. package/src/components/facing-pair.ts +12 -5
  470. package/src/components/field-parts.tsx +190 -7
  471. package/src/components/field-sync.tsx +17 -3
  472. package/src/components/field.tsx +2 -2
  473. package/src/components/floating-panel.tsx +7 -4
  474. package/src/components/focus-ring.ts +12 -0
  475. package/src/components/form-actions.tsx +55 -18
  476. package/src/components/full-bleed-dialog.tsx +18 -4
  477. package/src/components/image-grid.tsx +1 -1
  478. package/src/components/inline-edit-field.tsx +26 -4
  479. package/src/components/legal-page.tsx +28 -2
  480. package/src/components/legal.tsx +19 -1
  481. package/src/components/lightbox.tsx +24 -2
  482. package/src/components/line-items.tsx +7 -1
  483. package/src/components/list.tsx +36 -10
  484. package/src/components/measured-grid.tsx +49 -24
  485. package/src/components/menu-item.tsx +2 -2
  486. package/src/components/mini-calendar.tsx +8 -4
  487. package/src/components/month-picker.tsx +15 -6
  488. package/src/components/nav-pills.tsx +1 -1
  489. package/src/components/number-input.tsx +14 -7
  490. package/src/components/numpad-sheet.tsx +2 -2
  491. package/src/components/one-time-code-input.tsx +2 -2
  492. package/src/components/page-contents.tsx +2 -2
  493. package/src/components/phone-input.tsx +9 -4
  494. package/src/components/pie-chart.tsx +19 -9
  495. package/src/components/popover.tsx +22 -5
  496. package/src/components/row-actions.tsx +450 -0
  497. package/src/components/scroll-area.tsx +1 -1
  498. package/src/components/search-field.tsx +4 -2
  499. package/src/components/series-chart-budget.ts +40 -8
  500. package/src/components/series-chart-ticks.ts +10 -0
  501. package/src/components/series-chart.tsx +123 -72
  502. package/src/components/settings-fields.tsx +89 -5
  503. package/src/components/share-card.tsx +31 -2
  504. package/src/components/sign-chip.tsx +28 -4
  505. package/src/components/signature-pad.tsx +13 -7
  506. package/src/components/slider.tsx +22 -9
  507. package/src/components/sparkline.tsx +11 -3
  508. package/src/components/stat-tile.tsx +70 -14
  509. package/src/components/status-dot.tsx +25 -2
  510. package/src/components/swipeable-row.tsx +10 -2
  511. package/src/components/switch.tsx +57 -23
  512. package/src/components/table.tsx +2 -2
  513. package/src/components/text-link.tsx +1 -1
  514. package/src/components/text.tsx +11 -6
  515. package/src/components/tile-radio.tsx +4 -1
  516. package/src/components/toast.tsx +27 -7
  517. package/src/components/toggle-group.tsx +115 -14
  518. package/src/components/toggle-legend.tsx +53 -8
  519. package/src/components/translation-review-editor.tsx +1 -1
  520. package/src/components/translation-review.tsx +11 -5
  521. package/src/components/tree-view.tsx +3 -2
  522. package/src/components/treemap.tsx +27 -10
  523. package/src/components/ui.tsx +353 -153
  524. package/src/components/user-avatar.tsx +1 -1
  525. package/src/components/wizard-stepper.tsx +5 -1
  526. package/src/demo/demo-banner.tsx +5 -0
  527. package/src/feedback/feedback-dialog.tsx +3 -3
  528. package/src/feedback/feedback-inbox.tsx +2 -2
  529. package/src/feedback/feedback-thread.tsx +3 -3
  530. package/src/hooks/use-anchored-panel.ts +67 -5
  531. package/src/hooks/use-breakpoint.ts +132 -0
  532. package/src/hooks/use-large-text.ts +41 -0
  533. package/src/hooks/use-windowed-rows.ts +168 -12
  534. package/src/i18n/defaults.ts +8 -0
  535. package/src/i18n/german.ts +107 -0
  536. package/src/i18n/kit-labels.tsx +12 -0
  537. package/src/i18n/locales/es.ts +98 -0
  538. package/src/i18n/locales/fr.ts +102 -0
  539. package/src/i18n/locales/hu.ts +100 -0
  540. package/src/i18n/locales/it.ts +103 -0
  541. package/src/i18n/locales/zh.ts +94 -0
  542. package/src/i18n/review.ts +14 -0
  543. package/src/index.ts +76 -6
  544. package/src/landing/landing-sections.tsx +7 -3
  545. package/src/landing/public-header.tsx +4 -4
  546. package/src/lib/cn.ts +13 -2
  547. package/src/rhf/fields.tsx +1 -1
  548. package/src/rhf/form.tsx +2 -2
  549. package/src/search/command-palette.tsx +10 -9
  550. package/src/settings/settings-layout.tsx +2 -1
  551. package/src/settings/use-account-appearance.ts +148 -0
  552. package/src/settings/use-settings-route.ts +3 -4
  553. package/src/shell/app-shell.tsx +278 -54
  554. package/src/shell/auth-layout.tsx +1 -1
  555. package/src/shell/option-switcher-menu.tsx +1 -1
  556. package/src/shell/top-bar-brand.tsx +3 -1
  557. package/src/shell/topbar-action-menu.tsx +3 -3
  558. package/src/shell/topbar-controls.tsx +4 -4
  559. package/src/theme/contrast-tokens.ts +112 -0
  560. package/src/theme/contrast.ts +225 -0
  561. package/src/theme/palette-presets.ts +24 -4
  562. package/src/theme/palette-store.ts +6 -1
  563. package/src/theme/text-size.ts +283 -0
  564. package/src/wizard/wizard-summary.tsx +4 -2
  565. package/tokens.css +158 -8
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/lightbox.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactNode, TouchEvent } from \"react\";\nimport { ChevronLeft, ChevronRight, Download, ExternalLink, FileText, ZoomIn } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { useAuthedSrc } from \"../hooks/use-authed-src\";\nimport type { AuthedFetcher } from \"../hooks/use-authed-src\";\nimport { FullBleedDialog } from \"./full-bleed-dialog\";\nimport { IconButton, buttonClasses } from \"./ui\";\nimport { ImageStates, DEFAULT_AUTHED_IMAGE_LABELS } from \"./authed-image\";\nimport { useAnchorDir } from \"./use-anchor-dir\";\n\n/* ── The item both the grid and the lightbox show ────────────────────────── */\n\n/** `image`: shown as a picture. `file`: anything a browser cannot put in an `<img>` —\n * a floor plan uploaded as a PDF — shown as a file card with open and download. */\nexport type ImageItemKind = \"image\" | \"file\";\n\nexport interface ImageItem {\n /** React key. Defaults to `src`. */\n key?: string | number;\n /** The full-size file — what the lightbox shows and downloads. An API path when the\n * component is given a `fetcher`. */\n src: string;\n /** A smaller rendition for the grid, when the server has one. Default: `src`. */\n thumbnailSrc?: string;\n /** Required: the picture's text alternative, and the open button's name. */\n alt: string;\n /** Shown under the thumbnail and at the foot of the lightbox. */\n caption?: ReactNode;\n /** The file's MIME type, when known (`\"application/pdf\"`). Decides {@link kind}. */\n mimeType?: string;\n /** Say it outright. Default: from {@link mimeType}, else from the file extension in\n * `src`, else — for a fetched file — from the type the server sent back. */\n kind?: ImageItemKind;\n /** The name a download is saved under, and a file card's heading. */\n fileName?: string;\n}\n\nconst FILE_EXTENSION = /\\.(pdf|docx?|xlsx?|pptx?|odt|ods|zip|txt|csv|rtf)(?:[?#]|$)/i;\n\n/** What an item is, from what is known about it WITHOUT fetching it — see\n * {@link ImageItem.kind}. `fetchedType` is the blob's type once it is in. */\nexport function imageItemKind(item: ImageItem, fetchedType?: string): ImageItemKind {\n if (item.kind) return item.kind;\n if (item.mimeType) return item.mimeType.startsWith(\"image/\") ? \"image\" : \"file\";\n if (FILE_EXTENSION.test(item.src)) return \"file\";\n if (fetchedType && !fetchedType.startsWith(\"image/\")) return \"file\";\n return \"image\";\n}\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\nexport interface LightboxLabels {\n /** The dialog's accessible name. */\n dialog: string;\n close: string;\n previous: string;\n next: string;\n /** The visible counter: \"3 / 7\". */\n counter: (index: number, count: number) => string;\n /** What a screen reader hears on each page turn: \"Image 3 of 7\". The alt follows. */\n position: (index: number, count: number) => string;\n download: string;\n /** The zoom toggle (a pressed/not-pressed button — its name does not change). */\n zoom: string;\n /** A file card's note: the file has no picture to show. */\n noPreview: string;\n /** A file card's link to the browser's own viewer (a PDF opens in its viewer). */\n openInNewTab: string;\n}\n\nexport const DEFAULT_LIGHTBOX_LABELS: LightboxLabels = {\n dialog: \"Image viewer\",\n close: \"Close\",\n previous: \"Previous image\",\n next: \"Next image\",\n counter: (index, count) => `${index} / ${count}`,\n position: (index, count) => `Image ${index} of ${count}`,\n download: \"Download\",\n zoom: \"Zoom\",\n noPreview: \"No preview available for this file\",\n openInNewTab: \"Open in new tab\",\n};\n\n/* ── The component ───────────────────────────────────────────────────────── */\n\nexport interface LightboxProps {\n open: boolean;\n /** The X, Escape and the Back gesture all call this. */\n onClose: () => void;\n items: readonly ImageItem[];\n /** The item on screen (0-based). Controlled, so the grid — or a URL — can own it. */\n index: number;\n onIndexChange: (index: number) => void;\n /** The app's authenticated GET (see {@link useAuthedSrc}); left out, `src` is\n * loaded by the browser as usual. */\n fetcher?: AuthedFetcher | null;\n /** Previous from the first goes to the last, and next from the last to the first.\n * Default `false`: the buttons at the ends are disabled, which says where the set\n * ends. */\n loop?: boolean;\n /** Double-click (or double-tap) the picture, or the zoom button, to see it at full\n * size and pan by scrolling. Default `true`. Pinch zoom is the browser's own. */\n zoom?: boolean;\n /** A download link in the header. Default `true`. */\n download?: boolean;\n /**\n * Download through the app instead — kastlan's floor plans have `openDocument(id)`,\n * which knows the stored file name. Replaces the link with a button that calls this.\n */\n onDownload?: (item: ImageItem, index: number) => void;\n /** Push a history entry so Back closes the viewer. Default `true`; see\n * `FullBleedDialog`'s `backCloses` for when to turn it off. */\n backCloses?: boolean;\n labels?: Partial<LightboxLabels>;\n}\n\n/** Swipes shorter than this are taps, and a drag that is mostly vertical is a scroll. */\nconst SWIPE_MIN_PX = 50;\n\n/**\n * A full-screen viewer for a set of images (and files that are not images).\n *\n * Built on {@link FullBleedDialog} rather than beside it, so the viewer has the kit's\n * one implementation of everything an overlay owes the page: the focus trap, Escape,\n * the Back gesture closing it instead of leaving the page (`useOverlayHistory`), the\n * body-scroll lock and the enter/exit motion.\n *\n * Paging: the previous/next buttons, ←/→ (which swap in a right-to-left page — the\n * direction is read at the point the viewer is rendered, since the dialog itself is\n * portalled out of it), Home/End, and a horizontal swipe on a touch screen.\n *\n * Zoom is a TOGGLE, not a gesture: double-click (or the zoom button) shows the picture\n * at its natural size and the body scrolls to pan. Pinch-to-zoom is not implemented —\n * the browser's own page pinch still works — because a pinch handler would have to own\n * every touch on the picture, and with it the swipe.\n *\n * A non-image (a PDF floor plan) is shown as a file card: its name, \"no preview\", a\n * link that opens it in the browser's own viewer, and the download. An `<iframe>`\n * preview was the alternative; phone browsers render a PDF in one inconsistently or\n * not at all, and a card works everywhere.\n */\nexport function Lightbox({\n open,\n onClose,\n items,\n index: indexProp,\n onIndexChange,\n fetcher,\n loop = false,\n zoom: zoomEnabled = true,\n download = true,\n onDownload,\n backCloses,\n labels: labelsProp,\n}: LightboxProps) {\n const labels = useKitLabels(\"lightbox\", DEFAULT_LIGHTBOX_LABELS, labelsProp);\n const imageLabels = useKitLabels(\"authedImage\", DEFAULT_AUTHED_IMAGE_LABELS);\n const count = items.length;\n // Clamped: the set can shrink under an open viewer (the photo on screen was deleted).\n const index = count === 0 ? 0 : Math.min(Math.max(indexProp, 0), count - 1);\n const item = items[index] as ImageItem | undefined;\n\n // The dialog is portalled to <body>, out of the subtree whose `dir` it would have\n // inherited; this marker stays where the viewer was rendered and reads it there.\n const anchorRef = useRef<HTMLSpanElement>(null);\n const dir = useAnchorDir(anchorRef, open);\n\n const fetched = useAuthedSrc(item?.src, { fetcher, enabled: open });\n const kind = item ? imageItemKind(item, fetched.type) : \"image\";\n\n const [zoomedKey, setZoomedKey] = useState<string | null>(null);\n const itemKey = item ? String(item.key ?? item.src) : null;\n // Zoom belongs to the picture it was turned on for: paging on resets it.\n const zoomed = zoomEnabled && kind === \"image\" && zoomedKey !== null && zoomedKey === itemKey;\n const toggleZoom = () => setZoomedKey(zoomed ? null : itemKey);\n\n const canPrev = loop ? count > 1 : index > 0;\n const canNext = loop ? count > 1 : index < count - 1;\n const go = (step: number) => {\n if (count === 0) return;\n let next = index + step;\n if (loop) next = (next + count) % count;\n if (next < 0 || next >= count || next === index) return;\n onIndexChange(next);\n };\n\n const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n // A key meant for a control stays with it: Space/Enter on a button, typing in any\n // field a caller's caption might hold.\n const target = e.target as HTMLElement;\n if (target.closest(\"input, textarea, select, [contenteditable='true']\")) return;\n const step = horizontalStep(e.key, e.currentTarget);\n if (step !== 0) {\n e.preventDefault();\n go(step);\n } else if (e.key === \"Home\") {\n e.preventDefault();\n if (index !== 0) onIndexChange(0);\n } else if (e.key === \"End\") {\n e.preventDefault();\n if (index !== count - 1) onIndexChange(count - 1);\n }\n };\n\n const touchStart = useRef<{ x: number; y: number } | null>(null);\n const onTouchStart = (e: TouchEvent) => {\n const t = e.touches[0];\n touchStart.current = e.touches.length === 1 && t ? { x: t.clientX, y: t.clientY } : null;\n };\n const onTouchEnd = (e: TouchEvent) => {\n const start = touchStart.current;\n touchStart.current = null;\n const t = e.changedTouches[0];\n // Zoomed, a drag is a PAN of the picture, not a page turn.\n if (!start || !t || zoomed) return;\n const dx = t.clientX - start.x;\n const dy = t.clientY - start.y;\n if (Math.abs(dx) < SWIPE_MIN_PX || Math.abs(dx) < Math.abs(dy)) return;\n // Swiping toward the START edge brings in the next picture from the end edge.\n const towardStart = dir === \"rtl\" ? dx > 0 : dx < 0;\n go(towardStart ? 1 : -1);\n };\n\n // Paging the viewer while zoomed would leave the new picture scrolled to where the\n // last one was panned; start each at its top-left.\n const bodyRef = useRef<HTMLDivElement>(null);\n useEffect(() => {\n bodyRef.current?.scrollTo?.({ top: 0, left: 0 });\n }, [index]);\n\n const downloadHref = fetched.src;\n const downloadControl =\n download && item ? (\n onDownload ? (\n <IconButton size=\"sm\" label={labels.download} onClick={() => onDownload(item, index)}>\n <Download />\n </IconButton>\n ) : downloadHref ? (\n <a\n href={downloadHref}\n // `download` names the saved file; a blob: URL has no name of its own.\n download={item.fileName ?? \"\"}\n className={buttonClasses(\"ghost\", { size: \"sm\" })}\n >\n <Download aria-hidden className=\"size-4\" />\n <span className=\"max-sm:sr-only\">{labels.download}</span>\n </a>\n ) : null\n ) : null;\n\n const header = item ? (\n <div className=\"flex min-w-0 items-center gap-2\">\n <span aria-hidden className=\"shrink-0 text-sm tabular-nums text-[var(--text-secondary)]\">\n {labels.counter(index + 1, count)}\n </span>\n {/* The page turn, said — focus does not move when the picture changes, so\n without this a reader pressing → hears nothing at all. */}\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {`${labels.position(index + 1, count)}${item.alt ? `: ${item.alt}` : \"\"}`}\n </span>\n <span className=\"min-w-0 flex-1\" />\n {zoomEnabled && kind === \"image\" && (\n <IconButton size=\"sm\" label={labels.zoom} pressed={zoomed} onClick={toggleZoom}>\n <ZoomIn />\n </IconButton>\n )}\n {downloadControl}\n </div>\n ) : null;\n\n const slide = !item ? null : kind === \"file\" ? (\n <div className=\"flex max-w-sm flex-col items-center gap-3 rounded-lg border border-[var(--border)] p-6 text-center\">\n <FileText aria-hidden className=\"size-10 text-[var(--text-muted)]\" />\n <p className=\"break-all font-medium text-[var(--text-primary)]\">{item.fileName ?? item.alt}</p>\n <p className=\"text-sm text-[var(--text-muted)]\">{labels.noPreview}</p>\n {fetched.src && (\n <a\n href={fetched.src}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n className={buttonClasses(\"secondary\", { size: \"sm\" })}\n >\n <ExternalLink aria-hidden className=\"size-4\" />\n {labels.openInNewTab}\n </a>\n )}\n </div>\n ) : (\n <ImageStates\n // Keyed by item: a new picture is a new decode, never the old one's \"loaded\".\n key={itemKey ?? undefined}\n fetched={fetched}\n alt={item.alt}\n labels={imageLabels}\n wrapperClassName={\n zoomed ? \"max-w-none shrink-0\" : \"flex h-full w-full items-center justify-center\"\n }\n className={cn(\n zoomed ? \"max-w-none cursor-zoom-out\" : \"max-h-full max-w-full object-contain\",\n !zoomed && zoomEnabled && \"cursor-zoom-in\",\n )}\n imgProps={{\n draggable: false,\n onDoubleClick: zoomEnabled ? toggleZoom : undefined,\n }}\n />\n );\n\n return (\n <>\n <span ref={anchorRef} hidden />\n <FullBleedDialog\n open={open && !!item}\n onClose={onClose}\n closeLabel={labels.close}\n aria-label={labels.dialog}\n dir={dir}\n header={header}\n backCloses={backCloses}\n onKeyDown={onKeyDown}\n data-lightbox=\"\"\n footer={\n item?.caption !== undefined && item.caption !== null && item.caption !== false ? (\n <p className=\"me-auto min-w-0 text-sm text-[var(--text-secondary)]\">{item.caption}</p>\n ) : undefined\n }\n >\n <div\n ref={bodyRef}\n data-lightbox-stage=\"\"\n data-zoomed={zoomed ? \"\" : undefined}\n onTouchStart={onTouchStart}\n onTouchEnd={onTouchEnd}\n className={cn(\n \"relative h-full min-h-48 w-full\",\n zoomed ? \"overflow-auto\" : \"flex items-center justify-center overflow-hidden\",\n )}\n >\n {slide}\n </div>\n {count > 1 && (\n <>\n <IconButton\n variant=\"overlay\"\n size=\"lg\"\n label={labels.previous}\n disabled={!canPrev}\n onClick={() => go(-1)}\n className=\"absolute start-3 top-1/2 -translate-y-1/2\"\n tooltip={false}\n >\n <ChevronLeft className=\"rtl:-scale-x-100\" />\n </IconButton>\n <IconButton\n variant=\"overlay\"\n size=\"lg\"\n label={labels.next}\n disabled={!canNext}\n onClick={() => go(1)}\n className=\"absolute end-3 top-1/2 -translate-y-1/2\"\n tooltip={false}\n >\n <ChevronRight className=\"rtl:-scale-x-100\" />\n </IconButton>\n </>\n )}\n </FullBleedDialog>\n </>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/components/lightbox.tsx"],"sourcesContent":["import { useEffect, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactNode, TouchEvent } from \"react\";\nimport { ChevronLeft, ChevronRight, Download, ExternalLink, FileText, ZoomIn } from \"lucide-react\";\n\nimport { cn } from \"../lib/cn\";\nimport { horizontalStep } from \"../lib/direction\";\nimport { useKitLabels } from \"../i18n/kit-labels\";\nimport { useAuthedSrc } from \"../hooks/use-authed-src\";\nimport { useBreakpoint } from \"../hooks/use-breakpoint\";\nimport type { AuthedFetcher } from \"../hooks/use-authed-src\";\nimport { FullBleedDialog } from \"./full-bleed-dialog\";\nimport { IconButton, buttonClasses } from \"./ui\";\nimport { ImageStates, DEFAULT_AUTHED_IMAGE_LABELS } from \"./authed-image\";\nimport { useAnchorDir } from \"./use-anchor-dir\";\n\n/* ── The item both the grid and the lightbox show ────────────────────────── */\n\n/** `image`: shown as a picture. `file`: anything a browser cannot put in an `<img>` —\n * a floor plan uploaded as a PDF — shown as a file card with open and download. */\nexport type ImageItemKind = \"image\" | \"file\";\n\nexport interface ImageItem {\n /** React key. Defaults to `src`. */\n key?: string | number;\n /** The full-size file — what the lightbox shows and downloads. An API path when the\n * component is given a `fetcher`. */\n src: string;\n /** A smaller rendition for the grid, when the server has one. Default: `src`. */\n thumbnailSrc?: string;\n /** Required: the picture's text alternative, and the open button's name. */\n alt: string;\n /** Shown under the thumbnail and at the foot of the lightbox. */\n caption?: ReactNode;\n /** The file's MIME type, when known (`\"application/pdf\"`). Decides {@link kind}. */\n mimeType?: string;\n /** Say it outright. Default: from {@link mimeType}, else from the file extension in\n * `src`, else — for a fetched file — from the type the server sent back. */\n kind?: ImageItemKind;\n /** The name a download is saved under, and a file card's heading. */\n fileName?: string;\n}\n\nconst FILE_EXTENSION = /\\.(pdf|docx?|xlsx?|pptx?|odt|ods|zip|txt|csv|rtf)(?:[?#]|$)/i;\n\n/** What an item is, from what is known about it WITHOUT fetching it — see\n * {@link ImageItem.kind}. `fetchedType` is the blob's type once it is in. */\nexport function imageItemKind(item: ImageItem, fetchedType?: string): ImageItemKind {\n if (item.kind) return item.kind;\n if (item.mimeType) return item.mimeType.startsWith(\"image/\") ? \"image\" : \"file\";\n if (FILE_EXTENSION.test(item.src)) return \"file\";\n if (fetchedType && !fetchedType.startsWith(\"image/\")) return \"file\";\n return \"image\";\n}\n\n/* ── Labels ──────────────────────────────────────────────────────────────── */\n\nexport interface LightboxLabels {\n /** The dialog's accessible name. */\n dialog: string;\n close: string;\n previous: string;\n next: string;\n /** The visible counter: \"3 / 7\". */\n counter: (index: number, count: number) => string;\n /** What a screen reader hears on each page turn: \"Image 3 of 7\". The alt follows. */\n position: (index: number, count: number) => string;\n download: string;\n /** The zoom toggle (a pressed/not-pressed button — its name does not change). */\n zoom: string;\n /** A file card's note: the file has no picture to show. */\n noPreview: string;\n /** A file card's link to the browser's own viewer (a PDF opens in its viewer). */\n openInNewTab: string;\n}\n\nexport const DEFAULT_LIGHTBOX_LABELS: LightboxLabels = {\n dialog: \"Image viewer\",\n close: \"Close\",\n previous: \"Previous image\",\n next: \"Next image\",\n counter: (index, count) => `${index} / ${count}`,\n position: (index, count) => `Image ${index} of ${count}`,\n download: \"Download\",\n zoom: \"Zoom\",\n noPreview: \"No preview available for this file\",\n openInNewTab: \"Open in new tab\",\n};\n\n/* ── The component ───────────────────────────────────────────────────────── */\n\nexport interface LightboxProps {\n open: boolean;\n /** The X, Escape and the Back gesture all call this. */\n onClose: () => void;\n items: readonly ImageItem[];\n /** The item on screen (0-based). Controlled, so the grid — or a URL — can own it. */\n index: number;\n onIndexChange: (index: number) => void;\n /** The app's authenticated GET (see {@link useAuthedSrc}); left out, `src` is\n * loaded by the browser as usual. */\n fetcher?: AuthedFetcher | null;\n /** Previous from the first goes to the last, and next from the last to the first.\n * Default `false`: the buttons at the ends are disabled, which says where the set\n * ends. */\n loop?: boolean;\n /** Double-click (or double-tap) the picture, or the zoom button, to see it at full\n * size and pan by scrolling. Default `true`. Pinch zoom is the browser's own. */\n zoom?: boolean;\n /** A download link in the header. Default `true`. */\n download?: boolean;\n /**\n * Download through the app instead — kastlan's floor plans have `openDocument(id)`,\n * which knows the stored file name. Replaces the link with a button that calls this.\n */\n onDownload?: (item: ImageItem, index: number) => void;\n /** Push a history entry so Back closes the viewer. Default `true`; see\n * `FullBleedDialog`'s `backCloses` for when to turn it off. */\n backCloses?: boolean;\n labels?: Partial<LightboxLabels>;\n}\n\n/** Swipes shorter than this are taps, and a drag that is mostly vertical is a scroll. */\nconst SWIPE_MIN_PX = 50;\n\n/**\n * A full-screen viewer for a set of images (and files that are not images).\n *\n * Built on {@link FullBleedDialog} rather than beside it, so the viewer has the kit's\n * one implementation of everything an overlay owes the page: the focus trap, Escape,\n * the Back gesture closing it instead of leaving the page (`useOverlayHistory`), the\n * body-scroll lock and the enter/exit motion.\n *\n * Paging: the previous/next buttons, ←/→ (which swap in a right-to-left page — the\n * direction is read at the point the viewer is rendered, since the dialog itself is\n * portalled out of it), Home/End, and a horizontal swipe on a touch screen.\n *\n * Zoom is a TOGGLE, not a gesture: double-click (or the zoom button) shows the picture\n * at its natural size and the body scrolls to pan. Pinch-to-zoom is not implemented —\n * the browser's own page pinch still works — because a pinch handler would have to own\n * every touch on the picture, and with it the swipe.\n *\n * A non-image (a PDF floor plan) is shown as a file card: its name, \"no preview\", a\n * link that opens it in the browser's own viewer, and the download. An `<iframe>`\n * preview was the alternative; phone browsers render a PDF in one inconsistently or\n * not at all, and a card works everywhere.\n */\nexport function Lightbox({\n open,\n onClose,\n items,\n index: indexProp,\n onIndexChange,\n fetcher,\n loop = false,\n zoom: zoomEnabled = true,\n download = true,\n onDownload,\n backCloses,\n labels: labelsProp,\n}: LightboxProps) {\n const labels = useKitLabels(\"lightbox\", DEFAULT_LIGHTBOX_LABELS, labelsProp);\n const imageLabels = useKitLabels(\"authedImage\", DEFAULT_AUTHED_IMAGE_LABELS);\n const count = items.length;\n // Clamped: the set can shrink under an open viewer (the photo on screen was deleted).\n const index = count === 0 ? 0 : Math.min(Math.max(indexProp, 0), count - 1);\n const item = items[index] as ImageItem | undefined;\n\n // The dialog is portalled to <body>, out of the subtree whose `dir` it would have\n // inherited; this marker stays where the viewer was rendered and reads it there.\n const anchorRef = useRef<HTMLSpanElement>(null);\n const dir = useAnchorDir(anchorRef, open);\n\n // The toolbar's words (docs/text-size-harmonization.md §4, §10.8). At Large an\n // IconButton shows its label beside the glyph; on a narrow screen the viewer's header —\n // the counter, Zoom, Download and the dialog's close — has no room for two words at\n // 150 % (a 390 px phone is 260 px of Normal type), so below `sm` the toolbar keeps its\n // icons, as the plain download link hides its word there (`max-sm:sr-only`). From `sm`\n // up, at the same breakpoint the CSS uses, the words show. Each name stays the\n // accessible name and the tooltip either way.\n const narrow = useBreakpoint(\"max-sm\");\n const toolbarLabelVisible = narrow ? false : undefined;\n\n const fetched = useAuthedSrc(item?.src, { fetcher, enabled: open });\n const kind = item ? imageItemKind(item, fetched.type) : \"image\";\n\n const [zoomedKey, setZoomedKey] = useState<string | null>(null);\n const itemKey = item ? String(item.key ?? item.src) : null;\n // Zoom belongs to the picture it was turned on for: paging on resets it.\n const zoomed = zoomEnabled && kind === \"image\" && zoomedKey !== null && zoomedKey === itemKey;\n const toggleZoom = () => setZoomedKey(zoomed ? null : itemKey);\n\n const canPrev = loop ? count > 1 : index > 0;\n const canNext = loop ? count > 1 : index < count - 1;\n const go = (step: number) => {\n if (count === 0) return;\n let next = index + step;\n if (loop) next = (next + count) % count;\n if (next < 0 || next >= count || next === index) return;\n onIndexChange(next);\n };\n\n const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n // A key meant for a control stays with it: Space/Enter on a button, typing in any\n // field a caller's caption might hold.\n const target = e.target as HTMLElement;\n if (target.closest(\"input, textarea, select, [contenteditable='true']\")) return;\n const step = horizontalStep(e.key, e.currentTarget);\n if (step !== 0) {\n e.preventDefault();\n go(step);\n } else if (e.key === \"Home\") {\n e.preventDefault();\n if (index !== 0) onIndexChange(0);\n } else if (e.key === \"End\") {\n e.preventDefault();\n if (index !== count - 1) onIndexChange(count - 1);\n }\n };\n\n const touchStart = useRef<{ x: number; y: number } | null>(null);\n const onTouchStart = (e: TouchEvent) => {\n const t = e.touches[0];\n touchStart.current = e.touches.length === 1 && t ? { x: t.clientX, y: t.clientY } : null;\n };\n const onTouchEnd = (e: TouchEvent) => {\n const start = touchStart.current;\n touchStart.current = null;\n const t = e.changedTouches[0];\n // Zoomed, a drag is a PAN of the picture, not a page turn.\n if (!start || !t || zoomed) return;\n const dx = t.clientX - start.x;\n const dy = t.clientY - start.y;\n if (Math.abs(dx) < SWIPE_MIN_PX || Math.abs(dx) < Math.abs(dy)) return;\n // Swiping toward the START edge brings in the next picture from the end edge.\n const towardStart = dir === \"rtl\" ? dx > 0 : dx < 0;\n go(towardStart ? 1 : -1);\n };\n\n // Paging the viewer while zoomed would leave the new picture scrolled to where the\n // last one was panned; start each at its top-left.\n const bodyRef = useRef<HTMLDivElement>(null);\n useEffect(() => {\n bodyRef.current?.scrollTo?.({ top: 0, left: 0 });\n }, [index]);\n\n const downloadHref = fetched.src;\n const downloadControl =\n download && item ? (\n onDownload ? (\n <IconButton\n size=\"sm\"\n label={labels.download}\n labelVisible={toolbarLabelVisible}\n onClick={() => onDownload(item, index)}\n >\n <Download />\n </IconButton>\n ) : downloadHref ? (\n <a\n href={downloadHref}\n // `download` names the saved file; a blob: URL has no name of its own.\n download={item.fileName ?? \"\"}\n className={buttonClasses(\"ghost\", { size: \"sm\" })}\n >\n <Download aria-hidden className=\"size-4\" />\n <span className=\"max-sm:sr-only\">{labels.download}</span>\n </a>\n ) : null\n ) : null;\n\n const header = item ? (\n <div className=\"flex min-w-0 items-center gap-2\">\n <span aria-hidden className=\"shrink-0 text-sm tabular-nums text-[var(--text-secondary)]\">\n {labels.counter(index + 1, count)}\n </span>\n {/* The page turn, said — focus does not move when the picture changes, so\n without this a reader pressing → hears nothing at all. */}\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {`${labels.position(index + 1, count)}${item.alt ? `: ${item.alt}` : \"\"}`}\n </span>\n <span className=\"min-w-0 flex-1\" />\n {zoomEnabled && kind === \"image\" && (\n <IconButton\n size=\"sm\"\n label={labels.zoom}\n labelVisible={toolbarLabelVisible}\n pressed={zoomed}\n onClick={toggleZoom}\n >\n <ZoomIn />\n </IconButton>\n )}\n {downloadControl}\n </div>\n ) : null;\n\n const slide = !item ? null : kind === \"file\" ? (\n <div className=\"flex max-w-sm flex-col items-center gap-3 rounded-lg border border-[var(--border)] p-6 text-center\">\n <FileText aria-hidden className=\"size-10 text-[var(--text-muted)]\" />\n <p className=\"break-all font-medium text-[var(--text-primary)]\">{item.fileName ?? item.alt}</p>\n <p className=\"text-sm text-[var(--text-muted)]\">{labels.noPreview}</p>\n {fetched.src && (\n <a\n href={fetched.src}\n target=\"_blank\"\n rel=\"noreferrer noopener\"\n className={buttonClasses(\"secondary\", { size: \"sm\" })}\n >\n <ExternalLink aria-hidden className=\"size-4\" />\n {labels.openInNewTab}\n </a>\n )}\n </div>\n ) : (\n <ImageStates\n // Keyed by item: a new picture is a new decode, never the old one's \"loaded\".\n key={itemKey ?? undefined}\n fetched={fetched}\n alt={item.alt}\n labels={imageLabels}\n wrapperClassName={\n zoomed ? \"max-w-none shrink-0\" : \"flex h-full w-full items-center justify-center\"\n }\n className={cn(\n zoomed ? \"max-w-none cursor-zoom-out\" : \"max-h-full max-w-full object-contain\",\n !zoomed && zoomEnabled && \"cursor-zoom-in\",\n )}\n imgProps={{\n draggable: false,\n onDoubleClick: zoomEnabled ? toggleZoom : undefined,\n }}\n />\n );\n\n return (\n <>\n <span ref={anchorRef} hidden />\n <FullBleedDialog\n open={open && !!item}\n onClose={onClose}\n closeLabel={labels.close}\n aria-label={labels.dialog}\n dir={dir}\n header={header}\n backCloses={backCloses}\n onKeyDown={onKeyDown}\n data-lightbox=\"\"\n footer={\n item?.caption !== undefined && item.caption !== null && item.caption !== false ? (\n <p className=\"me-auto min-w-0 text-sm text-[var(--text-secondary)]\">{item.caption}</p>\n ) : undefined\n }\n >\n <div\n ref={bodyRef}\n data-lightbox-stage=\"\"\n data-zoomed={zoomed ? \"\" : undefined}\n onTouchStart={onTouchStart}\n onTouchEnd={onTouchEnd}\n className={cn(\n \"relative h-full min-h-48 w-full\",\n zoomed ? \"overflow-auto\" : \"flex items-center justify-center overflow-hidden\",\n )}\n >\n {slide}\n </div>\n {count > 1 && (\n <>\n <IconButton\n variant=\"overlay\"\n size=\"lg\"\n label={labels.previous}\n disabled={!canPrev}\n onClick={() => go(-1)}\n className=\"absolute start-3 top-1/2 -translate-y-1/2\"\n tooltip={false}\n >\n <ChevronLeft className=\"rtl:-scale-x-100\" />\n </IconButton>\n <IconButton\n variant=\"overlay\"\n size=\"lg\"\n label={labels.next}\n disabled={!canNext}\n onClick={() => go(1)}\n className=\"absolute end-3 top-1/2 -translate-y-1/2\"\n tooltip={false}\n >\n <ChevronRight className=\"rtl:-scale-x-100\" />\n </IconButton>\n </>\n )}\n </FullBleedDialog>\n </>\n );\n}\n"],"mappings":"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","names":[]}
@@ -153,6 +153,7 @@ function LineItems({
153
153
  variant: isConfirming ? "danger" : "ghost",
154
154
  tone: isConfirming ? void 0 : "danger",
155
155
  label: isConfirming ? labels.confirmRemove(row) : labels.remove(row),
156
+ labelVisible: false,
156
157
  disabled: !canRemove,
157
158
  onClick: () => void removeAt(index),
158
159
  onBlur: () => {
@@ -330,7 +331,7 @@ function LineItems({
330
331
  ]
331
332
  }
332
333
  ),
333
- hasError && /* @__PURE__ */ jsx("p", { className: "text-[11px] leading-tight text-[var(--danger)]", children: error }),
334
+ hasError && /* @__PURE__ */ jsx("p", { className: "text-caption leading-tight text-[var(--danger)]", children: error }),
334
335
  onAdd && /* @__PURE__ */ jsxs(
335
336
  Button,
336
337
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/line-items.tsx"],"sourcesContent":["import { useCallback, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, CSSProperties, HTMLAttributes, 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 /** The column's own name (\"Debit\") for the field's VISIBLE label — set only when the\n * cell's labels are `\"floating\"` (see {@link LineItemsProps.fieldLabels}), so one\n * render serves both modes: `<Input label={fieldLabel} aria-label={label} … />`.\n * Undefined there, the Input stays a bare compact control; set, it floats \"Debit\"\n * while `aria-label` still says which row. */\n fieldLabel?: string;\n}\n\n/** How a cell's field is labelled. `aria`: the field is a bare control named by\n * `aria-label`, under a header row (stacked: a small copy of the header over it).\n * `floating`: the field shows its own floating label ({@link LineItemCellContext.fieldLabel}),\n * so the list draws no header for it. */\nexport type LineItemsFieldLabels = \"aria\" | \"floating\";\n\n/** The colour of a {@link LineItemsSummary}'s value — a verdict (`success` /\n * `danger`), quiet (`muted`), or the money pair (`income` / `expense`, the colours\n * `SignedAmount` draws a figure in) for a remainder that reads as the row it settles. */\nexport type LineItemsSummaryTone = \"neutral\" | \"success\" | \"danger\" | \"muted\" | \"income\" | \"expense\";\n\n/**\n * A line under the lines that says where they stand — a split's remainder (\"Left to\n * assign CHF 0.00\", green at zero, red otherwise, muted while the total is not known\n * yet), with an optional action that settles it (\"Set amount to total\").\n */\nexport interface LineItemsSummary {\n label: ReactNode;\n value?: ReactNode;\n /** The value's colour. Default `neutral`. The label stays neutral: the colour is a\n * second signal, never the only one — the value and label carry the meaning.\n * With no `value` the line is a state on its own (\"Waiting for the total\"), and\n * the tone colours the label instead — `muted` quiets the whole line. */\n tone?: LineItemsSummaryTone;\n /** Beside the value: a chip or badge that names the state (\"Balanced\" next to a\n * journal entry's zero difference). */\n status?: ReactNode;\n /** A small button at the end of the line. Disabled along with the list's\n * `disabled`. `variant`: `secondary` (default) is a boxed button; `link` a muted\n * text link, for an action that is an aside rather than the way to settle it. */\n action?: { label: ReactNode; onClick: () => void; disabled?: boolean; variant?: \"secondary\" | \"link\" };\n}\n\n/** Where a row's remove button sits. `row` (default): stacked, a row of the card to\n * itself (paired, the whole width); wide, its own track. `inline`: stacked, beside\n * the row's last field (\"category + amount\" over \"memo + delete\"); wide, unchanged. */\nexport type LineItemsRemovePlacement = \"row\" | \"inline\";\n\n/** How the remove button sits in the height of a wide row: at its top, centred, or at\n * its foot. */\nexport type LineItemsRemoveAlign = \"start\" | \"center\" | \"end\";\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 /** This column's labelling, over the list's {@link LineItemsProps.fieldLabels} — a\n * read-only \"Sum\" cell in a floating list keeps its header with `\"aria\"`. */\n fieldLabels?: LineItemsFieldLabels;\n /** With {@link LineItemsProps.narrowColumns} `2`: `2` gives the cell a whole row of\n * the stacked card (a description), where short fields pair up two to a row.\n * Default `1`. No effect on the wide grid. */\n narrowSpan?: 1 | 2;\n render: (ctx: LineItemCellContext<T>) => ReactNode;\n}\n\n/**\n * What {@link LineItemsProps.rowProps} returns for one row: `data-*` hooks, an `id`, a\n * class, handlers. A `data-*` attribute is typed so `{ \"data-tour\": \"rule-condition\" }`\n * needs no cast.\n */\nexport type LineItemsRowProps = Omit<HTMLAttributes<HTMLDivElement>, \"children\" | \"dangerouslySetInnerHTML\" | \"role\"> & {\n [key: `data-${string}`]: string | number | boolean | undefined;\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 /** A line under the lines and totals — the remainder of a split, say. See\n * {@link LineItemsSummary}. */\n summary?: LineItemsSummary;\n /** How the cells' fields are labelled. Default `aria` (compact bare controls under a\n * header row); `floating` hands each cell its column's name as\n * {@link LineItemCellContext.fieldLabel} for a visible floating label, and drops the\n * header for those columns. A column's own `fieldLabels` wins. */\n fieldLabels?: LineItemsFieldLabels;\n /** Stacked (a narrow pane), how many fields share a row of a line's card: `2` pairs\n * them up (a column's `narrowSpan={2}` takes a whole row), once the pane is wide\n * enough for two fields (18rem); narrower still, one. Default `1`. */\n narrowColumns?: 1 | 2;\n /** Stacked, where the remove button goes: a row of its own (`row`, default) or\n * beside the last field (`inline`). See {@link LineItemsRemovePlacement}. */\n removePlacement?: LineItemsRemovePlacement;\n /** On the wide grid, the remove button's place in the row's height. Default `start`\n * — or `center` when a column floats its label (`fieldLabels=\"floating\"`): a\n * floating field is taller than the button, and centred is level with its control.\n * A field that shows a message under itself grows the row; pin `start` then. */\n removeAlign?: LineItemsRemoveAlign;\n /** Disables add and remove (the fields are the caller's to disable). */\n disabled?: boolean;\n /**\n * Extra attributes for one row's element (the `role=\"group\"` that holds its fields) —\n * keksdose K20: its rule editor's tour points at the first condition row with\n * `[data-tour=\"rule-condition\"]` (dev#495), and with nowhere to put the anchor on the\n * row it wrapped a cell's field in a `span` instead, which the spotlight then drew\n * around one field rather than the row. Test ids the same way.\n *\n * MERGED under the list's own, which win: `role`, the group's `aria-label` (that is\n * {@link rowLabel}'s) and `data-line-items-row` stay the list's — the row's name and\n * the hook its focus handling finds rows by. `className` is added after the row's\n * own classes, `style` is passed through, and an `onKeyDown` runs BEFORE the row's\n * Ctrl/⌘+Enter \"add a row\" (call `preventDefault()` to keep that from happening).\n */\n rowProps?: (item: T, index: number) => LineItemsRowProps | undefined;\n labels?: Partial<LineItemsLabels>;\n}\n\nconst REMOVE_TRACK = \"2.25rem\";\nconst SUMMARY_TONE: Record<LineItemsSummaryTone, string> = {\n neutral: \"text-[var(--text-primary)]\",\n success: \"text-[var(--success)]\",\n danger: \"text-[var(--danger)]\",\n muted: \"text-[var(--text-muted)]\",\n // SignedAmount's money palette (toneTextClass \"income\" / \"expense\").\n income: \"text-[var(--money-income)]\",\n expense: \"text-[var(--money-expense)]\",\n};\nconst REMOVE_ALIGN: Record<LineItemsRemoveAlign, string> = {\n start: \"@lg:self-start\",\n center: \"@lg:self-center\",\n end: \"@lg:self-end\",\n};\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 summary,\n fieldLabels = \"aria\",\n narrowColumns = 1,\n removePlacement = \"row\",\n removeAlign: removeAlignProp,\n disabled = false,\n rowProps,\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 const floats = (column: LineItemsColumn<T>) => (column.fieldLabels ?? fieldLabels) === \"floating\";\n // A header row only for the columns that still need one: with every field floating\n // its own label, a row of headers would say each name twice.\n const showHeader = columns.some((c) => !floats(c));\n const pairs = narrowColumns === 2;\n const removeAlign = removeAlignProp ?? (columns.some(floats) ? \"center\" : \"start\");\n const inline = removePlacement === \"inline\" && Boolean(onRemove) && columns.length > 0;\n const lastColumn = columns[columns.length - 1];\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 {floats(column) ? null : column.header}\n </span>\n );\n\n const removeCell = (index: number, row: number, isConfirming: boolean) => (\n <div\n data-line-items-remove-cell=\"\"\n className={cn(\n \"flex justify-end @lg:justify-center\",\n REMOVE_ALIGN[removeAlign],\n // Paired, the remove button keeps a row of its own at the card's end rather\n // than squeezing in beside the last field — unless asked to go `inline`.\n inline ? \"shrink-0\" : pairs && \"@2xs:col-span-2 @lg:col-span-1\",\n )}\n >\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\n const showTotals = totals !== undefined;\n const summaryTone = summary?.tone ?? \"neutral\";\n const summaryHasValue = hasContent(summary?.value);\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 {showHeader && (\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\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 const extra = rowProps?.(item, 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 {...extra}\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={(e) => {\n if (extra?.onKeyDown) {\n extra.onKeyDown(e);\n if (e.defaultPrevented) return;\n }\n onRowKeyDown(e);\n }}\n className={cn(\n \"grid grid-cols-1 gap-2 @lg:grid-cols-[var(--line-items-cols)] @lg:items-start\",\n // Two short fields to a row of the card — from 18rem, where two fields\n // still fit a figure each; on anything narrower they stack after all.\n pairs && \"@2xs:grid-cols-2\",\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 extra?.className,\n )}\n >\n {columns.map((column) => {\n const floating = floats(column);\n const isLast = inline && column === lastColumn;\n const span = pairs && column.narrowSpan === 2 && \"@2xs:col-span-2 @lg:col-span-1\";\n const cell = (\n <div\n key={column.key}\n className={cn(\n \"min-w-0 space-y-1 @lg:space-y-0\",\n // The wide grid places every cell in its own track again.\n isLast ? \"flex-1\" : span,\n )}\n >\n {!floating && (\n <span aria-hidden className=\"block text-xs font-medium text-[var(--text-muted)] @lg:hidden\">\n {column.header}\n </span>\n )}\n {column.render({\n item,\n index,\n label: labels.cell(columnName(column), row),\n fieldLabel: floating ? columnName(column) : undefined,\n })}\n </div>\n );\n if (!isLast) return cell;\n // Inline, the last field and the remove button share one cell of the\n // card; on the wide grid the wrapper dissolves (`contents`) and each is a\n // track of its own again. Level with the field's control: its foot under\n // a header copy, its middle when it floats its own label.\n return (\n <div\n key={column.key}\n data-line-items-inline=\"\"\n className={cn(\"flex min-w-0 gap-2 @lg:contents\", floating ? \"items-center\" : \"items-end\", span)}\n >\n {cell}\n {removeCell(index, row, isConfirming)}\n </div>\n );\n })}\n {onRemove && !inline && removeCell(index, row, isConfirming)}\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 {summary && (\n <div\n data-line-items-summary=\"\"\n className=\"flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-[var(--text-primary)]\"\n >\n {/* With no value the line is a state of its own, and the label takes the\n tone — the only place left to carry it. */}\n <span\n data-tone={summaryHasValue ? undefined : summaryTone}\n className={cn(\"min-w-0\", !summaryHasValue && SUMMARY_TONE[summaryTone])}\n >\n {summary.label}\n </span>\n {summaryHasValue && (\n <span data-tone={summaryTone} className={cn(\"font-semibold tabular-nums\", SUMMARY_TONE[summaryTone])}>\n {summary.value}\n </span>\n )}\n {hasContent(summary.status) && (\n <span data-line-items-summary-status=\"\" className=\"inline-flex\">\n {summary.status}\n </span>\n )}\n {summary.action && (\n <Button\n type=\"button\"\n variant={summary.action.variant === \"link\" ? \"link\" : \"secondary\"}\n tone={summary.action.variant === \"link\" ? \"muted\" : undefined}\n size=\"sm\"\n className=\"ms-auto\"\n disabled={disabled || summary.action.disabled}\n onClick={summary.action.onClick}\n >\n {summary.action.label}\n </Button>\n )}\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":"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","names":["row"]}
1
+ {"version":3,"sources":["../../src/components/line-items.tsx"],"sourcesContent":["import { useCallback, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, CSSProperties, HTMLAttributes, 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 /** The column's own name (\"Debit\") for the field's VISIBLE label — set only when the\n * cell's labels are `\"floating\"` (see {@link LineItemsProps.fieldLabels}), so one\n * render serves both modes: `<Input label={fieldLabel} aria-label={label} … />`.\n * Undefined there, the Input stays a bare compact control; set, it floats \"Debit\"\n * while `aria-label` still says which row. */\n fieldLabel?: string;\n}\n\n/** How a cell's field is labelled. `aria`: the field is a bare control named by\n * `aria-label`, under a header row (stacked: a small copy of the header over it).\n * `floating`: the field shows its own floating label ({@link LineItemCellContext.fieldLabel}),\n * so the list draws no header for it. */\nexport type LineItemsFieldLabels = \"aria\" | \"floating\";\n\n/** The colour of a {@link LineItemsSummary}'s value — a verdict (`success` /\n * `danger`), quiet (`muted`), or the money pair (`income` / `expense`, the colours\n * `SignedAmount` draws a figure in) for a remainder that reads as the row it settles. */\nexport type LineItemsSummaryTone = \"neutral\" | \"success\" | \"danger\" | \"muted\" | \"income\" | \"expense\";\n\n/**\n * A line under the lines that says where they stand — a split's remainder (\"Left to\n * assign CHF 0.00\", green at zero, red otherwise, muted while the total is not known\n * yet), with an optional action that settles it (\"Set amount to total\").\n */\nexport interface LineItemsSummary {\n label: ReactNode;\n value?: ReactNode;\n /** The value's colour. Default `neutral`. The label stays neutral: the colour is a\n * second signal, never the only one — the value and label carry the meaning.\n * With no `value` the line is a state on its own (\"Waiting for the total\"), and\n * the tone colours the label instead — `muted` quiets the whole line. */\n tone?: LineItemsSummaryTone;\n /** Beside the value: a chip or badge that names the state (\"Balanced\" next to a\n * journal entry's zero difference). */\n status?: ReactNode;\n /** A small button at the end of the line. Disabled along with the list's\n * `disabled`. `variant`: `secondary` (default) is a boxed button; `link` a muted\n * text link, for an action that is an aside rather than the way to settle it. */\n action?: { label: ReactNode; onClick: () => void; disabled?: boolean; variant?: \"secondary\" | \"link\" };\n}\n\n/** Where a row's remove button sits. `row` (default): stacked, a row of the card to\n * itself (paired, the whole width); wide, its own track. `inline`: stacked, beside\n * the row's last field (\"category + amount\" over \"memo + delete\"); wide, unchanged. */\nexport type LineItemsRemovePlacement = \"row\" | \"inline\";\n\n/** How the remove button sits in the height of a wide row: at its top, centred, or at\n * its foot. */\nexport type LineItemsRemoveAlign = \"start\" | \"center\" | \"end\";\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 /** This column's labelling, over the list's {@link LineItemsProps.fieldLabels} — a\n * read-only \"Sum\" cell in a floating list keeps its header with `\"aria\"`. */\n fieldLabels?: LineItemsFieldLabels;\n /** With {@link LineItemsProps.narrowColumns} `2`: `2` gives the cell a whole row of\n * the stacked card (a description), where short fields pair up two to a row.\n * Default `1`. No effect on the wide grid. */\n narrowSpan?: 1 | 2;\n render: (ctx: LineItemCellContext<T>) => ReactNode;\n}\n\n/**\n * What {@link LineItemsProps.rowProps} returns for one row: `data-*` hooks, an `id`, a\n * class, handlers. A `data-*` attribute is typed so `{ \"data-tour\": \"rule-condition\" }`\n * needs no cast.\n */\nexport type LineItemsRowProps = Omit<HTMLAttributes<HTMLDivElement>, \"children\" | \"dangerouslySetInnerHTML\" | \"role\"> & {\n [key: `data-${string}`]: string | number | boolean | undefined;\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 /** A line under the lines and totals — the remainder of a split, say. See\n * {@link LineItemsSummary}. */\n summary?: LineItemsSummary;\n /** How the cells' fields are labelled. Default `aria` (compact bare controls under a\n * header row); `floating` hands each cell its column's name as\n * {@link LineItemCellContext.fieldLabel} for a visible floating label, and drops the\n * header for those columns. A column's own `fieldLabels` wins. */\n fieldLabels?: LineItemsFieldLabels;\n /** Stacked (a narrow pane), how many fields share a row of a line's card: `2` pairs\n * them up (a column's `narrowSpan={2}` takes a whole row), once the pane is wide\n * enough for two fields (18rem); narrower still, one. Default `1`. */\n narrowColumns?: 1 | 2;\n /** Stacked, where the remove button goes: a row of its own (`row`, default) or\n * beside the last field (`inline`). See {@link LineItemsRemovePlacement}. */\n removePlacement?: LineItemsRemovePlacement;\n /** On the wide grid, the remove button's place in the row's height. Default `start`\n * — or `center` when a column floats its label (`fieldLabels=\"floating\"`): a\n * floating field is taller than the button, and centred is level with its control.\n * A field that shows a message under itself grows the row; pin `start` then. */\n removeAlign?: LineItemsRemoveAlign;\n /** Disables add and remove (the fields are the caller's to disable). */\n disabled?: boolean;\n /**\n * Extra attributes for one row's element (the `role=\"group\"` that holds its fields) —\n * keksdose K20: its rule editor's tour points at the first condition row with\n * `[data-tour=\"rule-condition\"]` (dev#495), and with nowhere to put the anchor on the\n * row it wrapped a cell's field in a `span` instead, which the spotlight then drew\n * around one field rather than the row. Test ids the same way.\n *\n * MERGED under the list's own, which win: `role`, the group's `aria-label` (that is\n * {@link rowLabel}'s) and `data-line-items-row` stay the list's — the row's name and\n * the hook its focus handling finds rows by. `className` is added after the row's\n * own classes, `style` is passed through, and an `onKeyDown` runs BEFORE the row's\n * Ctrl/⌘+Enter \"add a row\" (call `preventDefault()` to keep that from happening).\n */\n rowProps?: (item: T, index: number) => LineItemsRowProps | undefined;\n labels?: Partial<LineItemsLabels>;\n}\n\nconst REMOVE_TRACK = \"2.25rem\";\nconst SUMMARY_TONE: Record<LineItemsSummaryTone, string> = {\n neutral: \"text-[var(--text-primary)]\",\n success: \"text-[var(--success)]\",\n danger: \"text-[var(--danger)]\",\n muted: \"text-[var(--text-muted)]\",\n // SignedAmount's money palette (toneTextClass \"income\" / \"expense\").\n income: \"text-[var(--money-income)]\",\n expense: \"text-[var(--money-expense)]\",\n};\nconst REMOVE_ALIGN: Record<LineItemsRemoveAlign, string> = {\n start: \"@lg:self-start\",\n center: \"@lg:self-center\",\n end: \"@lg:self-end\",\n};\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 summary,\n fieldLabels = \"aria\",\n narrowColumns = 1,\n removePlacement = \"row\",\n removeAlign: removeAlignProp,\n disabled = false,\n rowProps,\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 const floats = (column: LineItemsColumn<T>) => (column.fieldLabels ?? fieldLabels) === \"floating\";\n // A header row only for the columns that still need one: with every field floating\n // its own label, a row of headers would say each name twice.\n const showHeader = columns.some((c) => !floats(c));\n const pairs = narrowColumns === 2;\n const removeAlign = removeAlignProp ?? (columns.some(floats) ? \"center\" : \"start\");\n const inline = removePlacement === \"inline\" && Boolean(onRemove) && columns.length > 0;\n const lastColumn = columns[columns.length - 1];\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 {floats(column) ? null : column.header}\n </span>\n );\n\n const removeCell = (index: number, row: number, isConfirming: boolean) => (\n <div\n data-line-items-remove-cell=\"\"\n className={cn(\n \"flex justify-end @lg:justify-center\",\n REMOVE_ALIGN[removeAlign],\n // Paired, the remove button keeps a row of its own at the card's end rather\n // than squeezing in beside the last field — unless asked to go `inline`.\n inline ? \"shrink-0\" : pairs && \"@2xs:col-span-2 @lg:col-span-1\",\n )}\n >\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 // The icon alone at every text size (docs/text-size-harmonization.md §10.8): the\n // row's one control, in a grid column sized for an icon, whose name becomes a\n // sentence while it waits (\"Remove row 3? Press again to confirm\") — as text at\n // Large it would push the row's fields aside and change width between the two\n // presses. The name stays the accessible name; the red fill says \"confirm\".\n labelVisible={false}\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\n const showTotals = totals !== undefined;\n const summaryTone = summary?.tone ?? \"neutral\";\n const summaryHasValue = hasContent(summary?.value);\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 {showHeader && (\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\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 const extra = rowProps?.(item, 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 {...extra}\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={(e) => {\n if (extra?.onKeyDown) {\n extra.onKeyDown(e);\n if (e.defaultPrevented) return;\n }\n onRowKeyDown(e);\n }}\n className={cn(\n \"grid grid-cols-1 gap-2 @lg:grid-cols-[var(--line-items-cols)] @lg:items-start\",\n // Two short fields to a row of the card — from 18rem, where two fields\n // still fit a figure each; on anything narrower they stack after all.\n pairs && \"@2xs:grid-cols-2\",\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 extra?.className,\n )}\n >\n {columns.map((column) => {\n const floating = floats(column);\n const isLast = inline && column === lastColumn;\n const span = pairs && column.narrowSpan === 2 && \"@2xs:col-span-2 @lg:col-span-1\";\n const cell = (\n <div\n key={column.key}\n className={cn(\n \"min-w-0 space-y-1 @lg:space-y-0\",\n // The wide grid places every cell in its own track again.\n isLast ? \"flex-1\" : span,\n )}\n >\n {!floating && (\n <span aria-hidden className=\"block text-xs font-medium text-[var(--text-muted)] @lg:hidden\">\n {column.header}\n </span>\n )}\n {column.render({\n item,\n index,\n label: labels.cell(columnName(column), row),\n fieldLabel: floating ? columnName(column) : undefined,\n })}\n </div>\n );\n if (!isLast) return cell;\n // Inline, the last field and the remove button share one cell of the\n // card; on the wide grid the wrapper dissolves (`contents`) and each is a\n // track of its own again. Level with the field's control: its foot under\n // a header copy, its middle when it floats its own label.\n return (\n <div\n key={column.key}\n data-line-items-inline=\"\"\n className={cn(\"flex min-w-0 gap-2 @lg:contents\", floating ? \"items-center\" : \"items-end\", span)}\n >\n {cell}\n {removeCell(index, row, isConfirming)}\n </div>\n );\n })}\n {onRemove && !inline && removeCell(index, row, isConfirming)}\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 {summary && (\n <div\n data-line-items-summary=\"\"\n className=\"flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-[var(--text-primary)]\"\n >\n {/* With no value the line is a state of its own, and the label takes the\n tone — the only place left to carry it. */}\n <span\n data-tone={summaryHasValue ? undefined : summaryTone}\n className={cn(\"min-w-0\", !summaryHasValue && SUMMARY_TONE[summaryTone])}\n >\n {summary.label}\n </span>\n {summaryHasValue && (\n <span data-tone={summaryTone} className={cn(\"font-semibold tabular-nums\", SUMMARY_TONE[summaryTone])}>\n {summary.value}\n </span>\n )}\n {hasContent(summary.status) && (\n <span data-line-items-summary-status=\"\" className=\"inline-flex\">\n {summary.status}\n </span>\n )}\n {summary.action && (\n <Button\n type=\"button\"\n variant={summary.action.variant === \"link\" ? \"link\" : \"secondary\"}\n tone={summary.action.variant === \"link\" ? \"muted\" : undefined}\n size=\"sm\"\n className=\"ms-auto\"\n disabled={disabled || summary.action.disabled}\n onClick={summary.action.onClick}\n >\n {summary.action.label}\n </Button>\n )}\n </div>\n )}\n\n {hasError && (\n <p className=\"text-caption 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":"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","names":["row"]}
@@ -338,6 +338,11 @@ interface NotExpandable {
338
338
  * One row of a {@link List}: leading icon or avatar, title and subtitle (both
339
339
  * truncating by default), a trailing slot, and — beside the row, not inside it — its actions.
340
340
  *
341
+ * At Large and Extra large (0.32, docs/text-size-harmonization.md §4) nothing in it
342
+ * truncates: the overline, title, subtitle and meta wrap, a two-line clamp lets go, and
343
+ * a trailing slot drops under the text when the two no longer fit side by side. The
344
+ * rows lose their one height there; that is the trade the reader asked for.
345
+ *
341
346
  * All three apps draw this row by hand, a dozen times: kastlan's units
342
347
  * (building-detail-page.tsx:402), open tickets (group-overview-page.tsx:160), activity
343
348
  * feed (activity-feed.tsx:53), company users (platform-company-detail-page.tsx:140)
@@ -3,7 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { createContext, forwardRef, useContext, useId, useState } from "react";
4
4
  import { ChevronDown, ExternalLink } from "lucide-react";
5
5
  import { cn } from "../lib/cn.js";
6
- import { Spinner } from "./ui.js";
6
+ import { FOCUS_RING, Spinner } from "./ui.js";
7
7
  import { StatusDot } from "./status-dot.js";
8
8
  import { Collapse } from "./disclosure.js";
9
9
  import { toneTextClass } from "./signed-amount.js";
@@ -41,7 +41,9 @@ const PAD = {
41
41
  default: { target: "gap-3 px-3 py-2.5", actions: "pe-2", leadingActions: "ps-2", gap: "gap-1", body: "px-3 pb-2.5" },
42
42
  comfortable: { target: "gap-3 px-4 py-3", actions: "pe-3", leadingActions: "ps-3", gap: "gap-1", body: "px-4 pb-3" }
43
43
  };
44
- const TARGET_RING = "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--brand)]";
44
+ const TARGET_RING = cn(FOCUS_RING, "focus-visible:ring-inset");
45
+ const WRAP_AT_LARGE = "large:whitespace-normal large:[overflow-wrap:anywhere]";
46
+ const UNCLAMP_AT_LARGE = "large:line-clamp-none";
45
47
  const ListItem = forwardRef(function ListItem2({
46
48
  title,
47
49
  titleLines = 1,
@@ -118,15 +120,15 @@ const ListItem = forwardRef(function ListItem2({
118
120
  }
119
121
  ),
120
122
  leading,
121
- /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [
122
- overline != null && /* @__PURE__ */ jsx("span", { className: "block truncate text-[11px] font-medium leading-snug text-[var(--text-muted)]", children: overline }),
123
+ /* @__PURE__ */ jsxs("span", { className: cn("flex min-w-0 flex-1 flex-col", trailing != null && "large:basis-40"), children: [
124
+ overline != null && /* @__PURE__ */ jsx("span", { className: cn("block truncate text-caption font-medium leading-snug text-[var(--text-muted)]", WRAP_AT_LARGE), children: overline }),
123
125
  /* @__PURE__ */ jsxs(
124
126
  "span",
125
127
  {
126
128
  id: stacked ? titleId : void 0,
127
129
  className: cn(
128
130
  "block text-sm text-[var(--text-primary)]",
129
- titleLines === 1 ? "truncate" : titleLines === 2 ? "line-clamp-2 break-words" : "break-words",
131
+ titleLines === 1 ? cn("truncate", WRAP_AT_LARGE) : titleLines === 2 ? cn("line-clamp-2 break-words", UNCLAMP_AT_LARGE) : "break-words",
130
132
  unread ? "font-semibold" : "font-medium"
131
133
  ),
132
134
  children: [
@@ -149,7 +151,7 @@ const ListItem = forwardRef(function ListItem2({
149
151
  {
150
152
  className: cn(
151
153
  "block text-xs text-[var(--text-muted)]",
152
- subtitleLines === 2 ? "line-clamp-2" : "truncate"
154
+ subtitleLines === 2 ? cn("line-clamp-2 break-words", UNCLAMP_AT_LARGE) : cn("truncate", WRAP_AT_LARGE)
153
155
  ),
154
156
  children: subtitle
155
157
  }
@@ -158,8 +160,8 @@ const ListItem = forwardRef(function ListItem2({
158
160
  "span",
159
161
  {
160
162
  className: cn(
161
- "mt-0.5 text-[11px] leading-snug text-[var(--text-muted)]",
162
- metaWrap ? "flex flex-wrap items-center gap-1" : "block truncate"
163
+ "mt-0.5 text-caption leading-snug text-[var(--text-muted)]",
164
+ metaWrap ? "flex flex-wrap items-center gap-1" : cn("block truncate", WRAP_AT_LARGE)
163
165
  ),
164
166
  children: meta
165
167
  }
@@ -170,7 +172,7 @@ const ListItem = forwardRef(function ListItem2({
170
172
  "span",
171
173
  {
172
174
  className: cn(
173
- "flex shrink-0 items-center gap-2 text-xs",
175
+ "flex shrink-0 items-center gap-2 text-xs large:ms-auto",
174
176
  trailingTone !== "inherit" && toneTextClass(trailingTone)
175
177
  ),
176
178
  children: trailing
@@ -194,6 +196,8 @@ const ListItem = forwardRef(function ListItem2({
194
196
  ] });
195
197
  const target = cn(
196
198
  "flex min-w-0 flex-1 text-start",
199
+ // §4 at Large: the trailing slot drops under the text when both no longer fit.
200
+ trailing != null && "large:flex-wrap",
197
201
  align === "start" ? "items-start" : "items-center",
198
202
  pad.target,
199
203
  // When stacked the target is the top of the box, not all of it, so it takes its
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/list.tsx"],"sourcesContent":["import { createContext, forwardRef, useContext, useId, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, HTMLAttributes, MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { ChevronDown, 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 { Collapse } from \"./disclosure\";\nimport { toneTextClass } from \"./signed-amount\";\nimport type { TextTone } from \"./signed-amount\";\nimport { useKitLabels, useKitLink } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer } from \"./text-link\";\nimport { mergeDescribedBy } from \"./choice-parts\";\nimport { LockedReason, useLockReason } from \"./field-parts\";\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<\n ListDensity,\n { target: string; actions: string; leadingActions: string; gap: string; body: string }\n> = {\n compact: { target: \"gap-2 px-2 py-1.5\", actions: \"pe-1.5\", leadingActions: \"ps-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\", leadingActions: \"ps-2\", gap: \"gap-1\", body: \"px-3 pb-2.5\" },\n comfortable: { target: \"gap-3 px-4 py-3\", actions: \"pe-3\", leadingActions: \"ps-3\", gap: \"gap-1\", body: \"px-4 pb-3\" },\n};\n\n/** {@link ListItemBaseProps.trailingTone}: a {@link TextTone}, or `inherit` — no\n * colour of the slot's own, so an element's colour (or the row's) stands. */\nexport type ListItemTrailingTone = TextTone | \"inherit\";\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 — unless {@link titleLines} says otherwise. */\n title: ReactNode;\n /**\n * How many lines the title may take. `1` (default) truncates with an ellipsis, which\n * is right for a name the app chose — a unit number, a user, a file — where the\n * cut-off end is still recognisable and every row keeps one height.\n *\n * `2` clamps at two lines; `\"all\"` shows every line, breaking inside a long word\n * rather than overflowing. For rows whose only name is free text a person typed\n * (kastlan 44): a defect or a note titled by its description, where the first 30\n * characters on a phone say nothing and there is no other field to tell two rows\n * apart. Pair it with `align=\"start\"` so the icon stays by the first line.\n */\n titleLines?: 1 | 2 | \"all\";\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 /**\n * A small line ABOVE the title, in the caption type: the group a row belongs to, a\n * date heading, a category (\"Groceries · weekly\"). Part of the target and of its\n * name, read before the title. One line, truncated.\n */\n overline?: ReactNode;\n /** A third line in the caption type: a timestamp, \"Updated …\" (lenkbank's projects). */\n meta?: ReactNode;\n /**\n * Let {@link meta} wrap: it becomes a wrapping row (`flex-wrap`, a small gap)\n * instead of one truncated line — for a run of chips (keksdose's payee-backfill\n * tags), which otherwise had nowhere to go but the body, outside the target. Still\n * inside the target, so still no controls: chips that only show, not ones that\n * remove themselves.\n */\n metaWrap?: boolean;\n /**\n * Content INSIDE the main target, under the title, subtitle and meta: a progress or\n * budget bar, a sparkline, a strip of figures (keksdose's food-group bars). Unlike\n * {@link children}, which sits outside the target, a click on it is a click on the\n * row, and its hover fill covers it.\n *\n * Because it is inside the button or link it obeys the {@link trailing} rule: shown,\n * never operated — no buttons, links or inputs (invalid HTML in a button, and folded\n * into its name). Its text joins the row's accessible name, so give a bar that only\n * repeats a figure already in the row `aria-hidden`. Rendered in a block `<span>`,\n * full width of the TEXT COLUMN only — the column the title and subtitle are in, not\n * the whole row: it starts after the {@link icon} / {@link leading} and stops where\n * {@link trailing} begins, so a meter under a row with a trailing figure is as wide as\n * the title, not run on under the figure (keksdose's budget rows). For a bar across\n * the full row, put it in {@link children} (outside the target) or leave `trailing`\n * off and show the figure in `meta`. Strictly a button holds phrasing content only; the\n * kit's `ProgressBar` draws `<div>`s, which every browser lays out and reads fine\n * there but an HTML validator flags.\n */\n content?: 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 * The colour of the {@link trailing} slot. Default `muted` — today's look, right for\n * a size or a count in plain text. `inherit` sets none, so an element's own colour\n * (an amount coloured by its sign) stands without a second override; any\n * {@link TextTone} colours a plain-text figure (`\"danger\"` for an overdue count).\n */\n trailingTone?: ListItemTrailingTone;\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 * Controls BEFORE the main target, never inside it: a selection checkbox, a drag\n * handle that is a button. The mirror of {@link actions} at the start edge — a\n * sibling of the target, so checking the box does not activate (or expand) the row\n * and the checkbox keeps its own name. Give the control an `aria-label` naming the\n * row (\"Select Groceries\"), since it is not inside the row's name. Anything that only\n * shows goes in {@link leading}, inside the target.\n */\n leadingActions?: 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 /**\n * Wraps the visible row (the bordered box: target, actions and body) in an element\n * of your own, INSIDE the `<li>` — a `<Tooltip>` explaining why the row is disabled,\n * an app's save guard: `renderRow={(row) => <Tooltip label=\"Locked\">{row}</Tooltip>}`.\n *\n * The wrapper otherwise had to go round the whole `ListItem`, which put a `<span>`\n * between the `<ul>` and its `<li>` (invalid; the list stops counting items), so\n * callers wrote `<li><Wrapper><ListItem as=\"div\" /></Wrapper></li>` — and lost the\n * divider and its spacing, which sit on the row's own `<li>`. Here the `<li>` stays\n * the list's child and keeps both. The `<li>` becomes a column and the row takes the\n * full width, so an inline wrapper (the tooltip's `inline-flex` span) still stretches\n * the row across the list.\n *\n * `row` is the row BOX (a `<div>`), not the target: a wrapper that forces a prop onto\n * its child with `cloneElement` — a save guard that injects `disabled` while a form is\n * dirty (keksdose's SaveGuard) — sets it on that div, where it does nothing, and the\n * button inside stays live. Pass `disabled` (or `loading`) to the ListItem yourself,\n * from the same state the guard reads, and let the wrapper only explain it:\n * `<ListItem disabled={dirty} renderRow={(row) => <Tooltip label=\"Save first\">{row}</Tooltip>} />`.\n */\n renderRow?: (row: ReactElement) => ReactNode;\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 /**\n * Why the row's click is not available — {@link Button}'s `disabledReason`, for\n * a row whose click WRITES (mark read, assign, toggle a grant). keksdose K3: such\n * rows sat behind a `SaveGuard` that forced a native `disabled`, out of the tab\n * order, so the reason never reached a keyboard; `renderRow` with a Tooltip could\n * explain it but not keep the row reachable.\n *\n * With a reason the row's button is `aria-disabled` instead: still focusable, a\n * click does nothing, the reason is in the kit {@link Tooltip} over the row and on\n * its `aria-describedby`, and the row wears the disabled look. It wins over\n * `disabled`. The row's `actions` are untouched — each control there is its own.\n */\n disabledReason?: ReactNode;\n /**\n * The row's click COMMITS. Under a locked {@link WriteLockProvider} it is locked\n * the `disabledReason` way with the lock's reason (which wins over its own). No\n * provider, or an unlocked one: no effect. Button's `commit`, for keksdose K3.\n */\n commit?: boolean;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n } & NotExpandable)\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 disabledReason?: never;\n commit?: never;\n } & NotExpandable)\n | ({\n onClick?: never;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n disabledReason?: never;\n commit?: never;\n } & NotExpandable)\n | {\n /**\n * Makes the row a disclosure: the target is a button with `aria-expanded` and a\n * turning chevron at its end, and this opens under the row, outside the target\n * (so it may hold controls), animated by {@link Collapse}. keksdose's collapsible\n * groups — a group row with a select-all checkbox before it\n * ({@link ListItemBaseProps.leadingActions}) and its members inside.\n *\n * The row's ONE action is the toggle, so it takes no `onClick` or `href`. The\n * content is a `role=\"group\"` named by the title, unmounted while shut (as a\n * `Disclosure`'s body is).\n */\n expandedContent: ReactNode;\n /** Controlled open state. Leave it out for the row to keep its own. */\n expanded?: boolean;\n /** The uncontrolled row's first state. Default `false`. */\n defaultExpanded?: boolean;\n /** Runs on every toggle with the new state. */\n onExpandedChange?: (expanded: boolean) => void;\n onClick?: never;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n disabledReason?: never;\n commit?: never;\n }\n );\n\ninterface NotExpandable {\n expandedContent?: never;\n expanded?: never;\n defaultExpanded?: never;\n onExpandedChange?: never;\n}\n\n/**\n * One row of a {@link List}: leading icon or avatar, title and subtitle (both\n * truncating by default), 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 titleLines = 1,\n overline,\n subtitle,\n subtitleLines = 1,\n meta,\n metaWrap = false,\n content,\n icon: Icon,\n leading,\n trailing,\n trailingTone = \"muted\",\n actions,\n leadingActions,\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 renderRow,\n children,\n bodyClassName,\n href,\n renderLink,\n external = false,\n onClick,\n onAuxClick,\n disabledReason,\n commit,\n expandedContent,\n expanded: expandedProp,\n defaultExpanded = false,\n onExpandedChange,\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 [ownExpanded, setOwnExpanded] = useState(defaultExpanded);\n const density = densityProp ?? list.density;\n const pad = PAD[density];\n const expandable = expandedContent !== undefined;\n const expanded = expandable && (expandedProp ?? ownExpanded);\n const isLink = href !== undefined && !disabled;\n const isButton = !isLink && (onClick !== undefined || expandable) && href === undefined;\n const interactive = isLink || isButton;\n // Only an onClick row takes a reason (the types allow no other); the hook runs on\n // every row so the hook order never depends on which kind of row this is.\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked && isButton && !expandable;\n const inert = disabled || loading || locked;\n const hasBody = children !== undefined && children !== null && children !== false;\n // Anything under the row's own line: the box turns into a column, and the target\n // takes its own corners and hover fill (see `children`).\n const stacked = hasBody || expandable;\n const titleId = useId();\n const expandedId = useId();\n\n const toggle = () => {\n const next = !expanded;\n if (expandedProp === undefined) setOwnExpanded(next);\n onExpandedChange?.(next);\n };\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 {overline != null && (\n <span className=\"block truncate text-[11px] font-medium leading-snug text-[var(--text-muted)]\">\n {overline}\n </span>\n )}\n <span\n // Names the body's and the expanded content's group; harmless without one.\n id={stacked ? titleId : undefined}\n className={cn(\n \"block text-sm text-[var(--text-primary)]\",\n titleLines === 1 ? \"truncate\" : titleLines === 2 ? \"line-clamp-2 break-words\" : \"break-words\",\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 && (\n <span\n className={cn(\n \"mt-0.5 text-[11px] leading-snug text-[var(--text-muted)]\",\n metaWrap ? \"flex flex-wrap items-center gap-1\" : \"block truncate\",\n )}\n >\n {meta}\n </span>\n )}\n {content != null && <span className=\"mt-1.5 block min-w-0\">{content}</span>}\n </span>\n {trailing != null && (\n <span\n className={cn(\n \"flex shrink-0 items-center gap-2 text-xs\",\n trailingTone !== \"inherit\" && toneTextClass(trailingTone),\n )}\n >\n {trailing}\n </span>\n )}\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 {expandable && (\n // Turned, not swapped — the Disclosure card's convention: down while shut, up\n // while open. A rotation is its own mirror, so RTL needs nothing.\n <ChevronDown\n aria-hidden\n className={cn(\n \"size-4 shrink-0 text-[var(--text-muted)] transition-transform duration-200 ease-out motion-reduce:transition-none\",\n align === \"start\" && \"mt-0.5\",\n expanded && \"rotate-180\",\n )}\n />\n )}\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 // When stacked 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 stacked ? \"rounded-md\" : \"rounded-[inherit]\",\n stacked && interactive && !inert && !selected && \"transition-colors hover:bg-[var(--bg-hover)]\",\n interactive && TARGET_RING,\n interactive && !inert && \"cursor-pointer\",\n loading && \"cursor-progress\",\n locked && !loading && \"cursor-not-allowed\",\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` — and so is a locked one:\n // a reason wins over `disabled`, so the row stays reachable to say it.\n disabled={locked ? undefined : disabled}\n aria-disabled={loading || locked || undefined}\n aria-busy={loading || undefined}\n aria-current={selected ? \"true\" : undefined}\n aria-expanded={expandable ? expanded : undefined}\n aria-controls={expandable ? expandedId : undefined}\n aria-describedby={\n locked\n ? mergeDescribedBy(rest[\"aria-describedby\"] as string | undefined, lock.reasonId)\n : (rest[\"aria-describedby\"] as string | undefined)\n }\n onClick={\n loading || locked\n ? undefined\n : expandable\n ? toggle\n : (onClick as (event: MouseEvent<HTMLButtonElement>) => void)\n }\n className={target}\n >\n {body}\n </button>\n );\n if (locked) {\n // The Tooltip takes the target's place in the row, so it takes its flex sizing\n // too; the button fills it.\n main = (\n <LockedReason lock={lock} className=\"flex min-w-0 flex-1\">\n {main}\n </LockedReason>\n );\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 leadingActionsEl =\n leadingActions != null ? (\n <div className={cn(\"flex shrink-0 items-center\", pad.gap, pad.leadingActions)}>{leadingActions}</div>\n ) : null;\n const actionsEl =\n actions != null ? <div className={cn(\"flex shrink-0 items-center\", pad.gap, pad.actions)}>{actions}</div> : null;\n\n const row = (\n <div\n className={cn(\n \"flex min-w-0 rounded-md border transition-colors\",\n stacked ? \"flex-col\" : \"items-center\",\n // See `renderRow`: the wrapper may be inline, the row is not.\n renderRow && \"w-full\",\n selected\n ? \"border-[var(--brand)] bg-[var(--bg-surface-2)]\"\n : bordered\n ? \"border-[var(--border)]\"\n : \"border-transparent\",\n !stacked && interactive && !inert && !selected && \"hover:bg-[var(--bg-hover)]\",\n (disabled || locked) && \"opacity-50\",\n className,\n )}\n >\n {stacked ? (\n <>\n <div className=\"flex min-w-0 items-center\">\n {leadingActionsEl}\n {main}\n {actionsEl}\n </div>\n {expandable && (\n <Collapse open={expanded}>\n <div id={expandedId} role=\"group\" aria-labelledby={titleId} className={cn(\"min-w-0\", pad.body)}>\n {expandedContent}\n </div>\n </Collapse>\n )}\n {hasBody && (\n <div role=\"group\" aria-labelledby={titleId} className={cn(\"min-w-0\", pad.body, bodyClassName)}>\n {children}\n </div>\n )}\n </>\n ) : (\n <>\n {leadingActionsEl}\n {main}\n {actionsEl}\n </>\n )}\n </div>\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 renderRow && \"flex flex-col\",\n )}\n >\n {renderRow ? renderRow(row) : row}\n </Tag>\n );\n});\n\nfunction RenderedLink({\n render,\n ...props\n}: ListItemLinkProps & { render: (props: ListItemLinkProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":"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","names":["List","ListItem"]}
1
+ {"version":3,"sources":["../../src/components/list.tsx"],"sourcesContent":["import { createContext, forwardRef, useContext, useId, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, HTMLAttributes, MouseEvent, ReactElement, ReactNode, Ref } from \"react\";\nimport { ChevronDown, ExternalLink } from \"lucide-react\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { FOCUS_RING, Spinner } from \"./ui\";\nimport { StatusDot } from \"./status-dot\";\nimport { Collapse } from \"./disclosure\";\nimport { toneTextClass } from \"./signed-amount\";\nimport type { TextTone } from \"./signed-amount\";\nimport { useKitLabels, useKitLink } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer } from \"./text-link\";\nimport { mergeDescribedBy } from \"./choice-parts\";\nimport { LockedReason, useLockReason } from \"./field-parts\";\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<\n ListDensity,\n { target: string; actions: string; leadingActions: string; gap: string; body: string }\n> = {\n compact: { target: \"gap-2 px-2 py-1.5\", actions: \"pe-1.5\", leadingActions: \"ps-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\", leadingActions: \"ps-2\", gap: \"gap-1\", body: \"px-3 pb-2.5\" },\n comfortable: { target: \"gap-3 px-4 py-3\", actions: \"pe-3\", leadingActions: \"ps-3\", gap: \"gap-1\", body: \"px-4 pb-3\" },\n};\n\n/** {@link ListItemBaseProps.trailingTone}: a {@link TextTone}, or `inherit` — no\n * colour of the slot's own, so an element's colour (or the row's) stands. */\nexport type ListItemTrailingTone = TextTone | \"inherit\";\n\n// The kit's focus frame (§5), inset so the row's own border and its neighbours do not\n// clip it.\nconst TARGET_RING = cn(FOCUS_RING, \"focus-visible:ring-inset\");\n\n/**\n * §4 \"ListItem: title, subtitle and meta wrap instead of truncating\" at Large and Extra\n * large (docs/text-size-harmonization.md). `large:` variants over the Normal look, so\n * a list keeps its one-height rows at Normal and costs no render to switch: a one-line\n * `truncate` wraps (and breaks a word too long for the row), a `line-clamp-2` lets go.\n * An ellipsis hides the end of a name, which is the part a reader who needs big type\n * cannot recover by squinting.\n */\nconst WRAP_AT_LARGE = \"large:whitespace-normal large:[overflow-wrap:anywhere]\";\nconst UNCLAMP_AT_LARGE = \"large:line-clamp-none\";\n\ninterface ListItemBaseProps {\n /** The row's name: one line, truncated — unless {@link titleLines} says otherwise. */\n title: ReactNode;\n /**\n * How many lines the title may take. `1` (default) truncates with an ellipsis, which\n * is right for a name the app chose — a unit number, a user, a file — where the\n * cut-off end is still recognisable and every row keeps one height.\n *\n * `2` clamps at two lines; `\"all\"` shows every line, breaking inside a long word\n * rather than overflowing. For rows whose only name is free text a person typed\n * (kastlan 44): a defect or a note titled by its description, where the first 30\n * characters on a phone say nothing and there is no other field to tell two rows\n * apart. Pair it with `align=\"start\"` so the icon stays by the first line.\n */\n titleLines?: 1 | 2 | \"all\";\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 /**\n * A small line ABOVE the title, in the caption type: the group a row belongs to, a\n * date heading, a category (\"Groceries · weekly\"). Part of the target and of its\n * name, read before the title. One line, truncated.\n */\n overline?: ReactNode;\n /** A third line in the caption type: a timestamp, \"Updated …\" (lenkbank's projects). */\n meta?: ReactNode;\n /**\n * Let {@link meta} wrap: it becomes a wrapping row (`flex-wrap`, a small gap)\n * instead of one truncated line — for a run of chips (keksdose's payee-backfill\n * tags), which otherwise had nowhere to go but the body, outside the target. Still\n * inside the target, so still no controls: chips that only show, not ones that\n * remove themselves.\n */\n metaWrap?: boolean;\n /**\n * Content INSIDE the main target, under the title, subtitle and meta: a progress or\n * budget bar, a sparkline, a strip of figures (keksdose's food-group bars). Unlike\n * {@link children}, which sits outside the target, a click on it is a click on the\n * row, and its hover fill covers it.\n *\n * Because it is inside the button or link it obeys the {@link trailing} rule: shown,\n * never operated — no buttons, links or inputs (invalid HTML in a button, and folded\n * into its name). Its text joins the row's accessible name, so give a bar that only\n * repeats a figure already in the row `aria-hidden`. Rendered in a block `<span>`,\n * full width of the TEXT COLUMN only — the column the title and subtitle are in, not\n * the whole row: it starts after the {@link icon} / {@link leading} and stops where\n * {@link trailing} begins, so a meter under a row with a trailing figure is as wide as\n * the title, not run on under the figure (keksdose's budget rows). For a bar across\n * the full row, put it in {@link children} (outside the target) or leave `trailing`\n * off and show the figure in `meta`. Strictly a button holds phrasing content only; the\n * kit's `ProgressBar` draws `<div>`s, which every browser lays out and reads fine\n * there but an HTML validator flags.\n */\n content?: 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 * The colour of the {@link trailing} slot. Default `muted` — today's look, right for\n * a size or a count in plain text. `inherit` sets none, so an element's own colour\n * (an amount coloured by its sign) stands without a second override; any\n * {@link TextTone} colours a plain-text figure (`\"danger\"` for an overdue count).\n */\n trailingTone?: ListItemTrailingTone;\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 * Controls BEFORE the main target, never inside it: a selection checkbox, a drag\n * handle that is a button. The mirror of {@link actions} at the start edge — a\n * sibling of the target, so checking the box does not activate (or expand) the row\n * and the checkbox keeps its own name. Give the control an `aria-label` naming the\n * row (\"Select Groceries\"), since it is not inside the row's name. Anything that only\n * shows goes in {@link leading}, inside the target.\n */\n leadingActions?: 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 /**\n * Wraps the visible row (the bordered box: target, actions and body) in an element\n * of your own, INSIDE the `<li>` — a `<Tooltip>` explaining why the row is disabled,\n * an app's save guard: `renderRow={(row) => <Tooltip label=\"Locked\">{row}</Tooltip>}`.\n *\n * The wrapper otherwise had to go round the whole `ListItem`, which put a `<span>`\n * between the `<ul>` and its `<li>` (invalid; the list stops counting items), so\n * callers wrote `<li><Wrapper><ListItem as=\"div\" /></Wrapper></li>` — and lost the\n * divider and its spacing, which sit on the row's own `<li>`. Here the `<li>` stays\n * the list's child and keeps both. The `<li>` becomes a column and the row takes the\n * full width, so an inline wrapper (the tooltip's `inline-flex` span) still stretches\n * the row across the list.\n *\n * `row` is the row BOX (a `<div>`), not the target: a wrapper that forces a prop onto\n * its child with `cloneElement` — a save guard that injects `disabled` while a form is\n * dirty (keksdose's SaveGuard) — sets it on that div, where it does nothing, and the\n * button inside stays live. Pass `disabled` (or `loading`) to the ListItem yourself,\n * from the same state the guard reads, and let the wrapper only explain it:\n * `<ListItem disabled={dirty} renderRow={(row) => <Tooltip label=\"Save first\">{row}</Tooltip>} />`.\n */\n renderRow?: (row: ReactElement) => ReactNode;\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 /**\n * Why the row's click is not available — {@link Button}'s `disabledReason`, for\n * a row whose click WRITES (mark read, assign, toggle a grant). keksdose K3: such\n * rows sat behind a `SaveGuard` that forced a native `disabled`, out of the tab\n * order, so the reason never reached a keyboard; `renderRow` with a Tooltip could\n * explain it but not keep the row reachable.\n *\n * With a reason the row's button is `aria-disabled` instead: still focusable, a\n * click does nothing, the reason is in the kit {@link Tooltip} over the row and on\n * its `aria-describedby`, and the row wears the disabled look. It wins over\n * `disabled`. The row's `actions` are untouched — each control there is its own.\n */\n disabledReason?: ReactNode;\n /**\n * The row's click COMMITS. Under a locked {@link WriteLockProvider} it is locked\n * the `disabledReason` way with the lock's reason (which wins over its own). No\n * provider, or an unlocked one: no effect. Button's `commit`, for keksdose K3.\n */\n commit?: boolean;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n } & NotExpandable)\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 disabledReason?: never;\n commit?: never;\n } & NotExpandable)\n | ({\n onClick?: never;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n disabledReason?: never;\n commit?: never;\n } & NotExpandable)\n | {\n /**\n * Makes the row a disclosure: the target is a button with `aria-expanded` and a\n * turning chevron at its end, and this opens under the row, outside the target\n * (so it may hold controls), animated by {@link Collapse}. keksdose's collapsible\n * groups — a group row with a select-all checkbox before it\n * ({@link ListItemBaseProps.leadingActions}) and its members inside.\n *\n * The row's ONE action is the toggle, so it takes no `onClick` or `href`. The\n * content is a `role=\"group\"` named by the title, unmounted while shut (as a\n * `Disclosure`'s body is).\n */\n expandedContent: ReactNode;\n /** Controlled open state. Leave it out for the row to keep its own. */\n expanded?: boolean;\n /** The uncontrolled row's first state. Default `false`. */\n defaultExpanded?: boolean;\n /** Runs on every toggle with the new state. */\n onExpandedChange?: (expanded: boolean) => void;\n onClick?: never;\n href?: never;\n renderLink?: never;\n external?: never;\n onAuxClick?: never;\n disabledReason?: never;\n commit?: never;\n }\n );\n\ninterface NotExpandable {\n expandedContent?: never;\n expanded?: never;\n defaultExpanded?: never;\n onExpandedChange?: never;\n}\n\n/**\n * One row of a {@link List}: leading icon or avatar, title and subtitle (both\n * truncating by default), a trailing slot, and — beside the row, not inside it — its actions.\n *\n * At Large and Extra large (0.32, docs/text-size-harmonization.md §4) nothing in it\n * truncates: the overline, title, subtitle and meta wrap, a two-line clamp lets go, and\n * a trailing slot drops under the text when the two no longer fit side by side. The\n * rows lose their one height there; that is the trade the reader asked for.\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 titleLines = 1,\n overline,\n subtitle,\n subtitleLines = 1,\n meta,\n metaWrap = false,\n content,\n icon: Icon,\n leading,\n trailing,\n trailingTone = \"muted\",\n actions,\n leadingActions,\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 renderRow,\n children,\n bodyClassName,\n href,\n renderLink,\n external = false,\n onClick,\n onAuxClick,\n disabledReason,\n commit,\n expandedContent,\n expanded: expandedProp,\n defaultExpanded = false,\n onExpandedChange,\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 [ownExpanded, setOwnExpanded] = useState(defaultExpanded);\n const density = densityProp ?? list.density;\n const pad = PAD[density];\n const expandable = expandedContent !== undefined;\n const expanded = expandable && (expandedProp ?? ownExpanded);\n const isLink = href !== undefined && !disabled;\n const isButton = !isLink && (onClick !== undefined || expandable) && href === undefined;\n const interactive = isLink || isButton;\n // Only an onClick row takes a reason (the types allow no other); the hook runs on\n // every row so the hook order never depends on which kind of row this is.\n const lock = useLockReason(commit, disabledReason);\n const locked = lock.locked && isButton && !expandable;\n const inert = disabled || loading || locked;\n const hasBody = children !== undefined && children !== null && children !== false;\n // Anything under the row's own line: the box turns into a column, and the target\n // takes its own corners and hover fill (see `children`).\n const stacked = hasBody || expandable;\n const titleId = useId();\n const expandedId = useId();\n\n const toggle = () => {\n const next = !expanded;\n if (expandedProp === undefined) setOwnExpanded(next);\n onExpandedChange?.(next);\n };\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 {/* With a trailing slot, a basis at Large: below it the trailing chip or figure\n wraps under the text (the target wraps, see `target`) rather than squeezing the\n now-wrapping title into a column one word wide (§4, 240 px effective width). */}\n <span className={cn(\"flex min-w-0 flex-1 flex-col\", trailing != null && \"large:basis-40\")}>\n {overline != null && (\n <span className={cn(\"block truncate text-caption font-medium leading-snug text-[var(--text-muted)]\", WRAP_AT_LARGE)}>\n {overline}\n </span>\n )}\n <span\n // Names the body's and the expanded content's group; harmless without one.\n id={stacked ? titleId : undefined}\n className={cn(\n \"block text-sm text-[var(--text-primary)]\",\n titleLines === 1\n ? cn(\"truncate\", WRAP_AT_LARGE)\n : titleLines === 2\n ? cn(\"line-clamp-2 break-words\", UNCLAMP_AT_LARGE)\n : \"break-words\",\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 ? cn(\"line-clamp-2 break-words\", UNCLAMP_AT_LARGE) : cn(\"truncate\", WRAP_AT_LARGE),\n )}\n >\n {subtitle}\n </span>\n )}\n {meta != null && (\n <span\n className={cn(\n \"mt-0.5 text-caption leading-snug text-[var(--text-muted)]\",\n metaWrap ? \"flex flex-wrap items-center gap-1\" : cn(\"block truncate\", WRAP_AT_LARGE),\n )}\n >\n {meta}\n </span>\n )}\n {content != null && <span className=\"mt-1.5 block min-w-0\">{content}</span>}\n </span>\n {trailing != null && (\n <span\n className={cn(\n \"flex shrink-0 items-center gap-2 text-xs large:ms-auto\",\n trailingTone !== \"inherit\" && toneTextClass(trailingTone),\n )}\n >\n {trailing}\n </span>\n )}\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 {expandable && (\n // Turned, not swapped — the Disclosure card's convention: down while shut, up\n // while open. A rotation is its own mirror, so RTL needs nothing.\n <ChevronDown\n aria-hidden\n className={cn(\n \"size-4 shrink-0 text-[var(--text-muted)] transition-transform duration-200 ease-out motion-reduce:transition-none\",\n align === \"start\" && \"mt-0.5\",\n expanded && \"rotate-180\",\n )}\n />\n )}\n </>\n );\n\n const target = cn(\n \"flex min-w-0 flex-1 text-start\",\n // §4 at Large: the trailing slot drops under the text when both no longer fit.\n trailing != null && \"large:flex-wrap\",\n align === \"start\" ? \"items-start\" : \"items-center\",\n pad.target,\n // When stacked 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 stacked ? \"rounded-md\" : \"rounded-[inherit]\",\n stacked && interactive && !inert && !selected && \"transition-colors hover:bg-[var(--bg-hover)]\",\n interactive && TARGET_RING,\n interactive && !inert && \"cursor-pointer\",\n loading && \"cursor-progress\",\n locked && !loading && \"cursor-not-allowed\",\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` — and so is a locked one:\n // a reason wins over `disabled`, so the row stays reachable to say it.\n disabled={locked ? undefined : disabled}\n aria-disabled={loading || locked || undefined}\n aria-busy={loading || undefined}\n aria-current={selected ? \"true\" : undefined}\n aria-expanded={expandable ? expanded : undefined}\n aria-controls={expandable ? expandedId : undefined}\n aria-describedby={\n locked\n ? mergeDescribedBy(rest[\"aria-describedby\"] as string | undefined, lock.reasonId)\n : (rest[\"aria-describedby\"] as string | undefined)\n }\n onClick={\n loading || locked\n ? undefined\n : expandable\n ? toggle\n : (onClick as (event: MouseEvent<HTMLButtonElement>) => void)\n }\n className={target}\n >\n {body}\n </button>\n );\n if (locked) {\n // The Tooltip takes the target's place in the row, so it takes its flex sizing\n // too; the button fills it.\n main = (\n <LockedReason lock={lock} className=\"flex min-w-0 flex-1\">\n {main}\n </LockedReason>\n );\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 leadingActionsEl =\n leadingActions != null ? (\n <div className={cn(\"flex shrink-0 items-center\", pad.gap, pad.leadingActions)}>{leadingActions}</div>\n ) : null;\n const actionsEl =\n actions != null ? <div className={cn(\"flex shrink-0 items-center\", pad.gap, pad.actions)}>{actions}</div> : null;\n\n const row = (\n <div\n className={cn(\n \"flex min-w-0 rounded-md border transition-colors\",\n stacked ? \"flex-col\" : \"items-center\",\n // See `renderRow`: the wrapper may be inline, the row is not.\n renderRow && \"w-full\",\n selected\n ? \"border-[var(--brand)] bg-[var(--bg-surface-2)]\"\n : bordered\n ? \"border-[var(--border)]\"\n : \"border-transparent\",\n !stacked && interactive && !inert && !selected && \"hover:bg-[var(--bg-hover)]\",\n (disabled || locked) && \"opacity-50\",\n className,\n )}\n >\n {stacked ? (\n <>\n <div className=\"flex min-w-0 items-center\">\n {leadingActionsEl}\n {main}\n {actionsEl}\n </div>\n {expandable && (\n <Collapse open={expanded}>\n <div id={expandedId} role=\"group\" aria-labelledby={titleId} className={cn(\"min-w-0\", pad.body)}>\n {expandedContent}\n </div>\n </Collapse>\n )}\n {hasBody && (\n <div role=\"group\" aria-labelledby={titleId} className={cn(\"min-w-0\", pad.body, bodyClassName)}>\n {children}\n </div>\n )}\n </>\n ) : (\n <>\n {leadingActionsEl}\n {main}\n {actionsEl}\n </>\n )}\n </div>\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 renderRow && \"flex flex-col\",\n )}\n >\n {renderRow ? renderRow(row) : row}\n </Tag>\n );\n});\n\nfunction RenderedLink({\n render,\n ...props\n}: ListItemLinkProps & { render: (props: ListItemLinkProps) => ReactElement }) {\n return render(props);\n}\n"],"mappings":"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","names":["List","ListItem"]}