@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
@@ -72,7 +72,11 @@ export type AdminActionKind =
72
72
  | "group_rename"
73
73
  | "group_delete"
74
74
  | "membership_add"
75
- | "membership_role";
75
+ | "membership_role"
76
+ // 0.45 (admin §9.1.3): a group switched off or on again — kastlan's company PATCH
77
+ // (`is_active`). server-kit 0.15's `AdminAction.GROUP_DEACTIVATE` / `GROUP_REACTIVATE`.
78
+ | "group_deactivate"
79
+ | "group_reactivate";
76
80
 
77
81
  /** Every {@link AdminActionKind}, in the contract's order. */
78
82
  export const ADMIN_ACTION_KINDS: readonly AdminActionKind[] = [
@@ -98,6 +102,8 @@ export const ADMIN_ACTION_KINDS: readonly AdminActionKind[] = [
98
102
  "group_delete",
99
103
  "membership_add",
100
104
  "membership_role",
105
+ "group_deactivate",
106
+ "group_reactivate",
101
107
  ];
102
108
 
103
109
  /** The app's word for its groups (§11.5): Kurvenschmiede's teams, kastlan's companies,
@@ -105,11 +111,14 @@ export const ADMIN_ACTION_KINDS: readonly AdminActionKind[] = [
105
111
  * actions are said in. */
106
112
  export type AdminGroupNoun = "group" | "team" | "company" | "budget";
107
113
 
108
- /** The kinds said in the noun's words: the five of 0.34, and `membership_remove`. */
114
+ /** The kinds said in the noun's words: the five of 0.34, `membership_remove`, and 0.45's
115
+ * `group_deactivate` / `group_reactivate`. */
109
116
  type AdminGroupActionKind =
110
117
  | "group_create"
111
118
  | "group_rename"
112
119
  | "group_delete"
120
+ | "group_deactivate"
121
+ | "group_reactivate"
113
122
  | "membership_add"
114
123
  | "membership_role"
115
124
  | "membership_remove";
@@ -118,6 +127,8 @@ const GROUP_ACTION_KINDS: ReadonlySet<string> = new Set<AdminGroupActionKind>([
118
127
  "group_create",
119
128
  "group_rename",
120
129
  "group_delete",
130
+ "group_deactivate",
131
+ "group_reactivate",
121
132
  "membership_add",
122
133
  "membership_role",
123
134
  "membership_remove",
@@ -184,6 +195,13 @@ export interface AdminActionLogEntry {
184
195
  group?: AdminGroupTarget | null;
185
196
  /** The `detail` in the app's words — "Member → Admin", "12 items to Ben Example". */
186
197
  detail?: ReactNode;
198
+ /**
199
+ * 0.45 (admin §9.4, Marcel 2026-10-19): a `group_rename` row's `detail.{from, to}` —
200
+ * kastlan's company rename logs both names. With both, the row reads "Company renamed
201
+ * from A to B" (`renamedFromTo`) in place of the target. A rename that logs `{}` (KS's
202
+ * team rename: never a name) leaves it out and reads "Team renamed" and the group.
203
+ */
204
+ renamed?: { from?: string | null; to?: string | null } | null;
187
205
  }
188
206
 
189
207
  /** The `adminActionLog` namespace. */
@@ -206,6 +224,8 @@ export interface AdminActionLogLabels {
206
224
  showAll: string;
207
225
  /** A target name as a filter button — "Show only the actions on Ada Example". */
208
226
  filterBy: (target: string) => string;
227
+ /** 0.45: after "Company renamed" when the row carries both names — "from A to B". */
228
+ renamedFromTo: (from: string, to: string) => string;
209
229
  /** What each action is called, past tense, keyed by the vocabulary. An app adds its
210
230
  * own here (`labels={{ actions: { plan: "Plan changed" } }}`). A word given here
211
231
  * outranks the noun's in `groupActions` — so no catalogue holds the group kinds
@@ -223,6 +243,11 @@ interface AdminGroupActionLabels {
223
243
  group_rename: string;
224
244
  /** "Team deleted". */
225
245
  group_delete: string;
246
+ /** "Company deactivated" (0.45, admin §9.1.3): the group switched off — its people
247
+ * locked out — not a person's `deactivate`. */
248
+ group_deactivate: string;
249
+ /** "Company reactivated" (0.45). */
250
+ group_reactivate: string;
226
251
  /** "Added to the team" — said of the person, so it agrees with the account where a
227
252
  * language agrees (es "Añadida", fr "Ajouté"). */
228
253
  membership_add: string;
@@ -384,12 +409,22 @@ export function AdminActionLog({
384
409
  aria-busy={loading || undefined}
385
410
  className={cn("divide-y divide-subtle", loading && "opacity-60")}
386
411
  >
387
- {entries.map((entry) => (
412
+ {entries.map((entry) => {
413
+ const from = (entry.renamed?.from ?? "").trim();
414
+ const to = (entry.renamed?.to ?? "").trim();
415
+ const renamed = entry.action === "group_rename" && from !== "" && to !== "";
416
+ return (
388
417
  <li key={entry.id} data-action={entry.action} className="space-y-0.5 py-2 first:pt-0 last:pb-0">
389
418
  <div className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5">
390
419
  <div className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5 text-sm">
391
420
  <span className="font-medium text-primary">{actionText(entry.action)}</span>
392
- {entry.target !== undefined && <span className="min-w-0 break-words">{targetNode(entry)}</span>}
421
+ {renamed ? (
422
+ <span data-private data-renamed className="min-w-0 break-words text-primary">
423
+ {labels.renamedFromTo(from, to)}
424
+ </span>
425
+ ) : (
426
+ entry.target !== undefined && <span className="min-w-0 break-words">{targetNode(entry)}</span>
427
+ )}
393
428
  {entry.group && (
394
429
  <span data-group className="min-w-0 break-words text-xs text-muted">
395
430
  {groupText(entry.group)}
@@ -410,7 +445,8 @@ export function AdminActionLog({
410
445
  )}
411
446
  </div>
412
447
  </li>
413
- ))}
448
+ );
449
+ })}
414
450
  </ol>
415
451
  )}
416
452
  {paging && paging.total > 0 && (
@@ -24,6 +24,15 @@ export interface AdminTileLabels {
24
24
  users: string;
25
25
  /** `users_new_7d`, the line under the users tile. */
26
26
  usersNew: (count: number) => string;
27
+ /** 0.46 (Kurvenschmiede 1, admin §9.5): server-kit's `awaiting_verdict` as an `extra`
28
+ * tile — answered reports that wait on a verdict (IN_EVALUATION, NEEDS_LIVE_TEST). */
29
+ awaitingVerdict: string;
30
+ /** Its line under the count. */
31
+ awaitingVerdictHint: string;
32
+ /** 0.46: server-kit's `released_for_work` — the reports released for the work queue. */
33
+ releasedForWork: string;
34
+ /** Its line under the count. */
35
+ releasedForWorkHint: string;
27
36
  }
28
37
 
29
38
  export interface AdminAreaLabels {
@@ -32,6 +41,12 @@ export interface AdminAreaLabels {
32
41
  /** The row's waiting count, for a reader ("Administration, 3 waiting"); the eye sees
33
42
  * the number in the badge. */
34
43
  waiting: (count: number) => string;
44
+ /**
45
+ * 0.45 (admin §9.3.4): a button's whole name with its count — "Platform, 2 waiting" —
46
+ * for an `IconButton` whose `badge` is an `AdminWaitingBadge`: the badge is decorative
47
+ * there, so the name carries the count. `label` is the button's own name.
48
+ */
49
+ waitingLabel: (label: string, count: number) => string;
35
50
  /** The 403 page's heading (§2.2 decision 8). */
36
51
  forbiddenTitle: string;
37
52
  /**
@@ -59,6 +74,7 @@ export interface AdminAreaLabels {
59
74
  export const DEFAULT_ADMIN_AREA_LABELS: AdminAreaLabels = {
60
75
  title: "Administration",
61
76
  waiting: (count) => `${count} waiting`,
77
+ waitingLabel: (label, count) => `${label}, ${count} waiting`,
62
78
  forbiddenTitle: "Only for administrators",
63
79
  forbiddenBody: (appName) => `This is where administrators run ${appName}. Your account cannot open this page.`,
64
80
  forbiddenHome: "Go to the start page",
@@ -76,6 +92,10 @@ export const DEFAULT_ADMIN_AREA_LABELS: AdminAreaLabels = {
76
92
  invitationsPending: "Pending invitations",
77
93
  users: "Users",
78
94
  usersNew: (count) => `${count} new in 7 days`,
95
+ awaitingVerdict: "Awaiting a verdict",
96
+ awaitingVerdictHint: "Answered, and waiting for you or the reporter to confirm the fix",
97
+ releasedForWork: "Released for work",
98
+ releasedForWorkHint: "Ready, rework requested or in progress",
79
99
  },
80
100
  overviewError: "The overview couldn’t be loaded.",
81
101
  retry: "Try again",
@@ -86,8 +86,18 @@ export function AttentionTile({ count, href, ...tile }: AttentionTileProps) {
86
86
  return <StatTile data-slot="attention-tile" {...tile} value={count ?? null} href={href} />;
87
87
  }
88
88
 
89
+ /**
90
+ * 0.46 (Kurvenschmiede 1, admin §9.5): the `extra` keys server-kit's
91
+ * `feedback_attention` fills, whose tiles have the kit's words
92
+ * (`adminArea.tiles.awaitingVerdict`, `releasedForWork`, with a line each).
93
+ */
94
+ export type AdminFeedbackTileKey = "awaiting_verdict" | "released_for_work";
95
+
89
96
  /** One of the app's own tiles (§2.4 decision 13: keksdose's "Support waiting"). */
90
- export interface AdminExtraTile {
97
+ export type AdminExtraTile = AdminOwnTile | AdminFeedbackTile;
98
+
99
+ /** A tile of the app's own: it brings its words. */
100
+ export interface AdminOwnTile {
91
101
  /** Unique among the tiles; usually the `extra` key it shows. */
92
102
  key: string;
93
103
  label: ReactNode;
@@ -100,6 +110,14 @@ export interface AdminExtraTile {
100
110
  description?: ReactNode;
101
111
  }
102
112
 
113
+ /** 0.46: a tile of server-kit's feedback counts — `label` and `description` default to
114
+ * the kit's words; an app's own still win. The kit's `description` fills in only when
115
+ * `label` is left out too: an app that names the tile gets no hint line it did not ask for. */
116
+ export interface AdminFeedbackTile extends Omit<AdminOwnTile, "key" | "label"> {
117
+ key: AdminFeedbackTileKey;
118
+ label?: ReactNode;
119
+ }
120
+
103
121
  export interface AdminOverviewProps {
104
122
  /** The server's answer; `undefined` while it loads (the grid shows its skeletons). */
105
123
  attention: AdminAttention | null | undefined;
@@ -186,9 +204,26 @@ export function AdminOverview({
186
204
  attention && typeof attention.users_new_7d === "number" ? labels.tiles.usersNew(attention.users_new_7d) : undefined,
187
205
  },
188
206
  ];
207
+ const feedbackWords: Record<AdminFeedbackTileKey, { label: string; description: string }> = {
208
+ awaiting_verdict: { label: labels.tiles.awaitingVerdict, description: labels.tiles.awaitingVerdictHint },
209
+ released_for_work: { label: labels.tiles.releasedForWork, description: labels.tiles.releasedForWorkHint },
210
+ };
189
211
  const tiles = [
190
212
  ...shared.map((t) => ({ ...t, count: attention ? attention[t.key] : undefined, href: href[t.key] })),
191
- ...extraTiles,
213
+ ...extraTiles.map((t) => {
214
+ const words = Object.prototype.hasOwnProperty.call(feedbackWords, t.key)
215
+ ? feedbackWords[t.key as AdminFeedbackTileKey]
216
+ : undefined;
217
+ // The kit's hint comes with the kit's label only: an app that names the tile
218
+ // itself gets no line it did not ask for (keksdose S5, round 0.46).
219
+ return words
220
+ ? {
221
+ ...t,
222
+ label: t.label ?? words.label,
223
+ description: t.description ?? (t.label === undefined ? words.description : undefined),
224
+ }
225
+ : t;
226
+ }),
192
227
  ].filter((t) => loading || (t.count !== null && t.count !== undefined));
193
228
  if (!loading && tiles.length === 0) return error ? errorBox(true) : null;
194
229
  const grid = (
@@ -0,0 +1,19 @@
1
+ // The English words of ./mail-test-card, in a module with no component code: the English
2
+ // catalogue (src/i18n/defaults.ts) imports them without the component, so `en` loads as one
3
+ // chunk like the other languages (0.34, KS U8). ./mail-test-card re-exports them.
4
+
5
+ import { englishRateLimited } from "../auth/auth-errors";
6
+ import type { MailTestCardLabels } from "./mail-test-card";
7
+
8
+ /** docs/admin-harmonization.md §9.2.4, word for word. */
9
+ export const DEFAULT_MAIL_TEST_CARD_LABELS: MailTestCardLabels = {
10
+ title: "Test mail",
11
+ description: "Sends one mail to your own address, through the transport this installation sends all its mail with.",
12
+ transport: (backend) => `Transport: ${backend}`,
13
+ action: "Send a test mail to me",
14
+ sent: (address) => `Sent to ${address}. Check the inbox.`,
15
+ console: "The console transport only logs mail; nothing was sent.",
16
+ failed: "Could not send. The server log has the provider’s answer.",
17
+ rateLimited: englishRateLimited,
18
+ keywords: "test mail email e-mail transport smtp resend console send deliver",
19
+ };
@@ -0,0 +1,190 @@
1
+ import { useState } from "react";
2
+ import type { ReactNode } from "react";
3
+ import { Send } from "lucide-react";
4
+
5
+ import { useKitLabels, type LabelOverride } from "../i18n/kit-labels";
6
+ import { isRateLimited, retryAfterSeconds } from "../auth/auth-errors";
7
+ import { AlertBanner } from "../components/alert-banner";
8
+ import { Button } from "../components/button";
9
+ import { Chip } from "../components/chip";
10
+ import { SettingsSection, type SettingsSectionProps } from "../settings/settings-section";
11
+ import { DEFAULT_MAIL_TEST_CARD_LABELS } from "./mail-test-card-labels";
12
+
13
+ /* ── Labels ──────────────────────────────────────────────────────────────── */
14
+
15
+ /** Every string {@link MailTestCard} renders — the `mailTestCard` namespace of
16
+ * `<UiKitProvider labels>`, overridable per instance through `labels`
17
+ * (docs/admin-harmonization.md §9.2.4). */
18
+ export interface MailTestCardLabels {
19
+ /** The card's title, and the app's catalogue entry's. */
20
+ title: string;
21
+ /** The sentence under it. */
22
+ description: string;
23
+ /** The transport's chip: "Transport: resend". `backend` is the server's `mail_backend`. */
24
+ transport: (backend: string) => string;
25
+ /** The button. */
26
+ action: string;
27
+ /** The answer said "sent" through a real transport. `address` is masked ("a…@example.com"). */
28
+ sent: (address: string) => string;
29
+ /** `mail_backend` is `console`: said whatever `sent` is — a console answers `true`. */
30
+ console: string;
31
+ /** Not sent, or the request failed. The provider's error stays in the server log. */
32
+ failed: string;
33
+ /** HTTP 429 — the app's limiter (server-kit's `MAIL_TEST_LIMIT`, 3 per 10 minutes);
34
+ * `seconds` from `Retry-After` when the response has one. */
35
+ rateLimited: (seconds?: number) => string;
36
+ /** For the app's catalogue entry (`keywords`); the card never reads it. */
37
+ keywords: string;
38
+ }
39
+
40
+ // Defined in ./mail-test-card-labels, which holds no component code.
41
+ export { DEFAULT_MAIL_TEST_CARD_LABELS };
42
+
43
+ /* ── The card ────────────────────────────────────────────────────────────── */
44
+
45
+ /** The answer of `POST /admin/mail/test` — server-kit 0.15's `MailTestResult`. */
46
+ export interface MailTestAnswer {
47
+ sent: boolean;
48
+ /** The transport that took it: `console`, `resend`, `smtp`, … */
49
+ mail_backend: string;
50
+ }
51
+
52
+ /** The card's default DOM id: its catalogue entry's `anchor`, the `?focus=` target. */
53
+ export const MAIL_TEST_ANCHOR = "admin-mail-test";
54
+
55
+ export interface MailTestCardProps
56
+ extends Omit<SettingsSectionProps, "anchor" | "title" | "description" | "children"> {
57
+ /** The installation's transport, as the admin page has it (`mail_backend`): the chip
58
+ * before a send. After one, the answer's. Optional (0.46): without it the chip shows
59
+ * from the first answer on, and an app needs no query only for the chip. */
60
+ mailBackend?: string;
61
+ /** The app's `POST /admin/mail/test` (kastlan `/platform/mail/test`), no body: it
62
+ * awaits the app's own send and answers `{sent, mail_backend}`. A rejection is read
63
+ * for a 429 (the limiter); anything else is "could not send". */
64
+ send: () => Promise<MailTestAnswer>;
65
+ /** The signed-in admin's own address — where the mail goes. Shown masked. */
66
+ address: string;
67
+ /** Default {@link MAIL_TEST_ANCHOR}; the app's catalogue entry names the same. */
68
+ anchor?: string;
69
+ /** The app's sentence after the kit's description. */
70
+ description?: ReactNode;
71
+ labels?: LabelOverride<MailTestCardLabels>;
72
+ }
73
+
74
+ /**
75
+ * An address with its local part masked to its first character: "ada@example.com" →
76
+ * "a…@example.com". The domain stays, so the admin sees which mailbox to open.
77
+ */
78
+ export function maskMailAddress(address: string): string {
79
+ const text = address.trim();
80
+ const at = text.lastIndexOf("@");
81
+ if (at <= 0) return text === "" ? "" : `${Array.from(text)[0]}…`;
82
+ return `${Array.from(text.slice(0, at))[0]}…${text.slice(at)}`;
83
+ }
84
+
85
+ type Outcome =
86
+ | { kind: "sent"; backend: string }
87
+ | { kind: "console"; backend: string }
88
+ | { kind: "failed"; backend?: string }
89
+ | { kind: "limited"; seconds?: number };
90
+
91
+ /**
92
+ * "Send a test mail to me" (docs/admin-harmonization.md §9.2): a `SettingsSection` with a
93
+ * sentence, the transport as a chip, the button and the result line. One mail goes to the
94
+ * signed-in admin's own address, never another, through the app's own send.
95
+ *
96
+ * **The result line reads `mail_backend` first**: with `console` it says "nothing was sent"
97
+ * whatever `sent` is, since every console path answers `true` (§9.2 Today). Otherwise
98
+ * `sent` says "Sent to a…@example.com. Check the inbox." or "Could not send." — the
99
+ * provider's error stays in the server log (§9.2.7). A 429 says the limiter's line. The
100
+ * button is busy while the request runs; the line is a live region.
101
+ *
102
+ * Not a commit: the test writes nothing, so it is not under the write lock (a demo never
103
+ * reaches the admin area, and the server refuses it anyway). The app adds the catalogue
104
+ * entry `{ id, anchor: MAIL_TEST_ANCHOR, title, keywords }` from these labels.
105
+ */
106
+ export function MailTestCard({
107
+ mailBackend,
108
+ send,
109
+ address,
110
+ anchor = MAIL_TEST_ANCHOR,
111
+ description,
112
+ labels: labelsProp,
113
+ ...rest
114
+ }: MailTestCardProps) {
115
+ const labels = useKitLabels("mailTestCard", DEFAULT_MAIL_TEST_CARD_LABELS, labelsProp);
116
+ const [busy, setBusy] = useState(false);
117
+ const [outcome, setOutcome] = useState<Outcome | null>(null);
118
+ // The last transport an answer named: a later send with no answer (429, network) keeps it.
119
+ const [answered, setAnswered] = useState<string | undefined>(undefined);
120
+
121
+ const run = async () => {
122
+ if (busy) return;
123
+ setBusy(true);
124
+ setOutcome(null);
125
+ try {
126
+ const answer = await send();
127
+ const backend = answer.mail_backend;
128
+ if (backend) setAnswered(backend);
129
+ if (backend === "console") setOutcome({ kind: "console", backend });
130
+ else setOutcome(answer.sent ? { kind: "sent", backend } : { kind: "failed", backend });
131
+ } catch (error) {
132
+ setOutcome(isRateLimited(error) ? { kind: "limited", seconds: retryAfterSeconds(error) } : { kind: "failed" });
133
+ } finally {
134
+ setBusy(false);
135
+ }
136
+ };
137
+
138
+ const backend = answered ?? mailBackend;
139
+
140
+ return (
141
+ <SettingsSection
142
+ {...rest}
143
+ anchor={anchor}
144
+ title={labels.title}
145
+ description={
146
+ description != null ? (
147
+ <>
148
+ {labels.description} {description}
149
+ </>
150
+ ) : (
151
+ labels.description
152
+ )
153
+ }
154
+ >
155
+ <div className="space-y-2">
156
+ <div className="flex flex-wrap items-center gap-2">
157
+ <Button variant="secondary" pending={busy} onClick={() => void run()}>
158
+ <Send className="size-4" aria-hidden />
159
+ {labels.action}
160
+ </Button>
161
+ {backend && (
162
+ <Chip size="sm" data-slot="mail-backend">
163
+ {labels.transport(backend)}
164
+ </Chip>
165
+ )}
166
+ </div>
167
+ {outcome?.kind === "sent" && (
168
+ <AlertBanner variant="inline" block tone="success">
169
+ <span data-private>{labels.sent(maskMailAddress(address))}</span>
170
+ </AlertBanner>
171
+ )}
172
+ {outcome?.kind === "console" && (
173
+ <AlertBanner variant="inline" block tone="info">
174
+ {labels.console}
175
+ </AlertBanner>
176
+ )}
177
+ {outcome?.kind === "failed" && (
178
+ <AlertBanner variant="inline" block tone="danger">
179
+ {labels.failed}
180
+ </AlertBanner>
181
+ )}
182
+ {outcome?.kind === "limited" && (
183
+ <AlertBanner variant="inline" block tone="warning">
184
+ {labels.rateLimited(outcome.seconds)}
185
+ </AlertBanner>
186
+ )}
187
+ </div>
188
+ </SettingsSection>
189
+ );
190
+ }
@@ -28,6 +28,16 @@ export interface AppearanceLabels {
28
28
  * toast or the line under the setting (0.32.1, keksdose's 0.32 report).
29
29
  */
30
30
  saveFailed: string;
31
+ /** The date-format setting's name (0.46, `DateFormatSetting`). */
32
+ dateFormat: string;
33
+ /** One line under it: the format is its own choice, apart from the language. */
34
+ dateFormatHelp: string;
35
+ /** Its first option, followed by that language's sample: "Follow language — 01 Aug 2026". */
36
+ dateFormatAuto: string;
37
+ /** `AccountMenuTextSize`'s A− button (0.46, keksdose live #426): one step smaller. */
38
+ textSmaller: string;
39
+ /** Its A+ button: one step larger. */
40
+ textLarger: string;
31
41
  }
32
42
 
33
43
  export const DEFAULT_APPEARANCE_LABELS: AppearanceLabels = {
@@ -38,6 +48,12 @@ export const DEFAULT_APPEARANCE_LABELS: AppearanceLabels = {
38
48
  contrastHelp: "More contrast darkens quiet text and lines, and thickens focus frames. System follows this device.",
39
49
  contrastModes: { system: "System", standard: "Standard", more: "More" },
40
50
  saveFailed: "Couldn't save this to your account. It still applies on this device.",
51
+ dateFormat: "Date format",
52
+ dateFormatHelp:
53
+ "How dates are written throughout the app. Independent of the interface language, so you can read the app in English and still see German dates.",
54
+ dateFormatAuto: "Follow language",
55
+ textSmaller: "Smaller text",
56
+ textLarger: "Larger text",
41
57
  };
42
58
 
43
59
  /** The `appearance` namespace, resolved — English, then the provider, then `labels`. For
@@ -4,7 +4,7 @@ import { X } from "lucide-react";
4
4
  import { cn } from "../lib/cn";
5
5
  import { Tooltip } from "./tooltip";
6
6
  import type { DataTableLabels } from "./data-table-labels";
7
- import { ROW_ACTIONS_KEY } from "./data-table-row-actions";
7
+ import { isTableOwnColumn } from "./data-table-row-actions";
8
8
  import { columnLabel } from "./data-table-shared";
9
9
  import type { DataTableColumn } from "./data-table";
10
10
 
@@ -101,7 +101,7 @@ export function ColumnSettingsPanel<T>({
101
101
  </button>
102
102
  <div className="flex flex-col gap-0.5">
103
103
  {columns.map((col) => {
104
- if (col.key === ROW_ACTIONS_KEY) return null;
104
+ if (isTableOwnColumn(col.key)) return null;
105
105
  const checked = !hiddenCols.has(col.key);
106
106
  const headerLabel = columnLabel(col);
107
107
  return (
@@ -8,7 +8,7 @@ import { FilterPopover } from "./data-table-filter-popover";
8
8
  import { Popover } from "./popover";
9
9
  import { Tooltip } from "./tooltip";
10
10
  import type { DataTableLabels } from "./data-table-labels";
11
- import { ROW_ACTIONS_KEY } from "./data-table-row-actions";
11
+ import { isTableOwnColumn } from "./data-table-row-actions";
12
12
  import { columnLabel, logicalAlign, splitCellProps } from "./data-table-shared";
13
13
  import type { DataTableColumn, DataTableProps, FilterState } from "./data-table";
14
14
 
@@ -223,7 +223,7 @@ export function DataTableHead<T>({
223
223
  </Popover>
224
224
  )}
225
225
  </div>
226
- {resizable && col.key !== ROW_ACTIONS_KEY && (
226
+ {resizable && !isTableOwnColumn(col.key) && (
227
227
  /* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a
228
228
  pointer-only drag handle for a visual preference (column width); it is
229
229
  deliberately not a tab stop, and no content or action depends on it. */
@@ -1,5 +1,5 @@
1
1
  import { Fragment, useEffect, useId, useMemo, useRef, useState } from "react";
2
- import type { ReactNode } from "react";
2
+ import type { MouseEvent as ReactMouseEvent, PointerEvent as ReactPointerEvent, ReactNode } from "react";
3
3
  import { ArrowDownWideNarrow, ArrowUpNarrowWide, ChevronDown, ChevronRight, Filter } from "lucide-react";
4
4
  import { FOCUS_RING } from "./focus-ring";
5
5
  import { IconButton } from "./icon-button";
@@ -15,7 +15,7 @@ import { DialogFrame } from "./dialog-frame";
15
15
  import { Checkbox } from "./checkbox";
16
16
  import { useLongPress } from "../hooks/use-long-press";
17
17
  import type { DataTableLabels } from "./data-table-labels";
18
- import { RowLink, cleanAttrs, columnLabel, fromOutside } from "./data-table-shared";
18
+ import { RowLink, cleanAttrs, columnLabel, fromOutside, fromOwnControl } from "./data-table-shared";
19
19
  import type {
20
20
  DataTableCellProps,
21
21
  DataTableColumn,
@@ -383,7 +383,25 @@ export function MobileRow<T>({
383
383
  // Attached whenever the table selects, the hook's `enabled` deciding whether a press
384
384
  // arms: its pointerdown also clears a stale swallow latch, which must happen in the
385
385
  // mode too, or a long press whose click never came would eat the first toggle.
386
- const press = select ? longPress.handlers : undefined;
386
+ //
387
+ // A press that starts on a control in a cell (or in a portal one opened) is that
388
+ // control's, not a way into the mode (keksdose live #428): it only clears the latch.
389
+ const press = select
390
+ ? {
391
+ ...longPress.handlers,
392
+ onPointerDown: (e: ReactPointerEvent<HTMLElement>) => {
393
+ if (fromOutside(e.target, e.currentTarget) || fromOwnControl(e.target, e.currentTarget)) {
394
+ longPress.consumeClick();
395
+ return;
396
+ }
397
+ longPress.handlers.onPointerDown(e);
398
+ },
399
+ onContextMenu: (e: ReactMouseEvent<HTMLElement>) => {
400
+ if (fromOutside(e.target, e.currentTarget) || fromOwnControl(e.target, e.currentTarget)) return;
401
+ longPress.handlers.onContextMenu(e);
402
+ },
403
+ }
404
+ : undefined;
387
405
  const body = selecting ? (
388
406
  // In the mode a card is a toggle — kk's register row: `role="button"` with
389
407
  // `aria-pressed`, the box beside it only a picture of that state (the card is the
@@ -399,6 +417,8 @@ export function MobileRow<T>({
399
417
  {...press}
400
418
  onClick={(e) => {
401
419
  if (!select!.selectable || fromOutside(e.target, e.currentTarget)) return;
420
+ // A tap on a control in a cell is that control's (keksdose live #428).
421
+ if (fromOwnControl(e.target, e.currentTarget)) return;
402
422
  select!.onToggle(!select!.selected);
403
423
  }}
404
424
  onKeyDown={(e) => {
@@ -455,6 +475,9 @@ export function MobileRow<T>({
455
475
  interactive
456
476
  ? (e) => {
457
477
  if (fromOutside(e.target, e.currentTarget)) return;
478
+ // A tap on a control in a cell (a select, a button) is that
479
+ // control's, as on the desktop `<tr>` (keksdose live #428).
480
+ if (fromOwnControl(e.target, e.currentTarget)) return;
458
481
  onRowClick!(row);
459
482
  }
460
483
  : undefined