@eifi1/ui-kit 0.44.0 → 0.45.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 (124) hide show
  1. package/README.md +10 -6
  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 +6 -0
  8. package/dist/admin/admin-area-labels.js +1 -0
  9. package/dist/admin/admin-area-labels.js.map +1 -1
  10. package/dist/admin/mail-test-card-labels.d.ts +3 -0
  11. package/dist/admin/mail-test-card-labels.js +17 -0
  12. package/dist/admin/mail-test-card-labels.js.map +1 -0
  13. package/dist/admin/mail-test-card.d.ts +74 -0
  14. package/dist/admin/mail-test-card.js +80 -0
  15. package/dist/admin/mail-test-card.js.map +1 -0
  16. package/dist/components/data-table-mobile.js +17 -2
  17. package/dist/components/data-table-mobile.js.map +1 -1
  18. package/dist/components/data-table-shared.d.ts +2 -0
  19. package/dist/components/data-table-shared.js +8 -0
  20. package/dist/components/data-table-shared.js.map +1 -1
  21. package/dist/components/data-table.js +1 -6
  22. package/dist/components/data-table.js.map +1 -1
  23. package/dist/eslint/index.js +1 -1
  24. package/dist/i18n/defaults.js +9 -1
  25. package/dist/i18n/defaults.js.map +1 -1
  26. package/dist/i18n/german.js +40 -0
  27. package/dist/i18n/german.js.map +1 -1
  28. package/dist/i18n/kit-labels.d.ts +13 -0
  29. package/dist/i18n/kit-labels.js.map +1 -1
  30. package/dist/i18n/locales/en.js +57 -1
  31. package/dist/i18n/locales/en.js.map +3 -3
  32. package/dist/i18n/locales/es.js +40 -0
  33. package/dist/i18n/locales/es.js.map +1 -1
  34. package/dist/i18n/locales/fr.js +40 -0
  35. package/dist/i18n/locales/fr.js.map +1 -1
  36. package/dist/i18n/locales/hu.js +40 -0
  37. package/dist/i18n/locales/hu.js.map +1 -1
  38. package/dist/i18n/locales/it.js +40 -0
  39. package/dist/i18n/locales/it.js.map +1 -1
  40. package/dist/i18n/locales/zh.js +40 -0
  41. package/dist/i18n/locales/zh.js.map +1 -1
  42. package/dist/i18n/review.js +13 -1
  43. package/dist/i18n/review.js.map +1 -1
  44. package/dist/index.d.ts +2 -0
  45. package/dist/index.js +5 -0
  46. package/dist/index.js.map +1 -1
  47. package/dist/lib/format.d.ts +2 -1
  48. package/dist/lib/format.js +6 -1
  49. package/dist/lib/format.js.map +1 -1
  50. package/dist/offline/pwa-update-prompt-labels.d.ts +2 -0
  51. package/dist/offline/pwa-update-prompt-labels.js +9 -0
  52. package/dist/offline/pwa-update-prompt-labels.js.map +1 -0
  53. package/dist/offline/pwa-update-prompt.d.ts +32 -0
  54. package/dist/offline/pwa-update-prompt.js +36 -0
  55. package/dist/offline/pwa-update-prompt.js.map +1 -0
  56. package/dist/offline/query-persistence.d.ts +100 -0
  57. package/dist/offline/query-persistence.js +179 -0
  58. package/dist/offline/query-persistence.js.map +1 -0
  59. package/dist/offline/shared-computer-labels.d.ts +2 -0
  60. package/dist/offline/shared-computer-labels.js +9 -0
  61. package/dist/offline/shared-computer-labels.js.map +1 -0
  62. package/dist/offline/shared-computer.d.ts +44 -0
  63. package/dist/offline/shared-computer.js +51 -0
  64. package/dist/offline/shared-computer.js.map +1 -0
  65. package/dist/offline/sign-out-pending-dialog-labels.d.ts +2 -0
  66. package/dist/offline/sign-out-pending-dialog-labels.js +14 -0
  67. package/dist/offline/sign-out-pending-dialog-labels.js.map +1 -0
  68. package/dist/offline/sign-out-pending-dialog.d.ts +58 -0
  69. package/dist/offline/sign-out-pending-dialog.js +66 -0
  70. package/dist/offline/sign-out-pending-dialog.js.map +1 -0
  71. package/dist/offline.d.ts +8 -0
  72. package/dist/offline.js +24 -0
  73. package/dist/offline.js.map +1 -0
  74. package/dist/shell/admin-menu.d.ts +24 -0
  75. package/dist/shell/admin-menu.js +12 -3
  76. package/dist/shell/admin-menu.js.map +1 -1
  77. package/dist/shell.d.ts +2 -0
  78. package/dist/shell.js +2 -0
  79. package/dist/shell.js.map +1 -1
  80. package/dist/tour/tour-catalogue.d.ts +7 -1
  81. package/dist/tour/tour-catalogue.js.map +1 -1
  82. package/dist/tour/tour.js +4 -2
  83. package/dist/tour/tour.js.map +1 -1
  84. package/dist/tour/use-tour-progress.d.ts +99 -1
  85. package/dist/tour/use-tour-progress.js +228 -12
  86. package/dist/tour/use-tour-progress.js.map +1 -1
  87. package/dist/tour.d.ts +2 -0
  88. package/dist/tour.js +2 -0
  89. package/dist/tour.js.map +1 -1
  90. package/package.json +25 -1
  91. package/src/admin/admin-action-log-labels.ts +9 -0
  92. package/src/admin/admin-action-log.tsx +41 -5
  93. package/src/admin/admin-area-labels.ts +7 -0
  94. package/src/admin/mail-test-card-labels.ts +19 -0
  95. package/src/admin/mail-test-card.tsx +186 -0
  96. package/src/components/data-table-mobile.tsx +26 -3
  97. package/src/components/data-table-shared.tsx +18 -1
  98. package/src/components/data-table.tsx +1 -15
  99. package/src/i18n/defaults.ts +8 -0
  100. package/src/i18n/german.ts +46 -0
  101. package/src/i18n/kit-labels.tsx +13 -0
  102. package/src/i18n/locales/en.ts +12 -0
  103. package/src/i18n/locales/es.ts +46 -0
  104. package/src/i18n/locales/fr.ts +46 -0
  105. package/src/i18n/locales/hu.ts +42 -0
  106. package/src/i18n/locales/it.ts +46 -0
  107. package/src/i18n/locales/zh.ts +40 -0
  108. package/src/i18n/review.ts +12 -0
  109. package/src/index.ts +3 -0
  110. package/src/lib/format.ts +13 -2
  111. package/src/offline/pwa-update-prompt-labels.ts +9 -0
  112. package/src/offline/pwa-update-prompt.tsx +72 -0
  113. package/src/offline/query-persistence.ts +323 -0
  114. package/src/offline/shared-computer-labels.ts +8 -0
  115. package/src/offline/shared-computer.tsx +87 -0
  116. package/src/offline/sign-out-pending-dialog-labels.ts +21 -0
  117. package/src/offline/sign-out-pending-dialog.tsx +135 -0
  118. package/src/offline.ts +26 -0
  119. package/src/shell/admin-menu.tsx +40 -3
  120. package/src/shell.ts +4 -0
  121. package/src/tour/tour-catalogue.tsx +7 -1
  122. package/src/tour/tour.tsx +5 -2
  123. package/src/tour/use-tour-progress.ts +378 -12
  124. package/src/tour.ts +3 -0
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from "react";
3
3
  import { readStored, removeStored, writeStored } from "../lib/safe-storage.js";
4
+ import { httpStatusOf } from "../auth/auth-errors.js";
4
5
  import { isTourDone } from "./tour-info.js";
5
6
  const EMPTY = Object.freeze({});
6
7
  const listeners = /* @__PURE__ */ new Map();
@@ -77,6 +78,203 @@ function write(key, record) {
77
78
  else memory.set(key, raw);
78
79
  emit(key);
79
80
  }
81
+ const accounts = /* @__PURE__ */ new Map();
82
+ const loaded = /* @__PURE__ */ new Set();
83
+ const running = /* @__PURE__ */ new Set();
84
+ const rerun = /* @__PURE__ */ new Set();
85
+ const queued = /* @__PURE__ */ new Set();
86
+ const epochs = /* @__PURE__ */ new Map();
87
+ const parked = /* @__PURE__ */ new Set();
88
+ const pendingMemory = /* @__PURE__ */ new Map();
89
+ let onlineListening = false;
90
+ const pendingKey = (key) => `${key}:pending`;
91
+ function devWarn(message) {
92
+ if (import.meta.env?.DEV) console.warn(message);
93
+ }
94
+ function rememberAccount(key, account) {
95
+ if (account && !accounts.has(key)) {
96
+ accounts.set(key, account);
97
+ if (account.adopt && account.adopt.storageKey === key) {
98
+ devWarn(
99
+ `[ui-kit] tour progress: the account key "${key}" is the key \`adopt\` takes over. With an account the key must be per account (a prefix plus the user id), apart from the old device-wide key.`
100
+ );
101
+ }
102
+ if (!onlineListening && typeof window !== "undefined") {
103
+ onlineListening = true;
104
+ window.addEventListener("online", () => {
105
+ for (const known of loaded) void sync(known);
106
+ });
107
+ }
108
+ }
109
+ return accounts.get(key);
110
+ }
111
+ function readPending(key) {
112
+ const raw = pendingMemory.get(key) ?? readStored(pendingKey(key));
113
+ if (raw === null) return null;
114
+ try {
115
+ const value = JSON.parse(raw);
116
+ if (!value || typeof value !== "object") return null;
117
+ const completed = {};
118
+ for (const [id, at] of Object.entries(value.completed ?? {})) {
119
+ if (typeof at === "string") completed[id] = at;
120
+ else if (at === null) completed[id] = "";
121
+ }
122
+ const forget = Array.isArray(value.forget) ? value.forget.filter((id) => typeof id === "string") : [];
123
+ return { synced: value.synced === true, completed, forget };
124
+ } catch {
125
+ return null;
126
+ }
127
+ }
128
+ function writePending(key, record) {
129
+ const raw = JSON.stringify(record);
130
+ writeStored(pendingKey(key), raw);
131
+ if (readStored(pendingKey(key)) === raw) pendingMemory.delete(key);
132
+ else pendingMemory.set(key, raw);
133
+ }
134
+ const hasWork = (record) => Object.keys(record.completed).length > 0 || record.forget.length > 0;
135
+ function later(a, b) {
136
+ if (a === void 0 || a === "") return b;
137
+ if (b === "") return a;
138
+ return Date.parse(b) > Date.parse(a) ? b : a;
139
+ }
140
+ function markPending(key, id, at, forget = false) {
141
+ const record = readPending(key) ?? { synced: false, completed: {}, forget: [] };
142
+ if (forget) {
143
+ delete record.completed[id];
144
+ if (!record.forget.includes(id)) record.forget.push(id);
145
+ } else {
146
+ record.completed[id] = at ?? "";
147
+ record.forget = record.forget.filter((other) => other !== id);
148
+ }
149
+ writePending(key, record);
150
+ }
151
+ function clearSent(key, sent, synced) {
152
+ const record = readPending(key) ?? { synced: false, completed: {}, forget: [] };
153
+ for (const [id, at] of Object.entries(sent.completed ?? {})) {
154
+ if (record.completed[id] === at) delete record.completed[id];
155
+ }
156
+ const forgotten = new Set(sent.forget ?? []);
157
+ record.forget = record.forget.filter((id) => !forgotten.has(id));
158
+ record.synced = record.synced || synced;
159
+ writePending(key, record);
160
+ }
161
+ function refill(key, answer) {
162
+ const next = {};
163
+ for (const [id, at] of Object.entries(fromRecord(answer?.completed) ?? {})) next[id] = at ?? "";
164
+ const waiting = readPending(key);
165
+ if (waiting) {
166
+ for (const [id, at] of Object.entries(waiting.completed)) next[id] = later(next[id], at);
167
+ for (const id of waiting.forget) delete next[id];
168
+ }
169
+ const record = {};
170
+ for (const [id, at] of Object.entries(next)) record[id] = at === "" ? null : at;
171
+ write(key, record);
172
+ }
173
+ function adoptOldRecord(key, account) {
174
+ const adopt = account.adopt;
175
+ if (!adopt || adopt.storageKey === key) return;
176
+ const old = adopt.storageKey;
177
+ if (!adopt.discard) {
178
+ const { value } = parse(rawOf(old), adopt.migrate ?? migrates.get(old));
179
+ const entries = Object.entries(value);
180
+ if (entries.length > 0) {
181
+ const record = readPending(key) ?? { synced: false, completed: {}, forget: [] };
182
+ for (const [id, at] of entries) record.completed[id] = later(record.completed[id], at ?? "");
183
+ writePending(key, record);
184
+ }
185
+ }
186
+ removeStored(old);
187
+ memory.delete(old);
188
+ cache.delete(old);
189
+ emit(old);
190
+ }
191
+ function uploadUnsynced(key) {
192
+ const record = readPending(key);
193
+ if (record?.synced) return;
194
+ const device = snapshot(key, migrates.get(key));
195
+ const entries = Object.entries(device);
196
+ const next = record ?? { synced: false, completed: {}, forget: [] };
197
+ if (entries.length === 0 && record) return;
198
+ for (const [id, at] of entries) {
199
+ if (!next.forget.includes(id)) next.completed[id] = later(next.completed[id], at ?? "");
200
+ }
201
+ writePending(key, next);
202
+ }
203
+ function prepare(key) {
204
+ loaded.add(key);
205
+ adoptOldRecord(key, accounts.get(key));
206
+ uploadUnsynced(key);
207
+ }
208
+ function queueSync(key) {
209
+ if (!loaded.has(key)) prepare(key);
210
+ if (queued.has(key)) return;
211
+ queued.add(key);
212
+ queueMicrotask(() => {
213
+ queued.delete(key);
214
+ void sync(key);
215
+ });
216
+ }
217
+ function ensureLoaded(key) {
218
+ if (!accounts.has(key) || loaded.has(key)) return;
219
+ prepare(key);
220
+ void sync(key);
221
+ }
222
+ async function sync(key) {
223
+ const account = accounts.get(key);
224
+ if (!account || !loaded.has(key)) return;
225
+ if (running.has(key)) {
226
+ rerun.add(key);
227
+ return;
228
+ }
229
+ if (typeof navigator !== "undefined" && navigator.onLine === false) return;
230
+ running.add(key);
231
+ const epoch = epochs.get(key) ?? 0;
232
+ try {
233
+ const waiting = readPending(key);
234
+ if (waiting && hasWork(waiting)) {
235
+ if (parked.has(key)) return;
236
+ const body = {};
237
+ if (Object.keys(waiting.completed).length > 0) body.completed = { ...waiting.completed };
238
+ if (waiting.forget.length > 0) body.forget = [...waiting.forget];
239
+ let answer;
240
+ try {
241
+ answer = await account.save(body);
242
+ } catch (error) {
243
+ const status = httpStatusOf(error);
244
+ if (status === 403 || status === 422) {
245
+ if ((epochs.get(key) ?? 0) === epoch) clearSent(key, body, false);
246
+ console.warn(`[ui-kit] tour progress: the account refused the pending record (${status}); dropped.`, error);
247
+ } else if (status === 402) {
248
+ parked.add(key);
249
+ }
250
+ return;
251
+ }
252
+ if ((epochs.get(key) ?? 0) !== epoch) return;
253
+ clearSent(key, body, true);
254
+ refill(key, answer);
255
+ } else {
256
+ const answer = await account.load();
257
+ if ((epochs.get(key) ?? 0) !== epoch) return;
258
+ if (waiting && !waiting.synced) writePending(key, { ...waiting, synced: true });
259
+ else if (!waiting) writePending(key, { synced: true, completed: {}, forget: [] });
260
+ refill(key, answer);
261
+ }
262
+ } catch {
263
+ } finally {
264
+ running.delete(key);
265
+ if (rerun.delete(key)) void sync(key);
266
+ }
267
+ }
268
+ function signOutTourProgress(storageKey) {
269
+ epochs.set(storageKey, (epochs.get(storageKey) ?? 0) + 1);
270
+ loaded.delete(storageKey);
271
+ rerun.delete(storageKey);
272
+ parked.delete(storageKey);
273
+ removeStored(storageKey);
274
+ memory.delete(storageKey);
275
+ cache.delete(storageKey);
276
+ emit(storageKey);
277
+ }
80
278
  function subscribe(key, listener) {
81
279
  let set = listeners.get(key);
82
280
  if (!set) listeners.set(key, set = /* @__PURE__ */ new Set());
@@ -93,6 +291,7 @@ function subscribe(key, listener) {
93
291
  };
94
292
  }
95
293
  function resetTourProgress(storageKey) {
294
+ if (accounts.has(storageKey)) loaded.delete(storageKey);
96
295
  removeStored(storageKey);
97
296
  memory.delete(storageKey);
98
297
  cache.delete(storageKey);
@@ -102,19 +301,43 @@ function completeStored(storageKey, migrate, tour) {
102
301
  const subject = typeof tour === "string" ? { id: tour } : tour;
103
302
  const current = readForWrite(storageKey, migrate);
104
303
  if (isTourDone(subject, current[subject.id])) return false;
105
- write(storageKey, { ...current, [subject.id]: (/* @__PURE__ */ new Date()).toISOString() });
304
+ const at = (/* @__PURE__ */ new Date()).toISOString();
305
+ write(storageKey, { ...current, [subject.id]: at });
306
+ if (accounts.has(storageKey)) {
307
+ markPending(storageKey, subject.id, at);
308
+ queueSync(storageKey);
309
+ }
106
310
  return true;
107
311
  }
312
+ function resetStored(storageKey, migrate, id) {
313
+ const current = snapshot(storageKey, migrate);
314
+ if (current[id] === void 0) return;
315
+ const next = { ...current };
316
+ delete next[id];
317
+ write(storageKey, next);
318
+ if (accounts.has(storageKey)) {
319
+ markPending(storageKey, id, void 0, true);
320
+ queueSync(storageKey);
321
+ }
322
+ }
108
323
  function completeTour(storageKey, tour, at, opts) {
109
324
  const id = typeof tour === "string" ? tour : tour.id;
110
325
  const when = at === null ? null : at instanceof Date ? at.toISOString() : at ?? (/* @__PURE__ */ new Date()).toISOString();
111
326
  write(storageKey, { ...readForWrite(storageKey, opts?.migrate, true), [id]: when });
327
+ if (accounts.has(storageKey)) markPending(storageKey, id, when);
112
328
  }
113
- function useTourProgress({ storageKey, migrate }) {
329
+ function useTourProgress({ storageKey, migrate, account }) {
114
330
  const migrateRef = useRef(migrate);
115
331
  useEffect(() => {
116
332
  migrateRef.current = migrate;
117
333
  });
334
+ const accountRef = useRef(account);
335
+ useEffect(() => {
336
+ accountRef.current = account;
337
+ });
338
+ useEffect(() => {
339
+ if (rememberAccount(storageKey, accountRef.current)) ensureLoaded(storageKey);
340
+ }, [storageKey]);
118
341
  const completed = useSyncExternalStore(
119
342
  useCallback((listener) => subscribe(storageKey, listener), [storageKey]),
120
343
  () => snapshot(storageKey, migrateRef.current),
@@ -124,23 +347,16 @@ function useTourProgress({ storageKey, migrate }) {
124
347
  (tour) => completeStored(storageKey, migrateRef.current, tour),
125
348
  [storageKey]
126
349
  );
127
- const reset = useCallback(
128
- (id) => {
129
- const current = snapshot(storageKey, migrateRef.current);
130
- if (current[id] === void 0) return;
131
- const next = { ...current };
132
- delete next[id];
133
- write(storageKey, next);
134
- },
135
- [storageKey]
136
- );
350
+ const reset = useCallback((id) => resetStored(storageKey, migrateRef.current, id), [storageKey]);
137
351
  return useMemo(() => ({ completed, complete, reset }), [completed, complete, reset]);
138
352
  }
139
353
  export {
140
354
  completeStored,
141
355
  completeTour,
356
+ rememberAccount,
142
357
  rememberMigrate,
143
358
  resetTourProgress,
359
+ signOutTourProgress,
144
360
  useTourProgress
145
361
  };
146
362
  //# sourceMappingURL=use-tour-progress.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tour/use-tour-progress.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from \"react\";\nimport { readStored, removeStored, writeStored } from \"../lib/safe-storage\";\nimport { isTourDone, type TourCompletions } from \"./tour-info\";\n\n/**\n * Which tours this device has completed (docs/tours-harmonization.md §2.6, §6.2) —\n * keksdose's `tour-progress-store` and kastlan's `completions.ts`, as one hook.\n *\n * - **Device storage, the app's key.** `storageKey` is always the app's: an app on shared\n * devices scopes it per account (KS, bench PCs), and an app that lists its keys\n * (KS `storage-keys.ts`) spells it there. Every access is guarded: a blocked or full\n * storage keeps the marks for this page's life instead of throwing.\n * - **One store, every reader.** A change notifies every hook on the same key in this tab\n * (an in-tab emitter) and in other tabs (the `storage` event), so the tours page and\n * `TourMenu` agree without a reload (ka F3).\n * - **`migrate(raw)`** reads an older shape, so nobody loses their done marks at adoption:\n * it gets the stored JSON (or the raw text, if it is no JSON) and returns the record for\n * a shape it recognises, `undefined` for any other — the kit's own shape included,\n * which it then reads itself. keksdose: zustand's `{state: {completed}, version: 1}`;\n * kastlan: an array of ids, completed without a date (`null`). It runs only on a stored\n * text that is not the kit's shape already, once per text and key. The FIRST `migrate`\n * any reader, `TourProvider` or {@link completeTour} passes for a key is remembered for\n * that key (later identities are ignored), so a reader without one, mounted first, no\n * longer hides the old marks from the rest (keksdose B-2). **Still, define the options\n * once, at module level, and hand the provider and every reader the same object.**\n * - **Never overwritten unread** (keksdose B-1): a write (`complete`, the provider's,\n * `completeTour`) on a stored text that is neither empty nor the kit's shape, and that no\n * known `migrate` reads, THROWS instead of replacing the old marks.\n * - **Resetting the device** (settings §11): register the key with `reset: () =>\n * resetTourProgress(storageKey)`, which clears storage and the page's memory and tells\n * every reader in this tab.\n *\n * **The stored shape** (§9.3, §9.6.6), for an app's tests and seeds: the JSON record\n * `{ [id]: ISO instant | \"\" }` under `storageKey`, where `\"\"` is a completion without a\n * date (a migrated record: kastlan's id list). That is the one written form; the hooks\n * and callbacks (`completed`, `TourCompletions`) give `null` for `\"\"`. A `null` read from\n * storage (or returned by `migrate`) is accepted as the same thing and written back as\n * `\"\"`. To write a completion from outside a page, use {@link completeTour}.\n */\nexport interface UseTourProgressOptions {\n storageKey: string;\n migrate?: (raw: unknown) => Record<string, string | null> | undefined;\n}\n\n/** What `complete` takes: a tour id, or the tour (`{id, updated}`, a `TourInfo` fits). */\ntype CompletedTour = string | { id: string; updated?: { date: string } | null };\n\nexport interface TourProgress {\n /** Tour id → the completion's ISO timestamp, or `null` when recorded without a date. */\n completed: TourCompletions;\n /**\n * Record a completion. Keeps the FIRST completion's date, so a replay changes nothing —\n * unless the tour was reset by an update since (§8): pass the tour (`{id, updated}`) and\n * a completion from before `updated.date` is replaced by today's.\n *\n * Returns `true` when this counts as a new completion — the moment to award the tour's\n * points and show the app's toast (§6.1) — and `false` for a replay.\n *\n * A tour started with `start(steps, {tour})` under a `TourProvider` that has `progress`\n * is recorded by the provider (§9.6.2): do not call this from that start's `onFinish`\n * too, or the second write answers `false`.\n */\n complete: (tour: CompletedTour) => boolean;\n /** \"Mark as not done\": forget the completion. */\n reset: (id: string) => void;\n}\n\n/* ── The store: one per key, shared by every hook ───────────────────────────── */\n\nconst EMPTY: TourCompletions = Object.freeze({});\n/** In-tab listeners per key. */\nconst listeners = new Map<string, Set<() => void>>();\n/** What this page wrote where storage refused to keep it (blocked, full): per key. */\nconst memory = new Map<string, string>();\n/**\n * The last raw text read per key and what it parsed to, so a snapshot keeps its identity:\n * the same text gives the same object, whichever reader asks and whatever `migrate` it\n * holds. Several readers of one key with inline `migrate` arrows used to evict each other\n * here and loop (kastlan B1, 0.43).\n */\nconst cache = new Map<string, { raw: string | null; value: TourCompletions; readable: boolean }>();\ntype Migrate = NonNullable<UseTourProgressOptions[\"migrate\"]>;\n/** Each key's migrate: the first one any reader, the provider or `completeTour` passes (B-2). */\nconst migrates = new Map<string, Migrate>();\n\n/**\n * Remember `migrate` for `key` if it is the first one, and return the key's migrate. A\n * snapshot parsed before the key had one is dropped and re-parsed, and the key's readers\n * hear of it after this render (never from inside one).\n * @internal Not part of the barrel; `TourProvider` registers its `progress` with it.\n */\nexport function rememberMigrate(key: string, migrate: UseTourProgressOptions[\"migrate\"]): Migrate | undefined {\n if (migrate && !migrates.has(key)) {\n migrates.set(key, migrate);\n if (cache.delete(key)) queueMicrotask(() => emit(key));\n }\n return migrates.get(key);\n}\n\nfunction emit(key: string) {\n listeners.get(key)?.forEach((listener) => listener());\n}\n\nfunction rawOf(key: string): string | null {\n return memory.get(key) ?? readStored(key);\n}\n\n/** Is this the kit's own shape already: a record whose every value is a string or null? */\nfunction isKitShape(value: unknown): boolean {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return false;\n return Object.values(value as Record<string, unknown>).every((at) => at === null || typeof at === \"string\");\n}\n\n/** The kit's own shape: a record of string or null; anything else is dropped. */\nfunction fromRecord(value: unknown): TourCompletions | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n const out: Record<string, string | null> = {};\n for (const [id, at] of Object.entries(value as Record<string, unknown>)) {\n if (at === null || at === \"\") out[id] = null;\n else if (typeof at === \"string\") out[id] = at;\n }\n return out;\n}\n\n/**\n * The record, and whether the text was READ (empty, the kit's shape, or a shape `migrate`\n * knows) rather than dropped: a write over an unread text would lose it (B-1).\n */\nfunction parse(raw: string | null, migrate: UseTourProgressOptions[\"migrate\"]): { value: TourCompletions; readable: boolean } {\n if (raw === null || raw.trim() === \"\") return { value: EMPTY, readable: true };\n let value: unknown = raw;\n try {\n value = JSON.parse(raw);\n } catch {\n // Not JSON: `migrate` still sees the text.\n }\n if (migrate && !isKitShape(value)) {\n try {\n const migrated = fromRecord(migrate(value));\n if (migrated !== undefined) return { value: migrated, readable: true };\n } catch {\n // A migrate that throws reads as \"not a shape it knows\".\n }\n }\n return { value: fromRecord(value) ?? EMPTY, readable: isKitShape(value) };\n}\n\nfunction snapshot(key: string, migrate: UseTourProgressOptions[\"migrate\"]): TourCompletions {\n const known = rememberMigrate(key, migrate);\n const raw = rawOf(key);\n const hit = cache.get(key);\n if (hit && hit.raw === raw) return hit.value;\n const parsed = parse(raw, known);\n cache.set(key, { raw, ...parsed });\n return parsed.value;\n}\n\n/** The record a write builds on; throws rather than overwrite a text nobody could read (B-1). */\nfunction readForWrite(key: string, migrate: UseTourProgressOptions[\"migrate\"], own = false): TourCompletions {\n // `own`: this migrate, even where the key registered another (completeTour's `{migrate}`).\n const known = rememberMigrate(key, migrate);\n const { value, readable } =\n own && migrate && known !== migrate\n ? parse(rawOf(key), migrate)\n : { value: snapshot(key, migrate), readable: cache.get(key)?.readable };\n if (!readable) {\n throw new Error(\n `[ui-kit] tour progress: the text stored under \"${key}\" is not the kit's shape and no migrate reads it; ` +\n \"refusing to overwrite it. Pass the key's migrate (the shared useTourProgress / TourProvider options, \" +\n \"or completeTour's {migrate}).\",\n );\n }\n return value;\n}\n\n/** The one written form (§9.6.6): a completion without a date is stored as `\"\"`, never `null`. */\nfunction write(key: string, record: TourCompletions) {\n const stored: Record<string, string> = {};\n for (const [id, at] of Object.entries(record)) stored[id] = at ?? \"\";\n const raw = JSON.stringify(stored);\n writeStored(key, raw);\n // Storage refused it (blocked, full): keep it for this page's life instead.\n if (readStored(key) === raw) memory.delete(key);\n else memory.set(key, raw);\n emit(key);\n}\n\nfunction subscribe(key: string, listener: () => void): () => void {\n let set = listeners.get(key);\n if (!set) listeners.set(key, (set = new Set()));\n set.add(listener);\n // Another tab wrote (or cleared all storage: `key` null).\n const onStorage = (event: StorageEvent) => {\n if (event.key !== null && event.key !== key) return;\n memory.delete(key);\n listener();\n };\n if (typeof window !== \"undefined\") window.addEventListener(\"storage\", onStorage);\n return () => {\n set.delete(listener);\n if (typeof window !== \"undefined\") window.removeEventListener(\"storage\", onStorage);\n };\n}\n\n/**\n * Forget every tour completion under `storageKey` on this device: the stored key, what the\n * page kept in memory where storage refused it, and every reader in this tab moves at\n * once. For \"Reset this device's settings\" (settings §11):\n *\n * registerDeviceSetting({ key: TOUR_KEY, kind: \"view\", reset: () => resetTourProgress(TOUR_KEY) });\n */\nexport function resetTourProgress(storageKey: string): void {\n removeStored(storageKey);\n memory.delete(storageKey);\n cache.delete(storageKey);\n emit(storageKey);\n}\n\n/**\n * Record a completion under `storageKey`, as {@link TourProgress.complete} does: through\n * the one shared store and snapshot (never a copy), so every reader of the key moves.\n * Returns `true` when it counts anew. `TourProvider` calls this on a tour's last step.\n * @internal Not part of the barrel.\n */\nexport function completeStored(\n storageKey: string,\n migrate: UseTourProgressOptions[\"migrate\"],\n tour: CompletedTour,\n): boolean {\n const subject = typeof tour === \"string\" ? { id: tour } : tour;\n // Read now rather than from a render: two completions in one handler both count.\n const current = readForWrite(storageKey, migrate);\n if (isTourDone(subject, current[subject.id])) return false;\n write(storageKey, { ...current, [subject.id]: new Date().toISOString() });\n return true;\n}\n\n/**\n * Write a completion from outside a page (§9.2): for an app's tests, seeds and scripts,\n * beside {@link resetTourProgress}. It writes the kit's stored shape (`{[id]: ISO | \"\"}`)\n * under `storageKey`, keeps the other tours' marks, and notifies every reader of the key\n * in this tab directly (the `storage` event reaches only OTHER tabs), so a mounted\n * `useTourProgress` or `TourCatalogue` moves at once.\n *\n * Unlike `complete`, it always writes (over a readable text; see below): `at` is the\n * completion's instant (an ISO string or a `Date`, default now), `null` a completion\n * without a date (stored as `\"\"`, read back as `null`).\n *\n * completeTour(TOUR_KEY, \"budget\", \"2026-09-01T10:00:00Z\");\n *\n * A stored text in an app's older shape is read through `opts.migrate`, or else the\n * migrate a reader or the provider registered for the key. If the text is neither empty\n * nor the kit's shape and no migrate reads it, this THROWS and writes nothing (B-1):\n *\n * completeTour(TOUR_KEY, \"budget\", undefined, { migrate: fromZustand });\n */\nexport function completeTour(\n storageKey: string,\n tour: string | { id: string },\n at?: string | Date | null,\n opts?: { migrate?: UseTourProgressOptions[\"migrate\"] },\n): void {\n const id = typeof tour === \"string\" ? tour : tour.id;\n const when = at === null ? null : at instanceof Date ? at.toISOString() : (at ?? new Date().toISOString());\n write(storageKey, { ...readForWrite(storageKey, opts?.migrate, true), [id]: when });\n}\n\n/** The device's tour completions under `storageKey`; see {@link UseTourProgressOptions}. */\nexport function useTourProgress({ storageKey, migrate }: UseTourProgressOptions): TourProgress {\n // Held, not depended on: an inline arrow is a new function every render, and neither the\n // snapshot nor the callbacks may change with it.\n const migrateRef = useRef(migrate);\n useEffect(() => {\n migrateRef.current = migrate;\n });\n const completed = useSyncExternalStore(\n useCallback((listener: () => void) => subscribe(storageKey, listener), [storageKey]),\n () => snapshot(storageKey, migrateRef.current),\n () => EMPTY,\n );\n\n const complete = useCallback(\n (tour: CompletedTour) => completeStored(storageKey, migrateRef.current, tour),\n [storageKey],\n );\n\n const reset = useCallback(\n (id: string) => {\n const current = snapshot(storageKey, migrateRef.current);\n if (current[id] === undefined) return;\n const next = { ...current };\n delete next[id];\n write(storageKey, next);\n },\n [storageKey],\n );\n\n return useMemo(() => ({ completed, complete, reset }), [completed, complete, reset]);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/tour/use-tour-progress.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from \"react\";\nimport { readStored, removeStored, writeStored } from \"../lib/safe-storage\";\nimport { httpStatusOf } from \"../auth/auth-errors\";\nimport { isTourDone, type TourCompletions } from \"./tour-info\";\n\n/**\n * Which tours this device has completed (docs/tours-harmonization.md §2.6, §6.2) —\n * keksdose's `tour-progress-store` and kastlan's `completions.ts`, as one hook.\n *\n * - **Device storage, the app's key.** `storageKey` is always the app's: an app on shared\n * devices scopes it per account (KS, bench PCs), and an app that lists its keys\n * (KS `storage-keys.ts`) spells it there. Every access is guarded: a blocked or full\n * storage keeps the marks for this page's life instead of throwing.\n * - **One store, every reader.** A change notifies every hook on the same key in this tab\n * (an in-tab emitter) and in other tabs (the `storage` event), so the tours page and\n * `TourMenu` agree without a reload (ka F3).\n * - **`migrate(raw)`** reads an older shape, so nobody loses their done marks at adoption:\n * it gets the stored JSON (or the raw text, if it is no JSON) and returns the record for\n * a shape it recognises, `undefined` for any other — the kit's own shape included,\n * which it then reads itself. keksdose: zustand's `{state: {completed}, version: 1}`;\n * kastlan: an array of ids, completed without a date (`null`). It runs only on a stored\n * text that is not the kit's shape already, once per text and key. The FIRST `migrate`\n * any reader, `TourProvider` or {@link completeTour} passes for a key is remembered for\n * that key (later identities are ignored), so a reader without one, mounted first, no\n * longer hides the old marks from the rest (keksdose B-2). **Still, define the options\n * once, at module level, and hand the provider and every reader the same object.**\n * - **Never overwritten unread** (keksdose B-1): a write (`complete`, the provider's,\n * `completeTour`) on a stored text that is neither empty nor the kit's shape, and that no\n * known `migrate` reads, THROWS instead of replacing the old marks.\n * - **Resetting the device** (settings §11): register the key with `reset: () =>\n * resetTourProgress(storageKey)`, which clears storage and the page's memory and tells\n * every reader in this tab.\n *\n * **The stored shape** (§9.3, §9.6.6), for an app's tests and seeds: the JSON record\n * `{ [id]: ISO instant | \"\" }` under `storageKey`, where `\"\"` is a completion without a\n * date (a migrated record: kastlan's id list). That is the one written form; the hooks\n * and callbacks (`completed`, `TourCompletions`) give `null` for `\"\"`. A `null` read from\n * storage (or returned by `migrate`) is accepted as the same thing and written back as\n * `\"\"`. To write a completion from outside a page, use {@link completeTour}.\n *\n * **On the account** (0.45, §10.2–§10.3): with `account`, the device key becomes the\n * account's CACHE — see {@link TourProgressAccount}. Without it, nothing changes from 0.44.\n */\nexport interface UseTourProgressOptions {\n storageKey: string;\n migrate?: (raw: unknown) => Record<string, string | null> | undefined;\n /**\n * 0.45: the account's record behind the device key (§10.2.4). Remembered per key like\n * `migrate` (the FIRST one any reader or the provider passes wins). With it,\n * `storageKey` must be per account (a prefix plus the user id), so build the options per\n * account and keep ONE stable object per key — memoise them in a `Map` by user id — and\n * let `load`/`save` read the current token when they run, never capture one:\n *\n * const byUser = new Map<string, UseTourProgressOptions>();\n * function tourProgressFor(user: { id: string; isDemo: boolean } | null) {\n * if (!user || user.isDemo) return { storageKey: \"app.tours.completions\" }; // no account\n * let options = byUser.get(user.id);\n * if (!options) byUser.set(user.id, (options = { storageKey: `app.tours.completions:${user.id}`, account }));\n * return options;\n * }\n *\n * Signed out, and in a demo, pass none (and a fixed key): the device record works as in\n * 0.44, and a demo neither PATCHes into a 403 nor leaves an orphan key per demo id.\n */\n account?: TourProgressAccount;\n}\n\n/** The record on the wire (§10.2.2): tour id → ISO instant, or `\"\"` (done, no date). */\ntype TourProgressWire = Record<string, string>;\n\n/** The `PATCH /tours/progress` body (§10.2.2): what to mark done, and what to forget.\n * An id is never in both. Either may be left out. */\nexport interface TourProgressBody {\n completed?: TourProgressWire;\n forget?: string[];\n}\n\n/**\n * Taking over a device-wide record from before the account (§10.2.5), on the FIRST load\n * of the account key on this page:\n * - `{storageKey, discard: true}` (kastlan): removes the old key and uploads nothing — a\n * shared PC must not hand one person another's marks. Removing an absent key is a no-op.\n * - `{storageKey, migrate?}`: reads the old key once (through `migrate` for an older\n * shape), uploads it to the first account that signs in on this device, then removes it.\n */\nexport type TourProgressAdopt =\n | { storageKey: string; discard: true; migrate?: never }\n | { storageKey: string; discard?: false; migrate?: UseTourProgressOptions[\"migrate\"] };\n\n/**\n * The app's account record (§10.3): `load()` is its `GET /tours/progress`, `save(body)`\n * its `PATCH`, each resolving to the parsed answer, `{completed}` — the whole record.\n *\n * - The page paints from the device key at once (and offline); the account is loaded on\n * the first reader of the key on this page, on `online`, and after each save, and its\n * answer replaces the cache.\n * - A write (`complete`, the provider's finish, `reset`) goes to the cache at once and to\n * a PENDING record beside it, `<storageKey>:pending` (`{synced, completed, forget}`),\n * sent as one PATCH and cleared by its answer. A failure with no answer (offline, 5xx,\n * 401) keeps it for the next load or `online`; a 402 (a lapsed plan) keeps it but sends it\n * no more on this page (the next session or page load tries again); a 403 or 422 drops\n * it and logs.\n * - `awarded` stays the device's answer against the cache (§10.2.4).\n * - The pending record is NOT the app's outbox: leave it out of any \"N waiting\" count\n * and any sign-out guard (losing a tour mark is acceptable).\n * - A device record never synced (no `:pending` with `synced: true`) is uploaded whole on\n * its first load, by the server's merge rule (later completion wins).\n * - On every end of a session call {@link signOutTourProgress}.\n */\nexport interface TourProgressAccount {\n load: () => Promise<{ completed: TourProgressWire }>;\n save: (body: TourProgressBody) => Promise<{ completed: TourProgressWire }>;\n adopt?: TourProgressAdopt;\n}\n\n/** What `complete` takes: a tour id, or the tour (`{id, updated}`, a `TourInfo` fits). */\ntype CompletedTour = string | { id: string; updated?: { date: string } | null };\n\nexport interface TourProgress {\n /** Tour id → the completion's ISO timestamp, or `null` when recorded without a date. */\n completed: TourCompletions;\n /**\n * Record a completion. Keeps the FIRST completion's date, so a replay changes nothing —\n * unless the tour was reset by an update since (§8): pass the tour (`{id, updated}`) and\n * a completion from before `updated.date` is replaced by today's.\n *\n * Returns `true` when this counts as a new completion — the moment to award the tour's\n * points and show the app's toast (§6.1) — and `false` for a replay.\n *\n * A tour started with `start(steps, {tour})` under a `TourProvider` that has `progress`\n * is recorded by the provider (§9.6.2): do not call this from that start's `onFinish`\n * too, or the second write answers `false`.\n */\n complete: (tour: CompletedTour) => boolean;\n /** \"Mark as not done\": forget the completion. */\n reset: (id: string) => void;\n}\n\n/* ── The store: one per key, shared by every hook ───────────────────────────── */\n\nconst EMPTY: TourCompletions = Object.freeze({});\n/** In-tab listeners per key. */\nconst listeners = new Map<string, Set<() => void>>();\n/** What this page wrote where storage refused to keep it (blocked, full): per key. */\nconst memory = new Map<string, string>();\n/**\n * The last raw text read per key and what it parsed to, so a snapshot keeps its identity:\n * the same text gives the same object, whichever reader asks and whatever `migrate` it\n * holds. Several readers of one key with inline `migrate` arrows used to evict each other\n * here and loop (kastlan B1, 0.43).\n */\nconst cache = new Map<string, { raw: string | null; value: TourCompletions; readable: boolean }>();\ntype Migrate = NonNullable<UseTourProgressOptions[\"migrate\"]>;\n/** Each key's migrate: the first one any reader, the provider or `completeTour` passes (B-2). */\nconst migrates = new Map<string, Migrate>();\n\n/**\n * Remember `migrate` for `key` if it is the first one, and return the key's migrate. A\n * snapshot parsed before the key had one is dropped and re-parsed, and the key's readers\n * hear of it after this render (never from inside one).\n * @internal Not part of the barrel; `TourProvider` registers its `progress` with it.\n */\nexport function rememberMigrate(key: string, migrate: UseTourProgressOptions[\"migrate\"]): Migrate | undefined {\n if (migrate && !migrates.has(key)) {\n migrates.set(key, migrate);\n if (cache.delete(key)) queueMicrotask(() => emit(key));\n }\n return migrates.get(key);\n}\n\nfunction emit(key: string) {\n listeners.get(key)?.forEach((listener) => listener());\n}\n\nfunction rawOf(key: string): string | null {\n return memory.get(key) ?? readStored(key);\n}\n\n/** Is this the kit's own shape already: a record whose every value is a string or null? */\nfunction isKitShape(value: unknown): boolean {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return false;\n return Object.values(value as Record<string, unknown>).every((at) => at === null || typeof at === \"string\");\n}\n\n/** The kit's own shape: a record of string or null; anything else is dropped. */\nfunction fromRecord(value: unknown): TourCompletions | undefined {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) return undefined;\n const out: Record<string, string | null> = {};\n for (const [id, at] of Object.entries(value as Record<string, unknown>)) {\n if (at === null || at === \"\") out[id] = null;\n else if (typeof at === \"string\") out[id] = at;\n }\n return out;\n}\n\n/**\n * The record, and whether the text was READ (empty, the kit's shape, or a shape `migrate`\n * knows) rather than dropped: a write over an unread text would lose it (B-1).\n */\nfunction parse(raw: string | null, migrate: UseTourProgressOptions[\"migrate\"]): { value: TourCompletions; readable: boolean } {\n if (raw === null || raw.trim() === \"\") return { value: EMPTY, readable: true };\n let value: unknown = raw;\n try {\n value = JSON.parse(raw);\n } catch {\n // Not JSON: `migrate` still sees the text.\n }\n if (migrate && !isKitShape(value)) {\n try {\n const migrated = fromRecord(migrate(value));\n if (migrated !== undefined) return { value: migrated, readable: true };\n } catch {\n // A migrate that throws reads as \"not a shape it knows\".\n }\n }\n return { value: fromRecord(value) ?? EMPTY, readable: isKitShape(value) };\n}\n\nfunction snapshot(key: string, migrate: UseTourProgressOptions[\"migrate\"]): TourCompletions {\n const known = rememberMigrate(key, migrate);\n const raw = rawOf(key);\n const hit = cache.get(key);\n if (hit && hit.raw === raw) return hit.value;\n const parsed = parse(raw, known);\n cache.set(key, { raw, ...parsed });\n return parsed.value;\n}\n\n/** The record a write builds on; throws rather than overwrite a text nobody could read (B-1). */\nfunction readForWrite(key: string, migrate: UseTourProgressOptions[\"migrate\"], own = false): TourCompletions {\n // `own`: this migrate, even where the key registered another (completeTour's `{migrate}`).\n const known = rememberMigrate(key, migrate);\n const { value, readable } =\n own && migrate && known !== migrate\n ? parse(rawOf(key), migrate)\n : { value: snapshot(key, migrate), readable: cache.get(key)?.readable };\n if (!readable) {\n throw new Error(\n `[ui-kit] tour progress: the text stored under \"${key}\" is not the kit's shape and no migrate reads it; ` +\n \"refusing to overwrite it. Pass the key's migrate (the shared useTourProgress / TourProvider options, \" +\n \"or completeTour's {migrate}).\",\n );\n }\n return value;\n}\n\n/** The one written form (§9.6.6): a completion without a date is stored as `\"\"`, never `null`. */\nfunction write(key: string, record: TourCompletions) {\n const stored: Record<string, string> = {};\n for (const [id, at] of Object.entries(record)) stored[id] = at ?? \"\";\n const raw = JSON.stringify(stored);\n writeStored(key, raw);\n // Storage refused it (blocked, full): keep it for this page's life instead.\n if (readStored(key) === raw) memory.delete(key);\n else memory.set(key, raw);\n emit(key);\n}\n\n/* ── The account behind the device key (0.45, §10.2.4–§10.2.8) ─────────────── */\n\n/** The key's account: the first one any reader or the provider passes (as `migrate`). */\nconst accounts = new Map<string, TourProgressAccount>();\n/** Keys whose account was loaded (or is loading) on this page: the first reader loads. */\nconst loaded = new Set<string>();\n/** Keys with a sync running, and those asked for another while it ran. */\nconst running = new Set<string>();\nconst rerun = new Set<string>();\n/** Keys with a sync queued for the end of this task: two writes in one handler, one PATCH. */\nconst queued = new Set<string>();\n/** Bumped by a sign-out: an answer that comes back after it never refills the cache. */\nconst epochs = new Map<string, number>();\n/** Keys whose PATCH got a 402 (a lapsed plan): the pending record is kept but not sent\n * again on this page; the next session (or page load) tries once more. */\nconst parked = new Set<string>();\n/** The pending record where storage refused to keep it. */\nconst pendingMemory = new Map<string, string>();\nlet onlineListening = false;\n\n/** The pending record (§10.2.4): what waits to be sent, and whether the device key was\n * ever synced. */\ninterface PendingRecord {\n synced: boolean;\n completed: TourProgressWire;\n forget: string[];\n}\n\nconst pendingKey = (key: string) => `${key}:pending`;\n\nfunction devWarn(message: string) {\n if (import.meta.env?.DEV) console.warn(message);\n}\n\n/**\n * Remember `account` for `key` if it is the first one, and return the key's account.\n * @internal Not part of the barrel; `TourProvider` registers its `progress` with it.\n */\nexport function rememberAccount(key: string, account: TourProgressAccount | undefined): TourProgressAccount | undefined {\n if (account && !accounts.has(key)) {\n accounts.set(key, account);\n if (account.adopt && account.adopt.storageKey === key) {\n devWarn(\n `[ui-kit] tour progress: the account key \"${key}\" is the key \\`adopt\\` takes over. ` +\n \"With an account the key must be per account (a prefix plus the user id), apart from the old device-wide key.\",\n );\n }\n if (!onlineListening && typeof window !== \"undefined\") {\n onlineListening = true;\n window.addEventListener(\"online\", () => {\n for (const known of loaded) void sync(known);\n });\n }\n }\n return accounts.get(key);\n}\n\nfunction readPending(key: string): PendingRecord | null {\n const raw = pendingMemory.get(key) ?? readStored(pendingKey(key));\n if (raw === null) return null;\n try {\n const value = JSON.parse(raw) as Partial<PendingRecord> | null;\n if (!value || typeof value !== \"object\") return null;\n const completed: TourProgressWire = {};\n for (const [id, at] of Object.entries(value.completed ?? {})) {\n if (typeof at === \"string\") completed[id] = at;\n else if (at === null) completed[id] = \"\";\n }\n const forget = Array.isArray(value.forget) ? value.forget.filter((id): id is string => typeof id === \"string\") : [];\n return { synced: value.synced === true, completed, forget };\n } catch {\n return null;\n }\n}\n\nfunction writePending(key: string, record: PendingRecord) {\n const raw = JSON.stringify(record);\n writeStored(pendingKey(key), raw);\n if (readStored(pendingKey(key)) === raw) pendingMemory.delete(key);\n else pendingMemory.set(key, raw);\n}\n\nconst hasWork = (record: PendingRecord) => Object.keys(record.completed).length > 0 || record.forget.length > 0;\n\n/** \"Later completion wins\" (§10.2.3), as the server merges: an instant beats `\"\"`, `\"\"`\n * beats nothing, and between two instants the later is kept. */\nfunction later(a: string | undefined, b: string): string {\n if (a === undefined || a === \"\") return b;\n if (b === \"\") return a;\n return Date.parse(b) > Date.parse(a) ? b : a;\n}\n\n/** Note a completion (or, with `forget`, its removal) in the key's pending record. */\nfunction markPending(key: string, id: string, at: string | null | undefined, forget = false) {\n const record = readPending(key) ?? { synced: false, completed: {}, forget: [] };\n if (forget) {\n delete record.completed[id];\n if (!record.forget.includes(id)) record.forget.push(id);\n } else {\n record.completed[id] = at ?? \"\";\n record.forget = record.forget.filter((other) => other !== id);\n }\n writePending(key, record);\n}\n\n/** After an answer (or a 403/422): take what was sent out of the pending record, keeping\n * what was written while the request ran. */\nfunction clearSent(key: string, sent: TourProgressBody, synced: boolean) {\n const record = readPending(key) ?? { synced: false, completed: {}, forget: [] };\n for (const [id, at] of Object.entries(sent.completed ?? {})) {\n if (record.completed[id] === at) delete record.completed[id];\n }\n const forgotten = new Set(sent.forget ?? []);\n record.forget = record.forget.filter((id) => !forgotten.has(id));\n record.synced = record.synced || synced;\n writePending(key, record);\n}\n\n/** The account's answer as the cache, with what still waits to be sent laid over it. */\nfunction refill(key: string, answer: { completed: TourProgressWire } | null | undefined) {\n const next: Record<string, string> = {};\n for (const [id, at] of Object.entries(fromRecord(answer?.completed) ?? {})) next[id] = at ?? \"\";\n const waiting = readPending(key);\n if (waiting) {\n for (const [id, at] of Object.entries(waiting.completed)) next[id] = later(next[id], at);\n for (const id of waiting.forget) delete next[id];\n }\n const record: Record<string, string | null> = {};\n for (const [id, at] of Object.entries(next)) record[id] = at === \"\" ? null : at;\n write(key, record);\n}\n\n/** The first load of an account key on this page takes over the old device-wide key\n * (§10.2.5). */\nfunction adoptOldRecord(key: string, account: TourProgressAccount) {\n const adopt = account.adopt;\n if (!adopt || adopt.storageKey === key) return;\n const old = adopt.storageKey;\n if (!adopt.discard) {\n const { value } = parse(rawOf(old), adopt.migrate ?? migrates.get(old));\n const entries = Object.entries(value);\n if (entries.length > 0) {\n const record = readPending(key) ?? { synced: false, completed: {}, forget: [] };\n for (const [id, at] of entries) record.completed[id] = later(record.completed[id], at ?? \"\");\n writePending(key, record);\n }\n }\n // Removed either way, at once: no second account on this device gets it.\n removeStored(old);\n memory.delete(old);\n cache.delete(old);\n emit(old);\n}\n\n/** A device record never synced is uploaded whole on its first load (§10.2.5). */\nfunction uploadUnsynced(key: string) {\n const record = readPending(key);\n if (record?.synced) return;\n const device = snapshot(key, migrates.get(key));\n const entries = Object.entries(device);\n const next = record ?? { synced: false, completed: {}, forget: [] };\n if (entries.length === 0 && record) return;\n for (const [id, at] of entries) {\n if (!next.forget.includes(id)) next.completed[id] = later(next.completed[id], at ?? \"\");\n }\n writePending(key, next);\n}\n\n/** The key's first sync on this page: take over the old record, upload a never-synced one. */\nfunction prepare(key: string) {\n loaded.add(key);\n adoptOldRecord(key, accounts.get(key)!);\n uploadUnsynced(key);\n}\n\n/** Queue a sync for the end of this task, so a handler's writes go as one PATCH. */\nfunction queueSync(key: string) {\n if (!loaded.has(key)) prepare(key);\n if (queued.has(key)) return;\n queued.add(key);\n queueMicrotask(() => {\n queued.delete(key);\n void sync(key);\n });\n}\n\n/** The first reader of an account key on this page loads it. */\nfunction ensureLoaded(key: string) {\n if (!accounts.has(key) || loaded.has(key)) return;\n prepare(key);\n void sync(key);\n}\n\n/**\n * Send what waits (one PATCH) or, with nothing waiting, load; the answer replaces the\n * cache. One at a time per key; a request asked for while one runs follows it.\n */\nasync function sync(key: string): Promise<void> {\n const account = accounts.get(key);\n if (!account || !loaded.has(key)) return;\n if (running.has(key)) {\n rerun.add(key);\n return;\n }\n if (typeof navigator !== \"undefined\" && navigator.onLine === false) return;\n running.add(key);\n const epoch = epochs.get(key) ?? 0;\n try {\n const waiting = readPending(key);\n if (waiting && hasWork(waiting)) {\n if (parked.has(key)) return;\n const body: TourProgressBody = {};\n if (Object.keys(waiting.completed).length > 0) body.completed = { ...waiting.completed };\n if (waiting.forget.length > 0) body.forget = [...waiting.forget];\n let answer: { completed: TourProgressWire };\n try {\n answer = await account.save(body);\n } catch (error) {\n const status = httpStatusOf(error);\n if (status === 403 || status === 422) {\n // Refused for good (a demo, a malformed or full record): dropped, not retried.\n if ((epochs.get(key) ?? 0) === epoch) clearSent(key, body, false);\n console.warn(`[ui-kit] tour progress: the account refused the pending record (${status}); dropped.`, error);\n } else if (status === 402) {\n // A lapsed plan refuses every write: kept, but not re-sent after each finish\n // and each `online` — the next session or page load tries again.\n parked.add(key);\n }\n // No answer (offline, 5xx, 401): it stays for the next load or `online`.\n return;\n }\n if ((epochs.get(key) ?? 0) !== epoch) return;\n clearSent(key, body, true);\n refill(key, answer);\n } else {\n const answer = await account.load();\n if ((epochs.get(key) ?? 0) !== epoch) return;\n if (waiting && !waiting.synced) writePending(key, { ...waiting, synced: true });\n else if (!waiting) writePending(key, { synced: true, completed: {}, forget: [] });\n refill(key, answer);\n }\n } catch {\n // A load that failed keeps the cache as it is.\n } finally {\n running.delete(key);\n if (rerun.delete(key)) void sync(key);\n }\n}\n\n/**\n * The end of a session (§10.2.6) — sign-out, a 401 nothing could renew, the account's\n * deletion, every path: removes the CACHE only (the device key and the page's snapshot;\n * every reader in this tab moves), synchronously and with no request, and KEEPS\n * `<storageKey>:pending`. That record sits under this person's own key, holds tour ids and\n * dates only, and goes out on their next sign-in. An answer still on its way is ignored.\n *\n * function logout() { …; signOutTourProgress(`app.tours:${user.id}`); }\n */\nexport function signOutTourProgress(storageKey: string): void {\n epochs.set(storageKey, (epochs.get(storageKey) ?? 0) + 1);\n loaded.delete(storageKey);\n rerun.delete(storageKey);\n parked.delete(storageKey);\n removeStored(storageKey);\n memory.delete(storageKey);\n cache.delete(storageKey);\n emit(storageKey);\n}\n\nfunction subscribe(key: string, listener: () => void): () => void {\n let set = listeners.get(key);\n if (!set) listeners.set(key, (set = new Set()));\n set.add(listener);\n // Another tab wrote (or cleared all storage: `key` null).\n const onStorage = (event: StorageEvent) => {\n if (event.key !== null && event.key !== key) return;\n memory.delete(key);\n listener();\n };\n if (typeof window !== \"undefined\") window.addEventListener(\"storage\", onStorage);\n return () => {\n set.delete(listener);\n if (typeof window !== \"undefined\") window.removeEventListener(\"storage\", onStorage);\n };\n}\n\n/**\n * Forget every tour completion under `storageKey` on this device: the stored key, what the\n * page kept in memory where storage refused it, and every reader in this tab moves at\n * once. For \"Reset this device's settings\" (settings §11):\n *\n * registerDeviceSetting({ key: TOUR_KEY, kind: \"view\", reset: () => resetTourProgress(TOUR_KEY) });\n *\n * With an account (0.45) it clears the cache only: the account is never written, the\n * pending record stays (\"anything waiting to be sent\", settings §11.2.2) and the next load\n * refills the cache. Tour progress is never registered with the device reset (§10.2.8).\n */\nexport function resetTourProgress(storageKey: string): void {\n if (accounts.has(storageKey)) loaded.delete(storageKey);\n removeStored(storageKey);\n memory.delete(storageKey);\n cache.delete(storageKey);\n emit(storageKey);\n}\n\n/**\n * Record a completion under `storageKey`, as {@link TourProgress.complete} does: through\n * the one shared store and snapshot (never a copy), so every reader of the key moves.\n * Returns `true` when it counts anew. `TourProvider` calls this on a tour's last step.\n * @internal Not part of the barrel.\n */\nexport function completeStored(\n storageKey: string,\n migrate: UseTourProgressOptions[\"migrate\"],\n tour: CompletedTour,\n): boolean {\n const subject = typeof tour === \"string\" ? { id: tour } : tour;\n // Read now rather than from a render: two completions in one handler both count.\n const current = readForWrite(storageKey, migrate);\n if (isTourDone(subject, current[subject.id])) return false;\n const at = new Date().toISOString();\n write(storageKey, { ...current, [subject.id]: at });\n if (accounts.has(storageKey)) {\n markPending(storageKey, subject.id, at);\n queueSync(storageKey);\n }\n return true;\n}\n\n/** \"Mark as not done\" (§10.2.8): off the cache, and `forget` to the account. @internal */\nfunction resetStored(storageKey: string, migrate: UseTourProgressOptions[\"migrate\"], id: string) {\n const current = snapshot(storageKey, migrate);\n if (current[id] === undefined) return;\n const next = { ...current };\n delete next[id];\n write(storageKey, next);\n if (accounts.has(storageKey)) {\n markPending(storageKey, id, undefined, true);\n queueSync(storageKey);\n }\n}\n\n/**\n * Write a completion from outside a page (§9.2): for an app's tests, seeds and scripts,\n * beside {@link resetTourProgress}. It writes the kit's stored shape (`{[id]: ISO | \"\"}`)\n * under `storageKey`, keeps the other tours' marks, and notifies every reader of the key\n * in this tab directly (the `storage` event reaches only OTHER tabs), so a mounted\n * `useTourProgress` or `TourCatalogue` moves at once.\n *\n * Unlike `complete`, it always writes (over a readable text; see below): `at` is the\n * completion's instant (an ISO string or a `Date`, default now), `null` a completion\n * without a date (stored as `\"\"`, read back as `null`).\n *\n * completeTour(TOUR_KEY, \"budget\", \"2026-09-01T10:00:00Z\");\n *\n * A stored text in an app's older shape is read through `opts.migrate`, or else the\n * migrate a reader or the provider registered for the key. If the text is neither empty\n * nor the kit's shape and no migrate reads it, this THROWS and writes nothing (B-1):\n *\n * completeTour(TOUR_KEY, \"budget\", undefined, { migrate: fromZustand });\n *\n * With an account on the key (0.45) it stays a device write — no request — and is marked\n * pending, so the next load sends it.\n */\nexport function completeTour(\n storageKey: string,\n tour: string | { id: string },\n at?: string | Date | null,\n opts?: { migrate?: UseTourProgressOptions[\"migrate\"] },\n): void {\n const id = typeof tour === \"string\" ? tour : tour.id;\n const when = at === null ? null : at instanceof Date ? at.toISOString() : (at ?? new Date().toISOString());\n write(storageKey, { ...readForWrite(storageKey, opts?.migrate, true), [id]: when });\n if (accounts.has(storageKey)) markPending(storageKey, id, when);\n}\n\n/** The device's tour completions under `storageKey`; see {@link UseTourProgressOptions}. */\nexport function useTourProgress({ storageKey, migrate, account }: UseTourProgressOptions): TourProgress {\n // Held, not depended on: an inline arrow is a new function every render, and neither the\n // snapshot nor the callbacks may change with it.\n const migrateRef = useRef(migrate);\n useEffect(() => {\n migrateRef.current = migrate;\n });\n // The first account for the key wins (as `migrate`); its first reader on the page loads.\n const accountRef = useRef(account);\n useEffect(() => {\n accountRef.current = account;\n });\n useEffect(() => {\n if (rememberAccount(storageKey, accountRef.current)) ensureLoaded(storageKey);\n }, [storageKey]);\n const completed = useSyncExternalStore(\n useCallback((listener: () => void) => subscribe(storageKey, listener), [storageKey]),\n () => snapshot(storageKey, migrateRef.current),\n () => EMPTY,\n );\n\n const complete = useCallback(\n (tour: CompletedTour) => completeStored(storageKey, migrateRef.current, tour),\n [storageKey],\n );\n\n const reset = useCallback((id: string) => resetStored(storageKey, migrateRef.current, id), [storageKey]);\n\n return useMemo(() => ({ completed, complete, reset }), [completed, complete, reset]);\n}\n"],"mappings":"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","names":[]}
package/dist/tour.d.ts CHANGED
@@ -5,5 +5,7 @@ export { isTourNew, isTourReset, isTourDone, doneCount, earnedPoints } from "./t
5
5
  export type { TourInfo, TourGroup, TourUpdate, TourCompletions } from "./tour/tour-info.js";
6
6
  export { useTourProgress, resetTourProgress, completeTour } from "./tour/use-tour-progress.js";
7
7
  export type { UseTourProgressOptions, TourProgress } from "./tour/use-tour-progress.js";
8
+ export { signOutTourProgress } from "./tour/use-tour-progress.js";
9
+ export type { TourProgressAccount, TourProgressAdopt, TourProgressBody } from "./tour/use-tour-progress.js";
8
10
  export { useTourUrlSync, useTourCatalogueUrlState } from "./tour/use-tour-url-sync.js";
9
11
  export type { UseTourUrlSyncOptions, TourCatalogueFilter, TourStateFilter } from "./tour/use-tour-url-sync.js";
package/dist/tour.js CHANGED
@@ -3,6 +3,7 @@ export * from "./tour/tour.js";
3
3
  import { TourCatalogue, TourCatalogueCard, DEFAULT_TOUR_CATALOGUE_LABELS } from "./tour/tour-catalogue.js";
4
4
  import { isTourNew, isTourReset, isTourDone, doneCount, earnedPoints } from "./tour/tour-info.js";
5
5
  import { useTourProgress, resetTourProgress, completeTour } from "./tour/use-tour-progress.js";
6
+ import { signOutTourProgress } from "./tour/use-tour-progress.js";
6
7
  import { useTourUrlSync, useTourCatalogueUrlState } from "./tour/use-tour-url-sync.js";
7
8
  export {
8
9
  DEFAULT_TOUR_CATALOGUE_LABELS,
@@ -15,6 +16,7 @@ export {
15
16
  isTourNew,
16
17
  isTourReset,
17
18
  resetTourProgress,
19
+ signOutTourProgress,
18
20
  useTourCatalogueUrlState,
19
21
  useTourProgress,
20
22
  useTourUrlSync
package/dist/tour.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tour.ts"],"sourcesContent":["// `@eifi1/ui-kit/tour` — guided tours.\n// A re-slicing of the main barrel, not a new API.\nexport * from \"./tour/tour\";\n// 0.43.0: the tours page (docs/tours-harmonization.md §3, §8) — keksdose's catalogue, its\n// card, the NEW and reset rules, device progress and the URL. `useTourUrlSync` and\n// `useTourCatalogueUrlState` need react-router (a peer already, for /shell).\nexport { TourCatalogue, TourCatalogueCard, DEFAULT_TOUR_CATALOGUE_LABELS } from \"./tour/tour-catalogue\";\nexport type { TourCatalogueProps, TourCatalogueCardProps, TourCatalogueLabels } from \"./tour/tour-catalogue\";\nexport { isTourNew, isTourReset, isTourDone, doneCount, earnedPoints } from \"./tour/tour-info\";\nexport type { TourInfo, TourGroup, TourUpdate, TourCompletions } from \"./tour/tour-info\";\n// 0.44.0: the writer outside a page (§9.2).\nexport { useTourProgress, resetTourProgress, completeTour } from \"./tour/use-tour-progress\";\nexport type { UseTourProgressOptions, TourProgress } from \"./tour/use-tour-progress\";\nexport { useTourUrlSync, useTourCatalogueUrlState } from \"./tour/use-tour-url-sync\";\nexport type { UseTourUrlSyncOptions, TourCatalogueFilter, TourStateFilter } from \"./tour/use-tour-url-sync\";\n"],"mappings":";AAEA,cAAc;AAId,SAAS,eAAe,mBAAmB,qCAAqC;AAEhF,SAAS,WAAW,aAAa,YAAY,WAAW,oBAAoB;AAG5E,SAAS,iBAAiB,mBAAmB,oBAAoB;AAEjE,SAAS,gBAAgB,gCAAgC;","names":[]}
1
+ {"version":3,"sources":["../src/tour.ts"],"sourcesContent":["// `@eifi1/ui-kit/tour` — guided tours.\n// A re-slicing of the main barrel, not a new API.\nexport * from \"./tour/tour\";\n// 0.43.0: the tours page (docs/tours-harmonization.md §3, §8) — keksdose's catalogue, its\n// card, the NEW and reset rules, device progress and the URL. `useTourUrlSync` and\n// `useTourCatalogueUrlState` need react-router (a peer already, for /shell).\nexport { TourCatalogue, TourCatalogueCard, DEFAULT_TOUR_CATALOGUE_LABELS } from \"./tour/tour-catalogue\";\nexport type { TourCatalogueProps, TourCatalogueCardProps, TourCatalogueLabels } from \"./tour/tour-catalogue\";\nexport { isTourNew, isTourReset, isTourDone, doneCount, earnedPoints } from \"./tour/tour-info\";\nexport type { TourInfo, TourGroup, TourUpdate, TourCompletions } from \"./tour/tour-info\";\n// 0.44.0: the writer outside a page (§9.2).\nexport { useTourProgress, resetTourProgress, completeTour } from \"./tour/use-tour-progress\";\nexport type { UseTourProgressOptions, TourProgress } from \"./tour/use-tour-progress\";\n// 0.45.0: progress on the account (§10.2–§10.3) and the end of a session.\nexport { signOutTourProgress } from \"./tour/use-tour-progress\";\nexport type { TourProgressAccount, TourProgressAdopt, TourProgressBody } from \"./tour/use-tour-progress\";\nexport { useTourUrlSync, useTourCatalogueUrlState } from \"./tour/use-tour-url-sync\";\nexport type { UseTourUrlSyncOptions, TourCatalogueFilter, TourStateFilter } from \"./tour/use-tour-url-sync\";\n"],"mappings":";AAEA,cAAc;AAId,SAAS,eAAe,mBAAmB,qCAAqC;AAEhF,SAAS,WAAW,aAAa,YAAY,WAAW,oBAAoB;AAG5E,SAAS,iBAAiB,mBAAmB,oBAAoB;AAGjE,SAAS,2BAA2B;AAEpC,SAAS,gBAAgB,gCAAgC;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eifi1/ui-kit",
3
- "version": "0.44.0",
3
+ "version": "0.45.0",
4
4
  "type": "module",
5
5
  "description": "App-agnostic shared design system: domain-free React components, theme and tokens.",
6
6
  "_sideEffects": "The barrel (src/index.ts) re-exports EVERY component, including components/chart, which statically imports recharts. Without this field a bundler must assume each of those modules has side effects, so it cannot drop the unused ones: importing a Button from '@eifi1/ui-kit' dragged all 433KB of recharts into the consumer's ENTRY chunk, and Keksdose was preloading it on every page load, login included. Nothing in src/ imports for side effects (no bare imports, no CSS imports from JS, no module-level global writes) — the token stylesheet is its own './tokens.css' export — so this is safe. CSS is still listed so any future stylesheet survives. Re-checked when `flag-icons` left `dependencies` and tsup started emitting a `\"use client\"` banner: neither touches this. The flag stylesheet was never imported from JS in the first place (that was the finding), and a directive prologue is a string literal every bundler lifts out before it decides whether a module is inert.",
@@ -64,6 +64,10 @@
64
64
  "types": "./dist/rhf.d.ts",
65
65
  "import": "./dist/rhf.js"
66
66
  },
67
+ "./offline": {
68
+ "types": "./dist/offline.d.ts",
69
+ "import": "./dist/offline.js"
70
+ },
67
71
  "./i18n/*": {
68
72
  "types": "./dist/i18n/locales/*.d.ts",
69
73
  "import": "./dist/i18n/locales/*.js"
@@ -112,6 +116,10 @@
112
116
  "deps:outdated": "npm outdated || true"
113
117
  },
114
118
  "peerDependencies": {
119
+ "@tanstack/query-async-storage-persister": "^5",
120
+ "@tanstack/react-query": "^5",
121
+ "@tanstack/react-query-persist-client": "^5",
122
+ "idb-keyval": "^6",
115
123
  "lucide-react": ">=0.400.0 <2",
116
124
  "modern-screenshot": "^4.7.0",
117
125
  "react": ">=19.2",
@@ -122,6 +130,18 @@
122
130
  "sonner": ">=2"
123
131
  },
124
132
  "peerDependenciesMeta": {
133
+ "@tanstack/query-async-storage-persister": {
134
+ "optional": true
135
+ },
136
+ "@tanstack/react-query": {
137
+ "optional": true
138
+ },
139
+ "@tanstack/react-query-persist-client": {
140
+ "optional": true
141
+ },
142
+ "idb-keyval": {
143
+ "optional": true
144
+ },
125
145
  "modern-screenshot": {
126
146
  "optional": true
127
147
  },
@@ -141,6 +161,9 @@
141
161
  "devDependencies": {
142
162
  "@eslint/js": "^10.0.1",
143
163
  "@tailwindcss/vite": "^4.3.3",
164
+ "@tanstack/query-async-storage-persister": "^5.104.1",
165
+ "@tanstack/react-query": "^5.104.1",
166
+ "@tanstack/react-query-persist-client": "^5.104.1",
144
167
  "@testing-library/jest-dom": "^7.0.1",
145
168
  "@testing-library/react": "^16.3.0",
146
169
  "@testing-library/user-event": "^14.6.7",
@@ -159,6 +182,7 @@
159
182
  "flag-icons": "^7.5.0",
160
183
  "globals": "^17.12.0",
161
184
  "husky": "^9.1.7",
185
+ "idb-keyval": "^6.2.6",
162
186
  "jsdom": "^30.0.1",
163
187
  "lucide-react": "^1.47.0",
164
188
  "modern-screenshot": "^4.7.0",
@@ -33,11 +33,14 @@ export const DEFAULT_ADMIN_ACTION_LOG_LABELS: AdminActionLogLabels = {
33
33
  erase: "Account erased",
34
34
  plan: "Plan changed",
35
35
  },
36
+ renamedFromTo: (from, to) => `from ${from} to ${to}`,
36
37
  groupActions: {
37
38
  group: {
38
39
  group_create: "Group created",
39
40
  group_rename: "Group renamed",
40
41
  group_delete: "Group deleted",
42
+ group_deactivate: "Group deactivated",
43
+ group_reactivate: "Group reactivated",
41
44
  membership_add: "Added to the group",
42
45
  membership_role: "Role in the group changed",
43
46
  membership_remove: "Removed from the group",
@@ -47,6 +50,8 @@ export const DEFAULT_ADMIN_ACTION_LOG_LABELS: AdminActionLogLabels = {
47
50
  group_create: "Team created",
48
51
  group_rename: "Team renamed",
49
52
  group_delete: "Team deleted",
53
+ group_deactivate: "Team deactivated",
54
+ group_reactivate: "Team reactivated",
50
55
  membership_add: "Added to the team",
51
56
  membership_role: "Role in the team changed",
52
57
  membership_remove: "Removed from the team",
@@ -56,6 +61,8 @@ export const DEFAULT_ADMIN_ACTION_LOG_LABELS: AdminActionLogLabels = {
56
61
  group_create: "Company created",
57
62
  group_rename: "Company renamed",
58
63
  group_delete: "Company deleted",
64
+ group_deactivate: "Company deactivated",
65
+ group_reactivate: "Company reactivated",
59
66
  membership_add: "Added to the company",
60
67
  membership_role: "Role in the company changed",
61
68
  membership_remove: "Removed from the company",
@@ -65,6 +72,8 @@ export const DEFAULT_ADMIN_ACTION_LOG_LABELS: AdminActionLogLabels = {
65
72
  group_create: "Budget created",
66
73
  group_rename: "Budget renamed",
67
74
  group_delete: "Budget deleted",
75
+ group_deactivate: "Budget deactivated",
76
+ group_reactivate: "Budget reactivated",
68
77
  membership_add: "Added to the budget",
69
78
  membership_role: "Role in the budget changed",
70
79
  membership_remove: "Removed from the budget",