@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
@@ -0,0 +1,81 @@
1
+ import { useSyncExternalStore } from "react";
2
+
3
+ /**
4
+ * vite-plugin-pwa's `registerSW` from `virtual:pwa-register` (`registerType: "prompt"`) —
5
+ * the app passes it in, since a virtual module is the app's build's, not the kit's. It
6
+ * answers the `updateSW(reloadPage)` function.
7
+ */
8
+ export type RegisterServiceWorker = (options: {
9
+ onNeedRefresh?: () => void;
10
+ onOfflineReady?: () => void;
11
+ }) => (reloadPage?: boolean) => Promise<void>;
12
+
13
+ /** What {@link createServiceWorkerUpdates} answers: one store per app, a module constant. */
14
+ export interface ServiceWorkerUpdates {
15
+ /** Registers the service worker — once, however often it is called. Call it at start,
16
+ * before the router renders, so the sign-in pages get the offline shell too. */
17
+ start: () => void;
18
+ /** Whether a new version waits. */
19
+ needRefresh: () => boolean;
20
+ /** For `useSyncExternalStore`. */
21
+ subscribe: (listener: () => void) => () => void;
22
+ /** Activates the waiting version and reloads (`updateSW(true)`). Before `start`, nothing. */
23
+ reload: () => void;
24
+ /** The person closed the prompt: the version keeps waiting, the prompt stays down. */
25
+ dismiss: () => void;
26
+ }
27
+
28
+ /**
29
+ * 0.46 (kastlan K3, docs/local-first-harmonization.md §1.5 D8): the service worker
30
+ * registered ONCE AT START, and its "a new version waits" state as a small store the
31
+ * prompt reads wherever it lives. vite-plugin-pwa's `useRegisterSW` registers when the
32
+ * component that calls it mounts: an app whose {@link PwaUpdatePrompt} sits in its
33
+ * signed-in shell (it reads the tour state, the route) would register only after sign-in,
34
+ * and the sign-in pages would lose the offline shell on a first visit.
35
+ *
36
+ * ```ts
37
+ * // shared/pwa/sw-updates.ts
38
+ * import { registerSW } from "virtual:pwa-register";
39
+ * export const swUpdates = createServiceWorkerUpdates(registerSW);
40
+ * // main.tsx, before render: swUpdates.start();
41
+ * // the shell: <PwaUpdatePrompt {...useServiceWorkerUpdate(swUpdates)} hold={tourRunning} />
42
+ * ```
43
+ */
44
+ export function createServiceWorkerUpdates(register: RegisterServiceWorker): ServiceWorkerUpdates {
45
+ let waiting = false;
46
+ let update: ((reloadPage?: boolean) => Promise<void>) | null = null;
47
+ const listeners = new Set<() => void>();
48
+ const emit = () => listeners.forEach((listener) => listener());
49
+ const set = (value: boolean) => {
50
+ if (waiting === value) return;
51
+ waiting = value;
52
+ emit();
53
+ };
54
+ return {
55
+ start: () => {
56
+ if (update) return;
57
+ update = register({ onNeedRefresh: () => set(true) });
58
+ },
59
+ needRefresh: () => waiting,
60
+ subscribe: (listener) => {
61
+ listeners.add(listener);
62
+ return () => {
63
+ listeners.delete(listener);
64
+ };
65
+ },
66
+ reload: () => {
67
+ void update?.(true);
68
+ },
69
+ dismiss: () => set(false),
70
+ };
71
+ }
72
+
73
+ /** {@link PwaUpdatePrompt}'s props from the store: `needRefresh`, `onReload`, `onDismiss`. */
74
+ export function useServiceWorkerUpdate(updates: ServiceWorkerUpdates): {
75
+ needRefresh: boolean;
76
+ onReload: () => void;
77
+ onDismiss: () => void;
78
+ } {
79
+ const needRefresh = useSyncExternalStore(updates.subscribe, updates.needRefresh, () => false);
80
+ return { needRefresh, onReload: updates.reload, onDismiss: updates.dismiss };
81
+ }
@@ -0,0 +1,8 @@
1
+ // The English words of ./shared-computer, in a module with no component code (0.34, KS U8).
2
+
3
+ import type { SharedComputerLabels } from "./shared-computer";
4
+
5
+ export const DEFAULT_SHARED_COMPUTER_LABELS: SharedComputerLabels = {
6
+ label: "This is a shared computer",
7
+ hint: "No copy of your data stays on this device. Pages then open only with a connection.",
8
+ };
@@ -0,0 +1,87 @@
1
+ import { useState } from "react";
2
+ import { useKitLabels, type LabelOverride } from "../i18n/kit-labels";
3
+ import { Checkbox } from "../components/checkbox";
4
+ import { readSessionStored, readStored, removeStored, writeSessionStored, writeStored } from "../lib/safe-storage";
5
+ import { DEFAULT_SHARED_COMPUTER_LABELS } from "./shared-computer-labels";
6
+
7
+ /**
8
+ * "This is a shared computer" — a choice on the sign-in form that persists nothing
9
+ * (docs/local-first-harmonization.md §1.5 D6, D7). When it is on, the app does not mount
10
+ * the query persistence (`createQueryPersistence`): the cache lives in memory only.
11
+ *
12
+ * The choice must live exactly as long as the session it governs. By default it is held in
13
+ * `sessionStorage`, which fits a session that ends with the tab. An app whose session
14
+ * outlives the tab (a refresh token in `localStorage`: kastlan) passes `storage: "local"`
15
+ * and calls `setSharedComputer(false)` at every end of that session (logout, a failed
16
+ * refresh), beside removing the token. `isSharedComputer()` reads both places, so a choice
17
+ * made in either keeps the cache in memory. Where storage is blocked, `isSharedComputer()`
18
+ * is `false` and `setSharedComputer` does nothing.
19
+ */
20
+ export interface SharedComputerLabels {
21
+ label: string;
22
+ /** The one line under it: what the choice does. */
23
+ hint: string;
24
+ }
25
+
26
+ // Defined in ./shared-computer-labels, which holds no component code (0.34, KS U8).
27
+ export { DEFAULT_SHARED_COMPUTER_LABELS };
28
+
29
+ const KEY = "eifi1-shared-computer";
30
+
31
+ /** Where the choice is held: `"session"` (this tab, the default) or `"local"` (as long as
32
+ * a session kept in `localStorage`, until `setSharedComputer(false)`). */
33
+ export type SharedComputerStorage = "session" | "local";
34
+
35
+ /** Whether this session was signed in as on a shared computer, the choice held in either
36
+ * place. `false` when storage is unavailable. */
37
+ export function isSharedComputer(): boolean {
38
+ return readSessionStored(KEY) === "1" || readStored(KEY) === "1";
39
+ }
40
+
41
+ /** Records the choice in `options.storage` (default `"session"`), and only there; `false`
42
+ * forgets it in both places. */
43
+ export function setSharedComputer(on: boolean, options: { storage?: SharedComputerStorage } = {}): void {
44
+ const local = options.storage === "local";
45
+ writeSessionStored(KEY, on && !local ? "1" : null);
46
+ if (on && local) writeStored(KEY, "1");
47
+ else removeStored(KEY);
48
+ }
49
+
50
+ export interface SharedComputerChoiceProps {
51
+ /** Controlled. Left out, the box starts from {@link isSharedComputer}. */
52
+ checked?: boolean;
53
+ /** Told after the choice is stored. */
54
+ onCheckedChange?: (on: boolean) => void;
55
+ /** Where the choice is stored: see {@link setSharedComputer}. Default `"session"`. */
56
+ storage?: SharedComputerStorage;
57
+ disabled?: boolean;
58
+ className?: string;
59
+ labels?: LabelOverride<SharedComputerLabels>;
60
+ }
61
+
62
+ export function SharedComputerChoice({
63
+ checked,
64
+ onCheckedChange,
65
+ storage,
66
+ disabled,
67
+ className,
68
+ labels: labelsProp,
69
+ }: SharedComputerChoiceProps) {
70
+ const labels = useKitLabels("sharedComputer", DEFAULT_SHARED_COMPUTER_LABELS, labelsProp);
71
+ const [own, setOwn] = useState(isSharedComputer);
72
+ const on = checked ?? own;
73
+ return (
74
+ <Checkbox
75
+ className={className}
76
+ label={labels.label}
77
+ description={labels.hint}
78
+ checked={on}
79
+ disabled={disabled}
80
+ onCheckedChange={(next) => {
81
+ setSharedComputer(next, { storage });
82
+ setOwn(next);
83
+ onCheckedChange?.(next);
84
+ }}
85
+ />
86
+ );
87
+ }
@@ -0,0 +1,21 @@
1
+ // The English words of ./sign-out-pending-dialog, in a module with no component code: the
2
+ // English catalogue (src/i18n/defaults.ts) imports them without the component (0.34, KS U8).
3
+ // keksdose's reviewed words (nav.logout_queued_*, account-menu.tsx:247-317) are the canon.
4
+
5
+ import type { SignOutPendingDialogLabels } from "./sign-out-pending-dialog";
6
+
7
+ export const DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS: SignOutPendingDialogLabels = {
8
+ title: "Some changes have not been saved to the server yet",
9
+ lost: (count) =>
10
+ count === 1
11
+ ? "1 change is still waiting to sync. Logging out clears this device’s copy, and it is the only copy."
12
+ : `${count} changes are still waiting to sync. Logging out clears this device’s copy, and it is the only copy.`,
13
+ kept: (count) =>
14
+ count === 1
15
+ ? "1 change is still waiting to sync. It stays on this device for you and syncs the next time you log in here."
16
+ : `${count} changes are still waiting to sync. They stay on this device for you and sync the next time you log in here.`,
17
+ syncNow: "Sync now",
18
+ signOutAnyway: "Log out anyway",
19
+ discardWaiting: "Discard waiting changes",
20
+ cancel: "Cancel",
21
+ };
@@ -0,0 +1,135 @@
1
+ import { useState } from "react";
2
+ import type { ReactNode } from "react";
3
+ import { useKitLabels, type LabelOverride } from "../i18n/kit-labels";
4
+ import { Button } from "../components/button";
5
+ import { DialogFrame } from "../components/dialog-frame";
6
+ import { DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS } from "./sign-out-pending-dialog-labels";
7
+
8
+ /**
9
+ * The question before an explicit sign-out while changes still wait to sync — keksdose's
10
+ * confirm (account-menu.tsx:247-317) as a kit dialog (docs/local-first-harmonization.md
11
+ * §1.5 D6, §1.6). Show it only when `queued > 0` and only on a sign-out someone asked for:
12
+ * never from a session-ended hook, which cannot ask anyone.
13
+ *
14
+ * "Sync now" is the primary action: the queued writes are the person's own data. It runs
15
+ * `onSyncNow`, is busy while that runs, then calls `onClose` without signing out — the
16
+ * count has moved and the caller re-evaluates (signs out at once when nothing is left, or
17
+ * opens this again with the new number). "Log out anyway" stays as the escape hatch, usable
18
+ * while a sync runs (a hanging one would trap the person otherwise): a device that cannot
19
+ * reach the server must still be able to sign out. Only a running discard holds it.
20
+ *
21
+ * The words depend on what the app does with the queue at sign-out (`keepsChanges`):
22
+ * kastlan keeps the outbox for its owner, keksdose wipes it — and keksdose's copy must not
23
+ * say the changes stay.
24
+ */
25
+ export interface SignOutPendingDialogLabels {
26
+ title: string;
27
+ /** `keepsChanges={false}`: signing out destroys the device's copy. */
28
+ lost: (count: number) => string;
29
+ /** `keepsChanges`: the queue stays on the device for its owner. */
30
+ kept: (count: number) => string;
31
+ syncNow: string;
32
+ signOutAnyway: string;
33
+ /** Only with `onDiscardWaiting` and `waitingForPlan > 0` (the billing kit's words). */
34
+ discardWaiting: string;
35
+ cancel: string;
36
+ }
37
+
38
+ // Defined in ./sign-out-pending-dialog-labels, which holds no component code (0.34, KS U8).
39
+ export { DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS };
40
+
41
+ export interface SignOutPendingDialogProps {
42
+ open: boolean;
43
+ /** Changes not yet on the server: pending + blocked. The tour `:pending` record is not
44
+ * one of them (D6). */
45
+ queued: number;
46
+ /** Of those, the ones waiting for a plan (billing's read-only refusal). */
47
+ waitingForPlan: number;
48
+ /** Runs one sync round. The dialog closes when it settles, whatever it managed. */
49
+ onSyncNow: () => Promise<unknown> | void;
50
+ /** Signs out now, queue or not. */
51
+ onSignOutAnyway: () => void;
52
+ /** Lets the plan-waiting changes go; the dialog then signs out as asked. Without it
53
+ * (or with `waitingForPlan` 0) the button is not shown. */
54
+ onDiscardWaiting?: () => Promise<unknown> | void;
55
+ /** `true` (kastlan): the changes stay on this device for their owner. `false`
56
+ * (keksdose): they are lost. Picks the words. */
57
+ keepsChanges: boolean;
58
+ /** Room under the sentence — the billing kit's `waitingChanges` line. Shown only while
59
+ * `waitingForPlan > 0`, so a 0-count line never shows. */
60
+ extra?: ReactNode;
61
+ /** Cancel, Escape, the X — and after "Sync now". */
62
+ onClose: () => void;
63
+ labels?: LabelOverride<SignOutPendingDialogLabels>;
64
+ }
65
+
66
+ export function SignOutPendingDialog({
67
+ open,
68
+ queued,
69
+ waitingForPlan,
70
+ onSyncNow,
71
+ onSignOutAnyway,
72
+ onDiscardWaiting,
73
+ keepsChanges,
74
+ extra,
75
+ onClose,
76
+ labels: labelsProp,
77
+ }: SignOutPendingDialogProps) {
78
+ const labels = useKitLabels("signOutPending", DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS, labelsProp);
79
+ const [syncing, setSyncing] = useState(false);
80
+ const [discarding, setDiscarding] = useState(false);
81
+
82
+ const syncNow = async () => {
83
+ setSyncing(true);
84
+ try {
85
+ await onSyncNow();
86
+ } catch {
87
+ /* whatever the sync managed, the decision is the person's again */
88
+ } finally {
89
+ setSyncing(false);
90
+ }
91
+ onClose();
92
+ };
93
+
94
+ const discard = async () => {
95
+ if (!onDiscardWaiting) return;
96
+ setDiscarding(true);
97
+ try {
98
+ await onDiscardWaiting();
99
+ } finally {
100
+ setDiscarding(false);
101
+ }
102
+ onSignOutAnyway();
103
+ };
104
+
105
+ return (
106
+ <DialogFrame
107
+ open={open}
108
+ role="alertdialog"
109
+ onClose={onClose}
110
+ title={labels.title}
111
+ description={keepsChanges ? labels.kept(queued) : labels.lost(queued)}
112
+ closeButton
113
+ actions={
114
+ <>
115
+ <Button variant="secondary" onClick={onClose}>
116
+ {labels.cancel}
117
+ </Button>
118
+ {onDiscardWaiting && waitingForPlan > 0 && (
119
+ <Button variant="secondary" pending={discarding} disabled={syncing} onClick={() => void discard()}>
120
+ {labels.discardWaiting}
121
+ </Button>
122
+ )}
123
+ <Button pending={syncing} disabled={discarding} onClick={() => void syncNow()}>
124
+ {labels.syncNow}
125
+ </Button>
126
+ <Button variant="secondary" disabled={discarding} onClick={onSignOutAnyway}>
127
+ {labels.signOutAnyway}
128
+ </Button>
129
+ </>
130
+ }
131
+ >
132
+ {waitingForPlan > 0 && (typeof extra === "string" ? <p className="text-sm text-muted">{extra}</p> : extra)}
133
+ </DialogFrame>
134
+ );
135
+ }
package/src/offline.ts ADDED
@@ -0,0 +1,32 @@
1
+ // `@eifi1/ui-kit/offline` — local-first phase 1 (docs/local-first-harmonization.md §1.6,
2
+ // 0.45): the persisted query cache with its scrub list, the sign-out question while changes
3
+ // wait, the service-worker update prompt, and the shared-computer choice.
4
+ //
5
+ // The only entry point that requires `@tanstack/react-query`,
6
+ // `@tanstack/react-query-persist-client`, `@tanstack/query-async-storage-persister` and
7
+ // `idb-keyval` — optional peers for that reason. NOT re-exported from the main barrel, which
8
+ // would make them everyone's install; the packaging contract test holds that line.
9
+ export { createQueryPersistence, DEFAULT_SCRUB_FIELDS, isCacheSafe, removeStaleQueryCopies } from "./offline/query-persistence";
10
+ export type {
11
+ QueryPersistence,
12
+ QueryPersistenceOptions,
13
+ QueryPersistenceStorage,
14
+ RemoveStaleQueryCopiesOptions,
15
+ ScrubField,
16
+ } from "./offline/query-persistence";
17
+ export { SignOutPendingDialog, DEFAULT_SIGN_OUT_PENDING_DIALOG_LABELS } from "./offline/sign-out-pending-dialog";
18
+ export type { SignOutPendingDialogProps, SignOutPendingDialogLabels } from "./offline/sign-out-pending-dialog";
19
+ export { PwaUpdatePrompt, DEFAULT_PWA_UPDATE_PROMPT_LABELS } from "./offline/pwa-update-prompt";
20
+ export type { PwaUpdatePromptProps, PwaUpdatePromptLabels } from "./offline/pwa-update-prompt";
21
+ // 0.46 (kastlan K3): the service worker registered once at start, its update state as a store.
22
+ export { createServiceWorkerUpdates, useServiceWorkerUpdate } from "./offline/service-worker-updates";
23
+ export type { RegisterServiceWorker, ServiceWorkerUpdates } from "./offline/service-worker-updates";
24
+ export {
25
+ SharedComputerChoice,
26
+ DEFAULT_SHARED_COMPUTER_LABELS,
27
+ isSharedComputer,
28
+ setSharedComputer,
29
+ } from "./offline/shared-computer";
30
+ export type { SharedComputerChoiceProps, SharedComputerLabels, SharedComputerStorage } from "./offline/shared-computer";
31
+ // 0.46 (§2.3): the leaf utilities, shared with `@eifi1/ui-kit/sync` from one module that imports no peer.
32
+ export { isOfflineError, readLocalFirst, stampAfter, writeLocalFirst } from "./offline/local-first";
@@ -0,0 +1,25 @@
1
+ // `@eifi1/ui-kit/push-worker` — the service worker's half of Web Push (0.46,
2
+ // docs/notifications-harmonization.md §10.6 rule 5): re-registering a subscription the browser
3
+ // rotated. A worker bundle imports from here, never from the main barrel, which reaches React
4
+ // and the DOM components:
5
+ //
6
+ // // sw.ts
7
+ // import { handlePushSubscriptionChange } from "@eifi1/ui-kit/push-worker";
8
+ //
9
+ // Worker-safe: this entry reaches no package and no module but src/notifications/push-worker.ts
10
+ // (packaging-contract.test.ts, scripts/check-module-graph.mjs, scripts/verify-package.mjs).
11
+ // The main barrel keeps re-exporting the same names, so an existing import keeps working.
12
+ export {
13
+ PUSH_SUBSCRIPTION_CHANGED,
14
+ handlePushSubscriptionChange,
15
+ pushSubscriptionInput,
16
+ resubscribeAfterChange,
17
+ } from "./notifications/push-worker";
18
+ export type {
19
+ PushReplaceInput,
20
+ PushResubscribeOutcome,
21
+ PushSubscriptionChangeEventLike,
22
+ PushSubscriptionChangeOptions,
23
+ PushSubscriptionInput,
24
+ PushWorkerScope,
25
+ } from "./notifications/push-worker";
@@ -0,0 +1,77 @@
1
+ import { AArrowDown, AArrowUp, ALargeSmall } from "lucide-react";
2
+ import { IconButton } from "../components/icon-button";
3
+ import { useAppearanceLabels, type AppearanceLabels } from "../components/appearance-labels";
4
+ import type { LabelOverride } from "../i18n/kit-labels";
5
+ import { TEXT_SIZES, useTextSize, type TextSize } from "../theme/text-size";
6
+
7
+ export interface AccountMenuTextSizeProps {
8
+ /** The size in force. Default: the one on `<html>` ({@link useTextSize}), which is the
9
+ * size whoever applied it put there. */
10
+ value?: TextSize;
11
+ /**
12
+ * A step: the next size down or up. The app applies it and keeps it as its Settings ▸
13
+ * Appearance does — `useAccountAppearance`'s `pickTextSize` (this device, and the
14
+ * account when signed in), with `appearance.saveFailed` as the toast when the account
15
+ * refused. The kit does not apply it itself, so the two places can never disagree.
16
+ */
17
+ onPick: (size: TextSize) => void;
18
+ /** The row's name. Default: `appearance.textSize` ("Text size"). */
19
+ label?: string;
20
+ /** The `appearance` words (`textSize`, `textSizes`, `textSmaller`, `textLarger`). */
21
+ labels?: LabelOverride<AppearanceLabels>;
22
+ }
23
+
24
+ /**
25
+ * The text size one tap away, INSIDE the account menu (0.46, keksdose live #426, its
26
+ * `TextSizeRow`): the row's icon and name, the size in force, and A− / A+, which step
27
+ * through {@link TEXT_SIZES} (Normal → Large → Extra large). A− is disabled at the
28
+ * smallest, A+ at the largest. The browser's pinch zoom stays as it is — this is the
29
+ * quick path to the setting, not a replacement for zoom.
30
+ *
31
+ * It renders its own `<li>`: drop it into `TopBarActionMenu`'s `children` beside
32
+ * `AccountMenuLanguage`. The menu promotes the two buttons to `menuitem`s, so ↑/↓ reach
33
+ * them like every other row, and it stays open on a step: the person sees the page grow
34
+ * behind it and can step again. The size's name is a polite live region, so a step is
35
+ * announced.
36
+ */
37
+ export function AccountMenuTextSize({ value, onPick, label, labels: labelsProp }: AccountMenuTextSizeProps) {
38
+ const labels = useAppearanceLabels(labelsProp);
39
+ const onHtml = useTextSize().size;
40
+ const size = value ?? onHtml;
41
+ const index = TEXT_SIZES.indexOf(size);
42
+ const step = (by: -1 | 1) => {
43
+ const next = TEXT_SIZES[index + by];
44
+ if (next) onPick(next);
45
+ };
46
+ return (
47
+ <li data-account-menu-text-size="" className="flex items-center gap-2 px-3 py-1.5 text-sm">
48
+ <ALargeSmall aria-hidden className="size-4 shrink-0 text-muted" />
49
+ <span className="min-w-0 flex-1 truncate">
50
+ {label ?? labels.textSize}
51
+ <span className="ms-2 text-xs text-muted" aria-live="polite">
52
+ {labels.textSizes[size]}
53
+ </span>
54
+ </span>
55
+ <IconButton
56
+ size="sm"
57
+ label={labels.textSmaller}
58
+ labelVisible={false}
59
+ tooltip={false}
60
+ disabled={index <= 0}
61
+ onClick={() => step(-1)}
62
+ >
63
+ <AArrowDown />
64
+ </IconButton>
65
+ <IconButton
66
+ size="sm"
67
+ label={labels.textLarger}
68
+ labelVisible={false}
69
+ tooltip={false}
70
+ disabled={index >= TEXT_SIZES.length - 1}
71
+ onClick={() => step(1)}
72
+ >
73
+ <AArrowUp />
74
+ </IconButton>
75
+ </li>
76
+ );
77
+ }
@@ -25,11 +25,19 @@ function badgeText(badge: number | null | undefined): string | null {
25
25
  * pending count's tone, with the number for the eye and `adminArea.waiting` for a reader
26
26
  * ("Administration, 3 waiting"). `null` for no count.
27
27
  */
28
- function WaitingBadge({ badge, labels }: { badge: number | null | undefined; labels: AdminAreaLabels }) {
28
+ function WaitingBadge({
29
+ badge,
30
+ labels,
31
+ className,
32
+ }: {
33
+ badge: number | null | undefined;
34
+ labels: AdminAreaLabels;
35
+ className?: string;
36
+ }) {
29
37
  const text = badgeText(badge);
30
38
  if (text === null) return null;
31
39
  return (
32
- <Chip size="sm" variant="solid" tone="warning" data-slot="admin-badge">
40
+ <Chip size="sm" variant="solid" tone="warning" data-slot="admin-badge" className={className}>
33
41
  <span aria-hidden>{text}</span>
34
42
  {/* The comma parts it from the row's label in the name: "Administration, 3 waiting". */}
35
43
  <span className="sr-only-fixed">, {labels.waiting(badge as number)}</span>
@@ -37,6 +45,35 @@ function WaitingBadge({ badge, labels }: { badge: number | null | undefined; lab
37
45
  );
38
46
  }
39
47
 
48
+ export interface AdminWaitingBadgeProps {
49
+ /** The waiting count: only work that waits on this admin. `0`, `null` or none draws
50
+ * nothing; past 99 it reads "99+". */
51
+ count: number | null | undefined;
52
+ /** Prop > `<UiKitProvider labels={{ adminArea }}>` > English. */
53
+ labels?: LabelOverride<AdminAreaLabels>;
54
+ className?: string;
55
+ }
56
+
57
+ /**
58
+ * The waiting pill of {@link AdminMenuRow} on its own (0.45, docs/admin-harmonization.md
59
+ * §9.3.4): the `solid` warning count, "99+" past 99, nothing for 0. Fit for `IconButton`'s
60
+ * `badge` — kastlan's top-bar platform button. There the badge is DECORATIVE (`aria-hidden`
61
+ * by IconButton), so the button's name carries the count: `adminArea.waitingLabel`.
62
+ *
63
+ * const area = useAdminAreaLabels();
64
+ * <IconButton
65
+ * label={waiting > 0 ? area.waitingLabel("Platform", waiting) : "Platform"}
66
+ * badge={<AdminWaitingBadge count={waiting} />}
67
+ * …
68
+ * />
69
+ *
70
+ * Read on its own (outside an IconButton), the pill still says ", 3 waiting" to a reader.
71
+ */
72
+ export function AdminWaitingBadge({ count, labels: labelsProp, className }: AdminWaitingBadgeProps) {
73
+ const labels = useAdminAreaLabels(labelsProp);
74
+ return <WaitingBadge badge={count} labels={labels} className={className} />;
75
+ }
76
+
40
77
  export interface AdminMenuEntryOptions {
41
78
  /** The admin area. Default `/admin`. */
42
79
  href?: string;
@@ -136,7 +173,7 @@ export function AdminMenuRow({ href = "/admin", badge, onNavigate, current, labe
136
173
  renderLink={({ href: to, ...p }) => <Link to={to} {...p} />}
137
174
  onClick={onNavigate ? () => onNavigate() : undefined}
138
175
  leading={<ShieldCheck className="size-4" aria-hidden />}
139
- trailing={badgeText(badge) === null ? undefined : <WaitingBadge badge={badge} labels={labels} />}
176
+ trailing={badgeText(badge) === null ? undefined : <AdminWaitingBadge count={badge} labels={labels} />}
140
177
  current={current}
141
178
  >
142
179
  {labels.title}
@@ -24,6 +24,10 @@ export interface TopBarActionsProps {
24
24
  dev?: ReactNode;
25
25
  /** kastlan's platform button. */
26
26
  platform?: ReactNode;
27
+ /** 0.46 (docs/notifications-harmonization.md §10.1): `NotificationBell`, right before the
28
+ * account menu. Optional per app: an app shows the bell or an unread dot on the avatar,
29
+ * never both (keksdose keeps its dot and leaves this empty). */
30
+ notifications?: ReactNode;
27
31
  /** The account menu. When it is set the signed-out slots are not drawn. */
28
32
  account?: ReactNode;
29
33
  /** Signed out (§6.6): the bar's `LanguageMenu`, since there is no account menu to hold
@@ -45,6 +49,7 @@ export const TOP_BAR_ACTION_ORDER = [
45
49
  "sync",
46
50
  "dev",
47
51
  "platform",
52
+ "notifications",
48
53
  "account",
49
54
  "language",
50
55
  "signIn",
@@ -55,7 +60,7 @@ const present = (node: ReactNode) => node !== undefined && node !== null && node
55
60
  /**
56
61
  * The top bar's actions in the one order every app shares (§2.1): company switcher ·
57
62
  * Search · Theme · Feedback · Tours (· Assistant) · Sync · dev role · dev plan · platform ·
58
- * account menu — and, signed out, Language · Sign in where the account menu goes (§6.6).
63
+ * notifications (the bell, 0.46) · account menu — and, signed out, Language · Sign in where the account menu goes (§6.6).
59
64
  * The app fills the slots it has; an empty slot is skipped, so it leaves no gap.
60
65
  *
61
66
  * It draws a fragment, no box of its own: hand it to `TopBar`'s `actions`, whose row
package/src/shell.ts CHANGED
@@ -20,6 +20,10 @@ export type { AdminMenuEntryOptions, AdminMenuRowProps } from "./shell/admin-men
20
20
  // 0.43.0 (kastlan): the entry with the provider's words.
21
21
  export { useAdminMenuEntry } from "./shell/admin-menu";
22
22
  export type { UseAdminMenuEntryOptions } from "./shell/admin-menu";
23
+ // 0.45.0 (docs/admin-harmonization.md §9.3.4): the row's waiting pill on its own, for
24
+ // IconButton's `badge` (kastlan's platform button); its name is `adminArea.waitingLabel`.
25
+ export { AdminWaitingBadge } from "./shell/admin-menu";
26
+ export type { AdminWaitingBadgeProps } from "./shell/admin-menu";
23
27
  // 0.31.0: the settings and admin pages' shell (docs/settings-harmonization.md §3, §5).
24
28
  export { SettingsLayout, SETTINGS_SEARCH_THRESHOLD } from "./settings/settings-layout";
25
29
  export type { SettingsLayoutProps, SettingsLayoutWidth, SettingsDocumentTitleParts } from "./settings/settings-layout";
@@ -78,6 +82,9 @@ export { TopBarActions, TOP_BAR_ACTION_ORDER } from "./shell/top-bar-actions";
78
82
  export type { TopBarActionsProps } from "./shell/top-bar-actions";
79
83
  export { AccountMenuLanguage } from "./shell/account-menu-language";
80
84
  export type { AccountMenuLanguageProps, AccountMenuLanguageOption } from "./shell/account-menu-language";
85
+ // 0.46.0 (keksdose live #426): A− / A+ for the text size, beside the language.
86
+ export { AccountMenuTextSize } from "./shell/account-menu-text-size";
87
+ export type { AccountMenuTextSizeProps } from "./shell/account-menu-text-size";
81
88
  export { TourMenu } from "./shell/tour-menu";
82
89
  export type { TourMenuProps, TourMenuTour } from "./shell/tour-menu";
83
90
  export { SyncStatusIndicator, syncState } from "./shell/sync-status-indicator";