@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/choice-card.tsx"],"sourcesContent":["import { createContext, forwardRef, useCallback, useContext, useEffect, useId, useRef } from \"react\";\nimport type {\n ButtonHTMLAttributes,\n ChangeEvent,\n ComponentType,\n InputHTMLAttributes,\n MouseEvent,\n ReactElement,\n ReactNode,\n Ref,\n} from \"react\";\nimport { Check, Minus } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { assignRef, hasMessage, mergeDescribedBy } from \"./choice-parts\";\nimport { useKitLink } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer } from \"./text-link\";\n\n/**\n * A checkbox or a radio, presented as a bordered card: a title, a line of description,\n * an optional icon — and the whole card is the hit area.\n *\n * Kastlan built this twice by hand: the \"manage roles\" dialog wraps a `Checkbox` in a\n * bordered `<label>` per role, and the lease wizard's \"set up recurring rent\" option is\n * a `Checkbox` with `className=\"rounded-md border p-3\"`. Neither had a focus ring on\n * the card or a checked look beyond the 16px box, and the role picker's border stayed\n * grey whether the role was on or off.\n *\n * Still a native `<input>` underneath, for the reasons {@link Checkbox} gives: it\n * submits with its form, answers to `required`, and a radio set sharing a `name` gets\n * the browser's own arrow-key behaviour — which already flips in a right-to-left page.\n * The card is a `<label>` wrapped round it, so a click anywhere on the card lands on\n * the input; the input is NAMED by the title alone and DESCRIBED by the description,\n * so a screen reader does not read the whole card as its name.\n *\n * Checked is said three ways, none of them colour alone: the box's tick (or the\n * radio's dot), a heavier border, and the brand-tinted surface.\n */\n\nexport type ChoiceCardType = \"checkbox\" | \"radio\";\n\nexport interface ChoiceCardProps extends Omit<InputHTMLAttributes<HTMLInputElement>, \"type\" | \"title\"> {\n /** \"checkbox\" (default) for an on/off option or one of several; \"radio\" for one of\n * a set — give every card of the set the same `name`, or use {@link ChoiceCardGroup}. */\n type?: ChoiceCardType;\n /** The option's name — the input's accessible name. */\n title: ReactNode;\n /** A line or two under the title. Attached with `aria-describedby`. */\n description?: ReactNode;\n /** A Lucide icon (or any component taking a `className`), shown at the card's start. */\n icon?: ComponentType<{ className?: string; \"aria-hidden\"?: boolean }>;\n /** Checkbox only: \"some but not all\". See {@link CheckboxProps.indeterminate}. */\n indeterminate?: boolean;\n /** Paints the card and sets `aria-invalid` together. */\n invalid?: boolean;\n /** What is wrong, in the caller's words. Rendered in the card and attached with\n * `aria-describedby`; implies `invalid`. */\n error?: ReactNode;\n /** The checked state as a boolean. Fires alongside `onChange`, never instead of it. */\n onCheckedChange?: (checked: boolean) => void;\n /** Classes for the `<input>`. `className` styles the card. */\n inputClassName?: string;\n /**\n * Must be ticked to submit. Reaches the `<input>` as the native `required` (a\n * `<form>` refuses to submit without it, a screen reader announces \"required\"),\n * and draws the kit's `aria-hidden` star after the title — {@link Checkbox}'s\n * contract, down to writing the title WITHOUT a literal \"*\". Inside a\n * {@link ChoiceCardGroup} the star goes on the legend instead, once.\n */\n required?: boolean;\n}\n\n/** Set by {@link ChoiceCardGroup}: its legend carries the one required mark, so the\n * cards inside it draw none of their own. Private — not a prop a caller could set\n * by accident on a lone card. */\nconst InGroupContext = createContext(false);\n\n/** The kit's required mark — {@link Label}'s: `aria-hidden`, because the word is\n * announced from the control and a star read out in the name is only noise. */\nfunction RequiredMark() {\n return (\n <span aria-hidden className=\"ms-0.5 text-[var(--danger)]\">\n *\n </span>\n );\n}\n\n// The card. `has-[:checked]` and `has-[:focus-visible]` read the INPUT's state, so an\n// uncontrolled card (`defaultChecked`, a plain form) looks right with no React state.\nconst CARD =\n \"flex cursor-pointer items-start gap-3 rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] p-3 shadow-sm transition-colors \" +\n \"hover:border-[var(--border-strong)] hover:bg-[var(--bg-hover)] \" +\n \"has-[:checked]:border-[var(--brand)] has-[:checked]:bg-[var(--brand-bg)] has-[:checked]:ring-1 has-[:checked]:ring-[var(--brand)] \" +\n // The focus mark is the CARD's, not the 16px box's: the card is what was clicked and\n // what the eye is on. An OUTLINE rather than a ring, because the checked state already\n // spends the ring — two `ring-*` widths on one element are decided by stylesheet\n // order, and the loser would be the focus mark. Offset, so the two never merge.\n \"has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-[var(--brand)] \" +\n // Read from the input too, so a card disabled by its `<fieldset>` fades as well.\n \"has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-60\";\n\n// No hover offer on a card that cannot be changed.\nconst CARD_DISABLED = \"hover:border-[var(--border)] hover:bg-[var(--bg-surface)]\";\n\nconst CARD_INVALID =\n \"border-[var(--danger-border-strong)] ring-1 ring-[var(--danger-border-strong)] has-[:checked]:border-[var(--danger-border-strong)] has-[:checked]:ring-[var(--danger-border-strong)]\";\n\nconst BOX =\n \"peer size-4 shrink-0 appearance-none border border-[var(--border-strong)] bg-[var(--bg-surface)] transition-colors \" +\n \"checked:border-[var(--brand)] focus:outline-none disabled:cursor-not-allowed\";\nconst CHECKBOX = \"rounded-[4px] checked:bg-[var(--brand)]\";\n// A radio's dot is its BORDER: a 5px brand border round a surface-coloured 4px\n// centre. No glyph to lay over it, and it scales with the box.\nconst RADIO = \"rounded-full checked:border-[5px]\";\n\nexport const ChoiceCard = forwardRef<HTMLInputElement, ChoiceCardProps>(function ChoiceCard(\n {\n type = \"checkbox\",\n title,\n description,\n icon: Icon,\n indeterminate = false,\n invalid,\n error,\n onCheckedChange,\n onChange,\n className,\n inputClassName,\n id,\n disabled,\n required,\n ...rest\n },\n ref,\n) {\n const inGroup = useContext(InGroupContext);\n const generated = useId();\n const inputId = id ?? generated;\n const titleId = `${inputId}-title`;\n const descriptionId = `${inputId}-description`;\n const errorId = `${inputId}-error`;\n const showDescription = hasMessage(description);\n const showError = hasMessage(error);\n const isInvalid = Boolean(invalid) || showError;\n const mixed = type === \"checkbox\" && indeterminate;\n\n const local = useRef<HTMLInputElement | null>(null);\n const setRef = useCallback(\n (node: HTMLInputElement | null) => {\n local.current = node;\n assignRef(ref, node);\n },\n [ref],\n );\n // After every render, as Checkbox does: a click clears the DOM property.\n useEffect(() => {\n if (local.current) local.current.indeterminate = mixed;\n });\n\n return (\n <label\n htmlFor={inputId}\n className={cn(CARD, disabled && CARD_DISABLED, isInvalid && CARD_INVALID, className)}\n >\n {/* A 20px cell, so the box centres on the title's first line (text-sm). */}\n <span className=\"relative flex h-5 shrink-0 items-center\">\n <input\n ref={setRef}\n id={inputId}\n disabled={disabled}\n required={required}\n {...rest}\n type={type}\n aria-labelledby={rest[\"aria-labelledby\"] ?? titleId}\n aria-invalid={isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={mergeDescribedBy(\n rest[\"aria-describedby\"],\n showDescription && descriptionId,\n showError && errorId,\n )}\n onChange={(e: ChangeEvent<HTMLInputElement>) => {\n onChange?.(e);\n onCheckedChange?.(e.target.checked);\n }}\n className={cn(\n BOX,\n type === \"radio\" ? RADIO : CHECKBOX,\n mixed && \"border-[var(--brand)] bg-[var(--brand)]\",\n inputClassName,\n )}\n />\n {type === \"checkbox\" &&\n (mixed ? (\n <Minus\n aria-hidden\n strokeWidth={3}\n className=\"pointer-events-none absolute inset-0 m-auto size-3 text-[var(--brand-contrast)]\"\n />\n ) : (\n <Check\n aria-hidden\n strokeWidth={3}\n className=\"pointer-events-none invisible absolute inset-0 m-auto size-3 text-[var(--brand-contrast)] peer-checked:visible\"\n />\n ))}\n </span>\n {Icon && <Icon aria-hidden className=\"mt-0.5 size-4 shrink-0 text-[var(--text-secondary)]\" />}\n <span className=\"min-w-0 flex-1\">\n <span\n id={titleId}\n // `select-none`: a double-click to toggle twice would otherwise select the words.\n className=\"block select-none text-sm font-medium leading-5 text-[var(--text-primary)]\"\n >\n {title}\n {required && !inGroup && <RequiredMark />}\n </span>\n {showDescription && (\n <span id={descriptionId} className=\"mt-0.5 block text-xs text-[var(--text-muted)]\">\n {description}\n </span>\n )}\n {showError && (\n <span id={errorId} className=\"mt-1 block text-[11px] leading-tight text-[var(--danger)]\">\n {error}\n </span>\n )}\n </span>\n </label>\n );\n});\nChoiceCard.displayName = \"ChoiceCard\";\n\n/* ── ActionCard ──────────────────────────────────────────────────────────── */\n\n/** The colour of {@link ActionCardProps.meta}. */\nexport type ActionCardMetaTone = \"muted\" | \"warning\" | \"danger\" | \"info\" | \"success\";\n\nconst META_TONE: Record<ActionCardMetaTone, string> = {\n muted: \"text-[var(--text-muted)]\",\n warning: \"text-[var(--warning)]\",\n danger: \"text-[var(--danger)]\",\n info: \"text-[var(--info)]\",\n success: \"text-[var(--success)]\",\n};\n\n/** The colour of {@link ActionCardProps.icon}. `default` is the secondary text colour\n * the icon has always had; `brand` tints it (keksdose's privacy enrolment). */\nexport type ActionCardIconTone = \"default\" | \"muted\" | \"brand\" | \"warning\" | \"danger\" | \"info\" | \"success\";\n\nconst ICON_TONE: Record<ActionCardIconTone, string> = {\n default: \"text-[var(--text-secondary)]\",\n ...META_TONE,\n brand: \"text-[var(--brand)]\",\n};\n\n/** What {@link ActionCardProps.renderLink} (and the provider's `linkComponent`) is\n * handed. Spread it onto your router's link — `({ href, ...p }) => <Link to={href} {...p} />`. */\nexport interface ActionCardLinkProps {\n href: string;\n /** The card's whole look — keep it. */\n className: string;\n children: ReactNode;\n ref?: Ref<HTMLAnchorElement>;\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\nexport interface ActionCardProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, \"title\"> {\n /** What the card does — the button's accessible name. */\n title: ReactNode;\n /** A line or two on what it means. Attached with `aria-describedby`. */\n description?: ReactNode;\n /**\n * What it costs, or what else to know — a third line, said as prose and not as fine\n * print (keksdose's custody choice: both options are legitimate and neither is free,\n * so a card listing only benefits would be selling rather than explaining). Attached\n * with `aria-describedby` after the description.\n */\n meta?: ReactNode;\n /** The meta line's colour. Default `muted`; keksdose's cost line is `warning`. */\n metaTone?: ActionCardMetaTone;\n /** A Lucide icon (or any component taking a `className`), shown at the card's start. */\n icon?: ChoiceCardProps[\"icon\"];\n /** The icon's colour. Default `default` (the secondary text colour). */\n iconTone?: ActionCardIconTone;\n /** Classes on the icon, after {@link iconTone} — a size, a tinted tile behind it. */\n iconClassName?: string;\n /**\n * Makes the card a LINK to another page rather than a button that acts: kastlan's\n * section tiles on the group overview (group-overview-page), each an icon, a name and\n * a line on what is there. A real `<a href>`, so a middle click opens it in a new\n * tab. A `disabled` card renders the inert button instead — a link cannot be\n * disabled. The ref then reaches the `<a>`.\n */\n href?: string;\n /** Your router's link for an in-app `href`. Default: the `<UiKitProvider\n * linkComponent>`, then a plain `<a>`. Ignored without `href`. */\n renderLink?: (props: ActionCardLinkProps) => ReactElement;\n}\n\n/**\n * A {@link ChoiceCard} that ACTS rather than holds a state: a `<button>` with the\n * card's icon, title and description, plus a `meta` line, that runs `onClick` the\n * moment it is pressed. keksdose's privacy enrolment (privacy-enroll-dialog:345,\n * `CustodyOption`) offers two custody modes this way — picking one IS the next step,\n * so there is no checked state to show and no \"Continue\" to press after it. With\n * `href` it is the same card as a link (kastlan's overview section tiles).\n *\n * A separate component rather than `ChoiceCard as=\"button\"`: a ChoiceCard is an\n * `<input>` (its ref, its `checked`, its form value), and a button shares none of it.\n * Named by the title alone and described by the rest, so a screen reader hears\n * \"Keep the key yourself, button\" and then the explanation, not one long name.\n */\nexport const ActionCard = forwardRef<HTMLButtonElement, ActionCardProps>(function ActionCard(\n {\n title,\n description,\n meta,\n metaTone = \"muted\",\n icon: Icon,\n iconTone = \"default\",\n iconClassName,\n href,\n renderLink,\n className,\n id,\n type = \"button\",\n ...rest\n },\n ref,\n) {\n const kitLink = useKitLink();\n const generated = useId();\n const baseId = id ?? generated;\n const titleId = `${baseId}-title`;\n const descriptionId = `${baseId}-description`;\n const metaId = `${baseId}-meta`;\n const showDescription = hasMessage(description);\n const showMeta = hasMessage(meta);\n const look = cn(\n \"flex w-full items-start gap-3 rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] p-3 text-start shadow-sm transition-colors\",\n \"hover:border-[var(--brand)] hover:bg-[var(--bg-hover)]\",\n // The outline, as on ChoiceCard: the card is what the eye is on.\n \"focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--brand)]\",\n \"disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:border-[var(--border)] disabled:hover:bg-[var(--bg-surface)]\",\n className,\n );\n const labelledBy = rest[\"aria-labelledby\"] ?? titleId;\n const describedBy = mergeDescribedBy(\n rest[\"aria-describedby\"],\n showDescription && descriptionId,\n showMeta && metaId,\n );\n const body = (\n <>\n {Icon && <Icon aria-hidden className={cn(\"mt-0.5 size-5 shrink-0\", ICON_TONE[iconTone], iconClassName)} />}\n <span className=\"min-w-0 flex-1 space-y-1\">\n <span id={titleId} className=\"block text-sm font-medium leading-5 text-[var(--text-primary)]\">\n {title}\n </span>\n {showDescription && (\n <span id={descriptionId} className=\"block text-sm text-[var(--text-secondary)]\">\n {description}\n </span>\n )}\n {showMeta && (\n <span id={metaId} className={cn(\"block text-sm\", META_TONE[metaTone])}>\n {meta}\n </span>\n )}\n </span>\n </>\n );\n\n if (href !== undefined && !rest.disabled) {\n // The button-only attributes stay behind; `aria-*`, `data-*` and handlers ride along.\n const {\n disabled: _disabled,\n form: _form,\n formAction: _formAction,\n name: _name,\n value: _value,\n onClick,\n ...anchorRest\n } = rest;\n const linkProps: ActionCardLinkProps = {\n ...(anchorRest as Partial<ActionCardLinkProps>),\n ref: ref as unknown as Ref<HTMLAnchorElement>,\n id,\n href,\n \"aria-labelledby\": labelledBy,\n \"aria-describedby\": describedBy,\n onClick: onClick as unknown as ActionCardLinkProps[\"onClick\"],\n className: look,\n children: body,\n };\n const render = pickLinkRenderer(renderLink, kitLink, href);\n if (render) return <RenderedActionLink render={render} {...linkProps} />;\n const { children: content, ...anchor } = linkProps;\n return <a {...anchor}>{content}</a>;\n }\n\n return (\n <button\n ref={ref}\n id={id}\n type={type}\n {...rest}\n aria-labelledby={labelledBy}\n aria-describedby={describedBy}\n className={look}\n >\n {body}\n </button>\n );\n});\nActionCard.displayName = \"ActionCard\";\n\n/** Calls the link renderer as a component, so a router link's hooks are its own. */\nfunction RenderedActionLink({\n render,\n ...props\n}: ActionCardLinkProps & { render: (props: ActionCardLinkProps) => ReactElement }) {\n return render(props);\n}\n\n/* ── ChoiceCardGroup ─────────────────────────────────────────────────────── */\n\nexport interface ChoiceCardOption<T extends string> {\n value: T;\n title: ReactNode;\n description?: ReactNode;\n icon?: ChoiceCardProps[\"icon\"];\n disabled?: boolean;\n}\n\ninterface ChoiceCardGroupBaseProps<T extends string> {\n options: ChoiceCardOption<T>[];\n /** The question the cards answer. Rendered as the fieldset's `<legend>`, which is\n * what names the group. Without one, pass `aria-label`. */\n legend?: ReactNode;\n \"aria-label\"?: string;\n /** Shared by every input — what a plain form submits under. Generated if omitted. */\n name?: string;\n /** Classes for the card grid. Default: one column, two from `sm` up. */\n className?: string;\n /** Classes for every card. */\n cardClassName?: string;\n disabled?: boolean;\n /** What is wrong with the answer as a whole. Rendered under the cards and attached\n * to the fieldset. */\n error?: ReactNode;\n /**\n * An answer is needed. Draws the required star after the `legend` (none without\n * one — the name then comes from `aria-label`, which has nowhere to draw it), and\n * reaches the inputs as the native `required`:\n * - radios: every radio of the set, which is how HTML marks a radio group — the\n * group is satisfied by any one of them, and each announces \"required\";\n * - checkboxes: every box while NONE is ticked, dropped from all of them once one\n * is — \"at least one\", which checkboxes cannot say natively. A form then refuses\n * to submit an empty set, and nothing is left demanding a second box.\n */\n required?: boolean;\n}\n\n/** One of the set: radios. `value` is `null` while nothing is chosen. */\nexport interface ChoiceCardSingleProps<T extends string> extends ChoiceCardGroupBaseProps<T> {\n multiple?: false;\n value: T | null;\n onChange: (value: T) => void;\n}\n\n/** Any of the set: checkboxes. */\nexport interface ChoiceCardMultipleProps<T extends string> extends ChoiceCardGroupBaseProps<T> {\n multiple: true;\n value: T[];\n onChange: (value: T[]) => void;\n}\n\nexport type ChoiceCardGroupProps<T extends string> =\n | ChoiceCardSingleProps<T>\n | ChoiceCardMultipleProps<T>;\n\n/**\n * A set of {@link ChoiceCard}s answering one question, in a `<fieldset>`.\n *\n * `multiple` picks the input: radios (one of the set, the browser's arrow keys\n * between them) or checkboxes (any of the set, Tab between them). The value is kept\n * in the order of `options`, not the order the boxes were ticked, so a list saved\n * from it does not depend on the order someone clicked in.\n */\nexport function ChoiceCardGroup<T extends string>(props: ChoiceCardGroupProps<T>) {\n const { options, legend, name, className, cardClassName, disabled, error, required } = props;\n const generated = useId();\n const groupName = name ?? generated;\n const errorId = `${generated}-error`;\n const showError = hasMessage(error);\n\n const isChecked = (v: T) => (props.multiple ? props.value.includes(v) : props.value === v);\n const toggle = (v: T, on: boolean) => {\n if (props.multiple) {\n const next = new Set(props.value);\n if (on) next.add(v);\n else next.delete(v);\n props.onChange(options.map((o) => o.value).filter((x) => next.has(x)));\n } else if (on) {\n props.onChange(v);\n }\n };\n\n return (\n <InGroupContext.Provider value={true}>\n <fieldset\n aria-label={props[\"aria-label\"]}\n aria-describedby={showError ? errorId : undefined}\n disabled={disabled}\n className=\"min-w-0\"\n >\n {legend !== undefined && (\n <legend className=\"mb-2 text-sm font-medium text-[var(--text-primary)]\">\n {legend}\n {required && <RequiredMark />}\n </legend>\n )}\n <div className={cn(\"grid gap-2 sm:grid-cols-2\", className)}>\n {options.map((o) => (\n <ChoiceCard\n key={o.value}\n type={props.multiple ? \"checkbox\" : \"radio\"}\n name={groupName}\n value={o.value}\n title={o.title}\n description={o.description}\n icon={o.icon}\n disabled={o.disabled}\n invalid={showError}\n required={required && (!props.multiple || props.value.length === 0) ? true : undefined}\n checked={isChecked(o.value)}\n onCheckedChange={(on) => toggle(o.value, on)}\n className={cardClassName}\n />\n ))}\n </div>\n {showError && (\n <p id={errorId} className=\"mt-1 text-[11px] leading-tight text-[var(--danger)]\">\n {error}\n </p>\n )}\n </fieldset>\n </InGroupContext.Provider>\n );\n}\n"],"mappings":";AAgFI,SAmRA,UAnRA,KAoFE,YApFF;AAhFJ,SAAS,eAAe,YAAY,aAAa,YAAY,WAAW,OAAO,cAAc;AAW7F,SAAS,OAAO,aAAa;AAC7B,SAAS,UAAU;AACnB,SAAS,WAAW,YAAY,wBAAwB;AACxD,SAAS,kBAAkB;AAC3B,SAAS,wBAAwB;AA2DjC,MAAM,iBAAiB,cAAc,KAAK;AAI1C,SAAS,eAAe;AACtB,SACE,oBAAC,UAAK,eAAW,MAAC,WAAU,+BAA8B,eAE1D;AAEJ;AAIA,MAAM,OACJ;AAYF,MAAM,gBAAgB;AAEtB,MAAM,eACJ;AAEF,MAAM,MACJ;AAEF,MAAM,WAAW;AAGjB,MAAM,QAAQ;AAEP,MAAM,aAAa,WAA8C,SAASA,YAC/E;AAAA,EACE,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN,gBAAgB;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GACA,KACA;AACA,QAAM,UAAU,WAAW,cAAc;AACzC,QAAM,YAAY,MAAM;AACxB,QAAM,UAAU,MAAM;AACtB,QAAM,UAAU,GAAG,OAAO;AAC1B,QAAM,gBAAgB,GAAG,OAAO;AAChC,QAAM,UAAU,GAAG,OAAO;AAC1B,QAAM,kBAAkB,WAAW,WAAW;AAC9C,QAAM,YAAY,WAAW,KAAK;AAClC,QAAM,YAAY,QAAQ,OAAO,KAAK;AACtC,QAAM,QAAQ,SAAS,cAAc;AAErC,QAAM,QAAQ,OAAgC,IAAI;AAClD,QAAM,SAAS;AAAA,IACb,CAAC,SAAkC;AACjC,YAAM,UAAU;AAChB,gBAAU,KAAK,IAAI;AAAA,IACrB;AAAA,IACA,CAAC,GAAG;AAAA,EACN;AAEA,YAAU,MAAM;AACd,QAAI,MAAM,QAAS,OAAM,QAAQ,gBAAgB;AAAA,EACnD,CAAC;AAED,SACE;AAAA,IAAC;AAAA;AAAA,MACC,SAAS;AAAA,MACT,WAAW,GAAG,MAAM,YAAY,eAAe,aAAa,cAAc,SAAS;AAAA,MAGnF;AAAA,6BAAC,UAAK,WAAU,2CACd;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,KAAK;AAAA,cACL,IAAI;AAAA,cACJ;AAAA,cACA;AAAA,cACC,GAAG;AAAA,cACJ;AAAA,cACA,mBAAiB,KAAK,iBAAiB,KAAK;AAAA,cAC5C,gBAAc,aAAa,KAAK,cAAc,KAAK;AAAA,cACnD,oBAAkB;AAAA,gBAChB,KAAK,kBAAkB;AAAA,gBACvB,mBAAmB;AAAA,gBACnB,aAAa;AAAA,cACf;AAAA,cACA,UAAU,CAAC,MAAqC;AAC9C,2BAAW,CAAC;AACZ,kCAAkB,EAAE,OAAO,OAAO;AAAA,cACpC;AAAA,cACA,WAAW;AAAA,gBACT;AAAA,gBACA,SAAS,UAAU,QAAQ;AAAA,gBAC3B,SAAS;AAAA,gBACT;AAAA,cACF;AAAA;AAAA,UACF;AAAA,UACC,SAAS,eACP,QACC;AAAA,YAAC;AAAA;AAAA,cACC,eAAW;AAAA,cACX,aAAa;AAAA,cACb,WAAU;AAAA;AAAA,UACZ,IAEA;AAAA,YAAC;AAAA;AAAA,cACC,eAAW;AAAA,cACX,aAAa;AAAA,cACb,WAAU;AAAA;AAAA,UACZ;AAAA,WAEN;AAAA,QACC,QAAQ,oBAAC,QAAK,eAAW,MAAC,WAAU,uDAAsD;AAAA,QAC3F,qBAAC,UAAK,WAAU,kBACd;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,IAAI;AAAA,cAEJ,WAAU;AAAA,cAET;AAAA;AAAA,gBACA,YAAY,CAAC,WAAW,oBAAC,gBAAa;AAAA;AAAA;AAAA,UACzC;AAAA,UACC,mBACC,oBAAC,UAAK,IAAI,eAAe,WAAU,iDAChC,uBACH;AAAA,UAED,aACC,oBAAC,UAAK,IAAI,SAAS,WAAU,6DAC1B,iBACH;AAAA,WAEJ;AAAA;AAAA;AAAA,EACF;AAEJ,CAAC;AACD,WAAW,cAAc;AAOzB,MAAM,YAAgD;AAAA,EACpD,OAAO;AAAA,EACP,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,SAAS;AACX;AAMA,MAAM,YAAgD;AAAA,EACpD,SAAS;AAAA,EACT,GAAG;AAAA,EACH,OAAO;AACT;AA8DO,MAAM,aAAa,WAA+C,SAASC,YAChF;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,MAAM;AAAA,EACN,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP,GAAG;AACL,GACA,KACA;AACA,QAAM,UAAU,WAAW;AAC3B,QAAM,YAAY,MAAM;AACxB,QAAM,SAAS,MAAM;AACrB,QAAM,UAAU,GAAG,MAAM;AACzB,QAAM,gBAAgB,GAAG,MAAM;AAC/B,QAAM,SAAS,GAAG,MAAM;AACxB,QAAM,kBAAkB,WAAW,WAAW;AAC9C,QAAM,WAAW,WAAW,IAAI;AAChC,QAAM,OAAO;AAAA,IACX;AAAA,IACA;AAAA;AAAA,IAEA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,QAAM,aAAa,KAAK,iBAAiB,KAAK;AAC9C,QAAM,cAAc;AAAA,IAClB,KAAK,kBAAkB;AAAA,IACvB,mBAAmB;AAAA,IACnB,YAAY;AAAA,EACd;AACA,QAAM,OACJ,iCACG;AAAA,YAAQ,oBAAC,QAAK,eAAW,MAAC,WAAW,GAAG,0BAA0B,UAAU,QAAQ,GAAG,aAAa,GAAG;AAAA,IACxG,qBAAC,UAAK,WAAU,4BACd;AAAA,0BAAC,UAAK,IAAI,SAAS,WAAU,kEAC1B,iBACH;AAAA,MACC,mBACC,oBAAC,UAAK,IAAI,eAAe,WAAU,8CAChC,uBACH;AAAA,MAED,YACC,oBAAC,UAAK,IAAI,QAAQ,WAAW,GAAG,iBAAiB,UAAU,QAAQ,CAAC,GACjE,gBACH;AAAA,OAEJ;AAAA,KACF;AAGF,MAAI,SAAS,UAAa,CAAC,KAAK,UAAU;AAExC,UAAM;AAAA,MACJ,UAAU;AAAA,MACV,MAAM;AAAA,MACN,YAAY;AAAA,MACZ,MAAM;AAAA,MACN,OAAO;AAAA,MACP;AAAA,MACA,GAAG;AAAA,IACL,IAAI;AACJ,UAAM,YAAiC;AAAA,MACrC,GAAI;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA,mBAAmB;AAAA,MACnB,oBAAoB;AAAA,MACpB;AAAA,MACA,WAAW;AAAA,MACX,UAAU;AAAA,IACZ;AACA,UAAM,SAAS,iBAAiB,YAAY,SAAS,IAAI;AACzD,QAAI,OAAQ,QAAO,oBAAC,sBAAmB,QAAiB,GAAG,WAAW;AACtE,UAAM,EAAE,UAAU,SAAS,GAAG,OAAO,IAAI;AACzC,WAAO,oBAAC,OAAG,GAAG,QAAS,mBAAQ;AAAA,EACjC;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA;AAAA,MACC,GAAG;AAAA,MACJ,mBAAiB;AAAA,MACjB,oBAAkB;AAAA,MAClB,WAAW;AAAA,MAEV;AAAA;AAAA,EACH;AAEJ,CAAC;AACD,WAAW,cAAc;AAGzB,SAAS,mBAAmB;AAAA,EAC1B;AAAA,EACA,GAAG;AACL,GAAmF;AACjF,SAAO,OAAO,KAAK;AACrB;AAmEO,SAAS,gBAAkC,OAAgC;AAChF,QAAM,EAAE,SAAS,QAAQ,MAAM,WAAW,eAAe,UAAU,OAAO,SAAS,IAAI;AACvF,QAAM,YAAY,MAAM;AACxB,QAAM,YAAY,QAAQ;AAC1B,QAAM,UAAU,GAAG,SAAS;AAC5B,QAAM,YAAY,WAAW,KAAK;AAElC,QAAM,YAAY,CAAC,MAAU,MAAM,WAAW,MAAM,MAAM,SAAS,CAAC,IAAI,MAAM,UAAU;AACxF,QAAM,SAAS,CAAC,GAAM,OAAgB;AACpC,QAAI,MAAM,UAAU;AAClB,YAAM,OAAO,IAAI,IAAI,MAAM,KAAK;AAChC,UAAI,GAAI,MAAK,IAAI,CAAC;AAAA,UACb,MAAK,OAAO,CAAC;AAClB,YAAM,SAAS,QAAQ,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC;AAAA,IACvE,WAAW,IAAI;AACb,YAAM,SAAS,CAAC;AAAA,IAClB;AAAA,EACF;AAEA,SACE,oBAAC,eAAe,UAAf,EAAwB,OAAO,MAC9B;AAAA,IAAC;AAAA;AAAA,MACC,cAAY,MAAM,YAAY;AAAA,MAC9B,oBAAkB,YAAY,UAAU;AAAA,MACxC;AAAA,MACA,WAAU;AAAA,MAET;AAAA,mBAAW,UACV,qBAAC,YAAO,WAAU,uDACf;AAAA;AAAA,UACA,YAAY,oBAAC,gBAAa;AAAA,WAC7B;AAAA,QAEF,oBAAC,SAAI,WAAW,GAAG,6BAA6B,SAAS,GACtD,kBAAQ,IAAI,CAAC,MACZ;AAAA,UAAC;AAAA;AAAA,YAEC,MAAM,MAAM,WAAW,aAAa;AAAA,YACpC,MAAM;AAAA,YACN,OAAO,EAAE;AAAA,YACT,OAAO,EAAE;AAAA,YACT,aAAa,EAAE;AAAA,YACf,MAAM,EAAE;AAAA,YACR,UAAU,EAAE;AAAA,YACZ,SAAS;AAAA,YACT,UAAU,aAAa,CAAC,MAAM,YAAY,MAAM,MAAM,WAAW,KAAK,OAAO;AAAA,YAC7E,SAAS,UAAU,EAAE,KAAK;AAAA,YAC1B,iBAAiB,CAAC,OAAO,OAAO,EAAE,OAAO,EAAE;AAAA,YAC3C,WAAW;AAAA;AAAA,UAZN,EAAE;AAAA,QAaT,CACD,GACH;AAAA,QACC,aACC,oBAAC,OAAE,IAAI,SAAS,WAAU,uDACvB,iBACH;AAAA;AAAA;AAAA,EAEJ,GACF;AAEJ;","names":["ChoiceCard","ActionCard"]}
1
+ {"version":3,"sources":["../../src/components/choice-card.tsx"],"sourcesContent":["import { createContext, forwardRef, useCallback, useContext, useEffect, useId, useRef } from \"react\";\nimport type {\n ButtonHTMLAttributes,\n ChangeEvent,\n ComponentType,\n InputHTMLAttributes,\n MouseEvent,\n ReactElement,\n ReactNode,\n Ref,\n} from \"react\";\nimport { Check, Minus } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\nimport { assignRef, hasMessage, mergeDescribedBy } from \"./choice-parts\";\nimport { useKitLink } from \"../i18n/kit-labels\";\nimport { pickLinkRenderer } from \"./text-link\";\n\n/**\n * A checkbox or a radio, presented as a bordered card: a title, a line of description,\n * an optional icon — and the whole card is the hit area.\n *\n * Kastlan built this twice by hand: the \"manage roles\" dialog wraps a `Checkbox` in a\n * bordered `<label>` per role, and the lease wizard's \"set up recurring rent\" option is\n * a `Checkbox` with `className=\"rounded-md border p-3\"`. Neither had a focus ring on\n * the card or a checked look beyond the 16px box, and the role picker's border stayed\n * grey whether the role was on or off.\n *\n * Still a native `<input>` underneath, for the reasons {@link Checkbox} gives: it\n * submits with its form, answers to `required`, and a radio set sharing a `name` gets\n * the browser's own arrow-key behaviour — which already flips in a right-to-left page.\n * The card is a `<label>` wrapped round it, so a click anywhere on the card lands on\n * the input; the input is NAMED by the title alone and DESCRIBED by the description,\n * so a screen reader does not read the whole card as its name.\n *\n * Checked is said three ways, none of them colour alone: the box's tick (or the\n * radio's dot), a heavier border, and the brand-tinted surface.\n */\n\nexport type ChoiceCardType = \"checkbox\" | \"radio\";\n\nexport interface ChoiceCardProps extends Omit<InputHTMLAttributes<HTMLInputElement>, \"type\" | \"title\"> {\n /** \"checkbox\" (default) for an on/off option or one of several; \"radio\" for one of\n * a set — give every card of the set the same `name`, or use {@link ChoiceCardGroup}. */\n type?: ChoiceCardType;\n /** The option's name — the input's accessible name. */\n title: ReactNode;\n /** A line or two under the title. Attached with `aria-describedby`. */\n description?: ReactNode;\n /** A Lucide icon (or any component taking a `className`), shown at the card's start. */\n icon?: ComponentType<{ className?: string; \"aria-hidden\"?: boolean }>;\n /** Checkbox only: \"some but not all\". See {@link CheckboxProps.indeterminate}. */\n indeterminate?: boolean;\n /** Paints the card and sets `aria-invalid` together. */\n invalid?: boolean;\n /** What is wrong, in the caller's words. Rendered in the card and attached with\n * `aria-describedby`; implies `invalid`. */\n error?: ReactNode;\n /** The checked state as a boolean. Fires alongside `onChange`, never instead of it. */\n onCheckedChange?: (checked: boolean) => void;\n /** Classes for the `<input>`. `className` styles the card. */\n inputClassName?: string;\n /**\n * Must be ticked to submit. Reaches the `<input>` as the native `required` (a\n * `<form>` refuses to submit without it, a screen reader announces \"required\"),\n * and draws the kit's `aria-hidden` star after the title — {@link Checkbox}'s\n * contract, down to writing the title WITHOUT a literal \"*\". Inside a\n * {@link ChoiceCardGroup} the star goes on the legend instead, once.\n */\n required?: boolean;\n}\n\n/** Set by {@link ChoiceCardGroup}: its legend carries the one required mark, so the\n * cards inside it draw none of their own. Private — not a prop a caller could set\n * by accident on a lone card. */\nconst InGroupContext = createContext(false);\n\n/** The kit's required mark — {@link Label}'s: `aria-hidden`, because the word is\n * announced from the control and a star read out in the name is only noise. */\nfunction RequiredMark() {\n return (\n <span aria-hidden className=\"ms-0.5 text-[var(--danger)]\">\n *\n </span>\n );\n}\n\n// The card. `has-[:checked]` and `has-[:focus-visible]` read the INPUT's state, so an\n// uncontrolled card (`defaultChecked`, a plain form) looks right with no React state.\nconst CARD =\n \"flex cursor-pointer items-start gap-3 rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] p-3 shadow-sm transition-colors \" +\n \"hover:border-[var(--border-strong)] hover:bg-[var(--bg-hover)] \" +\n \"has-[:checked]:border-[var(--brand)] has-[:checked]:bg-[var(--brand-bg)] has-[:checked]:ring-1 has-[:checked]:ring-[var(--brand)] \" +\n // The focus mark is the CARD's, not the 16px box's: the card is what was clicked and\n // what the eye is on. An OUTLINE rather than a ring, because the checked state already\n // spends the ring — two `ring-*` widths on one element are decided by stylesheet\n // order, and the loser would be the focus mark. Offset, so the two never merge. Its\n // width is the kit's focus frame (0.32, §5: `--focus-ring-width`, 3 px under More\n // contrast) — `FOCUS_RING` itself is a ring, which this mark cannot be.\n \"has-[:focus-visible]:outline-[length:var(--focus-ring-width)] has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-[var(--brand)] \" +\n // Read from the input too, so a card disabled by its `<fieldset>` fades as well.\n \"has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-60\";\n\n// No hover offer on a card that cannot be changed.\nconst CARD_DISABLED = \"hover:border-[var(--border)] hover:bg-[var(--bg-surface)]\";\n\nconst CARD_INVALID =\n \"border-[var(--danger-border-strong)] ring-1 ring-[var(--danger-border-strong)] has-[:checked]:border-[var(--danger-border-strong)] has-[:checked]:ring-[var(--danger-border-strong)]\";\n\nconst BOX =\n \"peer size-4 shrink-0 appearance-none border border-[var(--border-strong)] bg-[var(--bg-surface)] transition-colors \" +\n \"checked:border-[var(--brand)] focus:outline-none disabled:cursor-not-allowed\";\nconst CHECKBOX = \"rounded checked:bg-[var(--brand)]\";\n// A radio's dot is its BORDER: a 0.3125rem brand border (5px at Normal) round a\n// surface-coloured 0.375rem centre. No glyph to lay over it, and in rem since 0.32\n// (§3.2) it scales with the box — a 5px border on a 24px box at 150 % was a ring, not a\n// dot.\nconst RADIO = \"rounded-full checked:border-[0.3125rem]\";\n\nexport const ChoiceCard = forwardRef<HTMLInputElement, ChoiceCardProps>(function ChoiceCard(\n {\n type = \"checkbox\",\n title,\n description,\n icon: Icon,\n indeterminate = false,\n invalid,\n error,\n onCheckedChange,\n onChange,\n className,\n inputClassName,\n id,\n disabled,\n required,\n ...rest\n },\n ref,\n) {\n const inGroup = useContext(InGroupContext);\n const generated = useId();\n const inputId = id ?? generated;\n const titleId = `${inputId}-title`;\n const descriptionId = `${inputId}-description`;\n const errorId = `${inputId}-error`;\n const showDescription = hasMessage(description);\n const showError = hasMessage(error);\n const isInvalid = Boolean(invalid) || showError;\n const mixed = type === \"checkbox\" && indeterminate;\n\n const local = useRef<HTMLInputElement | null>(null);\n const setRef = useCallback(\n (node: HTMLInputElement | null) => {\n local.current = node;\n assignRef(ref, node);\n },\n [ref],\n );\n // After every render, as Checkbox does: a click clears the DOM property.\n useEffect(() => {\n if (local.current) local.current.indeterminate = mixed;\n });\n\n return (\n <label\n htmlFor={inputId}\n className={cn(CARD, disabled && CARD_DISABLED, isInvalid && CARD_INVALID, className)}\n >\n {/* A 20px cell, so the box centres on the title's first line (text-sm). */}\n <span className=\"relative flex h-5 shrink-0 items-center\">\n <input\n ref={setRef}\n id={inputId}\n disabled={disabled}\n required={required}\n {...rest}\n type={type}\n aria-labelledby={rest[\"aria-labelledby\"] ?? titleId}\n aria-invalid={isInvalid || rest[\"aria-invalid\"] || undefined}\n aria-describedby={mergeDescribedBy(\n rest[\"aria-describedby\"],\n showDescription && descriptionId,\n showError && errorId,\n )}\n onChange={(e: ChangeEvent<HTMLInputElement>) => {\n onChange?.(e);\n onCheckedChange?.(e.target.checked);\n }}\n className={cn(\n BOX,\n type === \"radio\" ? RADIO : CHECKBOX,\n mixed && \"border-[var(--brand)] bg-[var(--brand)]\",\n inputClassName,\n )}\n />\n {type === \"checkbox\" &&\n (mixed ? (\n <Minus\n aria-hidden\n strokeWidth={3}\n className=\"pointer-events-none absolute inset-0 m-auto size-3 text-[var(--brand-contrast)]\"\n />\n ) : (\n <Check\n aria-hidden\n strokeWidth={3}\n className=\"pointer-events-none invisible absolute inset-0 m-auto size-3 text-[var(--brand-contrast)] peer-checked:visible\"\n />\n ))}\n </span>\n {Icon && <Icon aria-hidden className=\"mt-0.5 size-4 shrink-0 text-[var(--text-secondary)]\" />}\n <span className=\"min-w-0 flex-1\">\n <span\n id={titleId}\n // `select-none`: a double-click to toggle twice would otherwise select the words.\n className=\"block select-none text-sm font-medium leading-5 text-[var(--text-primary)]\"\n >\n {title}\n {required && !inGroup && <RequiredMark />}\n </span>\n {showDescription && (\n <span id={descriptionId} className=\"mt-0.5 block text-xs text-[var(--text-muted)]\">\n {description}\n </span>\n )}\n {showError && (\n <span id={errorId} className=\"mt-1 block text-caption leading-tight text-[var(--danger)]\">\n {error}\n </span>\n )}\n </span>\n </label>\n );\n});\nChoiceCard.displayName = \"ChoiceCard\";\n\n/* ── ActionCard ──────────────────────────────────────────────────────────── */\n\n/** The colour of {@link ActionCardProps.meta}. */\nexport type ActionCardMetaTone = \"muted\" | \"warning\" | \"danger\" | \"info\" | \"success\";\n\nconst META_TONE: Record<ActionCardMetaTone, string> = {\n muted: \"text-[var(--text-muted)]\",\n warning: \"text-[var(--warning)]\",\n danger: \"text-[var(--danger)]\",\n info: \"text-[var(--info)]\",\n success: \"text-[var(--success)]\",\n};\n\n/** The colour of {@link ActionCardProps.icon}. `default` is the secondary text colour\n * the icon has always had; `brand` tints it (keksdose's privacy enrolment). */\nexport type ActionCardIconTone = \"default\" | \"muted\" | \"brand\" | \"warning\" | \"danger\" | \"info\" | \"success\";\n\nconst ICON_TONE: Record<ActionCardIconTone, string> = {\n default: \"text-[var(--text-secondary)]\",\n ...META_TONE,\n brand: \"text-[var(--brand)]\",\n};\n\n/** What {@link ActionCardProps.renderLink} (and the provider's `linkComponent`) is\n * handed. Spread it onto your router's link — `({ href, ...p }) => <Link to={href} {...p} />`. */\nexport interface ActionCardLinkProps {\n href: string;\n /** The card's whole look — keep it. */\n className: string;\n children: ReactNode;\n ref?: Ref<HTMLAnchorElement>;\n onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;\n id?: string;\n [key: `aria-${string}`]: string | boolean | number | undefined;\n [key: `data-${string}`]: unknown;\n}\n\nexport interface ActionCardProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, \"title\"> {\n /** What the card does — the button's accessible name. */\n title: ReactNode;\n /** A line or two on what it means. Attached with `aria-describedby`. */\n description?: ReactNode;\n /**\n * What it costs, or what else to know — a third line, said as prose and not as fine\n * print (keksdose's custody choice: both options are legitimate and neither is free,\n * so a card listing only benefits would be selling rather than explaining). Attached\n * with `aria-describedby` after the description.\n */\n meta?: ReactNode;\n /** The meta line's colour. Default `muted`; keksdose's cost line is `warning`. */\n metaTone?: ActionCardMetaTone;\n /** A Lucide icon (or any component taking a `className`), shown at the card's start. */\n icon?: ChoiceCardProps[\"icon\"];\n /** The icon's colour. Default `default` (the secondary text colour). */\n iconTone?: ActionCardIconTone;\n /** Classes on the icon, after {@link iconTone} — a size, a tinted tile behind it. */\n iconClassName?: string;\n /**\n * Makes the card a LINK to another page rather than a button that acts: kastlan's\n * section tiles on the group overview (group-overview-page), each an icon, a name and\n * a line on what is there. A real `<a href>`, so a middle click opens it in a new\n * tab. A `disabled` card renders the inert button instead — a link cannot be\n * disabled. The ref then reaches the `<a>`.\n */\n href?: string;\n /** Your router's link for an in-app `href`. Default: the `<UiKitProvider\n * linkComponent>`, then a plain `<a>`. Ignored without `href`. */\n renderLink?: (props: ActionCardLinkProps) => ReactElement;\n}\n\n/**\n * A {@link ChoiceCard} that ACTS rather than holds a state: a `<button>` with the\n * card's icon, title and description, plus a `meta` line, that runs `onClick` the\n * moment it is pressed. keksdose's privacy enrolment (privacy-enroll-dialog:345,\n * `CustodyOption`) offers two custody modes this way — picking one IS the next step,\n * so there is no checked state to show and no \"Continue\" to press after it. With\n * `href` it is the same card as a link (kastlan's overview section tiles).\n *\n * A separate component rather than `ChoiceCard as=\"button\"`: a ChoiceCard is an\n * `<input>` (its ref, its `checked`, its form value), and a button shares none of it.\n * Named by the title alone and described by the rest, so a screen reader hears\n * \"Keep the key yourself, button\" and then the explanation, not one long name.\n */\nexport const ActionCard = forwardRef<HTMLButtonElement, ActionCardProps>(function ActionCard(\n {\n title,\n description,\n meta,\n metaTone = \"muted\",\n icon: Icon,\n iconTone = \"default\",\n iconClassName,\n href,\n renderLink,\n className,\n id,\n type = \"button\",\n ...rest\n },\n ref,\n) {\n const kitLink = useKitLink();\n const generated = useId();\n const baseId = id ?? generated;\n const titleId = `${baseId}-title`;\n const descriptionId = `${baseId}-description`;\n const metaId = `${baseId}-meta`;\n const showDescription = hasMessage(description);\n const showMeta = hasMessage(meta);\n const look = cn(\n \"flex w-full items-start gap-3 rounded-lg border border-[var(--border)] bg-[var(--bg-surface)] p-3 text-start shadow-sm transition-colors\",\n \"hover:border-[var(--brand)] hover:bg-[var(--bg-hover)]\",\n // The outline, as on ChoiceCard: the card is what the eye is on — the focus frame's\n // width (§5).\n \"focus:outline-none focus-visible:outline-[length:var(--focus-ring-width)] focus-visible:outline-offset-2 focus-visible:outline-[var(--brand)]\",\n \"disabled:cursor-not-allowed disabled:opacity-60 disabled:hover:border-[var(--border)] disabled:hover:bg-[var(--bg-surface)]\",\n className,\n );\n const labelledBy = rest[\"aria-labelledby\"] ?? titleId;\n const describedBy = mergeDescribedBy(\n rest[\"aria-describedby\"],\n showDescription && descriptionId,\n showMeta && metaId,\n );\n const body = (\n <>\n {Icon && <Icon aria-hidden className={cn(\"mt-0.5 size-5 shrink-0\", ICON_TONE[iconTone], iconClassName)} />}\n <span className=\"min-w-0 flex-1 space-y-1\">\n <span id={titleId} className=\"block text-sm font-medium leading-5 text-[var(--text-primary)]\">\n {title}\n </span>\n {showDescription && (\n <span id={descriptionId} className=\"block text-sm text-[var(--text-secondary)]\">\n {description}\n </span>\n )}\n {showMeta && (\n <span id={metaId} className={cn(\"block text-sm\", META_TONE[metaTone])}>\n {meta}\n </span>\n )}\n </span>\n </>\n );\n\n if (href !== undefined && !rest.disabled) {\n // The button-only attributes stay behind; `aria-*`, `data-*` and handlers ride along.\n const {\n disabled: _disabled,\n form: _form,\n formAction: _formAction,\n name: _name,\n value: _value,\n onClick,\n ...anchorRest\n } = rest;\n const linkProps: ActionCardLinkProps = {\n ...(anchorRest as Partial<ActionCardLinkProps>),\n ref: ref as unknown as Ref<HTMLAnchorElement>,\n id,\n href,\n \"aria-labelledby\": labelledBy,\n \"aria-describedby\": describedBy,\n onClick: onClick as unknown as ActionCardLinkProps[\"onClick\"],\n className: look,\n children: body,\n };\n const render = pickLinkRenderer(renderLink, kitLink, href);\n if (render) return <RenderedActionLink render={render} {...linkProps} />;\n const { children: content, ...anchor } = linkProps;\n return <a {...anchor}>{content}</a>;\n }\n\n return (\n <button\n ref={ref}\n id={id}\n type={type}\n {...rest}\n aria-labelledby={labelledBy}\n aria-describedby={describedBy}\n className={look}\n >\n {body}\n </button>\n );\n});\nActionCard.displayName = \"ActionCard\";\n\n/** Calls the link renderer as a component, so a router link's hooks are its own. */\nfunction RenderedActionLink({\n render,\n ...props\n}: ActionCardLinkProps & { render: (props: ActionCardLinkProps) => ReactElement }) {\n return render(props);\n}\n\n/* ── ChoiceCardGroup ─────────────────────────────────────────────────────── */\n\nexport interface ChoiceCardOption<T extends string> {\n value: T;\n title: ReactNode;\n description?: ReactNode;\n icon?: ChoiceCardProps[\"icon\"];\n disabled?: boolean;\n}\n\ninterface ChoiceCardGroupBaseProps<T extends string> {\n options: ChoiceCardOption<T>[];\n /** The question the cards answer. Rendered as the fieldset's `<legend>`, which is\n * what names the group. Without one, pass `aria-label`. */\n legend?: ReactNode;\n \"aria-label\"?: string;\n /** Shared by every input — what a plain form submits under. Generated if omitted. */\n name?: string;\n /** Classes for the card grid. Default: one column, two from `sm` up. */\n className?: string;\n /** Classes for every card. */\n cardClassName?: string;\n disabled?: boolean;\n /** What is wrong with the answer as a whole. Rendered under the cards and attached\n * to the fieldset. */\n error?: ReactNode;\n /**\n * An answer is needed. Draws the required star after the `legend` (none without\n * one — the name then comes from `aria-label`, which has nowhere to draw it), and\n * reaches the inputs as the native `required`:\n * - radios: every radio of the set, which is how HTML marks a radio group — the\n * group is satisfied by any one of them, and each announces \"required\";\n * - checkboxes: every box while NONE is ticked, dropped from all of them once one\n * is — \"at least one\", which checkboxes cannot say natively. A form then refuses\n * to submit an empty set, and nothing is left demanding a second box.\n */\n required?: boolean;\n}\n\n/** One of the set: radios. `value` is `null` while nothing is chosen. */\nexport interface ChoiceCardSingleProps<T extends string> extends ChoiceCardGroupBaseProps<T> {\n multiple?: false;\n value: T | null;\n onChange: (value: T) => void;\n}\n\n/** Any of the set: checkboxes. */\nexport interface ChoiceCardMultipleProps<T extends string> extends ChoiceCardGroupBaseProps<T> {\n multiple: true;\n value: T[];\n onChange: (value: T[]) => void;\n}\n\nexport type ChoiceCardGroupProps<T extends string> =\n | ChoiceCardSingleProps<T>\n | ChoiceCardMultipleProps<T>;\n\n/**\n * A set of {@link ChoiceCard}s answering one question, in a `<fieldset>`.\n *\n * `multiple` picks the input: radios (one of the set, the browser's arrow keys\n * between them) or checkboxes (any of the set, Tab between them). The value is kept\n * in the order of `options`, not the order the boxes were ticked, so a list saved\n * from it does not depend on the order someone clicked in.\n */\nexport function ChoiceCardGroup<T extends string>(props: ChoiceCardGroupProps<T>) {\n const { options, legend, name, className, cardClassName, disabled, error, required } = props;\n const generated = useId();\n const groupName = name ?? generated;\n const errorId = `${generated}-error`;\n const showError = hasMessage(error);\n\n const isChecked = (v: T) => (props.multiple ? props.value.includes(v) : props.value === v);\n const toggle = (v: T, on: boolean) => {\n if (props.multiple) {\n const next = new Set(props.value);\n if (on) next.add(v);\n else next.delete(v);\n props.onChange(options.map((o) => o.value).filter((x) => next.has(x)));\n } else if (on) {\n props.onChange(v);\n }\n };\n\n return (\n <InGroupContext.Provider value={true}>\n <fieldset\n aria-label={props[\"aria-label\"]}\n aria-describedby={showError ? errorId : undefined}\n disabled={disabled}\n className=\"min-w-0\"\n >\n {legend !== undefined && (\n <legend className=\"mb-2 text-sm font-medium text-[var(--text-primary)]\">\n {legend}\n {required && <RequiredMark />}\n </legend>\n )}\n <div className={cn(\"grid grid-cols-1 gap-2 sm:grid-cols-2\", className)}>\n {options.map((o) => (\n <ChoiceCard\n key={o.value}\n type={props.multiple ? \"checkbox\" : \"radio\"}\n name={groupName}\n value={o.value}\n title={o.title}\n description={o.description}\n icon={o.icon}\n disabled={o.disabled}\n invalid={showError}\n required={required && (!props.multiple || props.value.length === 0) ? true : undefined}\n checked={isChecked(o.value)}\n onCheckedChange={(on) => toggle(o.value, on)}\n className={cardClassName}\n />\n ))}\n </div>\n {showError && (\n <p id={errorId} className=\"mt-1 text-caption leading-tight text-[var(--danger)]\">\n {error}\n </p>\n )}\n </fieldset>\n </InGroupContext.Provider>\n );\n}\n"],"mappings":"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","names":["ChoiceCard","ActionCard"]}
@@ -120,7 +120,7 @@ function ColumnRoleTable({
120
120
  scope: "col",
121
121
  className: "min-w-36 py-2 align-top",
122
122
  children: [
123
- /* @__PURE__ */ jsx("span", { className: "block max-w-56 truncate pb-1 text-[11px] font-normal text-[var(--text-muted)]", children: name }),
123
+ /* @__PURE__ */ jsx("span", { className: "block max-w-56 truncate pb-1 text-caption font-normal text-[var(--text-muted)]", children: name }),
124
124
  /* @__PURE__ */ jsxs(
125
125
  Select,
126
126
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/column-mapper.tsx"],"sourcesContent":["import { useEffect, useEffectEvent, useState } from \"react\";\nimport type { ChangeEvent, DragEvent, HTMLAttributes, ReactNode } from \"react\";\nimport { Upload } from \"lucide-react\";\n\nimport { useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport {\n assignColumnRole,\n guessMapping,\n missingRoles,\n readMappedTable,\n readTextFile,\n roleOfColumn,\n} from \"../lib/column-mapping\";\nimport type {\n ColumnMapperResult,\n ColumnMapping,\n ColumnRole,\n} from \"../lib/column-mapping\";\nimport { parseTextTable } from \"../lib/table-text\";\nimport type { TableSeparator, TextTable } from \"../lib/table-text\";\nimport { AlertBanner } from \"./alert-banner\";\nimport { Checkbox } from \"./checkbox\";\nimport { Disclosure } from \"./disclosure\";\nimport { FileButton, useFilePicker } from \"./file-button\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeaderCell,\n TableRow,\n} from \"./table\";\nimport { Select, Textarea } from \"./ui\";\n\n/**\n * A table pasted, dropped or chosen as a file, and which of its columns plays which\n * part — Kurvenschmiede's columns input and keksdose's bank-file mapping step, once.\n *\n * Kurvenschmiede (`features/control/columns-input.tsx` + `csv.ts`) reads a recorder's\n * export into a playground signal (time, value) and a comparison (time, setpoint,\n * actual): a textarea, a file button, a parse, a select per role, and a line that says\n * which decimal convention was assumed and how many lines were skipped. keksdose\n * (`import-map-step.tsx`) shows a bank's CSV as a table with a role select on every\n * column's header, the file's real values under it, ignored columns dimmed, and what\n * the import still needs named under it. Same problem, two hand-built answers.\n *\n * Two layers, so both shapes fit:\n *\n * * {@link ColumnMapper} — the whole single panel: paste, drop or choose → parse\n * ({@link parseTextTable}: separator, decimal convention, header line, all stated\n * back) → preview with a role per column → `onChange(result)`. Kurvenschmiede's.\n * * {@link ColumnRoleTable} — the preview with the role selects alone, controlled, over\n * rows the caller already has. keksdose's map step, whose rows come from its server's\n * sniff (encodings, quoted multi-line cells, skipped title rows) and whose mapping\n * lives in the wizard's state between steps.\n *\n * The rules under both are headless in `lib/column-mapping.ts`: a role moves rather\n * than doubling up, what is missing is named, and the rows a numeric role cannot read\n * are reported by line number rather than dropped.\n */\n\n// ── Labels ────────────────────────────────────────────────────────────────────\n\nexport interface ColumnMapperLabels {\n /** The paste box's label. */\n paste: string;\n /** The caption under the paste box. */\n pasteHint: string;\n /** The file button. */\n chooseFile: string;\n /** A chosen or dropped file whose bytes could not be read at all. */\n readError: (name: string) => string;\n /** The checkbox that says whether the first line names the columns. */\n headerRow: string;\n /** What was read: so many columns and rows. */\n summary: (columns: number, rows: number) => string;\n /** The separator that was assumed. */\n separatorSemicolon: string;\n separatorComma: string;\n separatorTab: string;\n separatorSpace: string;\n /** The decimal convention that was assumed — with an example, because \"decimal\n * comma\" alone is a term, and `1,5` is something a reader recognises. */\n decimalComma: string;\n decimalPoint: string;\n /** The title over the lines that could not be read. */\n unreadCount: (count: number) => string;\n /** One of them. */\n unreadLine: (line: number) => string;\n /** Past the first hundred listed. */\n unreadMore: (count: number) => string;\n /** The text has content, and none of it is a row. */\n noRows: string;\n /** The preview table's name. */\n table: string;\n /** A column the table names no header for. */\n columnN: (n: number) => string;\n /** The role select on a column's header — its accessible name. */\n roleOf: (column: string) => string;\n /** The role select's \"no role\" option. */\n ignore: string;\n /** A required role in the select — only while some role is NOT required: when\n * every one is, the mark tells no role from another (0.24, Kurvenschmiede). Since\n * 0.25 this is what a screen reader says for the option (its `aria-label`); the\n * option SHOWS {@link requiredRoleShort}. */\n requiredRole: (role: string) => string;\n /** The same required role as the select shows it (0.25): the role's name and a short\n * mark, so the mark does not cut the name short in a phone-width column — \"Booking\n * date (re…\" was all keksdose's closed select had room for. Spoken as\n * {@link requiredRole}. Optional, so a complete `UiKitLabels` typed before 0.25\n * still compiles; the asterisk needs no translation in most languages. */\n requiredRoleShort?: (role: string) => string;\n /** Under the preview, when it shows fewer rows than were read. */\n previewOf: (shown: number, total: number) => string;\n /** A required group's roles, already joined as a list with \"or\". */\n oneOf: (roles: string) => string;\n /** What the table still needs, already joined as a list with \"and\". */\n missing: (roles: string) => string;\n}\n\n/** {@link ColumnMapperLabels.requiredRoleShort}'s English — and its fallback for a\n * provider whose labels predate the key. */\nconst markedShort = (role: string) => `${role} *`;\n\nexport const DEFAULT_COLUMN_MAPPER_LABELS: ColumnMapperLabels = {\n paste: \"Paste a table\",\n pasteHint:\n \"Copy the rows out of a spreadsheet and paste them here, or drop a CSV or text file.\",\n chooseFile: \"Choose a file\",\n readError: (name) => `“${name}” could not be read`,\n headerRow: \"The first line names the columns\",\n summary: (columns, rows) =>\n `${columns} ${columns === 1 ? \"column\" : \"columns\"}, ${rows} ${rows === 1 ? \"row\" : \"rows\"}`,\n separatorSemicolon: \"separated by semicolons\",\n separatorComma: \"separated by commas\",\n separatorTab: \"separated by tabs\",\n separatorSpace: \"separated by spaces\",\n decimalComma: \"decimal comma (1,5)\",\n decimalPoint: \"decimal point (1.5)\",\n unreadCount: (count) =>\n count === 1\n ? \"1 line could not be read\"\n : `${count} lines could not be read`,\n unreadLine: (line) => `Line ${line} could not be read`,\n unreadMore: (count) => `…and ${count} more`,\n noRows: \"No line of this text reads as a row of the table.\",\n table: \"Columns and their roles\",\n columnN: (n) => `Column ${n}`,\n roleOf: (column) => `What does column “${column}” hold?`,\n ignore: \"Ignore\",\n requiredRole: (role) => `${role} (required)`,\n requiredRoleShort: markedShort,\n previewOf: (shown, total) =>\n shown === 1\n ? `The first of ${total} rows`\n : `The first ${shown} of ${total} rows`,\n oneOf: (roles) => `either ${roles}`,\n missing: (roles) => `Still needed: ${roles}.`,\n};\n\nconst SEPARATOR_LABEL: Record<\n TableSeparator,\n \"separatorSemicolon\" | \"separatorComma\" | \"separatorTab\" | \"separatorSpace\"\n> = {\n \";\": \"separatorSemicolon\",\n \",\": \"separatorComma\",\n \"\\t\": \"separatorTab\",\n \" \": \"separatorSpace\",\n};\n\n/** How many unread lines are listed before \"…and N more\". */\nconst UNREAD_LISTED = 100;\n\n/** A file a table is likely to be in. Extensions only, so a refusal reads \"Only .csv,\n * .tsv, … files\" rather than a list of MIME types; the parse is the real check. */\nconst DEFAULT_ACCEPT = \".csv,.tsv,.txt,.dat,.asc\";\n\nfunction formatList(\n items: string[],\n type: \"conjunction\" | \"disjunction\",\n locale: string | undefined,\n): string {\n try {\n return new Intl.ListFormat(locale, { type, style: \"long\" }).format(items);\n } catch {\n return items.join(\", \");\n }\n}\n\n// ── ColumnRoleTable ───────────────────────────────────────────────────────────\n\n/** A `data-*` attribute, typed so `{ \"data-private\": \"\" }` needs no cast. */\ntype DataAttributes = { [key: `data-${string}`]: string | number | boolean | undefined };\n\n/**\n * What {@link ColumnRoleTableProps.bodyProps} puts on the preview's `<tbody>`: `data-*`\n * hooks, an `id`, a class, a style.\n */\nexport type ColumnRoleTableBodyProps = Omit<\n HTMLAttributes<HTMLTableSectionElement>,\n \"children\" | \"dangerouslySetInnerHTML\"\n> &\n DataAttributes;\n\n/**\n * What {@link ColumnRoleTableProps.rowProps} returns for one preview row's `<tr>` —\n * the same shape as LineItems' `LineItemsRowProps`, on a table row.\n */\nexport type ColumnRoleTableRowProps = Omit<\n HTMLAttributes<HTMLTableRowElement>,\n \"children\" | \"dangerouslySetInnerHTML\"\n> &\n DataAttributes;\n\nexport interface ColumnRoleTableProps<\n R extends string,\n M extends ColumnMapping<R> = ColumnMapping<R>,\n> {\n /** The columns' names — the file's header line — or `null` for a table without one\n * (each column is then \"Column 1\", \"Column 2\", …). */\n header: readonly string[] | null;\n /** The rows to preview, as text. Only the first {@link previewRows} are drawn. */\n rows: readonly (readonly string[])[];\n /** How many columns. Default: the header's length or the widest previewed row. */\n width?: number;\n /** The parts a column can play. */\n roles: readonly ColumnRole<R>[];\n /** Role → column. Any object with the roles as nullable number fields — keksdose's\n * `CsvMapping` as it stands; the other fields are handed back untouched. */\n mapping: M;\n /** The next mapping, with the picked role moved onto its column. */\n onMappingChange: (next: M) => void;\n /** Default 5. */\n previewRows?: number;\n /** How many rows the table has in all, for \"The first 5 of 120 rows\". Default:\n * `rows.length`. */\n totalRows?: number;\n /** Every role select disabled — keksdose locks them while a re-sniff replaces the\n * table they point into. */\n disabled?: boolean;\n /**\n * Extra attributes for the preview's `<tbody>` (0.24) — keksdose: the sample rows are\n * the user's own bank data (payees, memos, amounts), and its demo mode blurs exactly\n * that through `.demo-mode [data-private]` (#336). With nowhere to put the attribute,\n * its map step reached into the table with a `useLayoutEffect` that set it on the\n * `<tbody>` after every commit; `bodyProps={{ \"data-private\": \"\" }}` declares it.\n * There is deliberately no head counterpart: the column names and the role selects\n * are what such a mode is there to show.\n *\n * Spread on the kit `TableBody`; a `className` is added to its own.\n */\n bodyProps?: ColumnRoleTableBodyProps;\n /**\n * Extra attributes for one preview row's `<tr>`, from its cells and its index in the\n * preview (0.24) — the contract of LineItems' `rowProps` (keksdose K20), on a table:\n * `data-*` hooks (a per-row `data-private`, a test id), an `id`, a class. Called for\n * the rows drawn, the first {@link previewRows}. The row has no attributes of its own\n * to protect; a `className` is added after its classes.\n */\n rowProps?: (row: readonly string[], index: number) => ColumnRoleTableRowProps | undefined;\n labels?: Partial<ColumnMapperLabels>;\n className?: string;\n}\n\n/**\n * A table's first rows, with a role select on every column's header.\n *\n * keksdose's mapping step (dev#462: \"make it more table like, where the table columns\n * can be chosen from the input and the column data changes with the chosen column\n * header\"): the file IS the form. The file's own column name sits on top, verbatim —\n * it is how the reader recognises the column; under it the role, picked in place;\n * under that the column's real values, so a column that is one off shows up here and\n * not after the import. A column with no role says \"Ignore\" and its values are dimmed:\n * the state that most needs to be visible in a twelve-column bank export.\n *\n * Picking a role another column holds MOVES it ({@link assignColumnRole}). What the\n * table still needs is named under it — a required group as \"either Amount, Debit or\n * Credit\". Required roles are marked in the list only where some role is not\n * (0.24): Kurvenschmiede's roles are all required, so the mark told no option from\n * another, and on a phone it cut the role's own name short (\"Zeit (s) (erforderl…\")\n * — the part of the option the reader has to read. The \"Still needed\" line already\n * says what is missing; a mark that distinguishes nothing is only noise.\n *\n * Where the mark does tell roles apart, it is short (0.25): keksdose's bank roles, on\n * a phone, still showed \"Booking date (re…\" in the closed select. The option shows\n * \"Booking date *\" ({@link ColumnMapperLabels.requiredRoleShort}) and is NAMED \"Booking\n * date (required)\" ({@link ColumnMapperLabels.requiredRole}, its `aria-label`), so a\n * screen reader hears the word rather than \"star\". And no role is cut at all: each\n * column is at least as wide as its select's longest option — the select's own width,\n * which a table cell otherwise ignores for a `w-full` control — up to 14rem, the width\n * the column's name and values already stop at. A long role name (\"Date de\n * comptabilisation\", \"Betrag (mit Vorzeichen)\") widens the columns, and the edge fade\n * says there are more of them to scroll to.\n *\n * The table scrolls sideways inside its own box (the kit {@link Table}'s wrapper, a\n * named, keyboard-reachable region while it overflows), so a wide export never widens\n * the page at 390px. An edge with columns behind it fades out (0.24, Kurvenschmiede:\n * on a phone the fourth column's select sat off-screen and nothing said there was more\n * to the right; phone scrollbars are overlays that show only while you drag). Since\n * 0.25 that is the kit Table's own `edgeFade`, not a box of this table's round it:\n * measured, so a table that fits is painted as before; both ends once scrolled into\n * the middle; the reading direction's own in RTL; never animated; a `mask-image`,\n * which changes no layout and needs no background colour to fade to. A role select\n * focused behind the fade is scrolled clear of it, cell and all, and the Table's\n * wrapper says which ends are cut in `data-overflow`.\n */\nexport function ColumnRoleTable<\n R extends string,\n M extends ColumnMapping<R> = ColumnMapping<R>,\n>({\n header,\n rows,\n width: widthProp,\n roles,\n mapping,\n onMappingChange,\n previewRows = 5,\n totalRows,\n disabled,\n bodyProps,\n rowProps,\n labels: labelsProp,\n className,\n}: ColumnRoleTableProps<R, M>) {\n const labels: ColumnMapperLabels = useKitLabels(\n \"columnMapper\",\n DEFAULT_COLUMN_MAPPER_LABELS,\n labelsProp,\n );\n const locale = useKitLocale();\n const shown = rows.slice(0, previewRows);\n const width =\n widthProp ??\n shown.reduce(\n (widest, row) => Math.max(widest, row.length),\n header?.length ?? 0,\n );\n const columns = Array.from({ length: width }, (_, index) => index);\n const total = totalRows ?? rows.length;\n const nameOf = (column: number) =>\n header?.[column] || labels.columnN(column + 1);\n const labelOf = (value: R) =>\n roles.find((role) => role.value === value)?.label ?? value;\n const gaps = missingRoles(roles, mapping, width).map((gap) =>\n gap.length === 1\n ? labelOf(gap[0])\n : labels.oneOf(formatList(gap.map(labelOf), \"disjunction\", locale)),\n );\n // The required mark only where it tells one option from another.\n const markRequired = roles.some((role) => role.required !== true);\n\n return (\n <div\n className={cn(\"min-w-0 space-y-1.5\", className)}\n data-slot=\"column-role-table\"\n >\n <Table density=\"compact\" framed edgeFade aria-label={labels.table}>\n <TableHead>\n <TableRow>\n {columns.map((column) => {\n const role = roleOfColumn(roles, mapping, column);\n const name = nameOf(column);\n return (\n <TableHeaderCell\n key={column}\n scope=\"col\"\n className=\"min-w-36 py-2 align-top\"\n >\n <span className=\"block max-w-56 truncate pb-1 text-[11px] font-normal text-[var(--text-muted)]\">\n {name}\n </span>\n <Select\n size=\"sm\"\n className=\"w-full\"\n // Its own width (the longest option) rather than the cell's: a cell\n // takes no width from a `w-full` select, so the role's name was\n // cut at the column's 9rem minimum. Stretched to the cell when that\n // is wider; capped where the column's name and values stop.\n selectClassName=\"w-auto min-w-full max-w-56\"\n aria-label={labels.roleOf(name)}\n value={role ?? \"\"}\n disabled={disabled}\n onChange={(event) =>\n onMappingChange(\n assignColumnRole(\n roles,\n mapping,\n column,\n event.target.value === \"\"\n ? null\n : (event.target.value as R),\n ),\n )\n }\n >\n <option value=\"\">{labels.ignore}</option>\n {roles.map((option) => {\n const marked = option.required === true && markRequired;\n return (\n <option\n key={option.value}\n value={option.value}\n // The short mark on screen, the word for a screen reader.\n aria-label={\n marked ? labels.requiredRole(option.label) : undefined\n }\n >\n {marked\n ? (labels.requiredRoleShort ?? markedShort)(option.label)\n : option.label}\n </option>\n );\n })}\n </Select>\n </TableHeaderCell>\n );\n })}\n </TableRow>\n </TableHead>\n <TableBody {...bodyProps}>\n {shown.map((row, index) => (\n <TableRow {...rowProps?.(row, index)} key={index}>\n {columns.map((column) => (\n <TableCell\n key={column}\n data-ignored={\n roleOfColumn(roles, mapping, column) === null || undefined\n }\n className={cn(\n \"font-mono whitespace-nowrap\",\n // Dimmed, not hidden: its values are what tell you whether ignoring\n // it was right.\n roleOfColumn(roles, mapping, column) === null &&\n \"text-[var(--text-muted)]\",\n )}\n >\n <span className=\"block max-w-56 truncate\">\n {row[column] ?? \"\"}\n </span>\n </TableCell>\n ))}\n </TableRow>\n ))}\n </TableBody>\n </Table>\n {total > shown.length && (\n <p className=\"text-xs text-[var(--text-muted)]\">\n {labels.previewOf(shown.length, total)}\n </p>\n )}\n {gaps.length > 0 && (\n <AlertBanner tone=\"warning\" variant=\"inline\" size=\"sm\" block>\n {labels.missing(formatList(gaps, \"conjunction\", locale))}\n </AlertBanner>\n )}\n </div>\n );\n}\n\n// ── ColumnMapper ──────────────────────────────────────────────────────────────\n\nexport interface ColumnMapperProps<R extends string> {\n /** The parts the columns play — translated labels, which are required, which must\n * be numbers. See {@link ColumnRole}. */\n roles: readonly ColumnRole<R>[];\n /**\n * The table read through the current mapping, on every change: the text, the header\n * checkbox, a role. `null` while there is no table at all (an empty box). Use it when\n * `result.complete`; until then `result.missing` says what is not placed yet.\n *\n * The rows are strings. A numeric role's cells are guaranteed to read with\n * `tableNumber(cell, result.decimalComma)`; anything else is the caller's to convert.\n */\n onChange: (result: ColumnMapperResult<R> | null) => void;\n /** The paste box's label. Default: `labels.paste`. */\n label?: ReactNode;\n /** The caption under the paste box. Default: `labels.pasteHint`. */\n hint?: ReactNode;\n /** What the file dialog offers, and what a drop is checked against. Default:\n * `.csv,.tsv,.txt,.dat,.asc`. */\n accept?: string;\n /** The largest file taken, in bytes. Default: no limit. */\n maxSize?: number;\n /** Text to start with. `onChange` is called with its result once, on mount. */\n defaultText?: string;\n /** Rows in the preview. Default 5. */\n previewRows?: number;\n /** The first mapping for a new table. Default {@link guessMapping}: a column named\n * like a role gets it, and the required roles left over take the free columns in\n * order. */\n guess?: (table: TextTable) => ColumnMapping<R>;\n disabled?: boolean;\n /** The preview table's `<tbody>` attributes — {@link ColumnRoleTableProps.bodyProps}\n * (0.24): `{ \"data-private\": \"\" }` for a table of someone's own data. */\n bodyProps?: ColumnRoleTableBodyProps;\n /** One preview row's `<tr>` attributes — {@link ColumnRoleTableProps.rowProps}\n * (0.24). `row` is the row as read, `index` its place in the preview. */\n rowProps?: (row: readonly string[], index: number) => ColumnRoleTableRowProps | undefined;\n labels?: Partial<ColumnMapperLabels>;\n className?: string;\n}\n\ninterface MapperState<R extends string> {\n text: string;\n /** The reader's answer to \"does the first line name the columns\", or `null` for\n * the parse's own. */\n header: boolean | null;\n table: TextTable;\n mapping: ColumnMapping<R>;\n}\n\nconst hasFiles = (event: DragEvent) =>\n Array.from(event.dataTransfer?.types ?? []).includes(\"Files\");\n\n/**\n * Paste, drop or choose a table, see what it was read as, and say which column is\n * which — Kurvenschmiede's playground signal and comparison input as one kit part.\n *\n * Everything the parse assumed is stated, because a guess the reader cannot see is a\n * guess they cannot correct: the shape it found, the separator, the decimal\n * convention (\"decimal comma (1,5)\"), and every line it could not read, by number and\n * with its text. The header line is a checkbox, since the detection is a guess either\n * way and toggling it does not move a column (the mapping is kept, keksdose's\n * `keepRoles`). A new file is a new table and is mapped afresh; typing that changes the\n * column count re-guesses, because the old indexes point at nothing.\n *\n * A file can be chosen, dropped on the button, or dropped on the paste box — which,\n * left to the browser, would open the file in place of the page. It is read as UTF-8,\n * or as Windows-1252 when it is not UTF-8 ({@link readTextFile}).\n *\n * A two-step wizard that keeps the drop and the mapping apart (keksdose's) uses the\n * parts: {@link parseTextTable} or its own server's sniff, then {@link ColumnRoleTable},\n * then {@link readMappedTable}.\n */\nexport function ColumnMapper<R extends string>({\n roles,\n onChange,\n label,\n hint,\n accept = DEFAULT_ACCEPT,\n maxSize,\n defaultText = \"\",\n previewRows = 5,\n guess,\n disabled = false,\n bodyProps,\n rowProps,\n labels: labelsProp,\n className,\n}: ColumnMapperProps<R>) {\n const labels: ColumnMapperLabels = useKitLabels(\n \"columnMapper\",\n DEFAULT_COLUMN_MAPPER_LABELS,\n labelsProp,\n );\n const firstMapping = (table: TextTable) =>\n guess ? guess(table) : guessMapping(roles, table.header, table.width);\n const [state, setState] = useState<MapperState<R>>(() => {\n const table = parseTextTable(defaultText);\n return {\n text: defaultText,\n header: null,\n table,\n mapping: firstMapping(table),\n };\n });\n const [reading, setReading] = useState(false);\n const [refusal, setRefusal] = useState<string | null>(null);\n const [dragOver, setDragOver] = useState(false);\n\n // What is drawn and what is handed over are the same computation. Not memoised: a\n // caller writes `roles={[…]}` inline, so a memo keyed on them would miss every time,\n // and the read is one pass over rows that are already split.\n const result = readMappedTable(state.table, roles, state.mapping);\n\n const commit = (next: MapperState<R>) => {\n setState(next);\n onChange(\n next.table.width === 0\n ? null\n : readMappedTable(next.table, roles, next.mapping),\n );\n };\n\n /** New text. `fresh` (a file): the reader's header answer and the mapping start over. */\n const take = (text: string, fresh: boolean) => {\n const header = fresh ? null : state.header;\n const table = parseTextTable(text, header === null ? {} : { header });\n const keep =\n !fresh && table.width === state.table.width && state.table.width > 0;\n commit({\n text,\n header,\n table,\n mapping: keep ? state.mapping : firstMapping(table),\n });\n };\n\n const load = (file: File) => {\n setRefusal(null);\n setReading(true);\n void readTextFile(file)\n .then(\n (text) => take(text, true),\n // Only the read: an error thrown by the caller's onChange is not the file's.\n () => setRefusal(labels.readError(file.name)),\n )\n .finally(() => setReading(false));\n };\n\n const refuse = (rejections: { message: string }[]) =>\n setRefusal(rejections[0]?.message ?? null);\n\n // A file dropped on the paste box. The same screening as the button's.\n const drop = useFilePicker({\n accept,\n maxSize,\n disabled,\n onFiles: ([file]) => load(file),\n onReject: refuse,\n });\n\n const seeded = useEffectEvent(() => {\n if (state.table.width > 0) onChange(result);\n });\n useEffect(() => {\n seeded();\n }, []);\n\n const { table } = state;\n const separator = labels[SEPARATOR_LABEL[table.separator]];\n const unread = result.unread;\n\n return (\n <div\n className={cn(\"min-w-0 space-y-3\", className)}\n data-slot=\"column-mapper\"\n >\n <Textarea\n label={label ?? labels.paste}\n hint={hint ?? labels.pasteHint}\n rows={5}\n wrap=\"off\"\n spellCheck={false}\n autoCapitalize=\"off\"\n autoCorrect=\"off\"\n value={state.text}\n disabled={disabled}\n className={cn(\n \"font-mono text-xs\",\n dragOver && \"rounded-md ring-2 ring-[var(--brand)]\",\n )}\n data-drag-over={dragOver || undefined}\n onChange={(event: ChangeEvent<HTMLTextAreaElement>) => {\n setRefusal(null);\n take(event.target.value, false);\n }}\n onDragEnter={(event) => {\n if (!hasFiles(event)) return;\n event.preventDefault();\n if (!disabled) setDragOver(true);\n }}\n onDragOver={(event) => {\n if (!hasFiles(event)) return;\n // Cancelled for every file drag: an uncancelled one makes the browser open\n // the file in place of the page.\n event.preventDefault();\n event.dataTransfer.dropEffect = disabled ? \"none\" : \"copy\";\n }}\n onDragLeave={() => setDragOver(false)}\n onDrop={(event) => {\n if (!hasFiles(event)) return;\n event.preventDefault();\n setDragOver(false);\n drop.take(event.dataTransfer.files);\n }}\n />\n {drop.element}\n\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-2\">\n <FileButton\n variant=\"secondary\"\n accept={accept}\n maxSize={maxSize}\n droppable\n pending={reading}\n disabled={disabled}\n onFiles={([file]) => load(file)}\n onReject={refuse}\n >\n <Upload className=\"size-4\" aria-hidden />\n {labels.chooseFile}\n </FileButton>\n {table.width > 0 && (\n <Checkbox\n label={labels.headerRow}\n checked={table.header !== null}\n disabled={disabled}\n onCheckedChange={(checked) => {\n const next = parseTextTable(state.text, { header: checked });\n // Same text, same split: the columns are where they were, and so are the\n // roles on them.\n commit({ ...state, header: checked, table: next });\n }}\n />\n )}\n </div>\n\n {refusal && (\n <AlertBanner tone=\"danger\" variant=\"inline\" size=\"sm\" block>\n {refusal}\n </AlertBanner>\n )}\n\n {table.width > 0 && (\n <>\n <p\n className=\"text-xs text-[var(--text-secondary)]\"\n data-slot=\"column-mapper-summary\"\n >\n {[\n labels.summary(table.width, result.rows.length),\n separator,\n table.decimalComma ? labels.decimalComma : labels.decimalPoint,\n ].join(\" · \")}\n </p>\n {result.rows.length === 0 && (\n <AlertBanner tone=\"warning\" variant=\"inline\" size=\"sm\" block>\n {labels.noRows}\n </AlertBanner>\n )}\n <ColumnRoleTable\n header={table.header}\n rows={result.rows}\n width={table.width}\n roles={roles}\n mapping={state.mapping}\n onMappingChange={(mapping) => commit({ ...state, mapping })}\n previewRows={previewRows}\n disabled={disabled}\n bodyProps={bodyProps}\n rowProps={rowProps}\n labels={labelsProp}\n />\n {unread.length > 0 && (\n <Disclosure\n variant=\"bare\"\n title={labels.unreadCount(unread.length)}\n data-slot=\"column-mapper-unread\"\n >\n <ul className=\"space-y-1 text-xs\">\n {unread.slice(0, UNREAD_LISTED).map((line) => (\n <li key={line.line} className=\"min-w-0\">\n <span className=\"block text-[var(--text-secondary)]\">\n {labels.unreadLine(line.line)}\n </span>\n <code className=\"block truncate font-mono text-[var(--text-muted)]\">\n {line.text}\n </code>\n </li>\n ))}\n {unread.length > UNREAD_LISTED && (\n <li className=\"text-[var(--text-muted)]\">\n {labels.unreadMore(unread.length - UNREAD_LISTED)}\n </li>\n )}\n </ul>\n </Disclosure>\n )}\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["table"]}
1
+ {"version":3,"sources":["../../src/components/column-mapper.tsx"],"sourcesContent":["import { useEffect, useEffectEvent, useState } from \"react\";\nimport type { ChangeEvent, DragEvent, HTMLAttributes, ReactNode } from \"react\";\nimport { Upload } from \"lucide-react\";\n\nimport { useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport {\n assignColumnRole,\n guessMapping,\n missingRoles,\n readMappedTable,\n readTextFile,\n roleOfColumn,\n} from \"../lib/column-mapping\";\nimport type {\n ColumnMapperResult,\n ColumnMapping,\n ColumnRole,\n} from \"../lib/column-mapping\";\nimport { parseTextTable } from \"../lib/table-text\";\nimport type { TableSeparator, TextTable } from \"../lib/table-text\";\nimport { AlertBanner } from \"./alert-banner\";\nimport { Checkbox } from \"./checkbox\";\nimport { Disclosure } from \"./disclosure\";\nimport { FileButton, useFilePicker } from \"./file-button\";\nimport {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeaderCell,\n TableRow,\n} from \"./table\";\nimport { Select, Textarea } from \"./ui\";\n\n/**\n * A table pasted, dropped or chosen as a file, and which of its columns plays which\n * part — Kurvenschmiede's columns input and keksdose's bank-file mapping step, once.\n *\n * Kurvenschmiede (`features/control/columns-input.tsx` + `csv.ts`) reads a recorder's\n * export into a playground signal (time, value) and a comparison (time, setpoint,\n * actual): a textarea, a file button, a parse, a select per role, and a line that says\n * which decimal convention was assumed and how many lines were skipped. keksdose\n * (`import-map-step.tsx`) shows a bank's CSV as a table with a role select on every\n * column's header, the file's real values under it, ignored columns dimmed, and what\n * the import still needs named under it. Same problem, two hand-built answers.\n *\n * Two layers, so both shapes fit:\n *\n * * {@link ColumnMapper} — the whole single panel: paste, drop or choose → parse\n * ({@link parseTextTable}: separator, decimal convention, header line, all stated\n * back) → preview with a role per column → `onChange(result)`. Kurvenschmiede's.\n * * {@link ColumnRoleTable} — the preview with the role selects alone, controlled, over\n * rows the caller already has. keksdose's map step, whose rows come from its server's\n * sniff (encodings, quoted multi-line cells, skipped title rows) and whose mapping\n * lives in the wizard's state between steps.\n *\n * The rules under both are headless in `lib/column-mapping.ts`: a role moves rather\n * than doubling up, what is missing is named, and the rows a numeric role cannot read\n * are reported by line number rather than dropped.\n */\n\n// ── Labels ────────────────────────────────────────────────────────────────────\n\nexport interface ColumnMapperLabels {\n /** The paste box's label. */\n paste: string;\n /** The caption under the paste box. */\n pasteHint: string;\n /** The file button. */\n chooseFile: string;\n /** A chosen or dropped file whose bytes could not be read at all. */\n readError: (name: string) => string;\n /** The checkbox that says whether the first line names the columns. */\n headerRow: string;\n /** What was read: so many columns and rows. */\n summary: (columns: number, rows: number) => string;\n /** The separator that was assumed. */\n separatorSemicolon: string;\n separatorComma: string;\n separatorTab: string;\n separatorSpace: string;\n /** The decimal convention that was assumed — with an example, because \"decimal\n * comma\" alone is a term, and `1,5` is something a reader recognises. */\n decimalComma: string;\n decimalPoint: string;\n /** The title over the lines that could not be read. */\n unreadCount: (count: number) => string;\n /** One of them. */\n unreadLine: (line: number) => string;\n /** Past the first hundred listed. */\n unreadMore: (count: number) => string;\n /** The text has content, and none of it is a row. */\n noRows: string;\n /** The preview table's name. */\n table: string;\n /** A column the table names no header for. */\n columnN: (n: number) => string;\n /** The role select on a column's header — its accessible name. */\n roleOf: (column: string) => string;\n /** The role select's \"no role\" option. */\n ignore: string;\n /** A required role in the select — only while some role is NOT required: when\n * every one is, the mark tells no role from another (0.24, Kurvenschmiede). Since\n * 0.25 this is what a screen reader says for the option (its `aria-label`); the\n * option SHOWS {@link requiredRoleShort}. */\n requiredRole: (role: string) => string;\n /** The same required role as the select shows it (0.25): the role's name and a short\n * mark, so the mark does not cut the name short in a phone-width column — \"Booking\n * date (re…\" was all keksdose's closed select had room for. Spoken as\n * {@link requiredRole}. Optional, so a complete `UiKitLabels` typed before 0.25\n * still compiles; the asterisk needs no translation in most languages. */\n requiredRoleShort?: (role: string) => string;\n /** Under the preview, when it shows fewer rows than were read. */\n previewOf: (shown: number, total: number) => string;\n /** A required group's roles, already joined as a list with \"or\". */\n oneOf: (roles: string) => string;\n /** What the table still needs, already joined as a list with \"and\". */\n missing: (roles: string) => string;\n}\n\n/** {@link ColumnMapperLabels.requiredRoleShort}'s English — and its fallback for a\n * provider whose labels predate the key. */\nconst markedShort = (role: string) => `${role} *`;\n\nexport const DEFAULT_COLUMN_MAPPER_LABELS: ColumnMapperLabels = {\n paste: \"Paste a table\",\n pasteHint:\n \"Copy the rows out of a spreadsheet and paste them here, or drop a CSV or text file.\",\n chooseFile: \"Choose a file\",\n readError: (name) => `“${name}” could not be read`,\n headerRow: \"The first line names the columns\",\n summary: (columns, rows) =>\n `${columns} ${columns === 1 ? \"column\" : \"columns\"}, ${rows} ${rows === 1 ? \"row\" : \"rows\"}`,\n separatorSemicolon: \"separated by semicolons\",\n separatorComma: \"separated by commas\",\n separatorTab: \"separated by tabs\",\n separatorSpace: \"separated by spaces\",\n decimalComma: \"decimal comma (1,5)\",\n decimalPoint: \"decimal point (1.5)\",\n unreadCount: (count) =>\n count === 1\n ? \"1 line could not be read\"\n : `${count} lines could not be read`,\n unreadLine: (line) => `Line ${line} could not be read`,\n unreadMore: (count) => `…and ${count} more`,\n noRows: \"No line of this text reads as a row of the table.\",\n table: \"Columns and their roles\",\n columnN: (n) => `Column ${n}`,\n roleOf: (column) => `What does column “${column}” hold?`,\n ignore: \"Ignore\",\n requiredRole: (role) => `${role} (required)`,\n requiredRoleShort: markedShort,\n previewOf: (shown, total) =>\n shown === 1\n ? `The first of ${total} rows`\n : `The first ${shown} of ${total} rows`,\n oneOf: (roles) => `either ${roles}`,\n missing: (roles) => `Still needed: ${roles}.`,\n};\n\nconst SEPARATOR_LABEL: Record<\n TableSeparator,\n \"separatorSemicolon\" | \"separatorComma\" | \"separatorTab\" | \"separatorSpace\"\n> = {\n \";\": \"separatorSemicolon\",\n \",\": \"separatorComma\",\n \"\\t\": \"separatorTab\",\n \" \": \"separatorSpace\",\n};\n\n/** How many unread lines are listed before \"…and N more\". */\nconst UNREAD_LISTED = 100;\n\n/** A file a table is likely to be in. Extensions only, so a refusal reads \"Only .csv,\n * .tsv, … files\" rather than a list of MIME types; the parse is the real check. */\nconst DEFAULT_ACCEPT = \".csv,.tsv,.txt,.dat,.asc\";\n\nfunction formatList(\n items: string[],\n type: \"conjunction\" | \"disjunction\",\n locale: string | undefined,\n): string {\n try {\n return new Intl.ListFormat(locale, { type, style: \"long\" }).format(items);\n } catch {\n return items.join(\", \");\n }\n}\n\n// ── ColumnRoleTable ───────────────────────────────────────────────────────────\n\n/** A `data-*` attribute, typed so `{ \"data-private\": \"\" }` needs no cast. */\ntype DataAttributes = { [key: `data-${string}`]: string | number | boolean | undefined };\n\n/**\n * What {@link ColumnRoleTableProps.bodyProps} puts on the preview's `<tbody>`: `data-*`\n * hooks, an `id`, a class, a style.\n */\nexport type ColumnRoleTableBodyProps = Omit<\n HTMLAttributes<HTMLTableSectionElement>,\n \"children\" | \"dangerouslySetInnerHTML\"\n> &\n DataAttributes;\n\n/**\n * What {@link ColumnRoleTableProps.rowProps} returns for one preview row's `<tr>` —\n * the same shape as LineItems' `LineItemsRowProps`, on a table row.\n */\nexport type ColumnRoleTableRowProps = Omit<\n HTMLAttributes<HTMLTableRowElement>,\n \"children\" | \"dangerouslySetInnerHTML\"\n> &\n DataAttributes;\n\nexport interface ColumnRoleTableProps<\n R extends string,\n M extends ColumnMapping<R> = ColumnMapping<R>,\n> {\n /** The columns' names — the file's header line — or `null` for a table without one\n * (each column is then \"Column 1\", \"Column 2\", …). */\n header: readonly string[] | null;\n /** The rows to preview, as text. Only the first {@link previewRows} are drawn. */\n rows: readonly (readonly string[])[];\n /** How many columns. Default: the header's length or the widest previewed row. */\n width?: number;\n /** The parts a column can play. */\n roles: readonly ColumnRole<R>[];\n /** Role → column. Any object with the roles as nullable number fields — keksdose's\n * `CsvMapping` as it stands; the other fields are handed back untouched. */\n mapping: M;\n /** The next mapping, with the picked role moved onto its column. */\n onMappingChange: (next: M) => void;\n /** Default 5. */\n previewRows?: number;\n /** How many rows the table has in all, for \"The first 5 of 120 rows\". Default:\n * `rows.length`. */\n totalRows?: number;\n /** Every role select disabled — keksdose locks them while a re-sniff replaces the\n * table they point into. */\n disabled?: boolean;\n /**\n * Extra attributes for the preview's `<tbody>` (0.24) — keksdose: the sample rows are\n * the user's own bank data (payees, memos, amounts), and its demo mode blurs exactly\n * that through `.demo-mode [data-private]` (#336). With nowhere to put the attribute,\n * its map step reached into the table with a `useLayoutEffect` that set it on the\n * `<tbody>` after every commit; `bodyProps={{ \"data-private\": \"\" }}` declares it.\n * There is deliberately no head counterpart: the column names and the role selects\n * are what such a mode is there to show.\n *\n * Spread on the kit `TableBody`; a `className` is added to its own.\n */\n bodyProps?: ColumnRoleTableBodyProps;\n /**\n * Extra attributes for one preview row's `<tr>`, from its cells and its index in the\n * preview (0.24) — the contract of LineItems' `rowProps` (keksdose K20), on a table:\n * `data-*` hooks (a per-row `data-private`, a test id), an `id`, a class. Called for\n * the rows drawn, the first {@link previewRows}. The row has no attributes of its own\n * to protect; a `className` is added after its classes.\n */\n rowProps?: (row: readonly string[], index: number) => ColumnRoleTableRowProps | undefined;\n labels?: Partial<ColumnMapperLabels>;\n className?: string;\n}\n\n/**\n * A table's first rows, with a role select on every column's header.\n *\n * keksdose's mapping step (dev#462: \"make it more table like, where the table columns\n * can be chosen from the input and the column data changes with the chosen column\n * header\"): the file IS the form. The file's own column name sits on top, verbatim —\n * it is how the reader recognises the column; under it the role, picked in place;\n * under that the column's real values, so a column that is one off shows up here and\n * not after the import. A column with no role says \"Ignore\" and its values are dimmed:\n * the state that most needs to be visible in a twelve-column bank export.\n *\n * Picking a role another column holds MOVES it ({@link assignColumnRole}). What the\n * table still needs is named under it — a required group as \"either Amount, Debit or\n * Credit\". Required roles are marked in the list only where some role is not\n * (0.24): Kurvenschmiede's roles are all required, so the mark told no option from\n * another, and on a phone it cut the role's own name short (\"Zeit (s) (erforderl…\")\n * — the part of the option the reader has to read. The \"Still needed\" line already\n * says what is missing; a mark that distinguishes nothing is only noise.\n *\n * Where the mark does tell roles apart, it is short (0.25): keksdose's bank roles, on\n * a phone, still showed \"Booking date (re…\" in the closed select. The option shows\n * \"Booking date *\" ({@link ColumnMapperLabels.requiredRoleShort}) and is NAMED \"Booking\n * date (required)\" ({@link ColumnMapperLabels.requiredRole}, its `aria-label`), so a\n * screen reader hears the word rather than \"star\". And no role is cut at all: each\n * column is at least as wide as its select's longest option — the select's own width,\n * which a table cell otherwise ignores for a `w-full` control — up to 14rem, the width\n * the column's name and values already stop at. A long role name (\"Date de\n * comptabilisation\", \"Betrag (mit Vorzeichen)\") widens the columns, and the edge fade\n * says there are more of them to scroll to.\n *\n * The table scrolls sideways inside its own box (the kit {@link Table}'s wrapper, a\n * named, keyboard-reachable region while it overflows), so a wide export never widens\n * the page at 390px. An edge with columns behind it fades out (0.24, Kurvenschmiede:\n * on a phone the fourth column's select sat off-screen and nothing said there was more\n * to the right; phone scrollbars are overlays that show only while you drag). Since\n * 0.25 that is the kit Table's own `edgeFade`, not a box of this table's round it:\n * measured, so a table that fits is painted as before; both ends once scrolled into\n * the middle; the reading direction's own in RTL; never animated; a `mask-image`,\n * which changes no layout and needs no background colour to fade to. A role select\n * focused behind the fade is scrolled clear of it, cell and all, and the Table's\n * wrapper says which ends are cut in `data-overflow`.\n */\nexport function ColumnRoleTable<\n R extends string,\n M extends ColumnMapping<R> = ColumnMapping<R>,\n>({\n header,\n rows,\n width: widthProp,\n roles,\n mapping,\n onMappingChange,\n previewRows = 5,\n totalRows,\n disabled,\n bodyProps,\n rowProps,\n labels: labelsProp,\n className,\n}: ColumnRoleTableProps<R, M>) {\n const labels: ColumnMapperLabels = useKitLabels(\n \"columnMapper\",\n DEFAULT_COLUMN_MAPPER_LABELS,\n labelsProp,\n );\n const locale = useKitLocale();\n const shown = rows.slice(0, previewRows);\n const width =\n widthProp ??\n shown.reduce(\n (widest, row) => Math.max(widest, row.length),\n header?.length ?? 0,\n );\n const columns = Array.from({ length: width }, (_, index) => index);\n const total = totalRows ?? rows.length;\n const nameOf = (column: number) =>\n header?.[column] || labels.columnN(column + 1);\n const labelOf = (value: R) =>\n roles.find((role) => role.value === value)?.label ?? value;\n const gaps = missingRoles(roles, mapping, width).map((gap) =>\n gap.length === 1\n ? labelOf(gap[0])\n : labels.oneOf(formatList(gap.map(labelOf), \"disjunction\", locale)),\n );\n // The required mark only where it tells one option from another.\n const markRequired = roles.some((role) => role.required !== true);\n\n return (\n <div\n className={cn(\"min-w-0 space-y-1.5\", className)}\n data-slot=\"column-role-table\"\n >\n <Table density=\"compact\" framed edgeFade aria-label={labels.table}>\n <TableHead>\n <TableRow>\n {columns.map((column) => {\n const role = roleOfColumn(roles, mapping, column);\n const name = nameOf(column);\n return (\n <TableHeaderCell\n key={column}\n scope=\"col\"\n className=\"min-w-36 py-2 align-top\"\n >\n <span className=\"block max-w-56 truncate pb-1 text-caption font-normal text-[var(--text-muted)]\">\n {name}\n </span>\n <Select\n size=\"sm\"\n className=\"w-full\"\n // Its own width (the longest option) rather than the cell's: a cell\n // takes no width from a `w-full` select, so the role's name was\n // cut at the column's 9rem minimum. Stretched to the cell when that\n // is wider; capped where the column's name and values stop.\n selectClassName=\"w-auto min-w-full max-w-56\"\n aria-label={labels.roleOf(name)}\n value={role ?? \"\"}\n disabled={disabled}\n onChange={(event) =>\n onMappingChange(\n assignColumnRole(\n roles,\n mapping,\n column,\n event.target.value === \"\"\n ? null\n : (event.target.value as R),\n ),\n )\n }\n >\n <option value=\"\">{labels.ignore}</option>\n {roles.map((option) => {\n const marked = option.required === true && markRequired;\n return (\n <option\n key={option.value}\n value={option.value}\n // The short mark on screen, the word for a screen reader.\n aria-label={\n marked ? labels.requiredRole(option.label) : undefined\n }\n >\n {marked\n ? (labels.requiredRoleShort ?? markedShort)(option.label)\n : option.label}\n </option>\n );\n })}\n </Select>\n </TableHeaderCell>\n );\n })}\n </TableRow>\n </TableHead>\n <TableBody {...bodyProps}>\n {shown.map((row, index) => (\n <TableRow {...rowProps?.(row, index)} key={index}>\n {columns.map((column) => (\n <TableCell\n key={column}\n data-ignored={\n roleOfColumn(roles, mapping, column) === null || undefined\n }\n className={cn(\n \"font-mono whitespace-nowrap\",\n // Dimmed, not hidden: its values are what tell you whether ignoring\n // it was right.\n roleOfColumn(roles, mapping, column) === null &&\n \"text-[var(--text-muted)]\",\n )}\n >\n <span className=\"block max-w-56 truncate\">\n {row[column] ?? \"\"}\n </span>\n </TableCell>\n ))}\n </TableRow>\n ))}\n </TableBody>\n </Table>\n {total > shown.length && (\n <p className=\"text-xs text-[var(--text-muted)]\">\n {labels.previewOf(shown.length, total)}\n </p>\n )}\n {gaps.length > 0 && (\n <AlertBanner tone=\"warning\" variant=\"inline\" size=\"sm\" block>\n {labels.missing(formatList(gaps, \"conjunction\", locale))}\n </AlertBanner>\n )}\n </div>\n );\n}\n\n// ── ColumnMapper ──────────────────────────────────────────────────────────────\n\nexport interface ColumnMapperProps<R extends string> {\n /** The parts the columns play — translated labels, which are required, which must\n * be numbers. See {@link ColumnRole}. */\n roles: readonly ColumnRole<R>[];\n /**\n * The table read through the current mapping, on every change: the text, the header\n * checkbox, a role. `null` while there is no table at all (an empty box). Use it when\n * `result.complete`; until then `result.missing` says what is not placed yet.\n *\n * The rows are strings. A numeric role's cells are guaranteed to read with\n * `tableNumber(cell, result.decimalComma)`; anything else is the caller's to convert.\n */\n onChange: (result: ColumnMapperResult<R> | null) => void;\n /** The paste box's label. Default: `labels.paste`. */\n label?: ReactNode;\n /** The caption under the paste box. Default: `labels.pasteHint`. */\n hint?: ReactNode;\n /** What the file dialog offers, and what a drop is checked against. Default:\n * `.csv,.tsv,.txt,.dat,.asc`. */\n accept?: string;\n /** The largest file taken, in bytes. Default: no limit. */\n maxSize?: number;\n /** Text to start with. `onChange` is called with its result once, on mount. */\n defaultText?: string;\n /** Rows in the preview. Default 5. */\n previewRows?: number;\n /** The first mapping for a new table. Default {@link guessMapping}: a column named\n * like a role gets it, and the required roles left over take the free columns in\n * order. */\n guess?: (table: TextTable) => ColumnMapping<R>;\n disabled?: boolean;\n /** The preview table's `<tbody>` attributes — {@link ColumnRoleTableProps.bodyProps}\n * (0.24): `{ \"data-private\": \"\" }` for a table of someone's own data. */\n bodyProps?: ColumnRoleTableBodyProps;\n /** One preview row's `<tr>` attributes — {@link ColumnRoleTableProps.rowProps}\n * (0.24). `row` is the row as read, `index` its place in the preview. */\n rowProps?: (row: readonly string[], index: number) => ColumnRoleTableRowProps | undefined;\n labels?: Partial<ColumnMapperLabels>;\n className?: string;\n}\n\ninterface MapperState<R extends string> {\n text: string;\n /** The reader's answer to \"does the first line name the columns\", or `null` for\n * the parse's own. */\n header: boolean | null;\n table: TextTable;\n mapping: ColumnMapping<R>;\n}\n\nconst hasFiles = (event: DragEvent) =>\n Array.from(event.dataTransfer?.types ?? []).includes(\"Files\");\n\n/**\n * Paste, drop or choose a table, see what it was read as, and say which column is\n * which — Kurvenschmiede's playground signal and comparison input as one kit part.\n *\n * Everything the parse assumed is stated, because a guess the reader cannot see is a\n * guess they cannot correct: the shape it found, the separator, the decimal\n * convention (\"decimal comma (1,5)\"), and every line it could not read, by number and\n * with its text. The header line is a checkbox, since the detection is a guess either\n * way and toggling it does not move a column (the mapping is kept, keksdose's\n * `keepRoles`). A new file is a new table and is mapped afresh; typing that changes the\n * column count re-guesses, because the old indexes point at nothing.\n *\n * A file can be chosen, dropped on the button, or dropped on the paste box — which,\n * left to the browser, would open the file in place of the page. It is read as UTF-8,\n * or as Windows-1252 when it is not UTF-8 ({@link readTextFile}).\n *\n * A two-step wizard that keeps the drop and the mapping apart (keksdose's) uses the\n * parts: {@link parseTextTable} or its own server's sniff, then {@link ColumnRoleTable},\n * then {@link readMappedTable}.\n */\nexport function ColumnMapper<R extends string>({\n roles,\n onChange,\n label,\n hint,\n accept = DEFAULT_ACCEPT,\n maxSize,\n defaultText = \"\",\n previewRows = 5,\n guess,\n disabled = false,\n bodyProps,\n rowProps,\n labels: labelsProp,\n className,\n}: ColumnMapperProps<R>) {\n const labels: ColumnMapperLabels = useKitLabels(\n \"columnMapper\",\n DEFAULT_COLUMN_MAPPER_LABELS,\n labelsProp,\n );\n const firstMapping = (table: TextTable) =>\n guess ? guess(table) : guessMapping(roles, table.header, table.width);\n const [state, setState] = useState<MapperState<R>>(() => {\n const table = parseTextTable(defaultText);\n return {\n text: defaultText,\n header: null,\n table,\n mapping: firstMapping(table),\n };\n });\n const [reading, setReading] = useState(false);\n const [refusal, setRefusal] = useState<string | null>(null);\n const [dragOver, setDragOver] = useState(false);\n\n // What is drawn and what is handed over are the same computation. Not memoised: a\n // caller writes `roles={[…]}` inline, so a memo keyed on them would miss every time,\n // and the read is one pass over rows that are already split.\n const result = readMappedTable(state.table, roles, state.mapping);\n\n const commit = (next: MapperState<R>) => {\n setState(next);\n onChange(\n next.table.width === 0\n ? null\n : readMappedTable(next.table, roles, next.mapping),\n );\n };\n\n /** New text. `fresh` (a file): the reader's header answer and the mapping start over. */\n const take = (text: string, fresh: boolean) => {\n const header = fresh ? null : state.header;\n const table = parseTextTable(text, header === null ? {} : { header });\n const keep =\n !fresh && table.width === state.table.width && state.table.width > 0;\n commit({\n text,\n header,\n table,\n mapping: keep ? state.mapping : firstMapping(table),\n });\n };\n\n const load = (file: File) => {\n setRefusal(null);\n setReading(true);\n void readTextFile(file)\n .then(\n (text) => take(text, true),\n // Only the read: an error thrown by the caller's onChange is not the file's.\n () => setRefusal(labels.readError(file.name)),\n )\n .finally(() => setReading(false));\n };\n\n const refuse = (rejections: { message: string }[]) =>\n setRefusal(rejections[0]?.message ?? null);\n\n // A file dropped on the paste box. The same screening as the button's.\n const drop = useFilePicker({\n accept,\n maxSize,\n disabled,\n onFiles: ([file]) => load(file),\n onReject: refuse,\n });\n\n const seeded = useEffectEvent(() => {\n if (state.table.width > 0) onChange(result);\n });\n useEffect(() => {\n seeded();\n }, []);\n\n const { table } = state;\n const separator = labels[SEPARATOR_LABEL[table.separator]];\n const unread = result.unread;\n\n return (\n <div\n className={cn(\"min-w-0 space-y-3\", className)}\n data-slot=\"column-mapper\"\n >\n <Textarea\n label={label ?? labels.paste}\n hint={hint ?? labels.pasteHint}\n rows={5}\n wrap=\"off\"\n spellCheck={false}\n autoCapitalize=\"off\"\n autoCorrect=\"off\"\n value={state.text}\n disabled={disabled}\n className={cn(\n \"font-mono text-xs\",\n dragOver && \"rounded-md ring-2 ring-[var(--brand)]\",\n )}\n data-drag-over={dragOver || undefined}\n onChange={(event: ChangeEvent<HTMLTextAreaElement>) => {\n setRefusal(null);\n take(event.target.value, false);\n }}\n onDragEnter={(event) => {\n if (!hasFiles(event)) return;\n event.preventDefault();\n if (!disabled) setDragOver(true);\n }}\n onDragOver={(event) => {\n if (!hasFiles(event)) return;\n // Cancelled for every file drag: an uncancelled one makes the browser open\n // the file in place of the page.\n event.preventDefault();\n event.dataTransfer.dropEffect = disabled ? \"none\" : \"copy\";\n }}\n onDragLeave={() => setDragOver(false)}\n onDrop={(event) => {\n if (!hasFiles(event)) return;\n event.preventDefault();\n setDragOver(false);\n drop.take(event.dataTransfer.files);\n }}\n />\n {drop.element}\n\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-2\">\n <FileButton\n variant=\"secondary\"\n accept={accept}\n maxSize={maxSize}\n droppable\n pending={reading}\n disabled={disabled}\n onFiles={([file]) => load(file)}\n onReject={refuse}\n >\n <Upload className=\"size-4\" aria-hidden />\n {labels.chooseFile}\n </FileButton>\n {table.width > 0 && (\n <Checkbox\n label={labels.headerRow}\n checked={table.header !== null}\n disabled={disabled}\n onCheckedChange={(checked) => {\n const next = parseTextTable(state.text, { header: checked });\n // Same text, same split: the columns are where they were, and so are the\n // roles on them.\n commit({ ...state, header: checked, table: next });\n }}\n />\n )}\n </div>\n\n {refusal && (\n <AlertBanner tone=\"danger\" variant=\"inline\" size=\"sm\" block>\n {refusal}\n </AlertBanner>\n )}\n\n {table.width > 0 && (\n <>\n <p\n className=\"text-xs text-[var(--text-secondary)]\"\n data-slot=\"column-mapper-summary\"\n >\n {[\n labels.summary(table.width, result.rows.length),\n separator,\n table.decimalComma ? labels.decimalComma : labels.decimalPoint,\n ].join(\" · \")}\n </p>\n {result.rows.length === 0 && (\n <AlertBanner tone=\"warning\" variant=\"inline\" size=\"sm\" block>\n {labels.noRows}\n </AlertBanner>\n )}\n <ColumnRoleTable\n header={table.header}\n rows={result.rows}\n width={table.width}\n roles={roles}\n mapping={state.mapping}\n onMappingChange={(mapping) => commit({ ...state, mapping })}\n previewRows={previewRows}\n disabled={disabled}\n bodyProps={bodyProps}\n rowProps={rowProps}\n labels={labelsProp}\n />\n {unread.length > 0 && (\n <Disclosure\n variant=\"bare\"\n title={labels.unreadCount(unread.length)}\n data-slot=\"column-mapper-unread\"\n >\n <ul className=\"space-y-1 text-xs\">\n {unread.slice(0, UNREAD_LISTED).map((line) => (\n <li key={line.line} className=\"min-w-0\">\n <span className=\"block text-[var(--text-secondary)]\">\n {labels.unreadLine(line.line)}\n </span>\n <code className=\"block truncate font-mono text-[var(--text-muted)]\">\n {line.text}\n </code>\n </li>\n ))}\n {unread.length > UNREAD_LISTED && (\n <li className=\"text-[var(--text-muted)]\">\n {labels.unreadMore(unread.length - UNREAD_LISTED)}\n </li>\n )}\n </ul>\n </Disclosure>\n )}\n </>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":["table"]}
@@ -7,8 +7,8 @@ import { cn } from "../lib/cn.js";
7
7
  import { CLIPS_ATTRIBUTE } from "../lib/clipping.js";
8
8
  import { DropdownSearchHeader } from "./dropdown.js";
9
9
  import { PickerSheet, SHEET_ROW_CLASS } from "./picker-sheet.js";
10
- import { useMediaQuery } from "../hooks/use-media-query.js";
11
- import { FLOATING_LABEL_STATIC, PHONE_QUERY, Spinner } from "./ui.js";
10
+ import { usePhoneLayout } from "../hooks/use-breakpoint.js";
11
+ import { FLOATING_LABEL_STATIC, Spinner } from "./ui.js";
12
12
  import { useAnchorDir } from "./use-anchor-dir.js";
13
13
  import { useAnchoredPanel } from "../hooks/use-anchored-panel.js";
14
14
  import { useEscapeKey, useOutsideClick } from "../hooks/use-dismiss.js";
@@ -119,7 +119,7 @@ function useComboboxCore({
119
119
  };
120
120
  const placement = useAnchoredPanel(triggerRef, open);
121
121
  const rect = placement.rect;
122
- const isPhone = useMediaQuery(PHONE_QUERY, false);
122
+ const isPhone = usePhoneLayout();
123
123
  useOutsideClick([triggerRef, panelRef], close, open && !isPhone);
124
124
  useEscapeKey(closeToTrigger, open);
125
125
  const [prevOpen, setPrevOpen] = useState(open);
@@ -174,7 +174,7 @@ function useComboboxCore({
174
174
  closeToTrigger
175
175
  };
176
176
  }
177
- const FIELD_ERROR_CLASS = "mt-1 text-[11px] leading-tight text-[var(--danger)]";
177
+ const FIELD_ERROR_CLASS = "mt-1 text-caption leading-tight text-[var(--danger)]";
178
178
  function useComboboxFieldError(error, invalid, describedBy) {
179
179
  const errorId = useId();
180
180
  const has = hasMessage(error);
@@ -230,7 +230,7 @@ function ComboboxPanel({
230
230
  setActive,
231
231
  closeToTrigger
232
232
  } = core;
233
- const isPhone = useMediaQuery(PHONE_QUERY, false);
233
+ const isPhone = usePhoneLayout();
234
234
  const rows = showCreate ? [...results, { disabled: createLocked }] : results;
235
235
  const rowCount = rows.length;
236
236
  const optionId = (index) => `${listboxId}-option-${index}`;
@@ -318,7 +318,7 @@ function ComboboxPanel({
318
318
  role: "presentation",
319
319
  className: cn(
320
320
  "font-semibold uppercase tracking-wide text-[var(--text-muted)]",
321
- isPhone ? "px-4 pb-1 pt-3 text-xs" : "px-3 pb-0.5 pt-2 text-[11px] first:pt-1"
321
+ isPhone ? "px-4 pb-1 pt-3 text-xs" : "px-3 pb-0.5 pt-2 text-caption first:pt-1"
322
322
  ),
323
323
  children: o.group
324
324
  }
@@ -445,7 +445,8 @@ function ComboboxPanel({
445
445
  top: placement.top,
446
446
  ...inline,
447
447
  width: rect.width,
448
- minWidth: 220,
448
+ // 220 px at Normal, in rem so the panel's text keeps its room at Large (§3.2).
449
+ minWidth: "13.75rem",
449
450
  maxHeight: placement.maxHeight
450
451
  },
451
452
  className: cn(