@eifi1/ui-kit 0.21.0 → 0.23.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 (320) hide show
  1. package/README.md +42 -25
  2. package/dist/chart.d.ts +19 -14
  3. package/dist/components/account-chips.d.ts +10 -3
  4. package/dist/components/account-chips.js +5 -2
  5. package/dist/components/account-chips.js.map +1 -1
  6. package/dist/components/account-settings.d.ts +33 -2
  7. package/dist/components/account-settings.js +43 -21
  8. package/dist/components/account-settings.js.map +1 -1
  9. package/dist/components/amount-input.d.ts +53 -14
  10. package/dist/components/amount-input.js +17 -28
  11. package/dist/components/amount-input.js.map +1 -1
  12. package/dist/components/autocomplete.d.ts +29 -0
  13. package/dist/components/autocomplete.js +134 -104
  14. package/dist/components/autocomplete.js.map +1 -1
  15. package/dist/components/button-group.d.ts +19 -14
  16. package/dist/components/calculator.d.ts +19 -14
  17. package/dist/components/checkbox-group.d.ts +104 -0
  18. package/dist/components/checkbox-group.js +111 -0
  19. package/dist/components/checkbox-group.js.map +1 -0
  20. package/dist/components/checkbox.d.ts +19 -0
  21. package/dist/components/checkbox.js +30 -7
  22. package/dist/components/checkbox.js.map +1 -1
  23. package/dist/components/chip.d.ts +18 -0
  24. package/dist/components/chip.js +47 -10
  25. package/dist/components/chip.js.map +1 -1
  26. package/dist/components/column-mapper.d.ts +182 -0
  27. package/dist/components/column-mapper.js +375 -0
  28. package/dist/components/column-mapper.js.map +1 -0
  29. package/dist/components/combobox-core.d.ts +39 -2
  30. package/dist/components/combobox-core.js +29 -6
  31. package/dist/components/combobox-core.js.map +1 -1
  32. package/dist/components/combobox.d.ts +91 -2
  33. package/dist/components/combobox.js +454 -308
  34. package/dist/components/combobox.js.map +1 -1
  35. package/dist/components/confirm-dialog.d.ts +69 -161
  36. package/dist/components/confirm-dialog.js +7 -8
  37. package/dist/components/confirm-dialog.js.map +1 -1
  38. package/dist/components/copy-button.d.ts +16 -11
  39. package/dist/components/country-select.d.ts +167 -0
  40. package/dist/components/country-select.js +293 -0
  41. package/dist/components/country-select.js.map +1 -0
  42. package/dist/components/currency-select.d.ts +17 -1
  43. package/dist/components/currency-select.js +56 -23
  44. package/dist/components/currency-select.js.map +1 -1
  45. package/dist/components/danger-confirm.d.ts +69 -166
  46. package/dist/components/danger-confirm.js +144 -50
  47. package/dist/components/danger-confirm.js.map +1 -1
  48. package/dist/components/data-table-cells.d.ts +1 -2
  49. package/dist/components/data-table-filter-popover.d.ts +1 -2
  50. package/dist/components/data-table-filters.d.ts +1 -2
  51. package/dist/components/data-table-labels.d.ts +4 -97
  52. package/dist/components/data-table-labels.js +9 -1
  53. package/dist/components/data-table-labels.js.map +1 -1
  54. package/dist/components/data-table-pagination.d.ts +3 -1
  55. package/dist/components/data-table.d.ts +1 -2
  56. package/dist/components/data-table.js +98 -3
  57. package/dist/components/data-table.js.map +1 -1
  58. package/dist/components/date-picker.d.ts +66 -5
  59. package/dist/components/date-picker.js +378 -66
  60. package/dist/components/date-picker.js.map +1 -1
  61. package/dist/components/entity-combobox.d.ts +42 -1
  62. package/dist/components/entity-combobox.js +154 -113
  63. package/dist/components/entity-combobox.js.map +1 -1
  64. package/dist/components/facing-pair.d.ts +19 -14
  65. package/dist/components/field-parts.d.ts +224 -0
  66. package/dist/components/field-parts.js +156 -0
  67. package/dist/components/field-parts.js.map +1 -0
  68. package/dist/components/field-strip.d.ts +105 -0
  69. package/dist/components/field-strip.js +46 -0
  70. package/dist/components/field-strip.js.map +1 -0
  71. package/dist/components/file-button.d.ts +19 -14
  72. package/dist/components/file-button.js +13 -3
  73. package/dist/components/file-button.js.map +1 -1
  74. package/dist/components/file-dropzone.d.ts +41 -15
  75. package/dist/components/file-dropzone.js +57 -34
  76. package/dist/components/file-dropzone.js.map +1 -1
  77. package/dist/components/form-actions.d.ts +18 -13
  78. package/dist/components/form-actions.js +69 -5
  79. package/dist/components/form-actions.js.map +1 -1
  80. package/dist/components/iban-input.d.ts +69 -0
  81. package/dist/components/iban-input.js +125 -0
  82. package/dist/components/iban-input.js.map +1 -0
  83. package/dist/components/icon-picker.d.ts +21 -2
  84. package/dist/components/icon-picker.js +19 -4
  85. package/dist/components/icon-picker.js.map +1 -1
  86. package/dist/components/inline-edit-field.d.ts +167 -0
  87. package/dist/components/inline-edit-field.js +293 -0
  88. package/dist/components/inline-edit-field.js.map +1 -0
  89. package/dist/components/language-select.d.ts +130 -0
  90. package/dist/components/language-select.js +18 -0
  91. package/dist/components/language-select.js.map +1 -0
  92. package/dist/components/line-items.d.ts +25 -3
  93. package/dist/components/line-items.js +55 -46
  94. package/dist/components/line-items.js.map +1 -1
  95. package/dist/components/list.d.ts +25 -0
  96. package/dist/components/list.js +16 -5
  97. package/dist/components/list.js.map +1 -1
  98. package/dist/components/money-field.d.ts +32 -14
  99. package/dist/components/money-field.js.map +1 -1
  100. package/dist/components/month-picker.d.ts +52 -4
  101. package/dist/components/month-picker.js +291 -86
  102. package/dist/components/month-picker.js.map +1 -1
  103. package/dist/components/multi-entity-combobox.d.ts +26 -1
  104. package/dist/components/multi-entity-combobox.js +143 -102
  105. package/dist/components/multi-entity-combobox.js.map +1 -1
  106. package/dist/components/multi-select.d.ts +8 -1
  107. package/dist/components/multi-select.js +9 -4
  108. package/dist/components/multi-select.js.map +1 -1
  109. package/dist/components/number-field.d.ts +34 -19
  110. package/dist/components/number-field.js +6 -20
  111. package/dist/components/number-field.js.map +1 -1
  112. package/dist/components/number-input.d.ts +79 -19
  113. package/dist/components/number-input.js +90 -38
  114. package/dist/components/number-input.js.map +1 -1
  115. package/dist/components/numpad-sheet.d.ts +19 -14
  116. package/dist/components/one-time-code-input.d.ts +100 -0
  117. package/dist/components/one-time-code-input.js +137 -0
  118. package/dist/components/one-time-code-input.js.map +1 -0
  119. package/dist/components/phone-input.d.ts +69 -0
  120. package/dist/components/phone-input.js +171 -0
  121. package/dist/components/phone-input.js.map +1 -0
  122. package/dist/components/reauth-dialog.d.ts +2 -2
  123. package/dist/components/reauth-dialog.js +6 -8
  124. package/dist/components/reauth-dialog.js.map +1 -1
  125. package/dist/components/series-chart.d.ts +19 -14
  126. package/dist/components/settings-fields.d.ts +19 -14
  127. package/dist/components/share-card.d.ts +16 -11
  128. package/dist/components/sign-chip.d.ts +66 -0
  129. package/dist/components/sign-chip.js +39 -0
  130. package/dist/components/sign-chip.js.map +1 -0
  131. package/dist/components/swatch-picker.d.ts +43 -2
  132. package/dist/components/swatch-picker.js +80 -32
  133. package/dist/components/swatch-picker.js.map +1 -1
  134. package/dist/components/switch.d.ts +19 -0
  135. package/dist/components/switch.js +24 -6
  136. package/dist/components/switch.js.map +1 -1
  137. package/dist/components/text-link.d.ts +18 -13
  138. package/dist/components/tile-radio.d.ts +88 -13
  139. package/dist/components/tile-radio.js +28 -11
  140. package/dist/components/tile-radio.js.map +1 -1
  141. package/dist/components/time-input.d.ts +24 -15
  142. package/dist/components/time-input.js.map +1 -1
  143. package/dist/components/toggle-group.d.ts +47 -1
  144. package/dist/components/toggle-group.js +110 -69
  145. package/dist/components/toggle-group.js.map +1 -1
  146. package/dist/components/trigger-aria.d.ts +3 -0
  147. package/dist/components/trigger-aria.js +3 -1
  148. package/dist/components/trigger-aria.js.map +1 -1
  149. package/dist/components/ui.d.ts +16 -11
  150. package/dist/components/ui.js +284 -88
  151. package/dist/components/ui.js.map +1 -1
  152. package/dist/components/use-table-state.d.ts +1 -2
  153. package/dist/{data-table-3mi2R3so.d.ts → data-table-labels-B7OdnM0S.d.ts} +140 -3
  154. package/dist/data-table.d.ts +1 -2
  155. package/dist/data-table.js +2 -1
  156. package/dist/data-table.js.map +1 -1
  157. package/dist/feedback/feedback-attachment.d.ts +1 -1
  158. package/dist/feedback/feedback-attachment.js +348 -41
  159. package/dist/feedback/feedback-attachment.js.map +1 -1
  160. package/dist/feedback/feedback-dialog.d.ts +1 -1
  161. package/dist/feedback/feedback-dialog.js.map +1 -1
  162. package/dist/feedback/feedback-inbox.d.ts +3 -2
  163. package/dist/feedback/feedback-inbox.js.map +1 -1
  164. package/dist/feedback/feedback-thread.d.ts +68 -253
  165. package/dist/feedback/feedback-thread.js +46 -10
  166. package/dist/feedback/feedback-thread.js.map +1 -1
  167. package/dist/{kit-labels-D1J2_jyD.d.ts → feedback-BxeQVzwq.d.ts} +1206 -20
  168. package/dist/{feedback-attachment-WFiXJ8We.d.ts → feedback-attachment-fGAzZPf0.d.ts} +178 -11
  169. package/dist/feedback.d.ts +66 -2
  170. package/dist/hooks/use-file-drop.d.ts +19 -14
  171. package/dist/i18n/defaults.d.ts +19 -14
  172. package/dist/i18n/defaults.js +19 -2
  173. package/dist/i18n/defaults.js.map +1 -1
  174. package/dist/i18n/german.d.ts +19 -14
  175. package/dist/i18n/german.js +84 -5
  176. package/dist/i18n/german.js.map +1 -1
  177. package/dist/i18n/kit-labels.d.ts +15 -10
  178. package/dist/i18n/kit-labels.js +8 -2
  179. package/dist/i18n/kit-labels.js.map +1 -1
  180. package/dist/i18n/languages.d.ts +19 -14
  181. package/dist/i18n/locales/de-CH.d.ts +19 -14
  182. package/dist/i18n/locales/en.d.ts +19 -14
  183. package/dist/i18n/locales/en.js +16 -0
  184. package/dist/i18n/locales/en.js.map +1 -1
  185. package/dist/i18n/locales/es.d.ts +19 -14
  186. package/dist/i18n/locales/es.js +83 -5
  187. package/dist/i18n/locales/es.js.map +1 -1
  188. package/dist/i18n/locales/fr.d.ts +19 -14
  189. package/dist/i18n/locales/fr.js +82 -5
  190. package/dist/i18n/locales/fr.js.map +1 -1
  191. package/dist/i18n/locales/hu.d.ts +19 -14
  192. package/dist/i18n/locales/hu.js +85 -5
  193. package/dist/i18n/locales/hu.js.map +1 -1
  194. package/dist/i18n/locales/it.d.ts +19 -14
  195. package/dist/i18n/locales/it.js +84 -5
  196. package/dist/i18n/locales/it.js.map +1 -1
  197. package/dist/i18n/locales/zh.d.ts +19 -14
  198. package/dist/i18n/locales/zh.js +80 -5
  199. package/dist/i18n/locales/zh.js.map +1 -1
  200. package/dist/i18n/review.d.ts +19 -14
  201. package/dist/i18n/review.js +27 -1
  202. package/dist/i18n/review.js.map +1 -1
  203. package/dist/index.d.ts +21 -10
  204. package/dist/index.js +73 -2
  205. package/dist/index.js.map +1 -1
  206. package/dist/lib/column-mapping.d.ts +81 -0
  207. package/dist/lib/column-mapping.js +108 -0
  208. package/dist/lib/column-mapping.js.map +1 -0
  209. package/dist/lib/countries.d.ts +43 -0
  210. package/dist/lib/countries.js +39 -0
  211. package/dist/lib/countries.js.map +1 -0
  212. package/dist/lib/decimal-marks.d.ts +46 -0
  213. package/dist/lib/decimal-marks.js +40 -0
  214. package/dist/lib/decimal-marks.js.map +1 -0
  215. package/dist/lib/iban.d.ts +98 -0
  216. package/dist/lib/iban.js +154 -0
  217. package/dist/lib/iban.js.map +1 -0
  218. package/dist/lib/isin.d.ts +30 -0
  219. package/dist/lib/isin.js +36 -0
  220. package/dist/lib/isin.js.map +1 -0
  221. package/dist/lib/phone.d.ts +69 -0
  222. package/dist/lib/phone.js +121 -0
  223. package/dist/lib/phone.js.map +1 -0
  224. package/dist/lib/table-text.d.ts +109 -1
  225. package/dist/lib/table-text.js +122 -1
  226. package/dist/lib/table-text.js.map +1 -1
  227. package/dist/rhf/fields.d.ts +233 -18
  228. package/dist/rhf/fields.js +456 -3
  229. package/dist/rhf/fields.js.map +1 -1
  230. package/dist/rhf/form.d.ts +19 -14
  231. package/dist/rhf.d.ts +22 -15
  232. package/dist/rhf.js.map +1 -1
  233. package/dist/shell/app-shell.d.ts +18 -13
  234. package/dist/shell/top-bar-brand.d.ts +19 -14
  235. package/dist/shell/topbar-action-menu.d.ts +36 -3
  236. package/dist/shell/topbar-action-menu.js +74 -33
  237. package/dist/shell/topbar-action-menu.js.map +1 -1
  238. package/dist/shell.d.ts +18 -13
  239. package/dist/table-text.d.ts +1 -1
  240. package/dist/tour/tour.js +20 -24
  241. package/dist/tour/tour.js.map +1 -1
  242. package/dist/wizard/stepper-nav.d.ts +49 -16
  243. package/dist/wizard/stepper-nav.js +4 -0
  244. package/dist/wizard/stepper-nav.js.map +1 -1
  245. package/dist/wizard.d.ts +19 -14
  246. package/package.json +1 -1
  247. package/src/components/account-chips.tsx +17 -3
  248. package/src/components/account-settings.tsx +92 -26
  249. package/src/components/amount-input.tsx +68 -56
  250. package/src/components/autocomplete.tsx +198 -114
  251. package/src/components/checkbox-group.tsx +229 -0
  252. package/src/components/checkbox.tsx +72 -8
  253. package/src/components/chip.tsx +80 -10
  254. package/src/components/column-mapper.tsx +666 -0
  255. package/src/components/combobox-core.tsx +82 -9
  256. package/src/components/combobox.tsx +661 -340
  257. package/src/components/confirm-dialog.tsx +12 -8
  258. package/src/components/country-select.tsx +556 -0
  259. package/src/components/currency-select.tsx +109 -33
  260. package/src/components/danger-confirm.tsx +429 -82
  261. package/src/components/data-table-labels.ts +16 -1
  262. package/src/components/data-table.tsx +175 -6
  263. package/src/components/date-picker.tsx +595 -71
  264. package/src/components/entity-combobox.tsx +235 -124
  265. package/src/components/field-parts.tsx +403 -0
  266. package/src/components/field-strip.tsx +149 -0
  267. package/src/components/file-button.tsx +36 -3
  268. package/src/components/file-dropzone.tsx +106 -38
  269. package/src/components/form-actions.tsx +147 -6
  270. package/src/components/iban-input.tsx +231 -0
  271. package/src/components/icon-picker.tsx +47 -6
  272. package/src/components/inline-edit-field.tsx +533 -0
  273. package/src/components/language-select.tsx +78 -0
  274. package/src/components/line-items.tsx +35 -2
  275. package/src/components/list.tsx +55 -6
  276. package/src/components/money-field.tsx +13 -0
  277. package/src/components/month-picker.tsx +400 -63
  278. package/src/components/multi-entity-combobox.tsx +207 -120
  279. package/src/components/multi-select.tsx +80 -58
  280. package/src/components/number-field.tsx +28 -46
  281. package/src/components/number-input.tsx +210 -43
  282. package/src/components/one-time-code-input.tsx +288 -0
  283. package/src/components/phone-input.tsx +299 -0
  284. package/src/components/reauth-dialog.tsx +17 -18
  285. package/src/components/sign-chip.tsx +103 -0
  286. package/src/components/swatch-picker.tsx +143 -31
  287. package/src/components/switch.tsx +58 -7
  288. package/src/components/tile-radio.tsx +145 -29
  289. package/src/components/time-input.tsx +8 -1
  290. package/src/components/toggle-group.tsx +206 -73
  291. package/src/components/trigger-aria.ts +5 -0
  292. package/src/components/ui.tsx +479 -138
  293. package/src/data-table.ts +2 -1
  294. package/src/feedback/feedback-attachment.tsx +597 -46
  295. package/src/feedback/feedback-dialog.tsx +9 -3
  296. package/src/feedback/feedback-inbox.tsx +3 -2
  297. package/src/feedback/feedback-thread.tsx +113 -9
  298. package/src/i18n/defaults.ts +18 -1
  299. package/src/i18n/german.ts +89 -0
  300. package/src/i18n/kit-labels.tsx +82 -2
  301. package/src/i18n/locales/en.ts +36 -4
  302. package/src/i18n/locales/es.ts +87 -0
  303. package/src/i18n/locales/fr.ts +87 -0
  304. package/src/i18n/locales/hu.ts +83 -0
  305. package/src/i18n/locales/it.ts +88 -0
  306. package/src/i18n/locales/zh.ts +76 -0
  307. package/src/i18n/review.ts +26 -0
  308. package/src/index.ts +71 -3
  309. package/src/lib/column-mapping.ts +234 -0
  310. package/src/lib/countries.ts +91 -0
  311. package/src/lib/decimal-marks.ts +99 -0
  312. package/src/lib/iban.ts +171 -0
  313. package/src/lib/isin.ts +62 -0
  314. package/src/lib/phone.ts +211 -0
  315. package/src/lib/table-text.ts +271 -0
  316. package/src/rhf/fields.tsx +860 -9
  317. package/src/rhf.ts +3 -1
  318. package/src/shell/topbar-action-menu.tsx +134 -38
  319. package/src/tour/tour.tsx +66 -28
  320. package/src/wizard/stepper-nav.tsx +35 -1
@@ -3,7 +3,14 @@
3
3
  // ships no translations; the English defaults below make the table usable with no
4
4
  // wiring.
5
5
 
6
- export interface DataTableLabels {
6
+ // A type-only import: erased at build time, so this module stays free of the table
7
+ // itself (and of react-router), which `i18n/defaults` relies on.
8
+ import type { DataTableSortLabels } from "./data-table";
9
+
10
+ /** The phone sort control's four keys (`sortBy`, `sortDefault`, `sortAscending`,
11
+ * `sortDescending`, 0.22.0) are {@link DataTableSortLabels}', declared beside the
12
+ * control and part of this namespace like every other key. */
13
+ export interface DataTableLabels extends DataTableSortLabels {
7
14
  // Table chrome
8
15
  columns: string;
9
16
  selectAllRows: string;
@@ -143,6 +150,14 @@ export const DEFAULT_DATA_TABLE_LABELS: DataTableLabels = {
143
150
  booleanTrue: "Yes",
144
151
  booleanFalse: "No",
145
152
  booleanUnset: "Not set",
153
+ // The phone sort control (0.22.0). The same words as `DEFAULT_DATA_TABLE_SORT_LABELS`
154
+ // in data-table.tsx, written out rather than spread in: importing that VALUE here
155
+ // would make data-table.tsx and this module import each other (data-table.tsx reads
156
+ // its resolver from here), and would pull the whole table into `i18n/defaults`.
157
+ sortBy: "Sort by",
158
+ sortDefault: "Default order",
159
+ sortAscending: "Ascending",
160
+ sortDescending: "Descending",
146
161
  };
147
162
 
148
163
  /**
@@ -3,8 +3,10 @@ import type { CSSProperties, ReactNode, TdHTMLAttributes, ThHTMLAttributes } fro
3
3
  import { createPortal } from "react-dom";
4
4
  import {
5
5
  ArrowDown,
6
+ ArrowDownWideNarrow,
6
7
  ArrowUp,
7
8
  ArrowUpDown,
9
+ ArrowUpNarrowWide,
8
10
  ChevronDown,
9
11
  ChevronRight,
10
12
  Filter,
@@ -12,7 +14,7 @@ import {
12
14
  Trash2,
13
15
  X,
14
16
  } from "lucide-react";
15
- import { Card, IconButton, Spinner } from "./ui";
17
+ import { Card, IconButton, Select, Spinner } from "./ui";
16
18
  import { cn } from "../lib/cn";
17
19
  import {
18
20
  defaultFilterState,
@@ -33,7 +35,7 @@ import { FullBleedDialog } from "./full-bleed-dialog";
33
35
  import { Popover } from "./popover";
34
36
  import { useMediaQuery } from "../hooks/use-media-query";
35
37
  import { useAnnounce } from "../hooks/use-announce";
36
- import { resolveDataTableLabels, type DataTableLabels } from "./data-table-labels";
38
+ import { DEFAULT_DATA_TABLE_LABELS, resolveDataTableLabels, type DataTableLabels } from "./data-table-labels";
37
39
  import { useKitLabelOverrides, useKitLocale } from "../i18n/kit-labels";
38
40
  import { Tooltip } from "./tooltip";
39
41
  import { dirOf, isRtl, type Direction } from "../lib/direction";
@@ -78,6 +80,14 @@ export interface DataTableColumn<T> {
78
80
  * Default `"asc"`.
79
81
  */
80
82
  firstSort?: SortDir;
83
+ /**
84
+ * The column's name as plain text, for the places a ReactNode `header` cannot go:
85
+ * the phone sort control's options ({@link DataTableProps.mobileSort}), the spoken
86
+ * "Sorted by …" and the column-settings checklist. Only needed when `header` is not
87
+ * a string (an icon, a unit suffix); without it those places fall back to the
88
+ * header string, then to `key`.
89
+ */
90
+ headerText?: string;
81
91
  /**
82
92
  * Extra attributes for this column's `<td>` in a given row — a `data-*` hook, a
83
93
  * `title`, an `aria-describedby`, a class. keksdose's accounts page
@@ -390,6 +400,18 @@ export interface DataTableProps<T> {
390
400
  * wastes space (feedback #317). Desktop is unaffected.
391
401
  */
392
402
  mobileCard?: (row: T) => ReactNode;
403
+ /**
404
+ * A "Sort by" select and a direction button above the phone card list (keksdose
405
+ * K10). The card list has no header row, so without it a phone user is stuck with
406
+ * whatever order the table opened in. It drives the same sort state as the column
407
+ * headers — `sorts`/`onSortsChange` when controlled, the URL and `storageKey` when
408
+ * not — and offers the columns a header click could sort (those with a `sortBy`).
409
+ * Each option is named by the column's `headerText`, else its string `header`.
410
+ *
411
+ * Off by default, so no existing phone layout gains a row. Strings:
412
+ * {@link DataTableSortLabels}. Phones only; the desktop table is unchanged.
413
+ */
414
+ mobileSort?: boolean;
393
415
  /**
394
416
  * Swipe actions for a mobile row, revealed by dragging it horizontally. Return
395
417
  * `null` (or empty sides) for rows that should not move — a locked row, one whose
@@ -683,7 +705,120 @@ function cleanAttrs(
683
705
  */
684
706
  function columnLabel<T>(col: DataTableColumn<T>, labels?: DataTableLabels): string {
685
707
  if (col.key === ROW_ACTIONS_KEY && labels) return labels.actions;
686
- return typeof col.header === "string" ? col.header : col.key;
708
+ return col.headerText ?? (typeof col.header === "string" ? col.header : col.key);
709
+ }
710
+
711
+ // ---------- Phone sort control (keksdose K10) ----------
712
+
713
+ /**
714
+ * The phone sort control's strings (keksdose K10). They are `dataTable` strings —
715
+ * read from the table's resolved labels, so the `labels` prop and the provider's
716
+ * `dataTable` namespace reach them — and are declared here, beside the control, with
717
+ * English defaults until `DataTableLabels` declares them itself.
718
+ */
719
+ export interface DataTableSortLabels {
720
+ /** The control's visible label: "Sort by". */
721
+ sortBy: string;
722
+ /** The "no sort" option — the rows in the order they were given: "Default order". */
723
+ sortDefault: string;
724
+ /** The direction button's name while the sort ascends: "Ascending". */
725
+ sortAscending: string;
726
+ /** …and while it descends: "Descending". */
727
+ sortDescending: string;
728
+ }
729
+
730
+ /** The phone sort control's words, as they sit in {@link DEFAULT_DATA_TABLE_LABELS}
731
+ * (DataTableLabels extends DataTableSortLabels; one source for the English). */
732
+ export const DEFAULT_DATA_TABLE_SORT_LABELS: DataTableSortLabels = {
733
+ sortBy: DEFAULT_DATA_TABLE_LABELS.sortBy,
734
+ sortDefault: DEFAULT_DATA_TABLE_LABELS.sortDefault,
735
+ sortAscending: DEFAULT_DATA_TABLE_LABELS.sortAscending,
736
+ sortDescending: DEFAULT_DATA_TABLE_LABELS.sortDescending,
737
+ };
738
+
739
+ const flipDir = (d: SortDir): SortDir => (d === "asc" ? "desc" : "asc");
740
+
741
+ /**
742
+ * The card list's sort control: a "Sort by" select and a direction button.
743
+ *
744
+ * keksdose K10 (inputs audit 2026-10-02 §5; its aggregated report tables, #258): a
745
+ * table is sorted by its header row and the phone card list has none, so the two
746
+ * decisions the header carries — which column, which direction — were unreachable
747
+ * below the breakpoint. keksdose hand-built this row over its DataTable to answer
748
+ * "who did I pay the most" on the device it was asked on; this is that row, driven by
749
+ * the SAME sort state as the headers (controlled `sorts` included), so turning the
750
+ * phone sideways keeps the order and a sort chosen here is the one the URL and the
751
+ * storage key hold.
752
+ *
753
+ * The rules are the header's: a newly picked column starts in its `firstSort`, the
754
+ * direction button flips the primary sort (secondary ones stay), and "Default order"
755
+ * — the third click of a `"tri"` cycle — is offered only under `"tri"`, since a
756
+ * `"toggle"` table promises it is always ranked by something. The select's label is
757
+ * a real `<label>` beside it, and the button sits outside that label: a click inside
758
+ * a label is forwarded to the control it names, so a nested button would flip AND
759
+ * open the select.
760
+ */
761
+ function MobileSort<T>({
762
+ columns,
763
+ sorts,
764
+ sortCycle,
765
+ compact,
766
+ labels,
767
+ onPick,
768
+ onFlip,
769
+ }: {
770
+ columns: DataTableColumn<T>[];
771
+ sorts: SortState[];
772
+ sortCycle: SortCycle;
773
+ compact: boolean;
774
+ labels: DataTableLabels;
775
+ onPick: (key: string | null) => void;
776
+ onFlip: () => void;
777
+ }) {
778
+ const selectId = useId();
779
+ const sortLabels: DataTableSortLabels = labels;
780
+ if (columns.length === 0) return null;
781
+ // A persisted or URL sort can name a column with no `sortBy` (or a hidden one): the
782
+ // sort chain drops it, and so does the control rather than claiming it.
783
+ const primary = sorts[0] && columns.some((c) => c.key === sorts[0].key) ? sorts[0] : null;
784
+ const offerDefault = sortCycle === "tri" || primary === null;
785
+ const desc = primary?.dir === "desc";
786
+ return (
787
+ <div
788
+ data-table-mobile-sort=""
789
+ className={cn(
790
+ "flex items-center gap-2 border-b border-[var(--border)]",
791
+ compact ? "px-3 py-1.5" : "px-4 py-2",
792
+ )}
793
+ >
794
+ <label htmlFor={selectId} className="shrink-0 text-xs text-[var(--text-muted)]">
795
+ {sortLabels.sortBy}
796
+ </label>
797
+ <Select
798
+ id={selectId}
799
+ size={compact ? "sm" : "md"}
800
+ className="min-w-0 flex-1"
801
+ value={primary?.key ?? ""}
802
+ onChange={(e) => onPick(e.target.value || null)}
803
+ >
804
+ {offerDefault && <option value="">{sortLabels.sortDefault}</option>}
805
+ {columns.map((col) => (
806
+ <option key={col.key} value={col.key}>
807
+ {columnLabel(col, labels)}
808
+ </option>
809
+ ))}
810
+ </Select>
811
+ <IconButton
812
+ size={compact ? "xs" : "md"}
813
+ variant="secondary"
814
+ label={desc ? sortLabels.sortDescending : sortLabels.sortAscending}
815
+ disabled={primary === null}
816
+ onClick={onFlip}
817
+ >
818
+ {desc ? <ArrowDownWideNarrow /> : <ArrowUpNarrowWide />}
819
+ </IconButton>
820
+ </div>
821
+ );
687
822
  }
688
823
 
689
824
  /**
@@ -857,6 +992,7 @@ export function DataTable<T>({
857
992
  mobileGroupBy,
858
993
  mobileGroupLabel,
859
994
  mobileCard,
995
+ mobileSort = false,
860
996
  mobileSwipeActions,
861
997
  leadingRow,
862
998
  density = "comfortable",
@@ -1125,6 +1261,13 @@ export function DataTable<T>({
1125
1261
  firstDir: col?.firstSort,
1126
1262
  cycle: sortCycle,
1127
1263
  });
1264
+ commitSorts(next, key);
1265
+ };
1266
+
1267
+ // Header clicks and the phone sort control (`mobileSort`) land here alike, so both
1268
+ // go through the same owner and say the same thing.
1269
+ const commitSorts = (next: SortState[], key: string) => {
1270
+ const col = columns.find((c) => c.key === key);
1128
1271
  if (onSortsChange) onSortsChange(next);
1129
1272
  else setInternalSorts(next);
1130
1273
 
@@ -1405,9 +1548,9 @@ export function DataTable<T>({
1405
1548
 
1406
1549
  // ---- Mobile card list (md:hidden) ----
1407
1550
  // Renders the same paged/filtered/sorted slice but as stacked cards instead
1408
- // of a horizontally-scrolling table. Filters / sort / column settings are
1409
- // intentionally hidden here — the mobile layout assumes the user wants to
1410
- // scan rows quickly. Switch to a wider viewport for fine-grained control.
1551
+ // of a horizontally-scrolling table. Column settings stay desktop-only; the
1552
+ // filters come through a bottom sheet (feedback #299) and the sort through the
1553
+ // opt-in `mobileSort` row (keksdose K10), both on the table's own state.
1411
1554
  const mobileColumns = visibleColumns.filter((c) => !c.mobileHidden);
1412
1555
  const mobilePrimaryCol =
1413
1556
  mobileColumns.find((c) => c.mobilePrimary) ?? mobileColumns[0] ?? null;
@@ -1765,6 +1908,32 @@ export function DataTable<T>({
1765
1908
  locale={locale}
1766
1909
  />
1767
1910
  )}
1911
+ {mobileSort && (
1912
+ <MobileSort
1913
+ columns={visibleColumns.filter((c) => (!isServer || !!onSortsChange) && !!c.sortBy)}
1914
+ sorts={sorts}
1915
+ sortCycle={sortCycle}
1916
+ compact={compact}
1917
+ labels={labels}
1918
+ onPick={(key) => {
1919
+ if (key === null) {
1920
+ if (sorts.length > 0) commitSorts([], sorts[0].key);
1921
+ return;
1922
+ }
1923
+ if (sorts[0]?.key === key) return;
1924
+ // A newly picked column starts where its header's first click would.
1925
+ const col = columns.find((c) => c.key === key);
1926
+ commitSorts([{ key, dir: col?.firstSort ?? "asc" }], key);
1927
+ }}
1928
+ onFlip={() => {
1929
+ if (sorts.length === 0) return;
1930
+ commitSorts(
1931
+ sorts.map((s, i) => (i === 0 ? { ...s, dir: flipDir(s.dir) } : s)),
1932
+ sorts[0].key,
1933
+ );
1934
+ }}
1935
+ />
1936
+ )}
1768
1937
  <ul
1769
1938
  className={cn(
1770
1939
  "divide-y divide-[var(--border)]",