@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
@@ -46,6 +46,7 @@ import type { TranslationReviewLabels } from "./translation-review-labels";
46
46
  import { ReviewStatusChip, TranslationProgress } from "./translation-review-parts";
47
47
  import { Button } from "./button";
48
48
  import { IconButton } from "./icon-button";
49
+ import { DEFAULT_ROW_ACTIONS_LABELS, rowActionsColumn, type RowAction } from "./row-actions";
49
50
  import { Select } from "./select";
50
51
  import { COMMIT_EXCEPT_BILLING, useWriteLock } from "./write-lock";
51
52
 
@@ -194,13 +195,23 @@ export interface TranslationReviewPanelProps {
194
195
  * DataTable's swipe sides (`mobileSwipeActions`) on {@link SwipeableRow}: the same
195
196
  * gesture, the same feedback, and the same keyboard path — every swipe is also a real
196
197
  * button, read out and reachable by Tab, so the gesture is a shortcut and never the only
197
- * way. A row has no swipe while the editor under it is open, while its own write is out,
198
- * under a {@link WriteLockProvider} lock, in a `readOnly` locale, and toward "Approve"
199
- * where there is nothing to approve (a missing string, an approved one). A reviewer
200
- * limited to `areas` never sees a row outside them, so never swipes one.
198
+ * way. A row has no swipe while its own write is out (a swipe's, a button's, or the open
199
+ * editor's own), under a {@link WriteLockProvider} lock, in a `readOnly` locale, and
200
+ * toward "Approve" where there is nothing to approve (a missing string, an approved one).
201
+ * A reviewer limited to `areas` never sees a row outside them, so never swipes one.
201
202
  *
202
- * Off by default, so no existing page gains a gesture on the bump. Phones only; the
203
- * desktop table keeps its approve button.
203
+ * An OPEN row swipes too, on its top line (keksdose live #416, Marcel: *"The swipe shall
204
+ * still be possible since it is faster than scrolling down to the buttons"*), as
205
+ * DataTable's open card does since 0.40: the editor below is outside the gesture, and a
206
+ * swipe neither toggles it shut nor resets it. A swipe acts on the STORED string, as the
207
+ * row's buttons do — Approve approves what the locale says, not the editor's draft — and
208
+ * the editor stays open with whatever was typed in it; "Needs a change" puts the cursor
209
+ * back at the end of that draft.
210
+ *
211
+ * Off by default, so no existing page gains a gesture on the bump. The gesture is the
212
+ * phone's; on a wide screen the binding is the row's actions column instead (0.46,
213
+ * keksdose dev#598): Approve where the row can take it, bound or not, then the bound
214
+ * `edit` and `clear`, each once — under a write lock they stay and say why.
204
215
  *
205
216
  * A BINDING instead of `true` (keksdose — Marcel's live #377 rework, 2026-10-03: *"Add
206
217
  * the swipe options to be shown also to the settings /settings#interaction area where
@@ -218,8 +229,8 @@ export interface TranslationReviewPanelProps {
218
229
  * `onClear`). An action a row cannot offer drops out and the ladder closes up behind it
219
230
  * ({@link translationReviewSwipePlan}): `approve` on an approved or missing string,
220
231
  * `clear` on a row with no verdict. Every rule above still holds for every binding —
221
- * no side swipes in a `readOnly` locale, under a write lock, with the editor open or
222
- * the row's write out.
232
+ * no side swipes in a `readOnly` locale, under a write lock, or while the row's write
233
+ * (or its open editor's) is out.
223
234
  *
224
235
  * `true` is {@link DEFAULT_TRANSLATION_REVIEW_SWIPE} — `{ end: ["approve"], start:
225
236
  * ["edit"] }`, 0.25's mapping exactly — and `false` or nothing is no swipe. A binding
@@ -482,6 +493,8 @@ export function TranslationReviewPanel({
482
493
  // (docs/billing-harmonization.md §12.13, §12.36): the buttons take
483
494
  // COMMIT_EXCEPT_BILLING, and the swipes read the lock as they see it.
484
495
  const lock = useWriteLock(COMMIT_EXCEPT_BILLING);
496
+ // The desktop actions column's plain-text name (dev#598), the provider's "Actions".
497
+ const rowActionLabels = useKitLabels("rowActions", DEFAULT_ROW_ACTIONS_LABELS);
485
498
  // The phone layout DataTable switches its layout on, so "a phone" means the same here
486
499
  // as in the table, at every text size: `usePhoneLayout()` itself (§10.4; 0.32.1,
487
500
  // keksdose's 0.32 report — `!useBreakpoint("md")` could disagree with it at 767.5 px).
@@ -514,6 +527,21 @@ export function TranslationReviewPanel({
514
527
  const [failure, setFailure] = useState<ReactNode>(null);
515
528
  const busy = queued.length > 0;
516
529
  const isQueued = (key: string) => queued.includes(key);
530
+ // The rows whose open editor has a write of its own out: their swipes stand down until
531
+ // it is answered, as for a write in the queue (0.46, keksdose live #416).
532
+ const [editorWrites, setEditorWrites] = useState<ReadonlySet<string>>(() => new Set());
533
+ const whileEditorWrites = async (rowId: string, write: () => MaybePromise) => {
534
+ setEditorWrites((prev) => new Set(prev).add(rowId));
535
+ try {
536
+ await write();
537
+ } finally {
538
+ setEditorWrites((prev) => {
539
+ const next = new Set(prev);
540
+ next.delete(rowId);
541
+ return next;
542
+ });
543
+ }
544
+ };
517
545
 
518
546
  const rows = useMemo(
519
547
  () => (areas ? rowsProp.filter((row) => keyInAreas(row.key, areas, inArea)) : [...rowsProp]),
@@ -650,11 +678,11 @@ export function TranslationReviewPanel({
650
678
  /** A swipe bound to `clear`: the row's verdict taken back, answered like an approval —
651
679
  * the Undo toast stores the verdict again. Under the row's own key, so it queues behind
652
680
  * (and never overlaps) an approval of the same row. */
653
- const clearRow = async (row: TranslationRow) => {
681
+ const clearRow = async (row: TranslationRow, origin: TranslationReviewOrigin = "swipe") => {
654
682
  if (!onClear || row.review === null) return;
655
683
  const clear = onClear;
656
684
  const keys = [{ locale: row.locale, key: row.key }];
657
- const ok = await run(`row:${row.id}`, () => clear(keys, { origin: "swipe", toasted: undoOn }));
685
+ const ok = await run(`row:${row.id}`, () => clear(keys, { origin, toasted: undoOn }));
658
686
  if (ok && undoOn) sayDone([row], row.key, true);
659
687
  };
660
688
 
@@ -662,7 +690,7 @@ export function TranslationReviewPanel({
662
690
  * not go through the queue — but an approval there is answered like any other. */
663
691
  const saveFromEditor = async (row: TranslationRow, write: TranslationReviewWrite) => {
664
692
  const toasted = undoOn && write.verdict === "APPROVED";
665
- await onSave?.([write], { origin: "editor", toasted });
693
+ await whileEditorWrites(row.id, () => onSave?.([write], { origin: "editor", toasted }));
666
694
  if (toasted) sayDone([row], row.key);
667
695
  };
668
696
 
@@ -763,7 +791,51 @@ export function TranslationReviewPanel({
763
791
  sortBy: (r) => REVIEW_STATUSES.indexOf(r.status),
764
792
  },
765
793
  ];
766
- if (editable) {
794
+ if (editable && binding) {
795
+ // keksdose dev#598 item 2: with a binding, the desktop row offers what the card's
796
+ // swipes offer — `clear` was reachable only by a swipe. Approve first on every row
797
+ // that can take it, bound or not (the desktop always had it), then the binding's
798
+ // actions, end side first, each once. The swipe's words, glyphs and handlers; unlike
799
+ // the swipe, a locked row keeps its buttons and says why (`commit`), and a row whose
800
+ // write is out shows it in flight. Hidden on the phone, where the swipes are.
801
+ columns.push(
802
+ rowActionsColumn<TranslationRow>({
803
+ key: "actions",
804
+ headerText: rowActionLabels.actions,
805
+ name: (r) => r.key,
806
+ tooltipSide: "start",
807
+ actions: (r) =>
808
+ translationReviewSwipePlan(
809
+ { end: ["approve", ...(binding.end ?? []), ...(binding.start ?? [])] },
810
+ r,
811
+ { canClear: onClear !== undefined },
812
+ ).end.map((id): RowAction => {
813
+ const swipe = swipeAction(r, id);
814
+ // `edit` only opens the editor, whose own Save is the commit.
815
+ const writes = id !== "edit";
816
+ return {
817
+ key: id,
818
+ // `edit` opens the editor: its own name, apart from the editor's submit
819
+ // ("Needs a change"), which saves (keksdose B2/S3).
820
+ label: id === "edit" ? (r.text === "" ? labels.suggestRow : labels.flagRow) : swipe.label,
821
+ icon: <>{swipe.icon}</>,
822
+ tone: "muted",
823
+ commit: writes ? COMMIT_EXCEPT_BILLING : undefined,
824
+ pending: writes && isQueued(`row:${r.id}`),
825
+ disabled: busy,
826
+ onSelect:
827
+ id === "approve"
828
+ ? () => void approve(`row:${r.id}`, [r], "row", r.key)
829
+ : id === "clear"
830
+ ? () => void clearRow(r, "row")
831
+ : // The row's group stays open while its editor is (`keepGroupOpen`).
832
+ () => swipeAction(r, id, groups?.find((g) => g.rows.includes(r))?.key).onCommit(),
833
+ };
834
+ }),
835
+ column: { mobileHidden: true },
836
+ }),
837
+ );
838
+ } else if (editable) {
767
839
  columns.push({
768
840
  key: "approve",
769
841
  header: <span className="sr-only">{labels.approve}</span>,
@@ -801,6 +873,7 @@ export function TranslationReviewPanel({
801
873
  switch (id) {
802
874
  case "approve":
803
875
  return {
876
+ id,
804
877
  label: labels.approve,
805
878
  icon: <Check className="size-4" aria-hidden />,
806
879
  onCommit: () => void approve(`row:${r.id}`, [r], "swipe", r.key),
@@ -810,6 +883,7 @@ export function TranslationReviewPanel({
810
883
  };
811
884
  case "edit":
812
885
  return {
886
+ id,
813
887
  label: missing ? labels.suggest : labels.flag,
814
888
  icon: <PencilLine className="size-4" aria-hidden />,
815
889
  // Opens the editor rather than sending the string back: "needs a change"
@@ -818,11 +892,19 @@ export function TranslationReviewPanel({
818
892
  keepGroupOpen(groupKey);
819
893
  setOpenId(r.id);
820
894
  setFocusId(r.id);
895
+ // On a row already open (#416) the editor is mounted and keeps its draft:
896
+ // the cursor goes to the end of its wording now.
897
+ if (openId === r.id) {
898
+ const field = rootRef.current?.querySelector<HTMLTextAreaElement>("[data-review-wording]");
899
+ field?.focus();
900
+ field?.setSelectionRange(field.value.length, field.value.length);
901
+ }
821
902
  },
822
903
  tone: missing ? "brand" : "danger",
823
904
  };
824
905
  case "clear":
825
906
  return {
907
+ id,
826
908
  label: labels.reset,
827
909
  icon: <RotateCcw className="size-4" aria-hidden />,
828
910
  onCommit: () => void clearRow(r),
@@ -841,7 +923,7 @@ export function TranslationReviewPanel({
841
923
  * locked or absent.
842
924
  */
843
925
  const swipeActions = (r: TranslationRow, groupKey?: string): MobileSwipeActions | null => {
844
- if (!binding || !editable || lock.locked || isQueued(`row:${r.id}`)) return null;
926
+ if (!binding || !editable || lock.locked || isQueued(`row:${r.id}`) || editorWrites.has(r.id)) return null;
845
927
  const plan = translationReviewSwipePlan(binding, r, { canClear: onClear !== undefined });
846
928
  if (plan.start.length === 0 && plan.end.length === 0) return null;
847
929
  return {
@@ -908,7 +990,11 @@ export function TranslationReviewPanel({
908
990
  readOnly={!editable}
909
991
  focusWording={focusId === r.id}
910
992
  onSave={onSave ? (write) => saveFromEditor(r, write) : undefined}
911
- onClear={onClear ? (key) => onClear([key], { origin: "editor", toasted: false }) : undefined}
993
+ onClear={
994
+ onClear
995
+ ? (key) => whileEditorWrites(r.id, () => onClear([key], { origin: "editor", toasted: false }))
996
+ : undefined
997
+ }
912
998
  onClose={() => {
913
999
  setOpenId(null);
914
1000
  setFocusId(null);
@@ -935,6 +1021,8 @@ export function TranslationReviewPanel({
935
1021
  </div>
936
1022
  )}
937
1023
  mobileSwipeActions={binding ? (r) => swipeActions(r, group?.key) : undefined}
1024
+ // The desktop's buttons for the binding are the panel's own "actions" column.
1025
+ desktopSwipeActions={false}
938
1026
  selection={selectionFor(list)}
939
1027
  />
940
1028
  );
@@ -6,4 +6,5 @@ import type { WriteLockLabels } from "./write-lock";
6
6
 
7
7
  export const DEFAULT_WRITE_LOCK_LABELS: WriteLockLabels = {
8
8
  reason: "You can view this but not change it.",
9
+ offline: "Needs a connection.",
9
10
  };
@@ -8,6 +8,8 @@ import { DEFAULT_WRITE_LOCK_LABELS } from "./write-lock-labels";
8
8
  export interface WriteLockLabels {
9
9
  /** The tooltip of a locked commit when the provider gives no `reason`. */
10
10
  reason: string;
11
+ /** 0.46 (kastlan K4): the reason of {@link useOfflineWriteLock} — "Needs a connection." */
12
+ offline: string;
11
13
  }
12
14
 
13
15
  // Defined in ./write-lock-labels, which holds no component code (0.34, KS U8).
@@ -15,13 +17,13 @@ export { DEFAULT_WRITE_LOCK_LABELS };
15
17
 
16
18
  /**
17
19
  * What kind of lock a source is (docs/billing-harmonization.md §12.36, ui-kit 0.33).
18
- * `"demo"` (the read-only demo, `demo.writeLocked`) and `"billing"` (a lapsed plan,
19
- * `billing.lockReason`) are the kit's; an app names its own — Kurvenschmiede's
20
+ * `"demo"` (the read-only demo, `demo.writeLocked`), `"billing"` (a lapsed plan,
21
+ * `billing.lockReason`) and, since 0.46, `"offline"` ({@link OFFLINE_LOCK}) are the kit's; an app names its own — Kurvenschmiede's
20
22
  * `"access"` (a curve shared to view) and `"customer"` (an account that owns nothing).
21
23
  * A control exempts itself from a kind with {@link CommitScope}; a source without a
22
24
  * kind is never exempted.
23
25
  */
24
- export type WriteLockKind = "demo" | "billing" | (string & {});
26
+ export type WriteLockKind = "demo" | "billing" | "offline" | (string & {});
25
27
 
26
28
  /** One locked source, as the provider holds it: its kind and its one sentence. */
27
29
  export interface WriteLockHold {
@@ -50,6 +52,19 @@ export type CommitScope = boolean | { readonly except: readonly WriteLockKind[]
50
52
  * so the call sites read alike. */
51
53
  export const COMMIT_EXCEPT_BILLING = { except: ["billing"] } as const;
52
54
 
55
+ /**
56
+ * 0.46 (kastlan K4, docs/local-first-harmonization.md D4): the kind of the lock "this
57
+ * device has no connection". An action that only the server can do (numbers, money,
58
+ * mail, people, settings) is locked with "Needs a connection." while offline; a control
59
+ * that writes to the device's copy and syncs later takes {@link COMMIT_EXCEPT_OFFLINE}.
60
+ * Feed it with {@link useOfflineWriteLock}.
61
+ */
62
+ export const OFFLINE_LOCK = "offline" as const;
63
+
64
+ /** Every lock but the offline one: the scope of a control that works on the device's copy
65
+ * (it is still locked for a demo or a lapsed plan). */
66
+ export const COMMIT_EXCEPT_OFFLINE = { except: [OFFLINE_LOCK] } as const;
67
+
53
68
  /**
54
69
  * What {@link useWriteLock} returns: whether commits are locked here as a control with
55
70
  * the scope asked for sees it, and why.
@@ -218,3 +233,21 @@ export function useCommitReason(commit: CommitScope | undefined, own: ReactNode)
218
233
  const lock = writeLockFor(useContext(WriteLockContext), commit);
219
234
  return lock.locked ? lock.reason : own;
220
235
  }
236
+
237
+ /**
238
+ * 0.46 (kastlan K4): the offline lock as one source of `combineWriteLocks` —
239
+ * `{ locked: offline, kind: "offline", reason: writeLock.offline }` ("Needs a
240
+ * connection.", seven languages). `offline` is the app's own reading of the connection
241
+ * (`!navigator.onLine`, or its better guess); the kit does not watch the network here.
242
+ * List it after the demo's and billing's sources: the server refuses those first.
243
+ *
244
+ * <WriteLockProvider {...combineWriteLocks([demo, billing, useOfflineWriteLock(!online)])}>
245
+ * <Button commit={COMMIT_EXCEPT_OFFLINE}>Save handover</Button>
246
+ */
247
+ export function useOfflineWriteLock(
248
+ offline: boolean,
249
+ labels?: LabelOverride<WriteLockLabels>,
250
+ ): { locked: boolean; kind: typeof OFFLINE_LOCK; reason: string } {
251
+ const words = useKitLabels("writeLock", DEFAULT_WRITE_LOCK_LABELS, labels);
252
+ return { locked: offline, kind: OFFLINE_LOCK, reason: words.offline };
253
+ }
package/src/data-table.ts CHANGED
@@ -25,6 +25,7 @@ export type {
25
25
  DataTableDensity,
26
26
  DataTableChrome,
27
27
  DataTableRowAction,
28
+ DataTableDesktopSwipeActions,
28
29
  } from "./components/data-table";
29
30
  export * from "./components/data-table-cells";
30
31
  export { useTableUrlState, readTableUrlState } from "./components/use-table-state";
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useRef, useState } from "react";
2
2
  import type { MouseEvent, ReactNode } from "react";
3
- import { Download, ExternalLink, FileText, RotateCcw } from "lucide-react";
3
+ import { Download, FileText, RotateCcw } from "lucide-react";
4
4
  import { AuthedImage } from "../components/authed-image";
5
5
  import { CopyButton } from "../components/copy-button";
6
6
  import { TextLink } from "../components/text-link";
@@ -75,7 +75,7 @@ import { DEFAULT_FEEDBACK_COMMENTS_LABELS } from "./feedback-thread-parts-labels
75
75
  * `showSubject={false}` leaves it out), **Description** (the body; the author's
76
76
  * Edit while OPEN / READY / IN_PROGRESS), **URL**, **Attachment**, **Replies** (0.38,
77
77
  * §9.7: the timeline, the admin's reply composer with its status picker and the nudge,
78
- * Rework, Open page — on a row without `replies`, a server before 0.38, the frozen
78
+ * Rework — on a row without `replies`, a server before 0.38, the frozen
79
79
  * **Outcome** read-only in its place), **Comments** (0.38: the app's
80
80
  * `FeedbackCommentThread`, when passed), **Send for rework** (only while reworking) and
81
81
  * **Status** (with the **Visible to others** switch when `onPublicChange` is passed).
@@ -143,7 +143,9 @@ export interface FeedbackDetailLabels {
143
143
  outcomePlaceholder: string;
144
144
  /** The button that opens section 5 on an answered row. de-CH: "Nacharbeit" */
145
145
  rework: string;
146
- /** The link to the page the report was filed on. de-CH: "Seite öffnen" */
146
+ /** The link to the page the report was filed on. de-CH: "Seite öffnen"
147
+ * @deprecated 0.46 — the Replies section's "Open page" button is gone (keksdose live #449:
148
+ * it repeated the URL section's link); removed in 0.47. Unused, kept for one release. */
147
149
  openPage: string;
148
150
  /** Section 5's heading. de-CH: "Zur Nacharbeit senden" */
149
151
  reworkTitle: string;
@@ -853,27 +855,20 @@ export function FeedbackRowDetail({
853
855
  commit={commit}
854
856
  />
855
857
  )}
856
- {((canRework && !reworking) || pageHref) && (
858
+ {/* No "Open page" button here since 0.46 (keksdose live #449): the URL section
859
+ above already links the page, with the path on screen. */}
860
+ {canRework && !reworking && (
857
861
  <div className="flex flex-wrap gap-2">
858
- {canRework && !reworking && (
859
- <Button
860
- type="button"
861
- variant="secondary"
862
- onClick={(event) => {
863
- event.stopPropagation();
864
- setReworking(true);
865
- }}
866
- >
867
- <RotateCcw className="size-3.5" aria-hidden /> {labels.rework}
868
- </Button>
869
- )}
870
- {pageHref && (
871
- // A real href (keksdose dev #451): middle-click and ⌘/Ctrl-click are
872
- // the browser's, and only a link has them.
873
- <Button href={pageHref} variant="secondary" onClick={(event) => event.stopPropagation()}>
874
- <ExternalLink className="size-3.5" aria-hidden /> {labels.openPage}
875
- </Button>
876
- )}
862
+ <Button
863
+ type="button"
864
+ variant="secondary"
865
+ onClick={(event) => {
866
+ event.stopPropagation();
867
+ setReworking(true);
868
+ }}
869
+ >
870
+ <RotateCcw className="size-3.5" aria-hidden /> {labels.rework}
871
+ </Button>
877
872
  </div>
878
873
  )}
879
874
  </div>
@@ -1,7 +1,7 @@
1
1
  import { Ban, Check, ChevronsRight } from "lucide-react";
2
2
  import type { SwipeAction } from "../components/swipeable-row";
3
3
  import type { MobileSwipeActions } from "../components/data-table";
4
- import { nextFeedbackStatus, type FeedbackStatus } from "./feedback-vocabulary";
4
+ import { nextFeedbackStatus, visibleFeedbackStatuses, type FeedbackStatus } from "./feedback-vocabulary";
5
5
  import type { FeedbackStatusLabels } from "./feedback-labels";
6
6
  import type { FeedbackStatusChange, FeedbackStatusRow } from "./feedback-status-undo";
7
7
 
@@ -89,9 +89,12 @@ function panel(
89
89
  { change, labels }: FeedbackSwipeOptions,
90
90
  ): SwipeAction {
91
91
  const onCommit = () => change(row, target);
92
+ // `id`: the binding's own (0.46, dev#598) — DataTable's desktop column tells the
93
+ // actions apart by it, and `feedbackSwipeOmitShown` reads the target from it.
92
94
  switch (id) {
93
95
  case "advance":
94
96
  return {
97
+ id,
95
98
  onCommit,
96
99
  label: labels[target],
97
100
  icon: <ChevronsRight className="size-4" aria-hidden />,
@@ -99,6 +102,7 @@ function panel(
99
102
  };
100
103
  case "done":
101
104
  return {
105
+ id,
102
106
  onCommit,
103
107
  label: labels.DONE,
104
108
  icon: <Check className="size-4" aria-hidden />,
@@ -106,6 +110,7 @@ function panel(
106
110
  };
107
111
  case "wont_do":
108
112
  return {
113
+ id,
109
114
  onCommit,
110
115
  label: labels.WONT_DO,
111
116
  icon: <Ban className="size-4" aria-hidden />,
@@ -163,3 +168,25 @@ export function feedbackSwipePlan(
163
168
  if (start.length === 0 && end.length === 0) return null;
164
169
  return { start, end };
165
170
  }
171
+
172
+ /**
173
+ * For DataTable's `desktopSwipeActions.omit` beside the kit's status cell (0.46, keksdose
174
+ * dev#598 item 3): `true` for a planned action whose target status the cell already
175
+ * offers as its own icon — {@link visibleFeedbackStatuses}, the next and the previous step
176
+ * and the off-chain verdicts — so the desktop column adds only what the cell lacks ("Done"
177
+ * where DONE is more than one step away). Read from the action's `id` and the row, never
178
+ * from the translated label; an action without one of {@link FEEDBACK_SWIPE_ACTIONS} as its
179
+ * id (an app's own) is kept.
180
+ *
181
+ * ```tsx
182
+ * <DataTable
183
+ * mobileSwipeActions={(row) => feedbackSwipePlan(binding, row, options)}
184
+ * desktopSwipeActions={{ omit: feedbackSwipeOmitShown, name: (row) => row.title }}
185
+ * />
186
+ * ```
187
+ */
188
+ export function feedbackSwipeOmitShown(action: SwipeAction, row: FeedbackStatusRow): boolean {
189
+ if (action.id === undefined) return false;
190
+ const target = targetOf(action.id, row.status);
191
+ return target !== null && visibleFeedbackStatuses(row.status).includes(target);
192
+ }
@@ -100,6 +100,10 @@ import { DEFAULT_ROW_ACTIONS_LABELS } from "../components/row-actions-labels";
100
100
  import { DEFAULT_APP_SHELL_MORE_LABELS } from "../shell/app-shell-labels";
101
101
  import { DEFAULT_ACCOUNT_STATE_LABELS } from "../components/account-chips-labels";
102
102
  import { DEFAULT_ADMIN_AREA_LABELS } from "../admin/admin-area-labels";
103
+ import { DEFAULT_MAIL_TEST_CARD_LABELS } from "../admin/mail-test-card-labels";
104
+ import { DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS } from "../offline/sign-out-pending-dialog-labels";
105
+ import { DEFAULT_PWA_UPDATE_PROMPT_LABELS } from "../offline/pwa-update-prompt-labels";
106
+ import { DEFAULT_SHARED_COMPUTER_LABELS } from "../offline/shared-computer-labels";
103
107
  import { DEFAULT_TOP_BAR_PARTS_LABELS } from "../shell/top-bar-parts-labels";
104
108
  import { DEFAULT_SHARE_CARD_LABELS } from "../components/share-card-labels";
105
109
  import { DEFAULT_REAUTH_DIALOG_LABELS } from "../components/reauth-dialog-labels";
@@ -262,4 +266,8 @@ export const DEFAULT_UI_KIT_LABELS: UiKitLabels = {
262
266
  notifications: DEFAULT_NOTIFICATIONS_LABELS,
263
267
  adminArea: DEFAULT_ADMIN_AREA_LABELS,
264
268
  topBarParts: DEFAULT_TOP_BAR_PARTS_LABELS,
269
+ mailTestCard: DEFAULT_MAIL_TEST_CARD_LABELS,
270
+ signOutPending: DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS,
271
+ pwaUpdate: DEFAULT_PWA_UPDATE_PROMPT_LABELS,
272
+ sharedComputer: DEFAULT_SHARED_COMPUTER_LABELS,
265
273
  };