@eifi1/ui-kit 0.11.0 → 0.13.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 (397) hide show
  1. package/README.md +59 -34
  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/amount-input.d.ts +40 -4
  12. package/dist/components/amount-input.js +50 -12
  13. package/dist/components/amount-input.js.map +1 -1
  14. package/dist/components/authed-image.d.ts +68 -0
  15. package/dist/components/authed-image.js +117 -0
  16. package/dist/components/authed-image.js.map +1 -0
  17. package/dist/components/breadcrumbs.d.ts +2 -1
  18. package/dist/components/breadcrumbs.js +5 -2
  19. package/dist/components/breadcrumbs.js.map +1 -1
  20. package/dist/components/bulk-action-bar.d.ts +10 -2
  21. package/dist/components/bulk-action-bar.js.map +1 -1
  22. package/dist/components/button-group.d.ts +51 -2
  23. package/dist/components/button-group.js +5 -1
  24. package/dist/components/button-group.js.map +1 -1
  25. package/dist/components/calculator.d.ts +15 -4
  26. package/dist/components/calendar-heatmap.d.ts +41 -3
  27. package/dist/components/calendar-heatmap.js +48 -7
  28. package/dist/components/calendar-heatmap.js.map +1 -1
  29. package/dist/components/chart.js +14 -8
  30. package/dist/components/chart.js.map +1 -1
  31. package/dist/components/chip.d.ts +2 -1
  32. package/dist/components/chip.js +26 -18
  33. package/dist/components/chip.js.map +1 -1
  34. package/dist/components/choice-card.d.ts +35 -3
  35. package/dist/components/choice-card.js +83 -25
  36. package/dist/components/choice-card.js.map +1 -1
  37. package/dist/components/combobox-core.d.ts +3 -1
  38. package/dist/components/combobox-core.js +18 -5
  39. package/dist/components/combobox-core.js.map +1 -1
  40. package/dist/components/combobox.d.ts +2 -2
  41. package/dist/components/combobox.js +31 -9
  42. package/dist/components/combobox.js.map +1 -1
  43. package/dist/components/copy-button.d.ts +51 -70
  44. package/dist/components/copy-button.js +1 -1
  45. package/dist/components/copy-button.js.map +1 -1
  46. package/dist/components/data-table-cells.d.ts +63 -0
  47. package/dist/components/data-table-cells.js +64 -0
  48. package/dist/components/data-table-cells.js.map +1 -0
  49. package/dist/components/data-table-filter-popover.d.ts +1 -1
  50. package/dist/components/data-table-filter-popover.js +8 -1
  51. package/dist/components/data-table-filter-popover.js.map +1 -1
  52. package/dist/components/data-table-filters.d.ts +1 -1
  53. package/dist/components/data-table-filters.js +31 -1
  54. package/dist/components/data-table-filters.js.map +1 -1
  55. package/dist/components/data-table-labels.d.ts +13 -0
  56. package/dist/components/data-table-labels.js +8 -1
  57. package/dist/components/data-table-labels.js.map +1 -1
  58. package/dist/components/data-table.d.ts +1 -1
  59. package/dist/components/data-table.js +125 -27
  60. package/dist/components/data-table.js.map +1 -1
  61. package/dist/components/date-picker.d.ts +3 -1
  62. package/dist/components/date-picker.js +15 -2
  63. package/dist/components/date-picker.js.map +1 -1
  64. package/dist/components/description-list.d.ts +23 -3
  65. package/dist/components/description-list.js +23 -3
  66. package/dist/components/description-list.js.map +1 -1
  67. package/dist/components/dialog-frame.d.ts +1 -0
  68. package/dist/components/entity-combobox.d.ts +1 -1
  69. package/dist/components/entity-combobox.js +40 -20
  70. package/dist/components/entity-combobox.js.map +1 -1
  71. package/dist/components/error-boundary.d.ts +281 -0
  72. package/dist/components/error-boundary.js +302 -0
  73. package/dist/components/error-boundary.js.map +1 -0
  74. package/dist/components/field-sync.js +90 -83
  75. package/dist/components/field-sync.js.map +1 -1
  76. package/dist/components/field.d.ts +30 -4
  77. package/dist/components/field.js +40 -17
  78. package/dist/components/field.js.map +1 -1
  79. package/dist/components/file-button.d.ts +50 -208
  80. package/dist/components/file-dropzone.d.ts +49 -2
  81. package/dist/components/floating-panel.d.ts +13 -4
  82. package/dist/components/floating-panel.js +7 -3
  83. package/dist/components/floating-panel.js.map +1 -1
  84. package/dist/components/form-actions.d.ts +53 -0
  85. package/dist/components/form-actions.js +98 -0
  86. package/dist/components/form-actions.js.map +1 -0
  87. package/dist/components/full-bleed-dialog.js +4 -0
  88. package/dist/components/full-bleed-dialog.js.map +1 -1
  89. package/dist/components/hover-menu.js +64 -56
  90. package/dist/components/hover-menu.js.map +1 -1
  91. package/dist/components/image-grid.d.ts +67 -0
  92. package/dist/components/image-grid.js +112 -0
  93. package/dist/components/image-grid.js.map +1 -0
  94. package/dist/components/lightbox.d.ts +104 -0
  95. package/dist/components/lightbox.js +229 -0
  96. package/dist/components/lightbox.js.map +1 -0
  97. package/dist/components/line-items.d.ts +114 -0
  98. package/dist/components/line-items.js +238 -0
  99. package/dist/components/line-items.js.map +1 -0
  100. package/dist/components/list.d.ts +37 -4
  101. package/dist/components/list.js +35 -14
  102. package/dist/components/list.js.map +1 -1
  103. package/dist/components/loading-state.d.ts +24 -0
  104. package/dist/components/loading-state.js +32 -0
  105. package/dist/components/loading-state.js.map +1 -0
  106. package/dist/components/menu-item.d.ts +2 -1
  107. package/dist/components/menu-item.js +5 -1
  108. package/dist/components/menu-item.js.map +1 -1
  109. package/dist/components/mini-calendar.d.ts +13 -1
  110. package/dist/components/mini-calendar.js +46 -2
  111. package/dist/components/mini-calendar.js.map +1 -1
  112. package/dist/components/modal.d.ts +20 -1
  113. package/dist/components/modal.js +26 -2
  114. package/dist/components/modal.js.map +1 -1
  115. package/dist/components/month-picker.d.ts +21 -1
  116. package/dist/components/month-picker.js +139 -26
  117. package/dist/components/month-picker.js.map +1 -1
  118. package/dist/components/multi-entity-combobox.d.ts +1 -1
  119. package/dist/components/multi-entity-combobox.js +42 -20
  120. package/dist/components/multi-entity-combobox.js.map +1 -1
  121. package/dist/components/nav-pills.d.ts +2 -1
  122. package/dist/components/nav-pills.js +5 -1
  123. package/dist/components/nav-pills.js.map +1 -1
  124. package/dist/components/number-field.d.ts +24 -5
  125. package/dist/components/number-field.js +5 -7
  126. package/dist/components/number-field.js.map +1 -1
  127. package/dist/components/number-input.d.ts +25 -5
  128. package/dist/components/number-input.js +7 -1
  129. package/dist/components/number-input.js.map +1 -1
  130. package/dist/components/numpad-sheet.d.ts +20 -5
  131. package/dist/components/numpad-sheet.js +3 -2
  132. package/dist/components/numpad-sheet.js.map +1 -1
  133. package/dist/components/page-header.d.ts +35 -6
  134. package/dist/components/page-header.js +7 -1
  135. package/dist/components/page-header.js.map +1 -1
  136. package/dist/components/passkeys-setting.d.ts +67 -0
  137. package/dist/components/passkeys-setting.js +214 -0
  138. package/dist/components/passkeys-setting.js.map +1 -0
  139. package/dist/components/picker-sheet.js +5 -1
  140. package/dist/components/picker-sheet.js.map +1 -1
  141. package/dist/components/pie-chart-labels.d.ts +20 -0
  142. package/dist/components/pie-chart-labels.js +12 -0
  143. package/dist/components/pie-chart-labels.js.map +1 -0
  144. package/dist/components/pie-chart.d.ts +101 -0
  145. package/dist/components/pie-chart.js +283 -0
  146. package/dist/components/pie-chart.js.map +1 -0
  147. package/dist/components/popover.js +3 -0
  148. package/dist/components/popover.js.map +1 -1
  149. package/dist/components/progress-bar.d.ts +44 -3
  150. package/dist/components/progress-bar.js +58 -22
  151. package/dist/components/progress-bar.js.map +1 -1
  152. package/dist/components/qr-code.d.ts +34 -0
  153. package/dist/components/qr-code.js +57 -0
  154. package/dist/components/qr-code.js.map +1 -0
  155. package/dist/components/search-field.d.ts +15 -3
  156. package/dist/components/search-field.js +5 -1
  157. package/dist/components/search-field.js.map +1 -1
  158. package/dist/components/settings-fields.d.ts +49 -1
  159. package/dist/components/signed-amount.d.ts +122 -0
  160. package/dist/components/signed-amount.js +152 -0
  161. package/dist/components/signed-amount.js.map +1 -0
  162. package/dist/components/skeleton.d.ts +10 -3
  163. package/dist/components/skeleton.js +12 -2
  164. package/dist/components/skeleton.js.map +1 -1
  165. package/dist/components/stat-tile.d.ts +2 -1
  166. package/dist/components/stat-tile.js +6 -3
  167. package/dist/components/stat-tile.js.map +1 -1
  168. package/dist/components/swipeable-row.js +4 -3
  169. package/dist/components/swipeable-row.js.map +1 -1
  170. package/dist/components/table.d.ts +39 -5
  171. package/dist/components/table.js +45 -11
  172. package/dist/components/table.js.map +1 -1
  173. package/dist/components/text-link.d.ts +238 -0
  174. package/dist/components/text-link.js +144 -0
  175. package/dist/components/text-link.js.map +1 -0
  176. package/dist/components/text.d.ts +26 -2
  177. package/dist/components/text.js +16 -2
  178. package/dist/components/text.js.map +1 -1
  179. package/dist/components/time-input.d.ts +49 -1
  180. package/dist/components/toggle-group.d.ts +35 -0
  181. package/dist/components/toggle-group.js +43 -14
  182. package/dist/components/toggle-group.js.map +1 -1
  183. package/dist/components/toggle-legend.d.ts +53 -3
  184. package/dist/components/toggle-legend.js +54 -1
  185. package/dist/components/toggle-legend.js.map +1 -1
  186. package/dist/components/tooltip.d.ts +51 -1
  187. package/dist/components/tooltip.js +59 -35
  188. package/dist/components/tooltip.js.map +1 -1
  189. package/dist/components/ui.d.ts +51 -779
  190. package/dist/components/ui.js +364 -34
  191. package/dist/components/ui.js.map +1 -1
  192. package/dist/components/use-table-state.d.ts +3 -52
  193. package/dist/components/use-table-state.js +128 -30
  194. package/dist/components/use-table-state.js.map +1 -1
  195. package/dist/{data-table-filters-Dh9uF_S-.d.ts → data-table-Drun8Fk9.d.ts} +305 -61
  196. package/dist/data-table.d.ts +2 -1
  197. package/dist/data-table.js +5 -1
  198. package/dist/data-table.js.map +1 -1
  199. package/dist/feedback/feedback-attachment.js +4 -6
  200. package/dist/feedback/feedback-attachment.js.map +1 -1
  201. package/dist/feedback/feedback-dialog.d.ts +26 -3
  202. package/dist/feedback/feedback-dialog.js +21 -5
  203. package/dist/feedback/feedback-dialog.js.map +1 -1
  204. package/dist/feedback/feedback-inbox.js +86 -75
  205. package/dist/feedback/feedback-inbox.js.map +1 -1
  206. package/dist/feedback/feedback-thread.d.ts +173 -0
  207. package/dist/feedback/feedback-thread.js +264 -0
  208. package/dist/feedback/feedback-thread.js.map +1 -0
  209. package/dist/feedback.d.ts +2 -1
  210. package/dist/feedback.js +1 -0
  211. package/dist/feedback.js.map +1 -1
  212. package/dist/hooks/use-authed-src.d.ts +71 -0
  213. package/dist/hooks/use-authed-src.js +63 -0
  214. package/dist/hooks/use-authed-src.js.map +1 -0
  215. package/dist/hooks/use-file-drop.d.ts +49 -2
  216. package/dist/hooks/use-hotkey.d.ts +59 -0
  217. package/dist/hooks/use-hotkey.js +105 -0
  218. package/dist/hooks/use-hotkey.js.map +1 -0
  219. package/dist/hooks/use-search-param-state.d.ts +83 -0
  220. package/dist/hooks/use-search-param-state.js +94 -0
  221. package/dist/hooks/use-search-param-state.js.map +1 -0
  222. package/dist/i18n/defaults.d.ts +15 -4
  223. package/dist/i18n/defaults.js +31 -1
  224. package/dist/i18n/defaults.js.map +1 -1
  225. package/dist/i18n/kit-labels.d.ts +46 -312
  226. package/dist/i18n/kit-labels.js +20 -5
  227. package/dist/i18n/kit-labels.js.map +1 -1
  228. package/dist/i18n/locales/de-CH-informal.d.ts +15 -4
  229. package/dist/i18n/locales/de-CH.d.ts +15 -4
  230. package/dist/i18n/locales/de-informal.d.ts +15 -4
  231. package/dist/i18n/locales/de.d.ts +15 -4
  232. package/dist/i18n/locales/de.js +164 -3
  233. package/dist/i18n/locales/de.js.map +1 -1
  234. package/dist/i18n/locales/es.d.ts +15 -4
  235. package/dist/i18n/locales/es.js +161 -3
  236. package/dist/i18n/locales/es.js.map +1 -1
  237. package/dist/i18n/locales/fr.d.ts +15 -4
  238. package/dist/i18n/locales/fr.js +161 -3
  239. package/dist/i18n/locales/fr.js.map +1 -1
  240. package/dist/i18n/locales/hu.d.ts +15 -4
  241. package/dist/i18n/locales/hu.js +161 -3
  242. package/dist/i18n/locales/hu.js.map +1 -1
  243. package/dist/i18n/locales/it.d.ts +15 -4
  244. package/dist/i18n/locales/it.js +161 -3
  245. package/dist/i18n/locales/it.js.map +1 -1
  246. package/dist/i18n/locales/zh.d.ts +15 -4
  247. package/dist/i18n/locales/zh.js +161 -3
  248. package/dist/i18n/locales/zh.js.map +1 -1
  249. package/dist/index.d.ts +39 -19
  250. package/dist/index.js +40 -1
  251. package/dist/index.js.map +1 -1
  252. package/dist/kit-labels-Mz8zZHIF.d.ts +1744 -0
  253. package/dist/lib/format.d.ts +162 -0
  254. package/dist/lib/format.js +151 -0
  255. package/dist/lib/format.js.map +1 -0
  256. package/dist/lib/qr-encode.d.ts +44 -0
  257. package/dist/lib/qr-encode.js +338 -0
  258. package/dist/lib/qr-encode.js.map +1 -0
  259. package/dist/rhf/fields.d.ts +283 -0
  260. package/dist/rhf/fields.js +631 -0
  261. package/dist/rhf/fields.js.map +1 -0
  262. package/dist/rhf/form.d.ts +49 -1
  263. package/dist/rhf/line-items.d.ts +31 -0
  264. package/dist/rhf/line-items.js +59 -0
  265. package/dist/rhf/line-items.js.map +1 -0
  266. package/dist/rhf.d.ts +61 -1
  267. package/dist/rhf.js +2 -0
  268. package/dist/rhf.js.map +1 -1
  269. package/dist/search/command-palette.js +20 -14
  270. package/dist/search/command-palette.js.map +1 -1
  271. package/dist/shell/app-shell.d.ts +99 -2
  272. package/dist/shell/app-shell.js +120 -57
  273. package/dist/shell/app-shell.js.map +1 -1
  274. package/dist/shell/auth-layout.d.ts +74 -0
  275. package/dist/shell/auth-layout.js +97 -0
  276. package/dist/shell/auth-layout.js.map +1 -0
  277. package/dist/shell/top-bar-brand.d.ts +81 -0
  278. package/dist/shell/top-bar-brand.js +47 -0
  279. package/dist/shell/top-bar-brand.js.map +1 -0
  280. package/dist/shell/topbar-action-menu.d.ts +21 -4
  281. package/dist/shell/topbar-action-menu.js +18 -2
  282. package/dist/shell/topbar-action-menu.js.map +1 -1
  283. package/dist/shell.d.ts +56 -1
  284. package/dist/shell.js +2 -0
  285. package/dist/shell.js.map +1 -1
  286. package/dist/tour/tour.js +10 -4
  287. package/dist/tour/tour.js.map +1 -1
  288. package/dist/wizard/stepper-nav.d.ts +48 -1
  289. package/dist/wizard/use-wizard.js +4 -2
  290. package/dist/wizard/use-wizard.js.map +1 -1
  291. package/dist/wizard/wizard-context.d.ts +24 -1
  292. package/dist/wizard/wizard-context.js +19 -2
  293. package/dist/wizard/wizard-context.js.map +1 -1
  294. package/dist/wizard/wizard-step.d.ts +30 -5
  295. package/dist/wizard/wizard-step.js +34 -3
  296. package/dist/wizard/wizard-step.js.map +1 -1
  297. package/dist/wizard.d.ts +50 -3
  298. package/package.json +6 -6
  299. package/src/chart.ts +4 -0
  300. package/src/components/account-settings-labels.ts +190 -0
  301. package/src/components/account-settings.tsx +110 -63
  302. package/src/components/amount-input.tsx +109 -10
  303. package/src/components/authed-image.tsx +190 -0
  304. package/src/components/breadcrumbs.tsx +8 -2
  305. package/src/components/bulk-action-bar.tsx +16 -3
  306. package/src/components/button-group.tsx +7 -2
  307. package/src/components/calendar-heatmap.tsx +105 -10
  308. package/src/components/chart.tsx +19 -8
  309. package/src/components/chip.tsx +14 -4
  310. package/src/components/choice-card.tsx +136 -25
  311. package/src/components/combobox-core.tsx +33 -9
  312. package/src/components/combobox.tsx +33 -9
  313. package/src/components/copy-button.tsx +3 -2
  314. package/src/components/data-table-cells.tsx +119 -0
  315. package/src/components/data-table-filter-popover.tsx +13 -5
  316. package/src/components/data-table-filters.ts +77 -0
  317. package/src/components/data-table-labels.ts +23 -0
  318. package/src/components/data-table.tsx +230 -13
  319. package/src/components/date-picker.tsx +19 -3
  320. package/src/components/description-list.tsx +52 -3
  321. package/src/components/entity-combobox.tsx +25 -7
  322. package/src/components/error-boundary.tsx +640 -0
  323. package/src/components/field-sync.tsx +48 -40
  324. package/src/components/field.tsx +56 -5
  325. package/src/components/floating-panel.tsx +20 -7
  326. package/src/components/form-actions.tsx +191 -0
  327. package/src/components/full-bleed-dialog.tsx +4 -0
  328. package/src/components/hover-menu.tsx +26 -17
  329. package/src/components/image-grid.tsx +181 -0
  330. package/src/components/lightbox.tsx +374 -0
  331. package/src/components/line-items.tsx +385 -0
  332. package/src/components/list.tsx +82 -19
  333. package/src/components/loading-state.tsx +47 -0
  334. package/src/components/menu-item.tsx +7 -2
  335. package/src/components/mini-calendar.tsx +72 -3
  336. package/src/components/modal.tsx +65 -3
  337. package/src/components/month-picker.tsx +181 -26
  338. package/src/components/multi-entity-combobox.tsx +27 -7
  339. package/src/components/nav-pills.tsx +8 -1
  340. package/src/components/number-field.tsx +15 -10
  341. package/src/components/number-input.tsx +17 -1
  342. package/src/components/numpad-sheet.tsx +6 -1
  343. package/src/components/page-header.tsx +42 -5
  344. package/src/components/passkeys-setting.tsx +331 -0
  345. package/src/components/picker-sheet.tsx +10 -1
  346. package/src/components/pie-chart-labels.ts +32 -0
  347. package/src/components/pie-chart.tsx +479 -0
  348. package/src/components/popover.tsx +3 -0
  349. package/src/components/progress-bar.tsx +129 -19
  350. package/src/components/qr-code.tsx +83 -0
  351. package/src/components/search-field.tsx +20 -4
  352. package/src/components/signed-amount.tsx +295 -0
  353. package/src/components/skeleton.tsx +21 -3
  354. package/src/components/stat-tile.tsx +11 -3
  355. package/src/components/swipeable-row.tsx +7 -7
  356. package/src/components/table.tsx +102 -13
  357. package/src/components/text-link.tsx +356 -0
  358. package/src/components/text.tsx +46 -2
  359. package/src/components/toggle-group.tsx +93 -20
  360. package/src/components/toggle-legend.tsx +108 -2
  361. package/src/components/tooltip.tsx +79 -8
  362. package/src/components/ui.tsx +707 -49
  363. package/src/components/use-table-state.ts +265 -33
  364. package/src/data-table.ts +10 -0
  365. package/src/feedback/feedback-attachment.tsx +10 -7
  366. package/src/feedback/feedback-dialog.tsx +49 -7
  367. package/src/feedback/feedback-inbox.tsx +7 -0
  368. package/src/feedback/feedback-thread.tsx +510 -0
  369. package/src/feedback.ts +1 -0
  370. package/src/hooks/use-authed-src.ts +161 -0
  371. package/src/hooks/use-hotkey.ts +172 -0
  372. package/src/hooks/use-search-param-state.ts +187 -0
  373. package/src/i18n/defaults.ts +30 -0
  374. package/src/i18n/kit-labels.tsx +118 -8
  375. package/src/i18n/locales/de.ts +161 -0
  376. package/src/i18n/locales/es.ts +158 -0
  377. package/src/i18n/locales/fr.ts +161 -0
  378. package/src/i18n/locales/hu.ts +159 -0
  379. package/src/i18n/locales/it.ts +159 -0
  380. package/src/i18n/locales/zh.ts +158 -0
  381. package/src/index.ts +59 -0
  382. package/src/lib/format.ts +348 -0
  383. package/src/lib/qr-encode.ts +420 -0
  384. package/src/rhf/fields.tsx +1135 -0
  385. package/src/rhf/line-items.tsx +130 -0
  386. package/src/rhf.ts +6 -0
  387. package/src/search/command-palette.tsx +29 -19
  388. package/src/shell/app-shell.tsx +153 -3
  389. package/src/shell/auth-layout.tsx +190 -0
  390. package/src/shell/top-bar-brand.tsx +76 -0
  391. package/src/shell/topbar-action-menu.tsx +32 -4
  392. package/src/shell.ts +2 -0
  393. package/src/tour/tour.tsx +16 -5
  394. package/src/wizard/use-wizard.ts +8 -4
  395. package/src/wizard/wizard-context.tsx +41 -1
  396. package/src/wizard/wizard-step.tsx +75 -6
  397. package/tokens.css +12 -0
@@ -69,6 +69,22 @@ export interface DataTableLabels {
69
69
  rowCount: (total: number) => string;
70
70
  /** The column-settings rail and its heading: "Columns (4/9)". */
71
71
  columnsCount: (visible: number, total: number) => string;
72
+
73
+ // ---- Empty state, row actions, boolean cells (0.12.0) ----
74
+
75
+ /** The body of a table with no rows and no `empty` prop of its own. A sentence, not
76
+ * "—": a dash in the only row of a table reads as one row with a missing value. */
77
+ empty: string;
78
+ /** The accessible name of the `rowActions` column (its header is visually empty). */
79
+ actions: string;
80
+ /** The `kind: "edit"` row action's name — its tooltip and its swipe label. */
81
+ edit: string;
82
+ /** The `kind: "delete"` row action's name. */
83
+ delete: string;
84
+ /** What {@link BooleanMark} says to a screen reader for `true` / `false` / nothing. */
85
+ booleanTrue: string;
86
+ booleanFalse: string;
87
+ booleanUnset: string;
72
88
  }
73
89
 
74
90
  export const DEFAULT_DATA_TABLE_LABELS: DataTableLabels = {
@@ -120,6 +136,13 @@ export const DEFAULT_DATA_TABLE_LABELS: DataTableLabels = {
120
136
  pageRange: (from, to, total) => `${from}–${to} / ${total}`,
121
137
  rowCount: (total) => `${total}`,
122
138
  columnsCount: (visible, total) => `Columns (${visible}/${total})`,
139
+ empty: "No entries",
140
+ actions: "Actions",
141
+ edit: "Edit",
142
+ delete: "Delete",
143
+ booleanTrue: "Yes",
144
+ booleanFalse: "No",
145
+ booleanUnset: "Not set",
123
146
  };
124
147
 
125
148
  /**
@@ -1,4 +1,4 @@
1
- import { Fragment, useEffect, useMemo, useRef, useState } from "react";
1
+ import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import type { CSSProperties, ReactNode, TdHTMLAttributes, ThHTMLAttributes } from "react";
3
3
  import { createPortal } from "react-dom";
4
4
  import {
@@ -8,9 +8,11 @@ import {
8
8
  ChevronDown,
9
9
  ChevronRight,
10
10
  Filter,
11
+ Pencil,
12
+ Trash2,
11
13
  X,
12
14
  } from "lucide-react";
13
- import { Card, Spinner } from "./ui";
15
+ import { Card, IconButton, Spinner } from "./ui";
14
16
  import { cn } from "../lib/cn";
15
17
  import {
16
18
  defaultFilterState,
@@ -239,8 +241,11 @@ export interface DataTableProps<T> {
239
241
  * shareable and bookmarkable. URL params used: `f.<column>`, `sort`, `p`, `ps`.
240
242
  * URL state takes precedence over localStorage on initial load. Requires the
241
243
  * consuming app to render inside a react-router Router.
244
+ *
245
+ * `{ prefix }` namespaces them, for two URL-synced tables on one page:
246
+ * `urlSync={{ prefix: "inv." }}` uses `inv.f.<column>`, `inv.sort`, `inv.p`, `inv.ps`.
242
247
  */
243
- urlSync?: boolean;
248
+ urlSync?: TableUrlSync;
244
249
  /**
245
250
  * When true (desktop only), the table fills its parent's height and scrolls
246
251
  * INTERNALLY, with the header pinned — so the page itself doesn't add a second,
@@ -431,6 +436,98 @@ export interface DataTableProps<T> {
431
436
  /** Extra classes for the table's root element (the card, or with `frame={false}`
432
437
  * the plain wrapper). Merged last, so a caller's margin or width wins. */
433
438
  className?: string;
439
+ /**
440
+ * Per-row actions: an actions column of icon buttons at the row's end on desktop,
441
+ * and the same actions as swipe actions on a phone card (where the column is not
442
+ * shown). kastlan's `ResourceListPage` appended this column by hand to every CRUD
443
+ * list — the edit pencil, the delete bin, a `useConfirm` before the delete, and a
444
+ * `stopPropagation` on each so a press did not also open the row.
445
+ *
446
+ * `kind: "edit"` / `"delete"` bring the icon, the label (`labels.edit` /
447
+ * `labels.delete`), the tone and the swipe side; any other action states its own
448
+ * `label` and `icon`. `confirm` is the hook-in for a confirmation: pass the kit's
449
+ * `useConfirm()` (or anything answering a boolean) and the action runs only on a yes.
450
+ *
451
+ * const confirm = useConfirm();
452
+ * rowActions={[
453
+ * { kind: "edit", onAction: openEditor },
454
+ * { kind: "delete", onAction: (r) => remove(r.id),
455
+ * confirm: (r) => confirm({ title: t("delete", { name: r.name }), tone: "danger" }) },
456
+ * ]}
457
+ *
458
+ * A function receives the row, for actions that differ per row. An explicit
459
+ * `mobileSwipeActions` wins over the swipes derived from here.
460
+ */
461
+ rowActions?: DataTableRowAction<T>[] | ((row: T) => DataTableRowAction<T>[]);
462
+ /**
463
+ * The bar above the table — a search field, filter chips, a create button. Inside
464
+ * the table's frame, above the header, on desktop and phone alike (resource-list
465
+ * pages set a search box in a `div` above the card, outside the frame it filters).
466
+ */
467
+ toolbar?: ReactNode;
468
+ }
469
+
470
+ /** One entry of {@link DataTableProps.rowActions}. */
471
+ export interface DataTableRowAction<T> {
472
+ /** A preset: `"edit"` (pencil, `labels.edit`, swipes toward the END) or `"delete"`
473
+ * (bin, `labels.delete`, danger tone, swipes toward the START). */
474
+ kind?: "edit" | "delete";
475
+ /** The action's name — its tooltip, accessible name and swipe label. Required
476
+ * without a `kind`. */
477
+ label?: string;
478
+ /** The glyph. Required without a `kind`. */
479
+ icon?: ReactNode;
480
+ /** Runs after `confirm` (when given) has said yes. */
481
+ onAction: (row: T) => void;
482
+ /** Asked before `onAction`; a `false` (or a promise of one) cancels. */
483
+ confirm?: (row: T) => boolean | Promise<boolean>;
484
+ /** Default: `"danger"` for `kind: "delete"`, else `"default"`. */
485
+ tone?: "default" | "danger";
486
+ /** Leave the action out for this row. */
487
+ hidden?: (row: T) => boolean;
488
+ /** Why the action is locked for this row — the button stays focusable and says so,
489
+ * and the row gets no swipe for it. See `IconButton`'s `disabledReason`. */
490
+ disabledReason?: (row: T) => ReactNode;
491
+ /** Which way the phone card is dragged to commit it, or `false` for no swipe.
492
+ * Default: `"start"` for a danger action, `"end"` otherwise. */
493
+ swipe?: "start" | "end" | false;
494
+ }
495
+
496
+ /** The actions column's key. Not a column a caller declares, so it is kept out of the
497
+ * column-settings panel. */
498
+ const ROW_ACTIONS_KEY = "__rowActions";
499
+
500
+ interface ResolvedRowAction<T> {
501
+ key: string;
502
+ label: string;
503
+ icon: ReactNode;
504
+ tone: "default" | "danger";
505
+ swipe: "start" | "end" | false;
506
+ action: DataTableRowAction<T>;
507
+ }
508
+
509
+ function resolveRowAction<T>(
510
+ action: DataTableRowAction<T>,
511
+ index: number,
512
+ labels: DataTableLabels,
513
+ ): ResolvedRowAction<T> {
514
+ const tone = action.tone ?? (action.kind === "delete" ? "danger" : "default");
515
+ return {
516
+ key: action.kind ?? `${index}:${action.label ?? ""}`,
517
+ label: action.label ?? (action.kind === "edit" ? labels.edit : action.kind === "delete" ? labels.delete : ""),
518
+ icon:
519
+ action.icon ??
520
+ (action.kind === "edit" ? <Pencil /> : action.kind === "delete" ? <Trash2 /> : null),
521
+ tone,
522
+ swipe: action.swipe ?? (tone === "danger" ? "start" : "end"),
523
+ action,
524
+ };
525
+ }
526
+
527
+ /** Ask, then act. */
528
+ async function runRowAction<T>(action: DataTableRowAction<T>, row: T): Promise<void> {
529
+ if (action.confirm && !(await action.confirm(row))) return;
530
+ action.onAction(row);
434
531
  }
435
532
 
436
533
  /** See {@link DataTableProps.chrome}. */
@@ -516,7 +613,8 @@ function cleanAttrs(
516
613
  * header is not a string and whose key is not human-readable is a call site that
517
614
  * should pass a string header, and that is visible in what gets announced.
518
615
  */
519
- function columnLabel<T>(col: DataTableColumn<T>): string {
616
+ function columnLabel<T>(col: DataTableColumn<T>, labels?: DataTableLabels): string {
617
+ if (col.key === ROW_ACTIONS_KEY && labels) return labels.actions;
520
618
  return typeof col.header === "string" ? col.header : col.key;
521
619
  }
522
620
 
@@ -528,6 +626,20 @@ function columnLabel<T>(col: DataTableColumn<T>): string {
528
626
  const OWN_CONTROL =
529
627
  'a[href],button,input,select,textarea,label,summary,[role="button"],[role="link"],[role="checkbox"],[role="switch"],[role="menuitem"],[role="option"],[contenteditable=""],[contenteditable="true"],[tabindex]:not([tabindex="-1"])';
530
628
 
629
+ /**
630
+ * Whether a click that reached `row`'s handler did not start in the row's own DOM.
631
+ *
632
+ * React bubbles an event through the COMPONENT tree, portals included: a popover, a
633
+ * menu or a dialog a cell (or the expansion under it) opens renders at `<body>` but
634
+ * still reports its clicks to every React ancestor — the row among them. So a click
635
+ * in a cell's portalled date picker opened the row it came from, and a page wrapped
636
+ * such content in `<div onClick={(e) => e.stopPropagation()}>` to stop it (kastlan's
637
+ * feedback page did, round its comments). The row owns the clicks on the row.
638
+ */
639
+ function fromOutside(target: EventTarget | null, row: HTMLElement): boolean {
640
+ return !(target instanceof Node) || !row.contains(target);
641
+ }
642
+
531
643
  /** Whether an event on `row` started on a control nested inside it. */
532
644
  function fromOwnControl(target: EventTarget | null, row: HTMLElement): boolean {
533
645
  if (!(target instanceof Element)) return false;
@@ -617,9 +729,13 @@ function RowLink({
617
729
  // here happens before any descendant can silence the event; `stopPropagation`
618
730
  // does not undo a `preventDefault`.
619
731
  onClickCapture={(e) => {
732
+ // A click in a portal a descendant opened is not a click on the link: leave
733
+ // it (and a link inside it) alone. See `fromOutside`.
734
+ if (fromOutside(e.target, e.currentTarget)) return;
620
735
  if (isPlainLeftClick(e)) e.preventDefault();
621
736
  }}
622
737
  onClick={(e) => {
738
+ if (fromOutside(e.target, e.currentTarget)) return;
623
739
  if (!isPlainLeftClick(e)) {
624
740
  e.stopPropagation();
625
741
  return;
@@ -639,12 +755,12 @@ function RowLink({
639
755
 
640
756
 
641
757
 
642
- import { DEFAULT_PERSIST_PREFIX, useTableState } from "./use-table-state";
758
+ import { DEFAULT_PERSIST_PREFIX, useTableState, type TableUrlSync } from "./use-table-state";
643
759
  import { useMobileReveal } from "./use-mobile-reveal";
644
760
 
645
761
  export function DataTable<T>({
646
762
  rows,
647
- columns,
763
+ columns: columnsProp,
648
764
  rowKey,
649
765
  defaultPageSize = 25,
650
766
  onRowClick,
@@ -682,6 +798,8 @@ export function DataTable<T>({
682
798
  multiSort: multiSortProp,
683
799
  frame = true,
684
800
  className,
801
+ rowActions,
802
+ toolbar,
685
803
  }: DataTableProps<T>) {
686
804
  const compact = density === "compact";
687
805
  const minimal = chrome === "minimal";
@@ -711,6 +829,74 @@ export function DataTable<T>({
711
829
  [labelOverrides, labelsProp],
712
830
  );
713
831
  const locale = useKitLocale(localeProp);
832
+
833
+ // ---- Row actions ----
834
+ // The actions column is appended to the caller's columns rather than drawn as a
835
+ // second special case beside the selection column: it then gets the cell padding,
836
+ // the widths and the phone rules (`mobileHidden`) every other column gets.
837
+ const rowActionsFor = useCallback(
838
+ (row: T): ResolvedRowAction<T>[] =>
839
+ rowActions
840
+ ? (typeof rowActions === "function" ? rowActions(row) : rowActions)
841
+ .filter((a) => !a.hidden?.(row))
842
+ .map((a, i) => resolveRowAction(a, i, labels))
843
+ : [],
844
+ [rowActions, labels],
845
+ );
846
+ const columns = useMemo<DataTableColumn<T>[]>(
847
+ () =>
848
+ rowActions
849
+ ? [
850
+ ...columnsProp,
851
+ {
852
+ key: ROW_ACTIONS_KEY,
853
+ header: <span className="sr-only">{labels.actions}</span>,
854
+ cell: (row) => (
855
+ <div className="flex items-center justify-end gap-0.5">
856
+ {rowActionsFor(row).map((a) => (
857
+ <IconButton
858
+ key={a.key}
859
+ size={density === "compact" ? "2xs" : "xs"}
860
+ label={a.label}
861
+ tone={a.tone === "danger" ? "danger" : undefined}
862
+ disabledReason={a.action.disabledReason?.(row)}
863
+ // The row's click handler already skips its own controls; this is for
864
+ // the caller who wraps the table in a clickable element of their own.
865
+ stopPropagation
866
+ onClick={() => void runRowAction(a.action, row)}
867
+ >
868
+ {a.icon}
869
+ </IconButton>
870
+ ))}
871
+ </div>
872
+ ),
873
+ className: "w-px whitespace-nowrap text-end",
874
+ headClassName: "w-px",
875
+ // On a phone the same actions are the card's swipes.
876
+ mobileHidden: true,
877
+ noRowLink: true,
878
+ },
879
+ ]
880
+ : columnsProp,
881
+ [columnsProp, rowActions, rowActionsFor, labels.actions, density],
882
+ );
883
+ const swipesFromActions = (row: T): MobileSwipeActions | null => {
884
+ const swipes = rowActionsFor(row).filter((a) => a.swipe && !a.action.disabledReason?.(row));
885
+ if (!swipes.length) return null;
886
+ const toSwipe = (a: ResolvedRowAction<T>): SwipeAction => ({
887
+ label: a.label,
888
+ icon: a.icon,
889
+ onCommit: () => void runRowAction(a.action, row),
890
+ className: a.tone === "danger" ? "bg-[var(--danger)]" : "bg-[var(--brand)]",
891
+ armedClassName: a.tone === "danger" ? "bg-[var(--danger-hover)]" : "bg-[var(--brand-hover)]",
892
+ });
893
+ return {
894
+ start: swipes.filter((a) => a.swipe === "start").map(toSwipe),
895
+ end: swipes.filter((a) => a.swipe === "end").map(toSwipe),
896
+ };
897
+ };
898
+ const swipeActionsFor = mobileSwipeActions ?? (rowActions ? swipesFromActions : undefined);
899
+
714
900
  // Sorting, filtering and paging all change WHICH rows are on screen without
715
901
  // moving focus — the header button the user pressed is still the header button
716
902
  // they are on — so there is no other channel to say it on. See use-announce.ts.
@@ -875,7 +1061,7 @@ export function DataTable<T>({
875
1061
  // criterion BEHIND the existing one, and answering "Sorted by Date" to a click on
876
1062
  // Name misreports what the click did. The third click drops the column entirely,
877
1063
  // which is the state a user is most likely to have reached by accident.
878
- const name = col ? columnLabel(col) : key;
1064
+ const name = col ? columnLabel(col, labels) : key;
879
1065
  const entry = next.find((s) => s.key === key);
880
1066
  announce(
881
1067
  !entry
@@ -1122,7 +1308,10 @@ export function DataTable<T>({
1122
1308
  const totalCount = columns.length;
1123
1309
  // Spans the full row width including the optional leading selection column.
1124
1310
  const totalColSpan = visibleCount + (selection ? 1 : 0);
1125
- const columnsCountLabel = labels.columnsCount(visibleCount, totalCount);
1311
+ // The actions column is the table's own, not one the user can hide: out of the count.
1312
+ const columnsCountLabel = rowActions
1313
+ ? labels.columnsCount(visibleCount - 1, totalCount - 1)
1314
+ : labels.columnsCount(visibleCount, totalCount);
1126
1315
  // Match Tailwind's `md` breakpoint: we render either the table or the card
1127
1316
  // list — never both — so we don't double up DOM nodes that screen readers and
1128
1317
  // integration tests would have to disambiguate.
@@ -1252,7 +1441,7 @@ export function DataTable<T>({
1252
1441
  // uses for exactly this, and it points the way the row actually opens: right into
1253
1442
  // a sheet, or down into an inline panel that flips it when expanded.
1254
1443
  const opensDetail = interactive && !!expandedRow;
1255
- const swipeActions = mobileSwipeActions?.(row);
1444
+ const swipeActions = swipeActionsFor?.(row);
1256
1445
  const swipe = swipeActions ? physicalSwipe(swipeActions, mobileDir) : null;
1257
1446
  // An expanded row never swipes: the editor below it owns the horizontal space,
1258
1447
  // and dragging the header away from its own form reads as a glitch.
@@ -1359,10 +1548,20 @@ export function DataTable<T>({
1359
1548
  {cardInner}
1360
1549
  </RowLink>
1361
1550
  ) : (
1551
+ /* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- the handlers are
1552
+ only attached when `interactive`, and then so are role="button", tabIndex 0 and the
1553
+ Enter/Space handler below; the rule cannot follow the conditional role. */
1362
1554
  <div
1363
1555
  role={interactive ? "button" : undefined}
1364
1556
  tabIndex={interactive ? 0 : undefined}
1365
- onClick={interactive ? () => onRowClick!(row) : undefined}
1557
+ onClick={
1558
+ interactive
1559
+ ? (e) => {
1560
+ if (fromOutside(e.target, e.currentTarget)) return;
1561
+ onRowClick!(row);
1562
+ }
1563
+ : undefined
1564
+ }
1366
1565
  onKeyDown={
1367
1566
  interactive
1368
1567
  ? (e) => {
@@ -1445,6 +1644,19 @@ export function DataTable<T>({
1445
1644
  card list for the table wholesale, which would remount a region living
1446
1645
  inside either one. */}
1447
1646
  <span {...regionProps} />
1647
+ {toolbar != null && toolbar !== false && (
1648
+ <div
1649
+ data-table-toolbar=""
1650
+ className={cn(
1651
+ "flex flex-wrap items-center gap-2",
1652
+ frame
1653
+ ? cn("border-b border-[var(--border)]", compact ? "px-2 py-1.5" : "px-3 py-2")
1654
+ : "pb-3",
1655
+ )}
1656
+ >
1657
+ {toolbar}
1658
+ </div>
1659
+ )}
1448
1660
  {!isMdUp && (
1449
1661
  <div ref={mobileRootRef}>
1450
1662
  {/* Mobile filter access (feedback #299): the per-column filter popovers
@@ -1489,7 +1701,7 @@ export function DataTable<T>({
1489
1701
  compact ? "px-3 py-3 text-xs" : "px-4 py-6 text-sm",
1490
1702
  )}
1491
1703
  >
1492
- {isLoading ? <LoadingText label={labels.loading} /> : (empty ?? "—")}
1704
+ {isLoading ? <LoadingText label={labels.loading} /> : (empty ?? labels.empty)}
1493
1705
  </li>
1494
1706
  )}
1495
1707
  {/* Endless-scroll sentinel: observed by IntersectionObserver to pull in
@@ -1708,7 +1920,10 @@ export function DataTable<T>({
1708
1920
  </Popover>
1709
1921
  )}
1710
1922
  </div>
1711
- {resizable && (
1923
+ {resizable && col.key !== ROW_ACTIONS_KEY && (
1924
+ /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a
1925
+ pointer-only drag handle for a visual preference (column width); it is
1926
+ deliberately not a tab stop, and no content or action depends on it. */
1712
1927
  <span
1713
1928
  role="separator"
1714
1929
  aria-orientation="vertical"
@@ -1800,6 +2015,7 @@ export function DataTable<T>({
1800
2015
  // not also open the row. Cells no longer have to
1801
2016
  // `stopPropagation` to get that.
1802
2017
  if (fromOwnControl(e.target, e.currentTarget)) return;
2018
+ if (fromOutside(e.target, e.currentTarget)) return;
1803
2019
  // Modifier-clicks anywhere in the row drive
1804
2020
  // selection instead of expanding it (feedback #289):
1805
2021
  // Ctrl/Cmd toggles a single row, Shift selects the
@@ -1935,7 +2151,7 @@ export function DataTable<T>({
1935
2151
  >
1936
2152
  {/* Nothing is not the same as not-yet: "no results" while the first
1937
2153
  page is still in flight is a claim the table cannot make. */}
1938
- {isLoading ? <LoadingText label={labels.loading} /> : (empty ?? "—")}
2154
+ {isLoading ? <LoadingText label={labels.loading} /> : (empty ?? labels.empty)}
1939
2155
  </td>
1940
2156
  </tr>
1941
2157
  )}
@@ -2018,6 +2234,7 @@ export function DataTable<T>({
2018
2234
  </button>
2019
2235
  <div className="flex flex-col gap-0.5">
2020
2236
  {columns.map((col) => {
2237
+ if (col.key === ROW_ACTIONS_KEY) return null;
2021
2238
  const checked = !hiddenCols.has(col.key);
2022
2239
  const headerLabel = columnLabel(col);
2023
2240
  return (
@@ -91,6 +91,8 @@ export interface DateTriggerAttributes {
91
91
  "aria-label"?: string;
92
92
  "aria-describedby"?: string;
93
93
  "aria-invalid"?: true;
94
+ /** The field's required state — `Field required` hands it down with the id pair. */
95
+ "aria-required"?: boolean | "true" | "false";
94
96
  disabled?: boolean;
95
97
  onClick: () => void;
96
98
  className: string;
@@ -132,6 +134,7 @@ function DateFieldTrigger({
132
134
  padded,
133
135
  disabled,
134
136
  invalid,
137
+ required,
135
138
  panelId,
136
139
  aria,
137
140
  renderTrigger,
@@ -153,6 +156,8 @@ function DateFieldTrigger({
153
156
  padded: boolean;
154
157
  disabled?: boolean;
155
158
  invalid?: boolean;
159
+ /** `aria-required`, on the trigger with the rest of the field's ARIA. */
160
+ required?: boolean | "true" | "false";
156
161
  }) {
157
162
  const wasOpen = useRef(false);
158
163
  useEffect(() => {
@@ -177,6 +182,7 @@ function DateFieldTrigger({
177
182
  // One attribute object, worn either by the kit's own button below or by a caller's
178
183
  // (`renderTrigger`) — so a custom trigger cannot quietly fall out of step with the
179
184
  // naming and state this one announces.
185
+ const isInvalid = Boolean(invalid) || aria["aria-invalid"] === true || aria["aria-invalid"] === "true";
180
186
  const attrs: DateTriggerAttributes = {
181
187
  ref: triggerRef,
182
188
  type: "button",
@@ -209,14 +215,15 @@ function DateFieldTrigger({
209
215
  "aria-haspopup": "dialog",
210
216
  "aria-controls": panelId,
211
217
  "aria-expanded": open,
212
- "aria-invalid":
213
- invalid || aria["aria-invalid"] === true || aria["aria-invalid"] === "true" || undefined,
218
+ "aria-invalid": isInvalid || undefined,
219
+ "aria-required": required,
214
220
  className: cn(
215
221
  FIELD_TRIGGER,
216
222
  "pe-9",
217
223
  padded && FIELD_FLOATING_PAD,
218
224
  disabled && "cursor-not-allowed opacity-50",
219
- invalid && FIELD_INVALID,
225
+ // An `aria-invalid` from a Field spread paints too, as on Input and AmountInput.
226
+ isInvalid && FIELD_INVALID,
220
227
  ),
221
228
  };
222
229
  if (renderTrigger) return <CustomTrigger render={renderTrigger} attrs={attrs} valueId={valueId} />;
@@ -279,6 +286,10 @@ function DateField({
279
286
  sheetBackCloses = true,
280
287
  renderTrigger,
281
288
  children,
289
+ // The trigger's, not the wrapper's: `Field`'s render-prop spreads it with the id
290
+ // pair, and a required date read as optional while it sat on the role-less div.
291
+ // Taken here rather than in `splitTriggerAria`, which MonthPicker shares.
292
+ "aria-required": ariaRequired,
282
293
  ...rest
283
294
  }: Omit<ComponentPropsWithoutRef<"div">, "children"> & {
284
295
  triggerText: string;
@@ -393,6 +404,7 @@ function DateField({
393
404
  padded={label !== undefined}
394
405
  disabled={disabled}
395
406
  invalid={invalid}
407
+ required={ariaRequired}
396
408
  renderTrigger={renderTrigger}
397
409
  />
398
410
  );
@@ -577,6 +589,9 @@ export function DatePicker({
577
589
  clearLabel,
578
590
  disabled,
579
591
  invalid,
592
+ // Named so it reaches the field (and so its trigger) when a step/today row is the
593
+ // root and takes the rest of the caller's props.
594
+ "aria-required": ariaRequired,
580
595
  ...rest
581
596
  }: DatePickerProps) {
582
597
  const locale = useKitLocale(localeProp);
@@ -593,6 +608,7 @@ export function DatePicker({
593
608
  // With step/today buttons the flex row is the root and takes the caller's props —
594
609
  // except the ones that name the field, which still belong on its trigger.
595
610
  {...(wrapped ? triggerAria : rest)}
611
+ aria-required={ariaRequired}
596
612
  label={label}
597
613
  clearable={clearable}
598
614
  clearLabel={text.clear}
@@ -1,8 +1,20 @@
1
1
  import { createContext, useContext } from "react";
2
2
  import type { ComponentPropsWithoutRef, CSSProperties, ReactNode } from "react";
3
3
  import { cn } from "../lib/cn";
4
+ import { useKitLabels } from "../i18n/kit-labels";
4
5
  import { FieldHint } from "./ui";
5
6
 
7
+ /** The words a {@link DescriptionList} renders on its own behalf. */
8
+ export interface DescriptionListLabels {
9
+ /** What an item with no value shows (see {@link DescriptionItemProps.placeholder}).
10
+ * Default "—", an em dash. */
11
+ empty: string;
12
+ }
13
+
14
+ export const DEFAULT_DESCRIPTION_LIST_LABELS: DescriptionListLabels = {
15
+ empty: "—",
16
+ };
17
+
6
18
  export type DescriptionListLayout = "rows" | "cards" | "stacked";
7
19
  export type DescriptionListDensity = "comfortable" | "compact" | "tight";
8
20
  /** How many columns a `stacked` or `cards` list may use. */
@@ -14,6 +26,7 @@ interface ListContextValue {
14
26
  prose: boolean;
15
27
  density: DescriptionListDensity;
16
28
  columns: DescriptionListColumns;
29
+ placeholder?: ReactNode;
17
30
  }
18
31
 
19
32
  const ListContext = createContext<ListContextValue>({
@@ -103,6 +116,9 @@ export interface DescriptionListProps extends ComponentPropsWithoutRef<"dl"> {
103
116
  * all would not be an improvement. Per item: {@link DescriptionItemProps.prose}.
104
117
  */
105
118
  prose?: boolean;
119
+ /** What an item with no value shows, for every item that does not set its own —
120
+ * see {@link DescriptionItemProps.placeholder}. */
121
+ placeholder?: ReactNode;
106
122
  }
107
123
 
108
124
  /**
@@ -129,6 +145,7 @@ export function DescriptionList({
129
145
  density = "comfortable",
130
146
  minCardWidth = "10rem",
131
147
  columns,
148
+ placeholder,
132
149
  className,
133
150
  style,
134
151
  ...rest
@@ -147,7 +164,7 @@ export function DescriptionList({
147
164
  const grid: CSSProperties | undefined = cards
148
165
  ? { gridTemplateColumns: `repeat(auto-fill, minmax(min(100%, ${track}), 1fr))` }
149
166
  : undefined;
150
- const ctx: ListContextValue = { layout, numeric, prose, density, columns: columns ?? 2 };
167
+ const ctx: ListContextValue = { layout, numeric, prose, density, columns: columns ?? 2, placeholder };
151
168
  const text = density === "tight" ? "text-[11px] leading-4" : density === "compact" ? "text-xs" : "text-sm";
152
169
 
153
170
  if (stacked) {
@@ -215,6 +232,20 @@ export interface DescriptionItemProps extends Omit<ComponentPropsWithoutRef<"div
215
232
  * because a card grid's column count is not known to CSS ahead of layout.
216
233
  */
217
234
  span?: number | "full";
235
+ /**
236
+ * Shown in place of a value that is not there — `children` of `null`, `undefined`,
237
+ * `false` or `""` (a `0` is a value and is shown). Default: the list's own
238
+ * `placeholder`, else `descriptionList.empty` from the provider, else "—". `null`
239
+ * shows nothing, as an empty detail did before. kastlan's DetailField put its
240
+ * `EMPTY_VALUE` into every one of its call sites' values by hand.
241
+ *
242
+ * Drawn in the muted ink, so an absent value does not read as a value of "—".
243
+ */
244
+ placeholder?: ReactNode;
245
+ }
246
+
247
+ function isEmptyValue(node: ReactNode): boolean {
248
+ return node === undefined || node === null || node === false || node === "";
218
249
  }
219
250
 
220
251
  const ROW_PAD: Record<DescriptionListDensity, string> = {
@@ -236,11 +267,23 @@ export function DescriptionItem({
236
267
  numeric: numericProp,
237
268
  prose: proseProp,
238
269
  span,
270
+ placeholder: placeholderProp,
239
271
  detailClassName,
240
272
  className,
241
273
  ...rest
242
274
  }: DescriptionItemProps) {
243
- const { layout, numeric: listNumeric, prose: listProse, density, columns } = useContext(ListContext);
275
+ const {
276
+ layout,
277
+ numeric: listNumeric,
278
+ prose: listProse,
279
+ density,
280
+ columns,
281
+ placeholder: listPlaceholder,
282
+ } = useContext(ListContext);
283
+ const labels = useKitLabels("descriptionList", DEFAULT_DESCRIPTION_LIST_LABELS);
284
+ const empty = isEmptyValue(children);
285
+ const placeholder =
286
+ placeholderProp !== undefined ? placeholderProp : listPlaceholder !== undefined ? listPlaceholder : labels.empty;
244
287
  const numeric = numericProp ?? listNumeric;
245
288
  const prose = proseProp ?? listProse;
246
289
  const cards = layout === "cards";
@@ -277,7 +320,13 @@ export function DescriptionItem({
277
320
  detailClassName,
278
321
  )}
279
322
  >
280
- {children}
323
+ {empty && !isEmptyValue(placeholder) ? (
324
+ <span data-slot="description-empty" className="font-normal text-[var(--text-muted)]">
325
+ {placeholder}
326
+ </span>
327
+ ) : (
328
+ children
329
+ )}
281
330
  </dd>
282
331
  );
283
332