@trackunit/react-components 3.5.3 → 3.5.4-alpha-cb9408e01a9.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.
package/index.cjs.js CHANGED
@@ -14126,6 +14126,12 @@ const rawLocalStorage = {
14126
14126
  * `toUrlValue` + encoding). Duplicate writes where the state has not
14127
14127
  * changed (deep equality) are skipped.
14128
14128
  *
14129
+ * `commitHydratedState` writes the hydrated value to localStorage only,
14130
+ * when it came from the URL and differs from what this identity has stored.
14131
+ * It does not update the URL. It returns `false` while the snapshot is
14132
+ * still for a previous read, so the caller can retry after identity
14133
+ * resolution. The caller decides which identity is allowed to commit.
14134
+ *
14129
14135
  * `clearState` removes the entry from localStorage and from the URL slot,
14130
14136
  * rather than writing an empty value. Use it when the state a consumer
14131
14137
  * derives has become empty, so no leftover entry is stored for a value the
@@ -14216,7 +14222,11 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
14216
14222
  try {
14217
14223
  const { value } = unwrapUrlOwner(decode(raw));
14218
14224
  const transformed = transformFromUrlValue ? transformFromUrlValue(value) : value;
14219
- return validateState(transformed);
14225
+ const state = validateState(transformed);
14226
+ if (state === undefined) {
14227
+ return undefined;
14228
+ }
14229
+ return { state, payload: value };
14220
14230
  }
14221
14231
  catch {
14222
14232
  return undefined;
@@ -14238,17 +14248,21 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
14238
14248
  }
14239
14249
  };
14240
14250
  let urlState;
14251
+ let decodedPayload;
14241
14252
  let canonicalUrlValid = false;
14242
14253
  let usedLegacyUrl = false;
14243
14254
  if (enabled && valueFromUrl) {
14244
- urlState = tryDecode(valueFromUrl);
14245
- if (urlState !== undefined) {
14255
+ const decoded = tryDecode(valueFromUrl);
14256
+ if (decoded !== undefined) {
14257
+ urlState = decoded.state;
14258
+ decodedPayload = decoded.payload;
14246
14259
  canonicalUrlValid = true;
14247
14260
  }
14248
14261
  }
14249
14262
  if (urlState === undefined && enabled && legacyValueFromUrl) {
14250
- urlState = tryDecode(legacyValueFromUrl);
14251
- if (urlState !== undefined) {
14263
+ const decoded = tryDecode(legacyValueFromUrl);
14264
+ if (decoded !== undefined) {
14265
+ urlState = decoded.state;
14252
14266
  usedLegacyUrl = true;
14253
14267
  }
14254
14268
  }
@@ -14258,6 +14272,7 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
14258
14272
  return {
14259
14273
  initialState: urlState,
14260
14274
  canonicalUrlHasValidState: canonicalUrlValid,
14275
+ canonicalUrlPayload: decodedPayload,
14261
14276
  needsUrlRestore: usedLegacyUrl,
14262
14277
  };
14263
14278
  }
@@ -14268,6 +14283,7 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
14268
14283
  return {
14269
14284
  initialState: { ...storageState, ...urlState },
14270
14285
  canonicalUrlHasValidState: canonicalUrlValid,
14286
+ canonicalUrlPayload: decodedPayload,
14271
14287
  needsUrlRestore: usedLegacyUrl,
14272
14288
  };
14273
14289
  }
@@ -14275,6 +14291,7 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
14275
14291
  return {
14276
14292
  initialState: urlState,
14277
14293
  canonicalUrlHasValidState: canonicalUrlValid,
14294
+ canonicalUrlPayload: decodedPayload,
14278
14295
  needsUrlRestore: usedLegacyUrl,
14279
14296
  };
14280
14297
  }
@@ -14282,12 +14299,14 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
14282
14299
  return {
14283
14300
  initialState: storageState,
14284
14301
  canonicalUrlHasValidState: false,
14302
+ canonicalUrlPayload: undefined,
14285
14303
  needsUrlRestore: true,
14286
14304
  };
14287
14305
  }
14288
14306
  return {
14289
14307
  initialState: undefined,
14290
14308
  canonicalUrlHasValidState: false,
14309
+ canonicalUrlPayload: undefined,
14291
14310
  needsUrlRestore: false,
14292
14311
  };
14293
14312
  }, [decode, enabled, localStorageEnabled, storageKey, mergeWithStorageOnRead]);
@@ -14297,27 +14316,33 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
14297
14316
  const urlValue = toUrlValueRef.current ? toUrlValueRef.current(state) : state;
14298
14317
  return encode(currentUrlOwner === undefined ? urlValue : wrapUrlOwner(urlValue, currentUrlOwner));
14299
14318
  }, [encode, currentUrlOwner]);
14300
- const [persistedStateSnapshot, setPersistedStateSnapshot] = react.useState(() => readPersistedState({
14301
- valueFromUrl: searchValue,
14302
- legacyValueFromUrl: legacySearchValue,
14303
- validateState: validate,
14304
- transformFromUrlValue: fromUrlValue,
14305
- }));
14306
14319
  const persistedStateReadKey = `${enabled}:${localStorageEnabled}:${mergeWithStorageOnRead}:${urlLocation}:${storageKey}`;
14320
+ const [persistedStateSnapshot, setPersistedStateSnapshot] = react.useState(() => ({
14321
+ ...readPersistedState({
14322
+ valueFromUrl: searchValue,
14323
+ legacyValueFromUrl: legacySearchValue,
14324
+ validateState: validate,
14325
+ transformFromUrlValue: fromUrlValue,
14326
+ }),
14327
+ readKey: persistedStateReadKey,
14328
+ }));
14307
14329
  const persistedStateReadKeyRef = react.useRef(persistedStateReadKey);
14308
14330
  react.useEffect(() => {
14309
14331
  if (persistedStateReadKeyRef.current === persistedStateReadKey) {
14310
14332
  return;
14311
14333
  }
14312
14334
  persistedStateReadKeyRef.current = persistedStateReadKey;
14313
- setPersistedStateSnapshot(readPersistedState({
14314
- valueFromUrl: searchValueRef.current,
14315
- legacyValueFromUrl: legacySearchValueRef.current,
14316
- validateState: validateRef.current,
14317
- transformFromUrlValue: fromUrlValueRef.current,
14318
- }));
14335
+ setPersistedStateSnapshot({
14336
+ ...readPersistedState({
14337
+ valueFromUrl: searchValueRef.current,
14338
+ legacyValueFromUrl: legacySearchValueRef.current,
14339
+ validateState: validateRef.current,
14340
+ transformFromUrlValue: fromUrlValueRef.current,
14341
+ }),
14342
+ readKey: persistedStateReadKey,
14343
+ });
14319
14344
  }, [persistedStateReadKey, readPersistedState]);
14320
- const { initialState, canonicalUrlHasValidState, needsUrlRestore } = persistedStateSnapshot;
14345
+ const { initialState, canonicalUrlHasValidState, canonicalUrlPayload, needsUrlRestore } = persistedStateSnapshot;
14321
14346
  // Late-hydration recovery: TanStack Router's `useSearch` can return an
14322
14347
  // empty object on the very first render of a route that's still in a
14323
14348
  // loader-driven pending phase. The `useState` initializer above runs
@@ -14338,13 +14363,16 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
14338
14363
  if (legacySearchValue === undefined) {
14339
14364
  return;
14340
14365
  }
14341
- setPersistedStateSnapshot(readPersistedState({
14342
- valueFromUrl: searchValueRef.current,
14343
- legacyValueFromUrl: legacySearchValue,
14344
- validateState: validateRef.current,
14345
- transformFromUrlValue: fromUrlValueRef.current,
14346
- }));
14347
- }, [legacySearchValue, initialState, canonicalUrlHasValidState, readPersistedState]);
14366
+ setPersistedStateSnapshot({
14367
+ ...readPersistedState({
14368
+ valueFromUrl: searchValueRef.current,
14369
+ legacyValueFromUrl: legacySearchValue,
14370
+ validateState: validateRef.current,
14371
+ transformFromUrlValue: fromUrlValueRef.current,
14372
+ }),
14373
+ readKey: persistedStateReadKey,
14374
+ });
14375
+ }, [legacySearchValue, initialState, canonicalUrlHasValidState, persistedStateReadKey, readPersistedState]);
14348
14376
  const lastPersistedRef = react.useRef(initialState);
14349
14377
  react.useEffect(() => {
14350
14378
  lastPersistedRef.current = initialState;
@@ -14414,7 +14442,52 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
14414
14442
  }
14415
14443
  updateSearchParam(undefined);
14416
14444
  }, [localStorageEnabled, storageKey, updateSearchParam]);
14417
- return react.useMemo(() => ({ initialState, persistState, clearState }), [initialState, persistState, clearState]);
14445
+ const snapshotIsCurrent = persistedStateSnapshot.readKey === persistedStateReadKey;
14446
+ // Opening a shared URL should become this identity's saved layout. The
14447
+ // caller decides which identity is allowed to commit: this only writes
14448
+ // when the snapshot was read for the current storage key, so a late
14449
+ // identity resolution cannot persist the pre-merge value and drop fields
14450
+ // that live only in localStorage. A hash that is only the shareable
14451
+ // projection of what is already stored is left alone, so reloading this
14452
+ // account's own link does not replace hidden columns with the shorter URL form.
14453
+ const commitHydratedState = react.useCallback(() => {
14454
+ if (!snapshotIsCurrent || !localStorageEnabled) {
14455
+ return false;
14456
+ }
14457
+ if (!canonicalUrlHasValidState || initialState === undefined) {
14458
+ return true;
14459
+ }
14460
+ let stored;
14461
+ try {
14462
+ const raw = rawLocalStorage.read(storageKey);
14463
+ if (raw) {
14464
+ stored = validateRef.current(sharedUtils.storageSerializer.deserialize(raw));
14465
+ }
14466
+ }
14467
+ catch {
14468
+ stored = undefined;
14469
+ }
14470
+ if (stored !== undefined) {
14471
+ const projected = toUrlValueRef.current ? toUrlValueRef.current(stored) : stored;
14472
+ if (dequal.dequal(projected, canonicalUrlPayload)) {
14473
+ return true;
14474
+ }
14475
+ }
14476
+ const serialized = serializeRef.current
14477
+ ? serializeRef.current(initialState)
14478
+ : sharedUtils.storageSerializer.serialize(initialState);
14479
+ rawLocalStorage.write(storageKey, serialized);
14480
+ lastPersistedRef.current = initialState;
14481
+ return true;
14482
+ }, [
14483
+ canonicalUrlHasValidState,
14484
+ canonicalUrlPayload,
14485
+ initialState,
14486
+ localStorageEnabled,
14487
+ snapshotIsCurrent,
14488
+ storageKey,
14489
+ ]);
14490
+ return react.useMemo(() => ({ initialState, persistState, clearState, commitHydratedState }), [initialState, persistState, clearState, commitHydratedState]);
14418
14491
  };
14419
14492
 
14420
14493
  /**
package/index.esm.js CHANGED
@@ -14125,6 +14125,12 @@ const rawLocalStorage = {
14125
14125
  * `toUrlValue` + encoding). Duplicate writes where the state has not
14126
14126
  * changed (deep equality) are skipped.
14127
14127
  *
14128
+ * `commitHydratedState` writes the hydrated value to localStorage only,
14129
+ * when it came from the URL and differs from what this identity has stored.
14130
+ * It does not update the URL. It returns `false` while the snapshot is
14131
+ * still for a previous read, so the caller can retry after identity
14132
+ * resolution. The caller decides which identity is allowed to commit.
14133
+ *
14128
14134
  * `clearState` removes the entry from localStorage and from the URL slot,
14129
14135
  * rather than writing an empty value. Use it when the state a consumer
14130
14136
  * derives has become empty, so no leftover entry is stored for a value the
@@ -14215,7 +14221,11 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
14215
14221
  try {
14216
14222
  const { value } = unwrapUrlOwner(decode(raw));
14217
14223
  const transformed = transformFromUrlValue ? transformFromUrlValue(value) : value;
14218
- return validateState(transformed);
14224
+ const state = validateState(transformed);
14225
+ if (state === undefined) {
14226
+ return undefined;
14227
+ }
14228
+ return { state, payload: value };
14219
14229
  }
14220
14230
  catch {
14221
14231
  return undefined;
@@ -14237,17 +14247,21 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
14237
14247
  }
14238
14248
  };
14239
14249
  let urlState;
14250
+ let decodedPayload;
14240
14251
  let canonicalUrlValid = false;
14241
14252
  let usedLegacyUrl = false;
14242
14253
  if (enabled && valueFromUrl) {
14243
- urlState = tryDecode(valueFromUrl);
14244
- if (urlState !== undefined) {
14254
+ const decoded = tryDecode(valueFromUrl);
14255
+ if (decoded !== undefined) {
14256
+ urlState = decoded.state;
14257
+ decodedPayload = decoded.payload;
14245
14258
  canonicalUrlValid = true;
14246
14259
  }
14247
14260
  }
14248
14261
  if (urlState === undefined && enabled && legacyValueFromUrl) {
14249
- urlState = tryDecode(legacyValueFromUrl);
14250
- if (urlState !== undefined) {
14262
+ const decoded = tryDecode(legacyValueFromUrl);
14263
+ if (decoded !== undefined) {
14264
+ urlState = decoded.state;
14251
14265
  usedLegacyUrl = true;
14252
14266
  }
14253
14267
  }
@@ -14257,6 +14271,7 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
14257
14271
  return {
14258
14272
  initialState: urlState,
14259
14273
  canonicalUrlHasValidState: canonicalUrlValid,
14274
+ canonicalUrlPayload: decodedPayload,
14260
14275
  needsUrlRestore: usedLegacyUrl,
14261
14276
  };
14262
14277
  }
@@ -14267,6 +14282,7 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
14267
14282
  return {
14268
14283
  initialState: { ...storageState, ...urlState },
14269
14284
  canonicalUrlHasValidState: canonicalUrlValid,
14285
+ canonicalUrlPayload: decodedPayload,
14270
14286
  needsUrlRestore: usedLegacyUrl,
14271
14287
  };
14272
14288
  }
@@ -14274,6 +14290,7 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
14274
14290
  return {
14275
14291
  initialState: urlState,
14276
14292
  canonicalUrlHasValidState: canonicalUrlValid,
14293
+ canonicalUrlPayload: decodedPayload,
14277
14294
  needsUrlRestore: usedLegacyUrl,
14278
14295
  };
14279
14296
  }
@@ -14281,12 +14298,14 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
14281
14298
  return {
14282
14299
  initialState: storageState,
14283
14300
  canonicalUrlHasValidState: false,
14301
+ canonicalUrlPayload: undefined,
14284
14302
  needsUrlRestore: true,
14285
14303
  };
14286
14304
  }
14287
14305
  return {
14288
14306
  initialState: undefined,
14289
14307
  canonicalUrlHasValidState: false,
14308
+ canonicalUrlPayload: undefined,
14290
14309
  needsUrlRestore: false,
14291
14310
  };
14292
14311
  }, [decode, enabled, localStorageEnabled, storageKey, mergeWithStorageOnRead]);
@@ -14296,27 +14315,33 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
14296
14315
  const urlValue = toUrlValueRef.current ? toUrlValueRef.current(state) : state;
14297
14316
  return encode(currentUrlOwner === undefined ? urlValue : wrapUrlOwner(urlValue, currentUrlOwner));
14298
14317
  }, [encode, currentUrlOwner]);
14299
- const [persistedStateSnapshot, setPersistedStateSnapshot] = useState(() => readPersistedState({
14300
- valueFromUrl: searchValue,
14301
- legacyValueFromUrl: legacySearchValue,
14302
- validateState: validate,
14303
- transformFromUrlValue: fromUrlValue,
14304
- }));
14305
14318
  const persistedStateReadKey = `${enabled}:${localStorageEnabled}:${mergeWithStorageOnRead}:${urlLocation}:${storageKey}`;
14319
+ const [persistedStateSnapshot, setPersistedStateSnapshot] = useState(() => ({
14320
+ ...readPersistedState({
14321
+ valueFromUrl: searchValue,
14322
+ legacyValueFromUrl: legacySearchValue,
14323
+ validateState: validate,
14324
+ transformFromUrlValue: fromUrlValue,
14325
+ }),
14326
+ readKey: persistedStateReadKey,
14327
+ }));
14306
14328
  const persistedStateReadKeyRef = useRef(persistedStateReadKey);
14307
14329
  useEffect(() => {
14308
14330
  if (persistedStateReadKeyRef.current === persistedStateReadKey) {
14309
14331
  return;
14310
14332
  }
14311
14333
  persistedStateReadKeyRef.current = persistedStateReadKey;
14312
- setPersistedStateSnapshot(readPersistedState({
14313
- valueFromUrl: searchValueRef.current,
14314
- legacyValueFromUrl: legacySearchValueRef.current,
14315
- validateState: validateRef.current,
14316
- transformFromUrlValue: fromUrlValueRef.current,
14317
- }));
14334
+ setPersistedStateSnapshot({
14335
+ ...readPersistedState({
14336
+ valueFromUrl: searchValueRef.current,
14337
+ legacyValueFromUrl: legacySearchValueRef.current,
14338
+ validateState: validateRef.current,
14339
+ transformFromUrlValue: fromUrlValueRef.current,
14340
+ }),
14341
+ readKey: persistedStateReadKey,
14342
+ });
14318
14343
  }, [persistedStateReadKey, readPersistedState]);
14319
- const { initialState, canonicalUrlHasValidState, needsUrlRestore } = persistedStateSnapshot;
14344
+ const { initialState, canonicalUrlHasValidState, canonicalUrlPayload, needsUrlRestore } = persistedStateSnapshot;
14320
14345
  // Late-hydration recovery: TanStack Router's `useSearch` can return an
14321
14346
  // empty object on the very first render of a route that's still in a
14322
14347
  // loader-driven pending phase. The `useState` initializer above runs
@@ -14337,13 +14362,16 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
14337
14362
  if (legacySearchValue === undefined) {
14338
14363
  return;
14339
14364
  }
14340
- setPersistedStateSnapshot(readPersistedState({
14341
- valueFromUrl: searchValueRef.current,
14342
- legacyValueFromUrl: legacySearchValue,
14343
- validateState: validateRef.current,
14344
- transformFromUrlValue: fromUrlValueRef.current,
14345
- }));
14346
- }, [legacySearchValue, initialState, canonicalUrlHasValidState, readPersistedState]);
14365
+ setPersistedStateSnapshot({
14366
+ ...readPersistedState({
14367
+ valueFromUrl: searchValueRef.current,
14368
+ legacyValueFromUrl: legacySearchValue,
14369
+ validateState: validateRef.current,
14370
+ transformFromUrlValue: fromUrlValueRef.current,
14371
+ }),
14372
+ readKey: persistedStateReadKey,
14373
+ });
14374
+ }, [legacySearchValue, initialState, canonicalUrlHasValidState, persistedStateReadKey, readPersistedState]);
14347
14375
  const lastPersistedRef = useRef(initialState);
14348
14376
  useEffect(() => {
14349
14377
  lastPersistedRef.current = initialState;
@@ -14413,7 +14441,52 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
14413
14441
  }
14414
14442
  updateSearchParam(undefined);
14415
14443
  }, [localStorageEnabled, storageKey, updateSearchParam]);
14416
- return useMemo(() => ({ initialState, persistState, clearState }), [initialState, persistState, clearState]);
14444
+ const snapshotIsCurrent = persistedStateSnapshot.readKey === persistedStateReadKey;
14445
+ // Opening a shared URL should become this identity's saved layout. The
14446
+ // caller decides which identity is allowed to commit: this only writes
14447
+ // when the snapshot was read for the current storage key, so a late
14448
+ // identity resolution cannot persist the pre-merge value and drop fields
14449
+ // that live only in localStorage. A hash that is only the shareable
14450
+ // projection of what is already stored is left alone, so reloading this
14451
+ // account's own link does not replace hidden columns with the shorter URL form.
14452
+ const commitHydratedState = useCallback(() => {
14453
+ if (!snapshotIsCurrent || !localStorageEnabled) {
14454
+ return false;
14455
+ }
14456
+ if (!canonicalUrlHasValidState || initialState === undefined) {
14457
+ return true;
14458
+ }
14459
+ let stored;
14460
+ try {
14461
+ const raw = rawLocalStorage.read(storageKey);
14462
+ if (raw) {
14463
+ stored = validateRef.current(storageSerializer.deserialize(raw));
14464
+ }
14465
+ }
14466
+ catch {
14467
+ stored = undefined;
14468
+ }
14469
+ if (stored !== undefined) {
14470
+ const projected = toUrlValueRef.current ? toUrlValueRef.current(stored) : stored;
14471
+ if (dequal(projected, canonicalUrlPayload)) {
14472
+ return true;
14473
+ }
14474
+ }
14475
+ const serialized = serializeRef.current
14476
+ ? serializeRef.current(initialState)
14477
+ : storageSerializer.serialize(initialState);
14478
+ rawLocalStorage.write(storageKey, serialized);
14479
+ lastPersistedRef.current = initialState;
14480
+ return true;
14481
+ }, [
14482
+ canonicalUrlHasValidState,
14483
+ canonicalUrlPayload,
14484
+ initialState,
14485
+ localStorageEnabled,
14486
+ snapshotIsCurrent,
14487
+ storageKey,
14488
+ ]);
14489
+ return useMemo(() => ({ initialState, persistState, clearState, commitHydratedState }), [initialState, persistState, clearState, commitHydratedState]);
14417
14490
  };
14418
14491
 
14419
14492
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-components",
3
- "version": "3.5.3",
3
+ "version": "3.5.4-alpha-cb9408e01a9.0",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "migrations": "./migrations.json",
@@ -14,16 +14,16 @@
14
14
  "@floating-ui/react": "^0.26.25",
15
15
  "string-ts": "^2.0.0",
16
16
  "tailwind-merge": "^2.0.0",
17
- "@trackunit/ui-design-tokens": "1.17.2",
18
- "@trackunit/css-class-variance-utilities": "2.2.2",
19
- "@trackunit/shared-utils": "1.18.2",
20
- "@trackunit/ui-icons": "1.16.2",
17
+ "@trackunit/ui-design-tokens": "1.17.3-alpha-cb9408e01a9.0",
18
+ "@trackunit/css-class-variance-utilities": "2.2.3-alpha-cb9408e01a9.0",
19
+ "@trackunit/shared-utils": "1.18.3-alpha-cb9408e01a9.0",
20
+ "@trackunit/ui-icons": "1.16.3-alpha-cb9408e01a9.0",
21
21
  "es-toolkit": "^1.52.0",
22
22
  "@tanstack/react-virtual": "^3.14.13",
23
23
  "dequal": "^2.0.3",
24
24
  "fflate": "^0.8.3",
25
25
  "zod": "^3.25.76",
26
- "@trackunit/i18n-library-translation": "2.7.2"
26
+ "@trackunit/i18n-library-translation": "2.7.3-alpha-cb9408e01a9.0"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "react": "^19.0.0",
@@ -58,6 +58,13 @@ type UsePersistedStateReturn<TState> = {
58
58
  readonly initialState: TState | undefined;
59
59
  readonly persistState: (state: TState) => void;
60
60
  readonly clearState: () => void;
61
+ /**
62
+ * Writes the hydrated state to localStorage when it came from the URL and
63
+ * differs from what this identity has stored. Returns `false` when the
64
+ * snapshot is still for a previous read (identity not resolved yet), so
65
+ * the caller can retry. A no-op success returns `true`.
66
+ */
67
+ readonly commitHydratedState: () => boolean;
61
68
  };
62
69
  /**
63
70
  * Generic persistence hook that loads state from a URL slot (search param
@@ -82,6 +89,12 @@ type UsePersistedStateReturn<TState> = {
82
89
  * `toUrlValue` + encoding). Duplicate writes where the state has not
83
90
  * changed (deep equality) are skipped.
84
91
  *
92
+ * `commitHydratedState` writes the hydrated value to localStorage only,
93
+ * when it came from the URL and differs from what this identity has stored.
94
+ * It does not update the URL. It returns `false` while the snapshot is
95
+ * still for a previous read, so the caller can retry after identity
96
+ * resolution. The caller decides which identity is allowed to commit.
97
+ *
85
98
  * `clearState` removes the entry from localStorage and from the URL slot,
86
99
  * rather than writing an empty value. Use it when the state a consumer
87
100
  * derives has become empty, so no leftover entry is stored for a value the