@eifi1/ui-kit 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (382) hide show
  1. package/README.md +66 -37
  2. package/dist/chart.d.ts +3 -1
  3. package/dist/chart.js +2 -0
  4. package/dist/chart.js.map +1 -1
  5. package/dist/components/account-settings-labels.d.ts +105 -0
  6. package/dist/components/account-settings-labels.js +81 -0
  7. package/dist/components/account-settings-labels.js.map +1 -0
  8. package/dist/components/account-settings.d.ts +36 -52
  9. package/dist/components/account-settings.js +44 -8
  10. package/dist/components/account-settings.js.map +1 -1
  11. package/dist/components/alert-banner.d.ts +26 -1
  12. package/dist/components/alert-banner.js +10 -7
  13. package/dist/components/alert-banner.js.map +1 -1
  14. package/dist/components/amount-input.d.ts +26 -4
  15. package/dist/components/amount-input.js +5 -2
  16. package/dist/components/amount-input.js.map +1 -1
  17. package/dist/components/authed-image.d.ts +68 -0
  18. package/dist/components/authed-image.js +117 -0
  19. package/dist/components/authed-image.js.map +1 -0
  20. package/dist/components/breadcrumbs.d.ts +2 -1
  21. package/dist/components/breadcrumbs.js +5 -2
  22. package/dist/components/breadcrumbs.js.map +1 -1
  23. package/dist/components/bulk-action-bar.d.ts +51 -5
  24. package/dist/components/bulk-action-bar.js +88 -41
  25. package/dist/components/bulk-action-bar.js.map +1 -1
  26. package/dist/components/button-group.d.ts +107 -3
  27. package/dist/components/button-group.js +60 -5
  28. package/dist/components/button-group.js.map +1 -1
  29. package/dist/components/calculator.d.ts +17 -4
  30. package/dist/components/calendar-heatmap.d.ts +162 -0
  31. package/dist/components/calendar-heatmap.js +336 -0
  32. package/dist/components/calendar-heatmap.js.map +1 -0
  33. package/dist/components/chip.d.ts +2 -1
  34. package/dist/components/chip.js +6 -3
  35. package/dist/components/chip.js.map +1 -1
  36. package/dist/components/choice-card.d.ts +66 -2
  37. package/dist/components/choice-card.js +108 -1
  38. package/dist/components/choice-card.js.map +1 -1
  39. package/dist/components/combobox-core.js +2 -0
  40. package/dist/components/combobox-core.js.map +1 -1
  41. package/dist/components/combobox.d.ts +2 -2
  42. package/dist/components/combobox.js +31 -9
  43. package/dist/components/combobox.js.map +1 -1
  44. package/dist/components/copy-button.d.ts +52 -70
  45. package/dist/components/copy-button.js +1 -1
  46. package/dist/components/copy-button.js.map +1 -1
  47. package/dist/components/data-table-cells.d.ts +63 -0
  48. package/dist/components/data-table-cells.js +64 -0
  49. package/dist/components/data-table-cells.js.map +1 -0
  50. package/dist/components/data-table-filter-popover.d.ts +1 -1
  51. package/dist/components/data-table-filters.d.ts +1 -1
  52. package/dist/components/data-table-filters.js +31 -1
  53. package/dist/components/data-table-filters.js.map +1 -1
  54. package/dist/components/data-table-labels.d.ts +13 -0
  55. package/dist/components/data-table-labels.js +8 -1
  56. package/dist/components/data-table-labels.js.map +1 -1
  57. package/dist/components/data-table.d.ts +1 -1
  58. package/dist/components/data-table.js +104 -12
  59. package/dist/components/data-table.js.map +1 -1
  60. package/dist/components/date-picker.d.ts +3 -1
  61. package/dist/components/date-picker.js +15 -2
  62. package/dist/components/date-picker.js.map +1 -1
  63. package/dist/components/description-list.d.ts +23 -3
  64. package/dist/components/description-list.js +23 -3
  65. package/dist/components/description-list.js.map +1 -1
  66. package/dist/components/dialog-frame.d.ts +1 -0
  67. package/dist/components/entity-combobox.d.ts +1 -1
  68. package/dist/components/entity-combobox.js +18 -3
  69. package/dist/components/entity-combobox.js.map +1 -1
  70. package/dist/components/error-boundary.d.ts +92 -0
  71. package/dist/components/error-boundary.js +111 -0
  72. package/dist/components/error-boundary.js.map +1 -0
  73. package/dist/components/field.d.ts +69 -0
  74. package/dist/components/field.js +75 -0
  75. package/dist/components/field.js.map +1 -0
  76. package/dist/components/file-button.d.ts +51 -208
  77. package/dist/components/file-dropzone.d.ts +50 -2
  78. package/dist/components/floating-panel.d.ts +156 -4
  79. package/dist/components/floating-panel.js +193 -29
  80. package/dist/components/floating-panel.js.map +1 -1
  81. package/dist/components/form-actions.d.ts +53 -0
  82. package/dist/components/form-actions.js +98 -0
  83. package/dist/components/form-actions.js.map +1 -0
  84. package/dist/components/image-grid.d.ts +67 -0
  85. package/dist/components/image-grid.js +112 -0
  86. package/dist/components/image-grid.js.map +1 -0
  87. package/dist/components/lightbox.d.ts +104 -0
  88. package/dist/components/lightbox.js +229 -0
  89. package/dist/components/lightbox.js.map +1 -0
  90. package/dist/components/line-items.d.ts +114 -0
  91. package/dist/components/line-items.js +238 -0
  92. package/dist/components/line-items.js.map +1 -0
  93. package/dist/components/list.d.ts +37 -3
  94. package/dist/components/list.js +39 -14
  95. package/dist/components/list.js.map +1 -1
  96. package/dist/components/loading-state.d.ts +24 -0
  97. package/dist/components/loading-state.js +32 -0
  98. package/dist/components/loading-state.js.map +1 -0
  99. package/dist/components/menu-item.d.ts +13 -2
  100. package/dist/components/menu-item.js +11 -2
  101. package/dist/components/menu-item.js.map +1 -1
  102. package/dist/components/mini-calendar.d.ts +82 -5
  103. package/dist/components/mini-calendar.js +212 -64
  104. package/dist/components/mini-calendar.js.map +1 -1
  105. package/dist/components/modal.d.ts +20 -1
  106. package/dist/components/modal.js +26 -2
  107. package/dist/components/modal.js.map +1 -1
  108. package/dist/components/month-picker.d.ts +21 -1
  109. package/dist/components/month-picker.js +139 -26
  110. package/dist/components/month-picker.js.map +1 -1
  111. package/dist/components/multi-entity-combobox.d.ts +1 -1
  112. package/dist/components/multi-entity-combobox.js +19 -3
  113. package/dist/components/multi-entity-combobox.js.map +1 -1
  114. package/dist/components/nav-pills.d.ts +67 -0
  115. package/dist/components/nav-pills.js +69 -0
  116. package/dist/components/nav-pills.js.map +1 -0
  117. package/dist/components/number-field.d.ts +26 -5
  118. package/dist/components/number-field.js +5 -7
  119. package/dist/components/number-field.js.map +1 -1
  120. package/dist/components/number-input.d.ts +27 -5
  121. package/dist/components/number-input.js +7 -1
  122. package/dist/components/number-input.js.map +1 -1
  123. package/dist/components/numpad-sheet.d.ts +17 -4
  124. package/dist/components/passkeys-setting.d.ts +67 -0
  125. package/dist/components/passkeys-setting.js +214 -0
  126. package/dist/components/passkeys-setting.js.map +1 -0
  127. package/dist/components/picker-sheet.js +2 -1
  128. package/dist/components/picker-sheet.js.map +1 -1
  129. package/dist/components/pie-chart-labels.d.ts +20 -0
  130. package/dist/components/pie-chart-labels.js +12 -0
  131. package/dist/components/pie-chart-labels.js.map +1 -0
  132. package/dist/components/pie-chart.d.ts +101 -0
  133. package/dist/components/pie-chart.js +283 -0
  134. package/dist/components/pie-chart.js.map +1 -0
  135. package/dist/components/progress-bar.d.ts +73 -3
  136. package/dist/components/progress-bar.js +110 -8
  137. package/dist/components/progress-bar.js.map +1 -1
  138. package/dist/components/qr-code.d.ts +34 -0
  139. package/dist/components/qr-code.js +57 -0
  140. package/dist/components/qr-code.js.map +1 -0
  141. package/dist/components/search-field.d.ts +15 -3
  142. package/dist/components/search-field.js +5 -1
  143. package/dist/components/search-field.js.map +1 -1
  144. package/dist/components/series-chart.d.ts +16 -0
  145. package/dist/components/series-chart.js +4 -1
  146. package/dist/components/series-chart.js.map +1 -1
  147. package/dist/components/settings-fields.d.ts +50 -1
  148. package/dist/components/signed-amount.d.ts +121 -0
  149. package/dist/components/signed-amount.js +152 -0
  150. package/dist/components/signed-amount.js.map +1 -0
  151. package/dist/components/skeleton.d.ts +10 -3
  152. package/dist/components/skeleton.js +12 -2
  153. package/dist/components/skeleton.js.map +1 -1
  154. package/dist/components/stat-tile.d.ts +2 -1
  155. package/dist/components/stat-tile.js +5 -2
  156. package/dist/components/stat-tile.js.map +1 -1
  157. package/dist/components/status-dot.d.ts +6 -2
  158. package/dist/components/status-dot.js +6 -1
  159. package/dist/components/status-dot.js.map +1 -1
  160. package/dist/components/table.d.ts +77 -11
  161. package/dist/components/table.js +85 -17
  162. package/dist/components/table.js.map +1 -1
  163. package/dist/components/text-link.d.ts +176 -0
  164. package/dist/components/text-link.js +109 -0
  165. package/dist/components/text-link.js.map +1 -0
  166. package/dist/components/text.d.ts +8 -1
  167. package/dist/components/text.js +1 -0
  168. package/dist/components/text.js.map +1 -1
  169. package/dist/components/time-input.d.ts +50 -1
  170. package/dist/components/toggle-group.d.ts +28 -0
  171. package/dist/components/toggle-group.js +58 -9
  172. package/dist/components/toggle-group.js.map +1 -1
  173. package/dist/components/toggle-legend.d.ts +36 -3
  174. package/dist/components/toggle-legend.js +49 -0
  175. package/dist/components/toggle-legend.js.map +1 -1
  176. package/dist/components/tooltip.d.ts +11 -3
  177. package/dist/components/tooltip.js +2 -0
  178. package/dist/components/tooltip.js.map +1 -1
  179. package/dist/components/ui.d.ts +52 -729
  180. package/dist/components/ui.js +296 -27
  181. package/dist/components/ui.js.map +1 -1
  182. package/dist/components/use-table-state.d.ts +3 -52
  183. package/dist/components/use-table-state.js +128 -30
  184. package/dist/components/use-table-state.js.map +1 -1
  185. package/dist/components/user-avatar.d.ts +25 -3
  186. package/dist/components/user-avatar.js +32 -3
  187. package/dist/components/user-avatar.js.map +1 -1
  188. package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
  189. package/dist/data-table.d.ts +2 -1
  190. package/dist/data-table.js +5 -1
  191. package/dist/data-table.js.map +1 -1
  192. package/dist/feedback/feedback-dialog.d.ts +26 -3
  193. package/dist/feedback/feedback-dialog.js +16 -2
  194. package/dist/feedback/feedback-dialog.js.map +1 -1
  195. package/dist/feedback/feedback-thread.d.ts +133 -0
  196. package/dist/feedback/feedback-thread.js +237 -0
  197. package/dist/feedback/feedback-thread.js.map +1 -0
  198. package/dist/feedback.d.ts +2 -1
  199. package/dist/feedback.js +1 -0
  200. package/dist/feedback.js.map +1 -1
  201. package/dist/hooks/use-authed-src.d.ts +71 -0
  202. package/dist/hooks/use-authed-src.js +63 -0
  203. package/dist/hooks/use-authed-src.js.map +1 -0
  204. package/dist/hooks/use-file-drop.d.ts +50 -2
  205. package/dist/hooks/use-hotkey.d.ts +59 -0
  206. package/dist/hooks/use-hotkey.js +105 -0
  207. package/dist/hooks/use-hotkey.js.map +1 -0
  208. package/dist/hooks/use-search-param-state.d.ts +83 -0
  209. package/dist/hooks/use-search-param-state.js +94 -0
  210. package/dist/hooks/use-search-param-state.js.map +1 -0
  211. package/dist/i18n/defaults.d.ts +17 -4
  212. package/dist/i18n/defaults.js +33 -1
  213. package/dist/i18n/defaults.js.map +1 -1
  214. package/dist/i18n/kit-labels.d.ts +47 -310
  215. package/dist/i18n/kit-labels.js +20 -5
  216. package/dist/i18n/kit-labels.js.map +1 -1
  217. package/dist/i18n/locales/de-CH-informal.d.ts +17 -4
  218. package/dist/i18n/locales/de-CH.d.ts +17 -4
  219. package/dist/i18n/locales/de-informal.d.ts +17 -4
  220. package/dist/i18n/locales/de.d.ts +17 -4
  221. package/dist/i18n/locales/de.js +165 -4
  222. package/dist/i18n/locales/de.js.map +1 -1
  223. package/dist/i18n/locales/es.d.ts +17 -4
  224. package/dist/i18n/locales/es.js +162 -4
  225. package/dist/i18n/locales/es.js.map +1 -1
  226. package/dist/i18n/locales/fr.d.ts +17 -4
  227. package/dist/i18n/locales/fr.js +162 -4
  228. package/dist/i18n/locales/fr.js.map +1 -1
  229. package/dist/i18n/locales/hu.d.ts +17 -4
  230. package/dist/i18n/locales/hu.js +162 -4
  231. package/dist/i18n/locales/hu.js.map +1 -1
  232. package/dist/i18n/locales/it.d.ts +17 -4
  233. package/dist/i18n/locales/it.js +162 -4
  234. package/dist/i18n/locales/it.js.map +1 -1
  235. package/dist/i18n/locales/zh.d.ts +17 -4
  236. package/dist/i18n/locales/zh.js +162 -4
  237. package/dist/i18n/locales/zh.js.map +1 -1
  238. package/dist/index.d.ts +45 -21
  239. package/dist/index.js +43 -1
  240. package/dist/index.js.map +1 -1
  241. package/dist/kit-labels-v3biUF1L.d.ts +1645 -0
  242. package/dist/lib/clipping.d.ts +21 -5
  243. package/dist/lib/clipping.js +3 -0
  244. package/dist/lib/clipping.js.map +1 -1
  245. package/dist/lib/dates.d.ts +17 -1
  246. package/dist/lib/dates.js +19 -0
  247. package/dist/lib/dates.js.map +1 -1
  248. package/dist/lib/format.d.ts +144 -0
  249. package/dist/lib/format.js +149 -0
  250. package/dist/lib/format.js.map +1 -0
  251. package/dist/lib/qr-encode.d.ts +44 -0
  252. package/dist/lib/qr-encode.js +338 -0
  253. package/dist/lib/qr-encode.js.map +1 -0
  254. package/dist/rhf/fields.d.ts +241 -0
  255. package/dist/rhf/fields.js +601 -0
  256. package/dist/rhf/fields.js.map +1 -0
  257. package/dist/rhf/form.d.ts +50 -1
  258. package/dist/rhf/line-items.d.ts +31 -0
  259. package/dist/rhf/line-items.js +59 -0
  260. package/dist/rhf/line-items.js.map +1 -0
  261. package/dist/rhf/use-rhf-wizard-step.d.ts +22 -0
  262. package/dist/rhf/use-rhf-wizard-step.js +38 -0
  263. package/dist/rhf/use-rhf-wizard-step.js.map +1 -0
  264. package/dist/rhf.d.ts +63 -1
  265. package/dist/rhf.js +3 -0
  266. package/dist/rhf.js.map +1 -1
  267. package/dist/shell/app-shell.d.ts +99 -2
  268. package/dist/shell/app-shell.js +58 -3
  269. package/dist/shell/app-shell.js.map +1 -1
  270. package/dist/shell/auth-layout.d.ts +74 -0
  271. package/dist/shell/auth-layout.js +97 -0
  272. package/dist/shell/auth-layout.js.map +1 -0
  273. package/dist/shell/top-bar-brand.d.ts +81 -0
  274. package/dist/shell/top-bar-brand.js +47 -0
  275. package/dist/shell/top-bar-brand.js.map +1 -0
  276. package/dist/shell/topbar-action-menu.d.ts +84 -3
  277. package/dist/shell/topbar-action-menu.js +115 -27
  278. package/dist/shell/topbar-action-menu.js.map +1 -1
  279. package/dist/shell.d.ts +58 -2
  280. package/dist/shell.js +2 -0
  281. package/dist/shell.js.map +1 -1
  282. package/dist/wizard/stepper-nav.d.ts +49 -1
  283. package/dist/wizard/wizard-context.d.ts +30 -1
  284. package/dist/wizard/wizard-context.js +23 -2
  285. package/dist/wizard/wizard-context.js.map +1 -1
  286. package/dist/wizard/wizard-step.d.ts +20 -5
  287. package/dist/wizard/wizard-step.js +16 -3
  288. package/dist/wizard/wizard-step.js.map +1 -1
  289. package/dist/wizard.d.ts +51 -3
  290. package/package.json +1 -1
  291. package/src/chart.ts +4 -0
  292. package/src/components/account-settings-labels.ts +190 -0
  293. package/src/components/account-settings.tsx +110 -63
  294. package/src/components/alert-banner.tsx +36 -7
  295. package/src/components/amount-input.tsx +13 -1
  296. package/src/components/authed-image.tsx +190 -0
  297. package/src/components/breadcrumbs.tsx +8 -2
  298. package/src/components/bulk-action-bar.tsx +163 -57
  299. package/src/components/button-group.tsx +129 -4
  300. package/src/components/calendar-heatmap.tsx +599 -0
  301. package/src/components/chip.tsx +8 -4
  302. package/src/components/choice-card.tsx +208 -1
  303. package/src/components/combobox-core.tsx +5 -0
  304. package/src/components/combobox.tsx +31 -9
  305. package/src/components/copy-button.tsx +3 -2
  306. package/src/components/data-table-cells.tsx +119 -0
  307. package/src/components/data-table-filters.ts +77 -0
  308. package/src/components/data-table-labels.ts +23 -0
  309. package/src/components/data-table.tsx +231 -13
  310. package/src/components/date-picker.tsx +19 -3
  311. package/src/components/description-list.tsx +52 -3
  312. package/src/components/entity-combobox.tsx +18 -3
  313. package/src/components/error-boundary.tsx +216 -0
  314. package/src/components/field.tsx +188 -0
  315. package/src/components/floating-panel.tsx +380 -16
  316. package/src/components/form-actions.tsx +191 -0
  317. package/src/components/image-grid.tsx +181 -0
  318. package/src/components/lightbox.tsx +374 -0
  319. package/src/components/line-items.tsx +385 -0
  320. package/src/components/list.tsx +93 -18
  321. package/src/components/loading-state.tsx +47 -0
  322. package/src/components/menu-item.tsx +29 -3
  323. package/src/components/mini-calendar.tsx +317 -57
  324. package/src/components/modal.tsx +61 -3
  325. package/src/components/month-picker.tsx +181 -26
  326. package/src/components/multi-entity-combobox.tsx +19 -3
  327. package/src/components/nav-pills.tsx +152 -0
  328. package/src/components/number-field.tsx +15 -10
  329. package/src/components/number-input.tsx +16 -1
  330. package/src/components/passkeys-setting.tsx +331 -0
  331. package/src/components/picker-sheet.tsx +7 -1
  332. package/src/components/pie-chart-labels.ts +32 -0
  333. package/src/components/pie-chart.tsx +479 -0
  334. package/src/components/progress-bar.tsx +241 -13
  335. package/src/components/qr-code.tsx +83 -0
  336. package/src/components/search-field.tsx +20 -4
  337. package/src/components/series-chart.tsx +21 -0
  338. package/src/components/signed-amount.tsx +286 -0
  339. package/src/components/skeleton.tsx +21 -3
  340. package/src/components/stat-tile.tsx +8 -2
  341. package/src/components/status-dot.tsx +10 -2
  342. package/src/components/table.tsx +186 -23
  343. package/src/components/text-link.tsx +248 -0
  344. package/src/components/text.tsx +9 -1
  345. package/src/components/toggle-group.tsx +106 -13
  346. package/src/components/toggle-legend.tsx +87 -2
  347. package/src/components/tooltip.tsx +17 -3
  348. package/src/components/ui.tsx +572 -44
  349. package/src/components/use-table-state.ts +265 -33
  350. package/src/components/user-avatar.tsx +57 -3
  351. package/src/data-table.ts +10 -0
  352. package/src/feedback/feedback-dialog.tsx +42 -3
  353. package/src/feedback/feedback-thread.tsx +434 -0
  354. package/src/feedback.ts +1 -0
  355. package/src/hooks/use-authed-src.ts +161 -0
  356. package/src/hooks/use-hotkey.ts +172 -0
  357. package/src/hooks/use-search-param-state.ts +187 -0
  358. package/src/i18n/defaults.ts +32 -0
  359. package/src/i18n/kit-labels.tsx +104 -8
  360. package/src/i18n/locales/de.ts +162 -0
  361. package/src/i18n/locales/es.ts +159 -0
  362. package/src/i18n/locales/fr.ts +162 -0
  363. package/src/i18n/locales/hu.ts +160 -0
  364. package/src/i18n/locales/it.ts +160 -0
  365. package/src/i18n/locales/zh.ts +159 -0
  366. package/src/index.ts +64 -0
  367. package/src/lib/clipping.ts +22 -4
  368. package/src/lib/dates.ts +37 -0
  369. package/src/lib/format.ts +325 -0
  370. package/src/lib/qr-encode.ts +420 -0
  371. package/src/rhf/fields.tsx +1054 -0
  372. package/src/rhf/line-items.tsx +130 -0
  373. package/src/rhf/use-rhf-wizard-step.ts +113 -0
  374. package/src/rhf.ts +8 -0
  375. package/src/shell/app-shell.tsx +147 -2
  376. package/src/shell/auth-layout.tsx +190 -0
  377. package/src/shell/top-bar-brand.tsx +76 -0
  378. package/src/shell/topbar-action-menu.tsx +260 -49
  379. package/src/shell.ts +2 -0
  380. package/src/wizard/wizard-context.tsx +50 -1
  381. package/src/wizard/wizard-step.tsx +43 -6
  382. package/tokens.css +12 -0
@@ -13,11 +13,37 @@ export type TableDensity = "comfortable" | "compact" | "none";
13
13
  * figures for room. */
14
14
  export type TableLayout = "auto" | "fixed";
15
15
  export type TableAlign = "start" | "center" | "end";
16
+ /**
17
+ * Vertical alignment of a cell's content (0.11.0). The defaults stay as they were — a
18
+ * head cell sits on the `bottom` (a wrapped heading ends on the line above the
19
+ * figures), a body cell at the `top` (a wrapped note starts level with its row). A
20
+ * table whose cells are INPUTS wants `middle`: keksdose's VAT summary
21
+ * (invoices/vat-summary.tsx) sets fields beside derived read-out figures in one row,
22
+ * and a figure at the top of its cell sat above the middle of the field next to it —
23
+ * so it writes `<tr className="align-middle">` over raw `<th>`/`<td>`, because the
24
+ * kit's cells each stated `align-top` and a class on the row could not reach them.
25
+ */
26
+ export type TableVAlign = "top" | "middle" | "bottom";
27
+ /** The head cell's type size: `xs` (the head's default) or `sm` (the body's). */
28
+ export type TableHeaderCellSize = "xs" | "sm";
29
+ /** The header cell's weight. Default `medium`. */
30
+ export type TableHeaderCellWeight = "normal" | "medium" | "semibold";
31
+
32
+ /**
33
+ * What a {@link TableRow} is in a statement-style table (0.12.0): an ordinary `row`, a
34
+ * `group` heading ("Current assets", a tinted full-width label row), a `subtotal` under
35
+ * a group, or the `total` of the whole table. kastlan's balance sheet, income statement
36
+ * and journal entry each spelled these as `className="font-medium"`, `"font-bold
37
+ * border-t-2"` and `bg-muted/50` on the cell — three pages, three sets of classes for
38
+ * the same four kinds of line.
39
+ */
40
+ export type TableRowVariant = "row" | "group" | "subtotal" | "total";
16
41
 
17
42
  interface TableContextValue {
18
43
  density: TableDensity;
19
44
  zebra: boolean;
20
45
  hover: boolean;
46
+ rowDividers: boolean;
21
47
  captionId: string;
22
48
  registerCaption: () => () => void;
23
49
  }
@@ -26,12 +52,66 @@ const TableContext = createContext<TableContextValue>({
26
52
  density: "comfortable",
27
53
  zebra: false,
28
54
  hover: false,
55
+ rowDividers: true,
29
56
  captionId: "",
30
57
  registerCaption: () => () => {},
31
58
  });
32
59
 
33
- /** Which section a row sits in — zebra and hover apply to body rows only. */
34
- const SectionContext = createContext<"head" | "body" | "foot">("body");
60
+ type TableSection = "head" | "body" | "foot";
61
+
62
+ /** Which section a row sits in — zebra and hover apply to body rows only. `undefined`
63
+ * outside the kit's section parts: a raw `<thead>` / `<tbody>`, which
64
+ * {@link useSection} then reads off the DOM. */
65
+ const SectionContext = createContext<TableSection | undefined>(undefined);
66
+
67
+ const SECTION_OF_TAG: Record<string, TableSection> = { THEAD: "head", TBODY: "body", TFOOT: "foot" };
68
+
69
+ /**
70
+ * The section a row or cell sits in: the kit part's context where there is one, else the
71
+ * nearest `<thead>` / `<tbody>` / `<tfoot>` in the DOM.
72
+ *
73
+ * keksdose's VAT summary writes a raw `<thead>` round kit cells in places, and a
74
+ * `TableHeaderCell` there took itself for a BODY cell — `scope="row"`, body type — with
75
+ * nothing on the page to say why. React cannot see an ancestor's tag, so the fallback
76
+ * reads it after mount; a layout effect, so the corrected cell is what gets painted.
77
+ */
78
+ function useSection<E extends HTMLElement>(): [TableSection, ((el: E | null) => void) | undefined] {
79
+ const fromContext = useContext(SectionContext);
80
+ const [node, setNode] = useState<E | null>(null);
81
+ const [detected, setDetected] = useState<TableSection | undefined>(undefined);
82
+ useLayoutEffect(() => {
83
+ if (fromContext !== undefined || !node) return;
84
+ const section = node.closest("thead, tbody, tfoot");
85
+ const found = section ? SECTION_OF_TAG[section.tagName] : undefined;
86
+ // eslint-disable-next-line react-hooks/set-state-in-effect -- read off the DOM, which only exists after mount
87
+ if (found !== detected) setDetected(found);
88
+ }, [fromContext, node, detected]);
89
+ // No ref at all under a kit section: holding the node would cost every row and cell
90
+ // a second render on mount for an answer the context already gave.
91
+ return [fromContext ?? detected ?? "body", fromContext === undefined ? setNode : undefined];
92
+ }
93
+
94
+ /** A {@link TableRow}'s `valign`, which its cells take unless they set their own. It
95
+ * cannot be left to CSS inheritance: every cell states its own default alignment,
96
+ * and a class on the cell beats one on the row. */
97
+ const RowVAlignContext = createContext<TableVAlign | undefined>(undefined);
98
+
99
+ const VALIGN: Record<TableVAlign, string> = {
100
+ top: "align-top",
101
+ middle: "align-middle",
102
+ bottom: "align-bottom",
103
+ };
104
+
105
+ const HEADER_SIZE: Record<TableHeaderCellSize, string> = {
106
+ xs: "text-xs",
107
+ sm: "text-sm",
108
+ };
109
+
110
+ const HEADER_WEIGHT: Record<TableHeaderCellWeight, string> = {
111
+ normal: "font-normal",
112
+ medium: "font-medium",
113
+ semibold: "font-semibold",
114
+ };
35
115
 
36
116
  const CELL_PAD: Record<TableDensity, string> = {
37
117
  comfortable: "px-3 py-2",
@@ -65,6 +145,22 @@ export interface TableProps extends ComponentPropsWithoutRef<"table"> {
65
145
  wrapperClassName?: string;
66
146
  /** See {@link TableLayout}. Unset leaves the browser's `auto` and adds no class. */
67
147
  layout?: TableLayout;
148
+ /**
149
+ * A rounded border round the whole table (0.12.0) — for a table standing on the page
150
+ * or in a dialog rather than inside a card. kastlan wraps six of them in
151
+ * `<div className="rounded-md border">` by hand (payment allocation, the new-budget
152
+ * lines, the meter readings, the deposit transactions, the unit values editor, the
153
+ * maintenance detail), and a hand-made frame outside the scroll wrapper does not clip
154
+ * the rows' tint to its corners.
155
+ */
156
+ framed?: boolean;
157
+ /**
158
+ * The rule between body rows. Default true. `false` for a small table whose rows are
159
+ * spaced by their own content — keksdose's VAT summary, a block of three quiet
160
+ * columns inside a card — which otherwise had to drop to raw `<tr>`s to lose it. One
161
+ * row can still say otherwise with {@link TableRowProps.bordered}.
162
+ */
163
+ rowDividers?: boolean;
68
164
  }
69
165
 
70
166
  /**
@@ -90,6 +186,8 @@ export function Table({
90
186
  hover = false,
91
187
  wrapperClassName,
92
188
  layout,
189
+ framed = false,
190
+ rowDividers = true,
93
191
  className,
94
192
  "aria-label": ariaLabel,
95
193
  ...rest
@@ -104,13 +202,14 @@ export function Table({
104
202
  density,
105
203
  zebra,
106
204
  hover,
205
+ rowDividers,
107
206
  captionId,
108
207
  registerCaption: () => {
109
208
  setCaptions((n) => n + 1);
110
209
  return () => setCaptions((n) => n - 1);
111
210
  },
112
211
  }),
113
- [density, zebra, hover, captionId],
212
+ [density, zebra, hover, rowDividers, captionId],
114
213
  );
115
214
 
116
215
  const regionName = captions > 0 ? { "aria-labelledby": captionId } : ariaLabel ? { "aria-label": ariaLabel } : null;
@@ -125,9 +224,14 @@ export function Table({
125
224
  {...(overflowing ? regionName : null)}
126
225
  tabIndex={overflowing ? 0 : undefined}
127
226
  data-overflowing={overflowing || undefined}
227
+ data-clips=""
228
+ data-framed={framed || undefined}
128
229
  className={cn(
129
230
  // `relative`: the containing block for an `sr-only` caption.
130
231
  "relative w-full overflow-x-auto",
232
+ // On the scroll wrapper itself, so its clipping keeps the rows inside the
233
+ // rounded corners.
234
+ framed && "rounded-md border border-[var(--border)]",
131
235
  THIN_SCROLLBAR_CLASS,
132
236
  "focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[var(--brand)]",
133
237
  wrapperClassName,
@@ -149,12 +253,16 @@ export function Table({
149
253
  );
150
254
  }
151
255
 
152
- export type TableHeadProps = ComponentPropsWithoutRef<"thead">;
256
+ export interface TableHeadProps extends ComponentPropsWithoutRef<"thead"> {
257
+ /** The rule under the head. Default true; `false` for a quiet header over a small
258
+ * table (keksdose's VAT summary wrote `className="border-b-0"` to lose it). */
259
+ bordered?: boolean;
260
+ }
153
261
 
154
- export function TableHead({ className, ...rest }: TableHeadProps) {
262
+ export function TableHead({ bordered = true, className, ...rest }: TableHeadProps) {
155
263
  return (
156
264
  <SectionContext.Provider value="head">
157
- <thead {...rest} className={cn("border-b border-[var(--border)]", className)} />
265
+ <thead {...rest} className={cn(bordered && "border-b border-[var(--border)]", className)} />
158
266
  </SectionContext.Provider>
159
267
  );
160
268
  }
@@ -252,23 +360,49 @@ export function TableFoot({ className, ...rest }: TableFootProps) {
252
360
  );
253
361
  }
254
362
 
255
- export type TableRowProps = ComponentPropsWithoutRef<"tr">;
363
+ const ROW_VARIANT: Record<TableRowVariant, string | false> = {
364
+ row: false,
365
+ group: "bg-[var(--bg-surface-2)] font-semibold",
366
+ subtotal: "font-medium",
367
+ // A 2px rule over the 1px divider the row above draws on its bottom: collapsed
368
+ // borders meet on one line, the wider wins, and the heavier rule is what says "sum".
369
+ total: "border-t-2 border-t-[var(--border-strong)] font-semibold",
370
+ };
256
371
 
257
- export function TableRow({ className, ...rest }: TableRowProps) {
258
- const { zebra, hover } = useContext(TableContext);
259
- const section = useContext(SectionContext);
372
+ /** The deprecated HTML `valign` attribute is replaced by a class-backed one. */
373
+ export interface TableRowProps extends ComponentPropsWithoutRef<"tr"> {
374
+ /** Vertical alignment for every cell of the row that does not set its own. See
375
+ * {@link TableVAlign}. Left out, each cell keeps its default. */
376
+ valign?: TableVAlign;
377
+ /** See {@link TableRowVariant}. Default `row`. Zebra and hover tint ordinary rows
378
+ * only: a group heading or a total is not one more record. */
379
+ variant?: TableRowVariant;
380
+ /** The rule under this body row. Default: the table's `rowDividers`. */
381
+ bordered?: boolean;
382
+ }
383
+
384
+ export function TableRow({ valign, variant = "row", bordered, className, ...rest }: TableRowProps) {
385
+ const { zebra, hover, rowDividers } = useContext(TableContext);
386
+ const [section, ref] = useSection<HTMLTableRowElement>();
260
387
  const body = section === "body";
261
- return (
388
+ const record = body && variant === "row";
389
+ const row = (
262
390
  <tr
391
+ ref={ref}
263
392
  {...rest}
393
+ data-variant={variant === "row" ? undefined : variant}
264
394
  className={cn(
265
- body && "border-b border-[var(--border)]",
266
- body && zebra && "even:bg-[var(--bg-surface-2)]",
267
- body && hover && "transition-colors hover:bg-[var(--bg-hover)]",
395
+ body && (bordered ?? rowDividers) && "border-b border-[var(--border)]",
396
+ record && zebra && "even:bg-[var(--bg-surface-2)]",
397
+ record && hover && "transition-colors hover:bg-[var(--bg-hover)]",
398
+ ROW_VARIANT[variant],
399
+ // On the row as well, for a raw `<td>` of the caller's, which inherits it.
400
+ valign && VALIGN[valign],
268
401
  className,
269
402
  )}
270
403
  />
271
404
  );
405
+ return valign ? <RowVAlignContext.Provider value={valign}>{row}</RowVAlignContext.Provider> : row;
272
406
  }
273
407
 
274
408
  interface CellAlignProps {
@@ -276,45 +410,74 @@ interface CellAlignProps {
276
410
  numeric?: boolean;
277
411
  /** Logical alignment. Default `start` (`end` when `numeric`). */
278
412
  align?: TableAlign;
413
+ /** Vertical alignment. Default: the row's `valign`, else `bottom` for a head cell and
414
+ * `top` for a body cell. See {@link TableVAlign}. */
415
+ valign?: TableVAlign;
279
416
  }
280
417
 
281
- /** The deprecated HTML `align` attribute is replaced by a logical one. */
282
- export interface TableHeaderCellProps extends Omit<ComponentPropsWithoutRef<"th">, "align">, CellAlignProps {}
418
+ /** The deprecated HTML `align` and `valign` attributes are replaced by class-backed ones. */
419
+ export interface TableHeaderCellProps extends Omit<ComponentPropsWithoutRef<"th">, "align" | "valign">, CellAlignProps {
420
+ /**
421
+ * Type size. Left out: `xs` in the head, the table's own size in the body — as
422
+ * before. keksdose's VAT summary heads its columns at the body size and normal weight
423
+ * (vat-summary.tsx:158–168, `font-normal` on each `<th>`), a quiet header over a
424
+ * small table of figures, and could not say so without overriding classes.
425
+ */
426
+ size?: TableHeaderCellSize;
427
+ /** Default `medium`. `normal` for the quiet header above; see {@link size}. */
428
+ weight?: TableHeaderCellWeight;
429
+ }
283
430
 
284
431
  /**
285
432
  * A `<th>`. `scope` defaults to `col` in the head and `row` in the body — a header
286
433
  * cell in a body row is the row's label ("Net rent" in a key-value table), and a
287
434
  * reader needs the scope to read it with each cell beside it.
288
435
  */
289
- export function TableHeaderCell({ numeric = false, align, scope, className, ...rest }: TableHeaderCellProps) {
436
+ export function TableHeaderCell({
437
+ numeric = false,
438
+ align,
439
+ valign,
440
+ size,
441
+ weight = "medium",
442
+ scope,
443
+ className,
444
+ ...rest
445
+ }: TableHeaderCellProps) {
290
446
  const { density } = useContext(TableContext);
291
- const section = useContext(SectionContext);
447
+ const [section, ref] = useSection<HTMLTableCellElement>();
448
+ const rowVAlign = useContext(RowVAlignContext);
449
+ const head = section === "head";
450
+ const textSize = size ?? (head ? "xs" : undefined);
292
451
  return (
293
452
  <th
453
+ ref={ref}
294
454
  {...rest}
295
455
  scope={scope ?? (section === "head" ? "col" : "row")}
296
456
  className={cn(
297
457
  CELL_PAD[density],
298
458
  ALIGN[align ?? (numeric ? "end" : "start")],
299
459
  numeric && "tabular-nums",
300
- "align-bottom font-medium",
301
- section === "head" ? "text-xs text-[var(--text-muted)]" : "align-top text-[var(--text-secondary)]",
460
+ VALIGN[valign ?? rowVAlign ?? (head ? "bottom" : "top")],
461
+ HEADER_WEIGHT[weight],
462
+ head ? "text-[var(--text-muted)]" : "text-[var(--text-secondary)]",
463
+ textSize && HEADER_SIZE[textSize],
302
464
  className,
303
465
  )}
304
466
  />
305
467
  );
306
468
  }
307
469
 
308
- export interface TableCellProps extends Omit<ComponentPropsWithoutRef<"td">, "align">, CellAlignProps {}
470
+ export interface TableCellProps extends Omit<ComponentPropsWithoutRef<"td">, "align" | "valign">, CellAlignProps {}
309
471
 
310
- export function TableCell({ numeric = false, align, className, ...rest }: TableCellProps) {
472
+ export function TableCell({ numeric = false, align, valign, className, ...rest }: TableCellProps) {
311
473
  const { density } = useContext(TableContext);
474
+ const rowVAlign = useContext(RowVAlignContext);
312
475
  return (
313
476
  <td
314
477
  {...rest}
315
478
  className={cn(
316
479
  CELL_PAD[density],
317
- "align-top",
480
+ VALIGN[valign ?? rowVAlign ?? "top"],
318
481
  ALIGN[align ?? (numeric ? "end" : "start")],
319
482
  numeric && "tabular-nums whitespace-nowrap",
320
483
  className,
@@ -0,0 +1,248 @@
1
+ import type { ComponentPropsWithoutRef, KeyboardEvent, MouseEvent, ReactElement, ReactNode, Ref } from "react";
2
+ import { ExternalLink } from "lucide-react";
3
+ import type { LucideIcon } from "lucide-react";
4
+ import { cn } from "../lib/cn";
5
+ import { DEFAULT_COMMON_LABELS, useKitLabels, useKitLink } from "../i18n/kit-labels";
6
+ import type { KitLinkComponent } from "../i18n/kit-labels";
7
+
8
+ /* ── Choosing what draws a link ──────────────────────────────────────────── */
9
+
10
+ /**
11
+ * An `href` that leaves the app: a scheme (`https:`, `mailto:`, `tel:`) or a
12
+ * protocol-relative `//host`. A router's link is for paths inside the app, so the
13
+ * provider's `linkComponent` is never handed one of these — they stay a plain `<a>`.
14
+ *
15
+ * NOT external, and so handed to the router link: a path (`/x`, `x`, `?q=1`) and the
16
+ * hash-router form `#/x` — that IS a route in a `HashRouter` app, which maps it to
17
+ * `to="/x"`. A plain in-page anchor (`#section`) is not external either, but it is not
18
+ * a route: see {@link isInPageAnchor}.
19
+ */
20
+ export function isExternalHref(href: string): boolean {
21
+ return /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(href);
22
+ }
23
+
24
+ /**
25
+ * An `href` that only scrolls the current page: `#section` (or a bare `#`). The
26
+ * hash-router form `#/x` is a ROUTE, not an anchor, and is not one of these. A router
27
+ * link must not get an anchor — under a `HashRouter` `#section` would navigate to the
28
+ * path `/section` — so every kit link draws it as a plain `<a>`.
29
+ */
30
+ export function isInPageAnchor(href: string): boolean {
31
+ return href.startsWith("#") && !href.startsWith("#/");
32
+ }
33
+
34
+ /**
35
+ * Whether the provider's `linkComponent` (or a kit component's router fallback) draws
36
+ * `href`: an in-app path, `#/x` included — not an external `href`
37
+ * ({@link isExternalHref}) and not an in-page `#anchor` ({@link isInPageAnchor}).
38
+ */
39
+ export function isRoutableHref(href: string): boolean {
40
+ return !isExternalHref(href) && !isInPageAnchor(href);
41
+ }
42
+
43
+ /**
44
+ * What draws a kit link to `href`: the component's own `renderLink` when it was given
45
+ * one (it always wins — the caller asked for it by name), else the
46
+ * `<UiKitProvider linkComponent>` for a routable `href` ({@link isRoutableHref}), else
47
+ * `undefined` — a plain `<a>`. The one rule every kit link follows: `https:` /
48
+ * `mailto:` / `//host` and `#section` stay plain anchors; `/x`, `x` and a hash-router
49
+ * `#/x` go to the router link. Every kit link props type is a subset of the anchor
50
+ * attributes `KitLinkProps` describes, which is what makes the cast sound.
51
+ * @internal Shared by the kit's link-bearing components; not part of the barrel.
52
+ */
53
+ export function pickLinkRenderer<P>(
54
+ own: ((props: P) => ReactElement) | undefined,
55
+ kitLink: KitLinkComponent | undefined,
56
+ href: string | undefined,
57
+ ): ((props: P) => ReactElement) | undefined {
58
+ if (own) return own;
59
+ if (!kitLink || href === undefined || !isRoutableHref(href)) return undefined;
60
+ return kitLink as unknown as (props: P) => ReactElement;
61
+ }
62
+
63
+ /**
64
+ * Draws `props` through `render` as a component of its own, so a router link's hooks
65
+ * belong to it and not to the kit component calling it — for the kit components that
66
+ * call their own `renderLink` inline and reach the provider's link through here.
67
+ * @internal
68
+ */
69
+ export function RenderedKitLink<P>({ render, props }: { render: (props: P) => ReactElement; props: P }) {
70
+ return render(props);
71
+ }
72
+
73
+ /* ── TextLink ─────────────────────────────────────────────────────────────── */
74
+
75
+ /** `brand` (default) for a link in running text, `muted` for footer and meta links
76
+ * that should not compete with the content, `danger` for a destructive one ("Leave
77
+ * group"), `inherit` for a link that takes its surroundings' colour. */
78
+ export type TextLinkTone = "brand" | "muted" | "danger" | "inherit";
79
+
80
+ /** `hover` (default): underlined under the pointer — the idiom all three apps draw.
81
+ * `always` for a link inside prose, where colour alone must not be the only mark
82
+ * (WCAG 1.4.1). `none` where the context already says "link" (a footer nav). */
83
+ export type TextLinkUnderline = "hover" | "always" | "none";
84
+
85
+ /** The `aria-current` token. `true` on {@link TextLinkProps.current} means `page`. */
86
+ export type TextLinkCurrent = "page" | "step" | "location" | "date" | "time" | "true";
87
+
88
+ /** What {@link TextLinkProps.renderLink} (and the provider's `linkComponent`) is
89
+ * handed. Spread it onto your router's link — `({ href, ...p }) => <Link to={href} {...p} />`. */
90
+ export interface TextLinkRenderProps {
91
+ href: string;
92
+ /** The link's look — keep it, or the tone, underline and focus ring go. */
93
+ className: string;
94
+ children: ReactNode;
95
+ ref?: Ref<HTMLAnchorElement>;
96
+ onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
97
+ onKeyDown?: (event: KeyboardEvent<HTMLAnchorElement>) => void;
98
+ "aria-current"?: TextLinkCurrent;
99
+ id?: string;
100
+ title?: string;
101
+ [key: `aria-${string}`]: string | boolean | number | undefined;
102
+ [key: `data-${string}`]: unknown;
103
+ }
104
+
105
+ export interface TextLinkProps
106
+ extends Omit<ComponentPropsWithoutRef<"a">, "href" | "children" | "aria-current" | "onClick" | "onKeyDown"> {
107
+ href: string;
108
+ children: ReactNode;
109
+ /** Default `brand`. */
110
+ tone?: TextLinkTone;
111
+ /** Default `hover`. */
112
+ underline?: TextLinkUnderline;
113
+ /**
114
+ * The link leaves the app: a plain `<a target="_blank" rel="noopener noreferrer">`
115
+ * (never the router's), an external-link mark after the text (mirrored in RTL), and
116
+ * "opens in a new tab" (`common.opensInNewTab`) read after it — the mark says it
117
+ * only to the eye. kastlan's BWO / BFS source links (rent-increase-calculator-page).
118
+ */
119
+ external?: boolean;
120
+ /**
121
+ * Keep the click (and the Enter that activates the link) from reaching an ancestor —
122
+ * a link in a DataTable row with `onRowClick` goes to its own target instead of
123
+ * running the row's action. kastlan's `CellLink` did this by hand.
124
+ */
125
+ stopPropagation?: boolean;
126
+ /** Mark the link as the current one of a set — `true` is `aria-current="page"`, the
127
+ * legal footer's link to the page you are on. Drawn in semibold. */
128
+ current?: boolean | TextLinkCurrent;
129
+ /** A Lucide icon before the text, at the text's size — kastlan's "← Back" link. */
130
+ icon?: LucideIcon;
131
+ /** Your router's link for an in-app `href`. Default: the `<UiKitProvider
132
+ * linkComponent>`, then a plain `<a>`. Ignored on an `external` link. */
133
+ renderLink?: (props: TextLinkRenderProps) => ReactElement;
134
+ onClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
135
+ onKeyDown?: (event: KeyboardEvent<HTMLAnchorElement>) => void;
136
+ ref?: Ref<HTMLAnchorElement>;
137
+ }
138
+
139
+ const TONE: Record<TextLinkTone, string> = {
140
+ brand: "text-[var(--brand)] hover:text-[var(--brand-hover)]",
141
+ muted: "text-[var(--text-muted)] hover:text-[var(--text-primary)]",
142
+ danger: "text-[var(--danger)] hover:text-[var(--danger-hover)]",
143
+ inherit: "",
144
+ };
145
+
146
+ const UNDERLINE: Record<TextLinkUnderline, string> = {
147
+ hover: "no-underline hover:underline",
148
+ always: "underline",
149
+ none: "no-underline",
150
+ };
151
+
152
+ /**
153
+ * The one inline link: a real `<a href>` (middle click, "open in new tab", link
154
+ * semantics) through the app's router for an in-app path, in one of four tones.
155
+ *
156
+ * kastlan draws this link a dozen ways by hand — `EntityLink` and `CellLink` (text-primary,
157
+ * hover underline, one stopping the row click), the legal pages' back link and footer,
158
+ * the feedback page's URL links, the calculator's external sources with a hand-placed
159
+ * ↗ and no word for a screen reader — and the lease wizard's unit step (lease-unit-step)
160
+ * used a raw `<a>` to an in-app route, which reloads the whole app. Here the router
161
+ * link comes from `<UiKitProvider linkComponent>`, set once.
162
+ *
163
+ * Inline, not `inline-flex`: a link inside a sentence must wrap with it. The icons sit
164
+ * on the text's baseline at the text's size, so they scale with whatever type the link
165
+ * is set in. The focus ring is an outline, which follows every line of a wrapped link.
166
+ */
167
+ export function TextLink({
168
+ href,
169
+ children,
170
+ tone = "brand",
171
+ underline = "hover",
172
+ external = false,
173
+ stopPropagation = false,
174
+ current,
175
+ icon: Icon,
176
+ renderLink,
177
+ className,
178
+ onClick,
179
+ onKeyDown,
180
+ ref,
181
+ ...rest
182
+ }: TextLinkProps) {
183
+ const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
184
+ const kitLink = useKitLink();
185
+ const ariaCurrent: TextLinkCurrent | undefined =
186
+ current === true ? "page" : current === false ? undefined : current;
187
+
188
+ const look = cn(
189
+ "rounded-sm underline-offset-2 transition-colors",
190
+ "focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--brand)]",
191
+ TONE[tone],
192
+ UNDERLINE[underline],
193
+ ariaCurrent && "font-semibold",
194
+ className,
195
+ );
196
+
197
+ const body = (
198
+ <>
199
+ {Icon && <Icon aria-hidden className="me-1 inline size-[1em] shrink-0 align-[-0.125em]" />}
200
+ {children}
201
+ {external && (
202
+ <>
203
+ <ExternalLink aria-hidden className="ms-1 inline size-[0.85em] align-[-0.1em] rtl:-scale-x-100" />
204
+ {" "}
205
+ <span className="sr-only">({common.opensInNewTab})</span>
206
+ </>
207
+ )}
208
+ </>
209
+ );
210
+
211
+ const props: TextLinkRenderProps = {
212
+ ...(rest as Partial<TextLinkRenderProps>),
213
+ ref,
214
+ href,
215
+ className: look,
216
+ "aria-current": ariaCurrent,
217
+ onClick: stopPropagation
218
+ ? (event) => {
219
+ event.stopPropagation();
220
+ onClick?.(event);
221
+ }
222
+ : onClick,
223
+ onKeyDown: stopPropagation
224
+ ? (event) => {
225
+ if (event.key === "Enter") event.stopPropagation();
226
+ onKeyDown?.(event);
227
+ }
228
+ : onKeyDown,
229
+ children: body,
230
+ };
231
+
232
+ const render = external ? undefined : pickLinkRenderer(renderLink, kitLink, href);
233
+ if (render) return <RenderedTextLink render={render} {...props} />;
234
+ const { children: content, ...anchor } = props;
235
+ return (
236
+ <a {...anchor} {...(external ? { target: "_blank", rel: "noopener noreferrer" } : null)}>
237
+ {content}
238
+ </a>
239
+ );
240
+ }
241
+
242
+ /** Calls the link renderer as a component, so a router link's hooks are its own. */
243
+ function RenderedTextLink({
244
+ render,
245
+ ...props
246
+ }: TextLinkRenderProps & { render: (props: TextLinkRenderProps) => ReactElement }) {
247
+ return render(props);
248
+ }
@@ -9,8 +9,15 @@ import { cn } from "../lib/cn";
9
9
  * (control-page.tsx:119/123/127) draw. Two sizes because the label sits either inside a
10
10
  * dense panel (a legend, a menu) or above a block of page content, and one size looked
11
11
  * wrong in the other place.
12
+ *
13
+ * `md` (0.11.0) is 11px, the rung between them: keksdose's budget-summary card labels
14
+ * its figures ("Assigned", "Activity", "Upcoming", "Available" —
15
+ * budget-summary-card:258/264/286/297) and its support thread its day divider and a message's meta line
16
+ * (support-thread:153/223) at `text-[11px]` by hand — 10px read as a footnote under a
17
+ * figure that size, 12px competed with it. Named `md` rather than slotted in order
18
+ * because `sm` was already the default and renaming it would move every caller.
12
19
  */
13
- export type SectionLabelSize = "xs" | "sm";
20
+ export type SectionLabelSize = "xs" | "md" | "sm";
14
21
 
15
22
  /**
16
23
  * The small, uppercase, tracked, muted label — as a class string, per size.
@@ -23,6 +30,7 @@ export type SectionLabelSize = "xs" | "sm";
23
30
  */
24
31
  export const SECTION_LABEL_CLASS: Record<SectionLabelSize, string> = {
25
32
  xs: "text-[10px] font-semibold uppercase tracking-wide text-[var(--text-muted)]",
33
+ md: "text-[11px] font-semibold uppercase tracking-wide text-[var(--text-muted)]",
26
34
  sm: "text-xs font-semibold uppercase tracking-wide text-[var(--text-muted)]",
27
35
  };
28
36