@eifi1/ui-kit 0.44.0 → 0.46.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 (285) hide show
  1. package/README.md +29 -14
  2. package/dist/admin/admin-action-log-labels.js +9 -0
  3. package/dist/admin/admin-action-log-labels.js.map +1 -1
  4. package/dist/admin/admin-action-log.d.ts +18 -1
  5. package/dist/admin/admin-action-log.js +25 -16
  6. package/dist/admin/admin-action-log.js.map +1 -1
  7. package/dist/admin/admin-area-labels.d.ts +15 -0
  8. package/dist/admin/admin-area-labels.js +6 -1
  9. package/dist/admin/admin-area-labels.js.map +1 -1
  10. package/dist/admin/admin-overview.d.ts +16 -1
  11. package/dist/admin/admin-overview.js +12 -1
  12. package/dist/admin/admin-overview.js.map +1 -1
  13. package/dist/admin/mail-test-card-labels.d.ts +3 -0
  14. package/dist/admin/mail-test-card-labels.js +17 -0
  15. package/dist/admin/mail-test-card-labels.js.map +1 -0
  16. package/dist/admin/mail-test-card.d.ts +75 -0
  17. package/dist/admin/mail-test-card.js +82 -0
  18. package/dist/admin/mail-test-card.js.map +1 -0
  19. package/dist/components/appearance-labels.d.ts +10 -0
  20. package/dist/components/appearance-labels.js +6 -1
  21. package/dist/components/appearance-labels.js.map +1 -1
  22. package/dist/components/data-table-column-settings.js +2 -2
  23. package/dist/components/data-table-column-settings.js.map +1 -1
  24. package/dist/components/data-table-head.js +2 -2
  25. package/dist/components/data-table-head.js.map +1 -1
  26. package/dist/components/data-table-mobile.js +17 -2
  27. package/dist/components/data-table-mobile.js.map +1 -1
  28. package/dist/components/data-table-row-actions.d.ts +18 -2
  29. package/dist/components/data-table-row-actions.js +86 -2
  30. package/dist/components/data-table-row-actions.js.map +1 -1
  31. package/dist/components/data-table-shared.d.ts +2 -0
  32. package/dist/components/data-table-shared.js +17 -2
  33. package/dist/components/data-table-shared.js.map +1 -1
  34. package/dist/components/data-table.d.ts +43 -2
  35. package/dist/components/data-table.js +10 -10
  36. package/dist/components/data-table.js.map +1 -1
  37. package/dist/components/date-format-setting.d.ts +29 -0
  38. package/dist/components/date-format-setting.js +36 -0
  39. package/dist/components/date-format-setting.js.map +1 -0
  40. package/dist/components/swipeable-row.d.ts +8 -0
  41. package/dist/components/swipeable-row.js.map +1 -1
  42. package/dist/components/tabs.d.ts +3 -2
  43. package/dist/components/tabs.js +1 -1
  44. package/dist/components/tabs.js.map +1 -1
  45. package/dist/components/translation-review-editor.js +1 -0
  46. package/dist/components/translation-review-editor.js.map +1 -1
  47. package/dist/components/translation-review-labels.d.ts +5 -0
  48. package/dist/components/translation-review-labels.js +2 -0
  49. package/dist/components/translation-review-labels.js.map +1 -1
  50. package/dist/components/translation-review.d.ts +18 -8
  51. package/dist/components/translation-review.js +64 -7
  52. package/dist/components/translation-review.js.map +1 -1
  53. package/dist/components/write-lock-labels.js +2 -1
  54. package/dist/components/write-lock-labels.js.map +1 -1
  55. package/dist/components/write-lock.d.ts +33 -3
  56. package/dist/components/write-lock.js +9 -0
  57. package/dist/components/write-lock.js.map +1 -1
  58. package/dist/data-table.d.ts +1 -1
  59. package/dist/data-table.js.map +1 -1
  60. package/dist/eslint/index.js +1 -1
  61. package/dist/feedback/feedback-row-detail.d.ts +4 -2
  62. package/dist/feedback/feedback-row-detail.js +17 -26
  63. package/dist/feedback/feedback-row-detail.js.map +1 -1
  64. package/dist/feedback/feedback-swipe.d.ts +18 -0
  65. package/dist/feedback/feedback-swipe.js +10 -1
  66. package/dist/feedback/feedback-swipe.js.map +1 -1
  67. package/dist/i18n/defaults.js +9 -1
  68. package/dist/i18n/defaults.js.map +1 -1
  69. package/dist/i18n/german.js +81 -5
  70. package/dist/i18n/german.js.map +1 -1
  71. package/dist/i18n/kit-labels.d.ts +25 -1
  72. package/dist/i18n/kit-labels.js +8 -2
  73. package/dist/i18n/kit-labels.js.map +1 -1
  74. package/dist/i18n/locales/en.js +97 -7
  75. package/dist/i18n/locales/en.js.map +3 -3
  76. package/dist/i18n/locales/es.js +81 -5
  77. package/dist/i18n/locales/es.js.map +1 -1
  78. package/dist/i18n/locales/fr.js +81 -5
  79. package/dist/i18n/locales/fr.js.map +1 -1
  80. package/dist/i18n/locales/hu.js +81 -5
  81. package/dist/i18n/locales/hu.js.map +1 -1
  82. package/dist/i18n/locales/it.js +81 -5
  83. package/dist/i18n/locales/it.js.map +1 -1
  84. package/dist/i18n/locales/zh.js +81 -5
  85. package/dist/i18n/locales/zh.js.map +1 -1
  86. package/dist/i18n/review.js +16 -2
  87. package/dist/i18n/review.js.map +1 -1
  88. package/dist/index.d.ts +17 -2
  89. package/dist/index.js +40 -0
  90. package/dist/index.js.map +1 -1
  91. package/dist/lib/date-format.d.ts +81 -0
  92. package/dist/lib/date-format.js +56 -0
  93. package/dist/lib/date-format.js.map +1 -0
  94. package/dist/lib/format.d.ts +5 -1
  95. package/dist/lib/format.js +11 -5
  96. package/dist/lib/format.js.map +1 -1
  97. package/dist/notifications/device-zone-report.d.ts +27 -0
  98. package/dist/notifications/device-zone-report.js +24 -0
  99. package/dist/notifications/device-zone-report.js.map +1 -0
  100. package/dist/notifications/feedback-notification-types-labels.d.ts +7 -1
  101. package/dist/notifications/feedback-notification-types-labels.js +4 -1
  102. package/dist/notifications/feedback-notification-types-labels.js.map +1 -1
  103. package/dist/notifications/feedback-notification-types.d.ts +11 -5
  104. package/dist/notifications/feedback-notification-types.js +2 -1
  105. package/dist/notifications/feedback-notification-types.js.map +1 -1
  106. package/dist/notifications/notification-bell.d.ts +33 -0
  107. package/dist/notifications/notification-bell.js +74 -0
  108. package/dist/notifications/notification-bell.js.map +1 -0
  109. package/dist/notifications/notification-inbox.d.ts +77 -0
  110. package/dist/notifications/notification-inbox.js +206 -0
  111. package/dist/notifications/notification-inbox.js.map +1 -0
  112. package/dist/notifications/notification-types-setting.d.ts +6 -4
  113. package/dist/notifications/notification-types-setting.js +103 -45
  114. package/dist/notifications/notification-types-setting.js.map +1 -1
  115. package/dist/notifications/notification-types.d.ts +19 -7
  116. package/dist/notifications/notifications-labels.d.ts +41 -7
  117. package/dist/notifications/notifications-labels.js +18 -1
  118. package/dist/notifications/notifications-labels.js.map +1 -1
  119. package/dist/notifications/push-devices-setting.d.ts +20 -3
  120. package/dist/notifications/push-devices-setting.js +93 -33
  121. package/dist/notifications/push-devices-setting.js.map +1 -1
  122. package/dist/notifications/push-worker.d.ts +108 -0
  123. package/dist/notifications/push-worker.js +62 -0
  124. package/dist/notifications/push-worker.js.map +1 -0
  125. package/dist/notifications/quiet-hours-setting.d.ts +29 -17
  126. package/dist/notifications/quiet-hours-setting.js +9 -41
  127. package/dist/notifications/quiet-hours-setting.js.map +1 -1
  128. package/dist/notifications/time-zone-select.d.ts +5 -2
  129. package/dist/notifications/time-zone-select.js.map +1 -1
  130. package/dist/notifications/use-push-device.d.ts +4 -17
  131. package/dist/notifications/use-push-device.js +7 -15
  132. package/dist/notifications/use-push-device.js.map +1 -1
  133. package/dist/offline/local-first.d.ts +33 -0
  134. package/dist/offline/local-first.js +39 -0
  135. package/dist/offline/local-first.js.map +1 -0
  136. package/dist/offline/pwa-update-prompt-labels.d.ts +2 -0
  137. package/dist/offline/pwa-update-prompt-labels.js +9 -0
  138. package/dist/offline/pwa-update-prompt-labels.js.map +1 -0
  139. package/dist/offline/pwa-update-prompt.d.ts +32 -0
  140. package/dist/offline/pwa-update-prompt.js +36 -0
  141. package/dist/offline/pwa-update-prompt.js.map +1 -0
  142. package/dist/offline/query-persistence.d.ts +125 -0
  143. package/dist/offline/query-persistence.js +183 -0
  144. package/dist/offline/query-persistence.js.map +1 -0
  145. package/dist/offline/service-worker-updates.d.ts +46 -0
  146. package/dist/offline/service-worker-updates.js +39 -0
  147. package/dist/offline/service-worker-updates.js.map +1 -0
  148. package/dist/offline/shared-computer-labels.d.ts +2 -0
  149. package/dist/offline/shared-computer-labels.js +9 -0
  150. package/dist/offline/shared-computer-labels.js.map +1 -0
  151. package/dist/offline/shared-computer.d.ts +44 -0
  152. package/dist/offline/shared-computer.js +51 -0
  153. package/dist/offline/shared-computer.js.map +1 -0
  154. package/dist/offline/sign-out-pending-dialog-labels.d.ts +2 -0
  155. package/dist/offline/sign-out-pending-dialog-labels.js +14 -0
  156. package/dist/offline/sign-out-pending-dialog-labels.js.map +1 -0
  157. package/dist/offline/sign-out-pending-dialog.d.ts +58 -0
  158. package/dist/offline/sign-out-pending-dialog.js +66 -0
  159. package/dist/offline/sign-out-pending-dialog.js.map +1 -0
  160. package/dist/offline.d.ts +11 -0
  161. package/dist/offline.js +33 -0
  162. package/dist/offline.js.map +1 -0
  163. package/dist/push-worker.d.ts +2 -0
  164. package/dist/push-worker.js +14 -0
  165. package/dist/push-worker.js.map +1 -0
  166. package/dist/shell/account-menu-text-size.d.ts +33 -0
  167. package/dist/shell/account-menu-text-size.js +51 -0
  168. package/dist/shell/account-menu-text-size.js.map +1 -0
  169. package/dist/shell/admin-menu.d.ts +24 -0
  170. package/dist/shell/admin-menu.js +12 -3
  171. package/dist/shell/admin-menu.js.map +1 -1
  172. package/dist/shell/top-bar-actions.d.ts +6 -2
  173. package/dist/shell/top-bar-actions.js +1 -0
  174. package/dist/shell/top-bar-actions.js.map +1 -1
  175. package/dist/shell.d.ts +4 -0
  176. package/dist/shell.js +4 -0
  177. package/dist/shell.js.map +1 -1
  178. package/dist/sync/db.d.ts +76 -0
  179. package/dist/sync/db.js +24 -0
  180. package/dist/sync/db.js.map +1 -0
  181. package/dist/sync/engine.d.ts +124 -0
  182. package/dist/sync/engine.js +474 -0
  183. package/dist/sync/engine.js.map +1 -0
  184. package/dist/sync/hooks.d.ts +30 -0
  185. package/dist/sync/hooks.js +67 -0
  186. package/dist/sync/hooks.js.map +1 -0
  187. package/dist/sync/testing.d.ts +28 -0
  188. package/dist/sync/testing.js +41 -0
  189. package/dist/sync/testing.js.map +1 -0
  190. package/dist/sync/wire.d.ts +50 -0
  191. package/dist/sync/wire.js +2 -0
  192. package/dist/sync/wire.js.map +1 -0
  193. package/dist/sync.d.ts +7 -0
  194. package/dist/sync.js +22 -0
  195. package/dist/sync.js.map +1 -0
  196. package/dist/tour/tour-catalogue.d.ts +7 -1
  197. package/dist/tour/tour-catalogue.js.map +1 -1
  198. package/dist/tour/tour-labels.js +1 -0
  199. package/dist/tour/tour-labels.js.map +1 -1
  200. package/dist/tour/tour.d.ts +30 -0
  201. package/dist/tour/tour.js +54 -8
  202. package/dist/tour/tour.js.map +1 -1
  203. package/dist/tour/use-tour-progress.d.ts +99 -1
  204. package/dist/tour/use-tour-progress.js +228 -12
  205. package/dist/tour/use-tour-progress.js.map +1 -1
  206. package/dist/tour.d.ts +2 -0
  207. package/dist/tour.js +2 -0
  208. package/dist/tour.js.map +1 -1
  209. package/package.json +43 -1
  210. package/src/admin/admin-action-log-labels.ts +9 -0
  211. package/src/admin/admin-action-log.tsx +41 -5
  212. package/src/admin/admin-area-labels.ts +20 -0
  213. package/src/admin/admin-overview.tsx +37 -2
  214. package/src/admin/mail-test-card-labels.ts +19 -0
  215. package/src/admin/mail-test-card.tsx +190 -0
  216. package/src/components/appearance-labels.ts +16 -0
  217. package/src/components/data-table-column-settings.tsx +2 -2
  218. package/src/components/data-table-head.tsx +2 -2
  219. package/src/components/data-table-mobile.tsx +26 -3
  220. package/src/components/data-table-row-actions.tsx +118 -2
  221. package/src/components/data-table-shared.tsx +39 -3
  222. package/src/components/data-table.tsx +52 -21
  223. package/src/components/date-format-setting.tsx +64 -0
  224. package/src/components/swipeable-row.tsx +8 -0
  225. package/src/components/tabs.tsx +4 -3
  226. package/src/components/translation-review-editor.tsx +2 -0
  227. package/src/components/translation-review-labels.ts +7 -0
  228. package/src/components/translation-review.tsx +102 -14
  229. package/src/components/write-lock-labels.ts +1 -0
  230. package/src/components/write-lock.tsx +36 -3
  231. package/src/data-table.ts +1 -0
  232. package/src/feedback/feedback-row-detail.tsx +18 -23
  233. package/src/feedback/feedback-swipe.tsx +28 -1
  234. package/src/i18n/defaults.ts +8 -0
  235. package/src/i18n/german.ts +90 -4
  236. package/src/i18n/kit-labels.tsx +31 -1
  237. package/src/i18n/locales/en.ts +13 -0
  238. package/src/i18n/locales/es.ts +90 -4
  239. package/src/i18n/locales/fr.ts +90 -4
  240. package/src/i18n/locales/hu.ts +86 -4
  241. package/src/i18n/locales/it.ts +90 -4
  242. package/src/i18n/locales/zh.ts +84 -2
  243. package/src/i18n/review.ts +15 -1
  244. package/src/index.ts +45 -0
  245. package/src/lib/date-format.ts +151 -0
  246. package/src/lib/format.ts +23 -6
  247. package/src/notifications/device-zone-report.ts +54 -0
  248. package/src/notifications/feedback-notification-types-labels.ts +10 -1
  249. package/src/notifications/feedback-notification-types.ts +12 -5
  250. package/src/notifications/notification-bell.tsx +116 -0
  251. package/src/notifications/notification-inbox.tsx +304 -0
  252. package/src/notifications/notification-types-setting.tsx +151 -55
  253. package/src/notifications/notification-types.ts +20 -8
  254. package/src/notifications/notifications-labels.ts +69 -11
  255. package/src/notifications/push-devices-setting.tsx +123 -34
  256. package/src/notifications/push-worker.ts +160 -0
  257. package/src/notifications/quiet-hours-setting.tsx +41 -58
  258. package/src/notifications/time-zone-select.tsx +5 -2
  259. package/src/notifications/use-push-device.ts +20 -33
  260. package/src/offline/local-first.ts +76 -0
  261. package/src/offline/pwa-update-prompt-labels.ts +9 -0
  262. package/src/offline/pwa-update-prompt.tsx +72 -0
  263. package/src/offline/query-persistence.ts +356 -0
  264. package/src/offline/service-worker-updates.ts +81 -0
  265. package/src/offline/shared-computer-labels.ts +8 -0
  266. package/src/offline/shared-computer.tsx +87 -0
  267. package/src/offline/sign-out-pending-dialog-labels.ts +21 -0
  268. package/src/offline/sign-out-pending-dialog.tsx +135 -0
  269. package/src/offline.ts +32 -0
  270. package/src/push-worker.ts +25 -0
  271. package/src/shell/account-menu-text-size.tsx +77 -0
  272. package/src/shell/admin-menu.tsx +40 -3
  273. package/src/shell/top-bar-actions.tsx +6 -1
  274. package/src/shell.ts +7 -0
  275. package/src/sync/db.ts +103 -0
  276. package/src/sync/engine.ts +699 -0
  277. package/src/sync/hooks.ts +93 -0
  278. package/src/sync/testing.ts +73 -0
  279. package/src/sync/wire.ts +61 -0
  280. package/src/sync.ts +29 -0
  281. package/src/tour/tour-catalogue.tsx +7 -1
  282. package/src/tour/tour-labels.ts +1 -0
  283. package/src/tour/tour.tsx +105 -9
  284. package/src/tour/use-tour-progress.ts +378 -12
  285. package/src/tour.ts +3 -0
@@ -1,12 +1,13 @@
1
1
  import { isValidElement, useCallback, useMemo } from "react";
2
2
  import type { ReactNode } from "react";
3
3
  import { Pencil, Trash2 } from "lucide-react";
4
- import { RowActions, type RowAction } from "./row-actions";
4
+ import { RowActions, type RowAction, type RowActionTone } from "./row-actions";
5
5
  import type { SwipeAction } from "./swipeable-row";
6
6
  import type { DataTableLabels } from "./data-table-labels";
7
7
  import type {
8
8
  DataTableColumn,
9
9
  DataTableDensity,
10
+ DataTableDesktopSwipeActions,
10
11
  DataTableProps,
11
12
  DataTableRowAction,
12
13
  MobileSwipeActions,
@@ -20,6 +21,66 @@ import type {
20
21
  * column-settings panel. */
21
22
  export const ROW_ACTIONS_KEY = "__rowActions";
22
23
 
24
+ /** The key of the column derived from `mobileSwipeActions` (0.46, dev#598) — the
25
+ * table's own too. */
26
+ export const SWIPE_ACTIONS_KEY = "__swipeActions";
27
+
28
+ /** A column the TABLE adds, not the caller: out of the column settings and the count,
29
+ * never resized, never the totals row's header. */
30
+ export function isTableOwnColumn(key: string): boolean {
31
+ return key === ROW_ACTIONS_KEY || key === SWIPE_ACTIONS_KEY;
32
+ }
33
+
34
+ /** A curtain's tone, as an icon button's (keksdose's `toneOf`): only the tones that MEAN
35
+ * something on a quiet icon carry over; a status colour, a hue or an app's `paint` stays
36
+ * on the curtain and reads muted. */
37
+ function swipeTone(action: SwipeAction): RowActionTone {
38
+ switch (action.tone) {
39
+ case "danger":
40
+ case "warning":
41
+ case "info":
42
+ return action.tone;
43
+ default:
44
+ return "muted";
45
+ }
46
+ }
47
+
48
+ /**
49
+ * A row's swipe plan as desktop row actions (dev#598): `end` then `start` — the physical
50
+ * `right` / `left` only where the logical side is missing, as the card reads them — each
51
+ * action once, told apart by its `id`, else by its label (two stages with one label would
52
+ * be one button twice).
53
+ */
54
+ export function swipePlanRowActions<T>(
55
+ plan: MobileSwipeActions | null | undefined,
56
+ row: T,
57
+ options: Pick<DataTableDesktopSwipeActions<T>, "omit" | "commit"> = {},
58
+ ): RowAction[] {
59
+ if (!plan) return [];
60
+ const out: RowAction[] = [];
61
+ const seen = new Set<string>();
62
+ for (const action of [...(plan.end ?? plan.right ?? []), ...(plan.start ?? plan.left ?? [])]) {
63
+ const key = action.id ?? action.label;
64
+ if (seen.has(key)) continue;
65
+ seen.add(key);
66
+ if (options.omit?.(action, row)) continue;
67
+ const commit = typeof options.commit === "function" ? options.commit(action, row) : options.commit;
68
+ out.push({
69
+ key,
70
+ label: action.label,
71
+ icon: isValidElement(action.icon)
72
+ ? action.icon
73
+ : action.icon != null && action.icon !== false
74
+ ? <>{action.icon}</>
75
+ : undefined,
76
+ tone: swipeTone(action),
77
+ commit,
78
+ onSelect: action.onCommit,
79
+ });
80
+ }
81
+ return out;
82
+ }
83
+
23
84
  interface ResolvedRowAction<T> {
24
85
  key: string;
25
86
  label: string;
@@ -81,12 +142,16 @@ export function useRowActionColumn<T>({
81
142
  rowName,
82
143
  labels,
83
144
  density,
145
+ mobileSwipeActions,
146
+ desktopSwipeActions,
84
147
  }: {
85
148
  columns: DataTableColumn<T>[];
86
149
  rowActions: DataTableProps<T>["rowActions"];
87
150
  rowName: DataTableProps<T>["rowName"];
88
151
  labels: DataTableLabels;
89
152
  density: DataTableDensity;
153
+ mobileSwipeActions?: DataTableProps<T>["mobileSwipeActions"];
154
+ desktopSwipeActions?: DataTableProps<T>["desktopSwipeActions"];
90
155
  }): {
91
156
  columns: DataTableColumn<T>[];
92
157
  swipesFromActions: (row: T) => MobileSwipeActions | null;
@@ -100,7 +165,54 @@ export function useRowActionColumn<T>({
100
165
  : [],
101
166
  [rowActions, labels],
102
167
  );
103
- const columns = useMemo<DataTableColumn<T>[]>(
168
+ // dev#598: the desktop half of the caller's swipes. On by default, except beside
169
+ // `rowActions`, whose column already is the desktop's (and whose swipes these usually
170
+ // are). The options object is often a fresh literal per render; its parts are the deps.
171
+ const swipeOptions = typeof desktopSwipeActions === "object" ? desktopSwipeActions : undefined;
172
+ const swipeColumnOn =
173
+ !!mobileSwipeActions && (desktopSwipeActions === undefined ? !rowActions : desktopSwipeActions !== false);
174
+ const swipeOmit = swipeOptions?.omit;
175
+ const swipeName = swipeOptions?.name;
176
+ const swipeCollapse = swipeOptions?.collapse;
177
+ const swipeCommit = swipeOptions?.commit;
178
+ const swipeColumn = useMemo<DataTableColumn<T> | null>(
179
+ () =>
180
+ swipeColumnOn
181
+ ? {
182
+ key: SWIPE_ACTIONS_KEY,
183
+ header: <span className="sr-only">{labels.actions}</span>,
184
+ cell: (row) => (
185
+ <RowActions
186
+ actions={swipePlanRowActions(mobileSwipeActions!(row), row, { omit: swipeOmit, commit: swipeCommit })}
187
+ name={(swipeName ?? rowName)?.(row)}
188
+ collapse={swipeCollapse}
189
+ size={density === "compact" ? "2xs" : "xs"}
190
+ // The last column's tooltips open toward the row's start.
191
+ tooltipSide="start"
192
+ labels={{ actions: labels.actions }}
193
+ className="justify-end"
194
+ />
195
+ ),
196
+ className: "w-px whitespace-nowrap text-end",
197
+ headClassName: "w-px",
198
+ // On a phone the same actions are the card's swipes.
199
+ mobileHidden: true,
200
+ noRowLink: true,
201
+ }
202
+ : null,
203
+ [
204
+ swipeColumnOn,
205
+ mobileSwipeActions,
206
+ swipeOmit,
207
+ swipeCommit,
208
+ swipeName,
209
+ rowName,
210
+ swipeCollapse,
211
+ density,
212
+ labels.actions,
213
+ ],
214
+ );
215
+ const actionColumns = useMemo<DataTableColumn<T>[]>(
104
216
  () =>
105
217
  rowActions
106
218
  ? [
@@ -132,6 +244,10 @@ export function useRowActionColumn<T>({
132
244
  : columnsProp,
133
245
  [columnsProp, rowActions, rowActionsFor, rowName, labels.actions, density],
134
246
  );
247
+ const columns = useMemo(
248
+ () => (swipeColumn ? [...actionColumns, swipeColumn] : actionColumns),
249
+ [actionColumns, swipeColumn],
250
+ );
135
251
  const swipesFromActions = (row: T): MobileSwipeActions | null => {
136
252
  const swipes = rowActionsFor(row).filter((a) => a.swipe && !a.action.disabledReason?.(row));
137
253
  if (!swipes.length) return null;
@@ -1,5 +1,5 @@
1
1
  import type { CSSProperties, ReactNode } from "react";
2
- import { ROW_ACTIONS_KEY } from "./data-table-row-actions";
2
+ import { isTableOwnColumn } from "./data-table-row-actions";
3
3
  import { DEFAULT_DATA_TABLE_LABELS, type DataTableLabels } from "./data-table-labels";
4
4
  import type { DataTableCellProps, DataTableColumn, DataTableHeadProps } from "./data-table";
5
5
  import type { LabelOverride } from "../i18n/kit-labels";
@@ -79,7 +79,7 @@ export function cleanAttrs(
79
79
  * should pass a string header, and that is visible in what gets announced.
80
80
  */
81
81
  export function columnLabel<T>(col: DataTableColumn<T>, labels?: DataTableLabels): string {
82
- if (col.key === ROW_ACTIONS_KEY && labels) return labels.actions;
82
+ if (isTableOwnColumn(col.key) && labels) return labels.actions;
83
83
  return col.headerText ?? (typeof col.header === "string" ? col.header : col.key);
84
84
  }
85
85
 
@@ -147,6 +147,37 @@ export function fromOutside(target: EventTarget | null, row: HTMLElement): boole
147
147
  return !(target instanceof Node) || !row.contains(target);
148
148
  }
149
149
 
150
+ /**
151
+ * What counts as a control of its own inside a clickable row: a click or key press
152
+ * that lands on one of these belongs to it, not to the row's `onRowClick`. The row's
153
+ * own link (`rowHref`, marked `data-row-link`) is the exception — it IS the row.
154
+ */
155
+ const OWN_CONTROL =
156
+ '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"])';
157
+
158
+ /** Whether an event on `row` started on a control nested inside it. */
159
+ export function fromOwnControl(target: EventTarget | null, row: HTMLElement): boolean {
160
+ if (!(target instanceof Element)) return false;
161
+ const hit = target.closest(OWN_CONTROL);
162
+ return !!hit && hit !== row && row.contains(hit) && !hit.hasAttribute("data-row-link");
163
+ }
164
+
165
+ /**
166
+ * The controls whose own default IS the click — a native checkbox's or radio's toggle, a
167
+ * label's click through to its field, a summary's disclosure (0.46, the 0.45 note from
168
+ * UM): cancelling the click undoes it. Each is the click's own activation target, so a
169
+ * click on one does not follow the link around it, and {@link RowLink} leaves its
170
+ * default alone.
171
+ */
172
+ const OWN_DEFAULT = 'input[type="checkbox"],input[type="radio"],label,summary';
173
+
174
+ /** Whether a click inside `link` landed on (or in) one of {@link OWN_DEFAULT}. */
175
+ function onOwnDefault(target: EventTarget | null, link: HTMLElement): boolean {
176
+ if (!(target instanceof Element)) return false;
177
+ const hit = target.closest(OWN_DEFAULT);
178
+ return !!hit && hit !== link && link.contains(hit);
179
+ }
180
+
150
181
  /** A click the APP owns. Anything else — middle, ⌘/Ctrl, Shift, Alt — belongs to
151
182
  * the browser, and the whole feature is not touching it. */
152
183
  const isPlainLeftClick = (e: React.MouseEvent) =>
@@ -205,6 +236,9 @@ export function RowLink({
205
236
  // A click in a portal a descendant opened is not a click on the link: leave
206
237
  // it (and a link inside it) alone. See `fromOutside`.
207
238
  if (fromOutside(e.target, e.currentTarget)) return;
239
+ // A checkbox, a radio, a label or a summary keeps its own default: cancelling
240
+ // it would undo the toggle, and the link is not followed from it anyway.
241
+ if (onOwnDefault(e.target, e.currentTarget)) return;
208
242
  if (isPlainLeftClick(e)) e.preventDefault();
209
243
  }}
210
244
  onClick={(e) => {
@@ -215,7 +249,9 @@ export function RowLink({
215
249
  }
216
250
  // Already cancelled above; this phase only decides what opens. A nested
217
251
  // control that stopped propagation deliberately never reaches it, which is
218
- // exactly how the row behaved before it became a link.
252
+ // exactly how the row behaved before it became a link — and one that did not
253
+ // (a select in a phone card's cell, keksdose live #428) is still its own.
254
+ if (fromOwnControl(e.target, e.currentTarget)) return;
219
255
  onActivate?.();
220
256
  }}
221
257
  >
@@ -2,7 +2,7 @@ import { Fragment, useEffect, useId, useLayoutEffect, useMemo, useRef, useState
2
2
  import type { ReactNode, TdHTMLAttributes, ThHTMLAttributes } from "react";
3
3
  import { Card } from "./card";
4
4
  import { Spinner } from "./spinner";
5
- import { ROW_ACTIONS_KEY, useRowActionColumn } from "./data-table-row-actions";
5
+ import { isTableOwnColumn, useRowActionColumn } from "./data-table-row-actions";
6
6
  import { useColumnResize } from "./data-table-resize";
7
7
  import { useRangeSelection } from "./data-table-selection";
8
8
  import {
@@ -10,6 +10,7 @@ import {
10
10
  cleanAttrs,
11
11
  columnLabel,
12
12
  fromOutside,
13
+ fromOwnControl,
13
14
  logicalAlign,
14
15
  resolveDataTableLabels,
15
16
  splitCellProps,
@@ -25,6 +26,8 @@ import { nextSorts } from "./data-table-sort";
25
26
  import type { SortCycle, SortDir, SortState } from "./data-table-sort";
26
27
  import { Pagination } from "./data-table-pagination";
27
28
  import type { SwipeAction } from "./swipeable-row";
29
+ import type { RowActionsCollapse } from "./row-actions";
30
+ import type { CommitScope } from "./write-lock";
28
31
  import { FullBleedDialog } from "./full-bleed-dialog";
29
32
  import { usePhoneLayout } from "../hooks/use-breakpoint";
30
33
  import { useAnnounce } from "../hooks/use-announce";
@@ -471,9 +474,28 @@ export interface DataTableProps<T> {
471
474
  * Mobile only, and deliberately so: the desktop table already has room for an
472
475
  * actions column, and a drag gesture on a pointer device is a worse version of a
473
476
  * button. Wraps only the row body, so an expansion panel below stays put while the
474
- * row above it slides.
477
+ * row above it slides. The desktop's buttons for the same actions come with it — see
478
+ * {@link DataTableProps.desktopSwipeActions}.
475
479
  */
476
480
  mobileSwipeActions?: (row: T) => MobileSwipeActions | null;
481
+ /**
482
+ * The desktop half of `mobileSwipeActions` (0.46, keksdose dev#598 — Marcel: every
483
+ * table whose phone cards swipe gets a desktop actions column with the same actions):
484
+ * an actions column DERIVED from the very plan the card swipes with, so the two
485
+ * viewports offer the same functions and cannot drift. Each button runs the swipe's own
486
+ * `onCommit` — the same confirm, the same undo toast. The reading-end side's actions
487
+ * first, then the start side's, each once (by `SwipeAction.id`, else by label); a row
488
+ * whose plan is `null` gets an empty cell. The column is hidden on the phone, where the
489
+ * actions are the swipes, kept off the column settings, and its tooltips open toward
490
+ * the row's start.
491
+ *
492
+ * On by default wherever `mobileSwipeActions` is given, so a new swipeable table cannot
493
+ * forget it — except beside `rowActions`, whose column is the desktop's already (pass
494
+ * `true` or options to add this one too). `false` where the desktop row carries every
495
+ * one of those actions as its own buttons; say so at the call site. Options:
496
+ * {@link DataTableDesktopSwipeActions}.
497
+ */
498
+ desktopSwipeActions?: boolean | DataTableDesktopSwipeActions<T>;
477
499
  /**
478
500
  * Renders as a full-width row above the data rows in the desktop table
479
501
  * (mobile has no equivalent list-row slot, so it's desktop-only). For a
@@ -662,6 +684,27 @@ export interface MobileSwipeActions {
662
684
  right?: SwipeAction[];
663
685
  }
664
686
 
687
+ /** How {@link DataTableProps.desktopSwipeActions} draws the column (0.46, dev#598). */
688
+ export interface DataTableDesktopSwipeActions<T> {
689
+ /** Leave an action out because the row already has its own button for it — keksdose's
690
+ * feedback inbox, whose status cell offers `advance` and `wont_do`
691
+ * (`feedbackSwipeOmitShown`). Match on `action.id` rather than the translated label. */
692
+ omit?: (action: SwipeAction, row: T) => boolean;
693
+ /** The row's name for the "⋯" menu ("Actions for …"). Default: the table's `rowName`. */
694
+ name?: (row: T) => string | null | undefined;
695
+ /** {@link RowActionsCollapse}; default `"auto"`. A table whose rows offer more than two
696
+ * actions passes `"menu"`, so the column does not switch between icons and a menu. */
697
+ collapse?: RowActionsCollapse;
698
+ /**
699
+ * Whether an action COMMITS, refused with the reason under a locked
700
+ * `WriteLockProvider` ({@link CommitScope}) — a swipe has no such state, so a plan
701
+ * usually returns nothing for a locked row and the desktop shows nothing either; with
702
+ * this the plan can stay and the desktop says why. Per action as a function. Default:
703
+ * no commit scope (the plan decides).
704
+ */
705
+ commit?: CommitScope | ((action: SwipeAction, row: T) => CommitScope | undefined);
706
+ }
707
+
665
708
  /**
666
709
  * A totals-row cell: pinned to the scroller's bottom edge, on the header's surface.
667
710
  * The rule above it is an inset shadow rather than a `border-t`: the table collapses
@@ -702,21 +745,6 @@ export const DEFAULT_DATA_TABLE_SORT_LABELS: DataTableSortLabels = {
702
745
 
703
746
  const flipDir = (d: SortDir): SortDir => (d === "asc" ? "desc" : "asc");
704
747
 
705
- /**
706
- * What counts as a control of its own inside a clickable row: a click or key press
707
- * that lands on one of these belongs to it, not to the row's `onRowClick`. The row's
708
- * own link (`rowHref`, marked `data-row-link`) is the exception — it IS the row.
709
- */
710
- const OWN_CONTROL =
711
- '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"])';
712
-
713
- /** Whether an event on `row` started on a control nested inside it. */
714
- function fromOwnControl(target: EventTarget | null, row: HTMLElement): boolean {
715
- if (!(target instanceof Element)) return false;
716
- const hit = target.closest(OWN_CONTROL);
717
- return !!hit && hit !== row && row.contains(hit) && !hit.hasAttribute("data-row-link");
718
- }
719
-
720
748
  /** Arrow/Home/End between the focusable rows of one `<tbody>` — the roving half of
721
749
  * the keyboard-activatable rows (see the row's `onKeyDown`). */
722
750
  function moveRowFocus(from: HTMLElement, key: string): boolean {
@@ -788,6 +816,7 @@ export function DataTable<T>({
788
816
  mobileCard,
789
817
  mobileSort = false,
790
818
  mobileSwipeActions,
819
+ desktopSwipeActions,
791
820
  leadingRow,
792
821
  density = "comfortable",
793
822
  sortCycle = "tri",
@@ -834,6 +863,8 @@ export function DataTable<T>({
834
863
  rowName,
835
864
  labels,
836
865
  density,
866
+ mobileSwipeActions,
867
+ desktopSwipeActions,
837
868
  });
838
869
  const swipeActionsFor = mobileSwipeActions ?? (rowActions ? swipesFromActions : undefined);
839
870
 
@@ -1144,16 +1175,16 @@ export function DataTable<T>({
1144
1175
  // show — so hiding the "Account" column moves "Total" into "Name" rather than
1145
1176
  // dropping it — and on the first visible column when every one carries a value.
1146
1177
  const footerLabelCol = showFooter
1147
- ? (visibleColumns.find((c) => !c.footer && c.key !== ROW_ACTIONS_KEY) ?? visibleColumns[0])
1178
+ ? (visibleColumns.find((c) => !c.footer && !isTableOwnColumn(c.key)) ?? visibleColumns[0])
1148
1179
  : undefined;
1149
1180
  const footerValue = (col: DataTableColumn<T>) => col.footer?.(sorted, slice);
1150
1181
  const footerHeadingId = useId();
1151
1182
  // Each open row's detail row, for its disclosure button's `aria-controls`.
1152
1183
  const detailIdBase = useId();
1153
1184
  // The actions column is the table's own, not one the user can hide: out of the count.
1154
- const columnsCountLabel = rowActions
1155
- ? labels.columnsCount(visibleCount - 1, totalCount - 1)
1156
- : labels.columnsCount(visibleCount, totalCount);
1185
+ const ownColumns = columns.filter((c) => isTableOwnColumn(c.key)).length;
1186
+ const ownVisible = visibleColumns.filter((c) => isTableOwnColumn(c.key)).length;
1187
+ const columnsCountLabel = labels.columnsCount(visibleCount - ownVisible, totalCount - ownColumns);
1157
1188
  // Match the `md:` breakpoint at the text size in force: we render either the table or
1158
1189
  // the card list — never both — so we don't double up DOM nodes that screen readers and
1159
1190
  // integration tests would have to disambiguate. `usePhoneLayout()` itself, not its twin
@@ -0,0 +1,64 @@
1
+ import type { ReactNode } from "react";
2
+ import { Select } from "./select";
3
+ import type { SelectProps } from "./select";
4
+ import { useAppearanceLabels } from "./appearance-labels";
5
+ import type { AppearanceLabels } from "./appearance-labels";
6
+ import { useKitLocale } from "../i18n/kit-labels";
7
+ import type { LabelOverride } from "../i18n/kit-labels";
8
+ import { DATE_FORMAT_PREFERENCES, dateFormatSample } from "../lib/date-format";
9
+ import type { DateFormatPreference } from "../lib/date-format";
10
+
11
+ /** A {@link Select}'s props minus the value, the change handler and the options, which
12
+ * this owns. */
13
+ export interface DateFormatSettingProps extends Omit<SelectProps, "value" | "onChange" | "children" | "multiple"> {
14
+ value: DateFormatPreference;
15
+ onChange: (value: DateFormatPreference) => void;
16
+ /** The field's label. Default: the kit's `appearance.dateFormat` ("Date format"). */
17
+ label?: ReactNode;
18
+ /** The line under it. Default: the kit's `appearance.dateFormatHelp`; `null` for none. */
19
+ hint?: ReactNode;
20
+ /** The interface language the samples are rendered for — `"auto"`'s sample is that
21
+ * language's shape. Default: the provider's locale. */
22
+ language?: string;
23
+ /** The `appearance` namespace, for this instance. */
24
+ labels?: LabelOverride<AppearanceLabels>;
25
+ }
26
+
27
+ /**
28
+ * The date format (0.46, keksdose's Settings ▸ Appearance select, feedback #180): "Follow
29
+ * language" and four explicit shapes, each option labelled by the date it produces —
30
+ * 01.08.2026, 01/08/2026, 08/01/2026, 2026-08-01 — rendered live by the resolver the
31
+ * app's dates go through ({@link dateFormatSample}), and `auto` as "Follow language —
32
+ * 01 Aug 2026". The words are the kit's `appearance` namespace; the value and where it
33
+ * is kept are the app's (see `formatDateWithPreference` and `dateFormatterFor`).
34
+ */
35
+ export function DateFormatSetting({
36
+ value,
37
+ onChange,
38
+ label,
39
+ hint,
40
+ language,
41
+ labels: labelsProp,
42
+ ...rest
43
+ }: DateFormatSettingProps) {
44
+ const labels = useAppearanceLabels(labelsProp);
45
+ const sampleLanguage = useKitLocale(language);
46
+ return (
47
+ <Select
48
+ {...rest}
49
+ label={label ?? labels.dateFormat}
50
+ hint={hint === null ? undefined : (hint ?? labels.dateFormatHelp)}
51
+ value={value}
52
+ onChange={(e) => onChange(e.target.value as DateFormatPreference)}
53
+ >
54
+ {DATE_FORMAT_PREFERENCES.map((preference) => {
55
+ const sample = dateFormatSample(preference, sampleLanguage);
56
+ return (
57
+ <option key={preference} value={preference}>
58
+ {preference === "auto" ? `${labels.dateFormatAuto} — ${sample}` : sample}
59
+ </option>
60
+ );
61
+ })}
62
+ </Select>
63
+ );
64
+ }
@@ -31,6 +31,14 @@ export interface SwipePaint {
31
31
  interface SwipeActionBase {
32
32
  onCommit: () => void;
33
33
  label: string;
34
+ /**
35
+ * What the action IS, apart from its words (0.46, keksdose dev#598): `"approve"`,
36
+ * `"wont_do"`. The swipe itself never reads it; DataTable's desktop column does — one
37
+ * button per id, and `desktopSwipeActions.omit` can name the actions the row already
38
+ * has a button for without matching on a translated label. The kit's own plans
39
+ * (`feedbackSwipePlan`, `TranslationReviewPanel`'s binding) set their binding's ids.
40
+ */
41
+ id?: string;
34
42
  /** Rendered inside the panel. Icons stay the consumer's choice — this package
35
43
  * ships no opinion about which icon set an app uses. */
36
44
  icon?: ReactNode;
@@ -78,7 +78,7 @@ export interface TabsProps<T extends string>
78
78
  onChange: (id: T) => void;
79
79
  className?: string;
80
80
  /**
81
- * The caret beside a SCROLLING strip below `md` (0.40, kk run 79 gap 1): a › while
81
+ * The caret beside a SCROLLING strip (0.40, kk run 79 gap 1): a › while
82
82
  * tabs lie past the strip's end — a tap scrolls it on by three quarters of its width —
83
83
  * turning into a ‹ at the end, which goes back to the first. Shown only while the strip
84
84
  * actually overflows.
@@ -88,7 +88,8 @@ export interface TabsProps<T extends string>
88
88
  * off (keksdose live #410, whose `tab-strip-more.tsx` this replaces). One button that
89
89
  * turns round rather than two that come and go, so the strip's width never changes
90
90
  * under the thumb. Out of the tab order — the keyboard walks the strip with the arrow
91
- * keys, which bring the focused tab into view. From `md` up nothing changes. Ignored on
91
+ * keys, which bring the focused tab into view. At every width since 0.46 (keksdose live
92
+ * #410 round 3: eleven report tabs overflow a laptop too); before, `md:hidden`. Ignored on
92
93
  * a vertical strip (a side nav is a column, not a row). Names:
93
94
  * `tabs.more` / `tabs.backToFirst`.
94
95
  */
@@ -269,7 +270,7 @@ export function Tabs<T extends string>({
269
270
  };
270
271
  const cueButton = cue ? (
271
272
  // The strip's bottom rule continues under the caret, so the row reads as one.
272
- <div data-tabs-overflow-cue="" className="flex shrink-0 items-center self-stretch border-b border-subtle md:hidden">
273
+ <div data-tabs-overflow-cue="" className="flex shrink-0 items-center self-stretch border-b border-subtle">
273
274
  <IconButton
274
275
  size="sm"
275
276
  variant="ghost"
@@ -221,6 +221,8 @@ export function TranslationReviewEditor({
221
221
  <>
222
222
  <Textarea
223
223
  ref={wordingRef}
224
+ // The panel's "Needs a change" swipe on an already open row finds it here (#416).
225
+ data-review-wording=""
224
226
  label={missing ? labels.translation : labels.suggestion}
225
227
  lang={row.locale}
226
228
  rows={Math.min(8, Math.max(2, Math.ceil(Math.max(row.text.length, row.reference.length) / 80)))}
@@ -52,6 +52,11 @@ export interface TranslationReviewLabels {
52
52
  flag: string;
53
53
  /** "Needs a change" for a MISSING string: the suggestion is the translation to add. */
54
54
  suggest: string;
55
+ /** 0.46 (keksdose B2/S3): the desktop row's button that OPENS the editor to send a
56
+ * string back — its own name, apart from the editor's `flag`, which saves. */
57
+ flagRow: string;
58
+ /** The same for a MISSING string: opens the editor to add the translation. */
59
+ suggestRow: string;
55
60
  /** Clear a verdict: back to unreviewed. */
56
61
  reset: string;
57
62
  /** The editor's way out without saving. */
@@ -126,6 +131,8 @@ export const DEFAULT_TRANSLATION_REVIEW_LABELS: TranslationReviewLabels = {
126
131
  approve: "Approve",
127
132
  flag: "Needs a change",
128
133
  suggest: "Suggest translation",
134
+ flagRow: "Flag for a change…",
135
+ suggestRow: "Add a translation…",
129
136
  reset: "Mark unreviewed",
130
137
  cancel: "Cancel",
131
138
  approveSelected: "Approve selected",