@trackunit/react-components 3.8.2-alpha-775ca1816d6.0 → 3.8.3-alpha-e1e6a0f33ce.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
@@ -13931,11 +13931,15 @@ const rawLocalStorage = {
13931
13931
  *
13932
13932
  * `persistState` writes to localStorage (`storageSerializer`) and to the URL
13933
13933
  * (via `toUrlValue` + encoding).
13934
- * Each slot skips a value deep-equal to its last write. The URL side starts
13935
- * from the hydrated state, so echoing `initialState` writes no URL entry. The
13936
- * storage side starts from what localStorage held, so a consumer that persists
13937
- * `initialState` after opening a link saves that link's state for the account
13938
- * that opened it, and a reload of the account's own link writes nothing.
13934
+ * Each slot skips a value equal to its last write. The URL side compares what
13935
+ * would land in the URL (the `toUrlValue` projection and the identity stamp),
13936
+ * starting from the projection of the hydrated state and the stamp the URL
13937
+ * carries: echoing `initialState`, or changing only a field the projection
13938
+ * strips, writes no URL entry, while an equal state persisted after the
13939
+ * identity resolves rewrites the URL with its stamp. The storage side starts
13940
+ * from what localStorage held, so a consumer that persists `initialState`
13941
+ * after opening a link saves that link's state for the account that opened
13942
+ * it, and a reload of the account's own link writes nothing.
13939
13943
  * `persistToStorage` and `persistToUrl` are the same two halves exposed
13940
13944
  * separately for consumers that schedule them differently; passing `undefined`
13941
13945
  * removes the entry rather than writing an empty value.
@@ -13992,14 +13996,17 @@ const usePersistedState = ({ key, validate, toUrlValue, fromUrlValue, localStora
13992
13996
  return undefined;
13993
13997
  }
13994
13998
  }, [localStorageEnabled, storageKey]);
13995
- const readPersistedState = react.useCallback(({ valueFromUrl, validateState, transformFromUrlValue }) => {
13999
+ const readPersistedState = react.useCallback(({ valueFromUrl, validateState, transformFromUrlValue, }) => {
13996
14000
  let urlState;
14001
+ let urlStamp;
13997
14002
  if (valueFromUrl) {
13998
14003
  try {
13999
- // The ownership stamp is only read at the login redirect (see
14000
- // `useStripForeignIdentityHash`); here it is unwrapped and discarded.
14001
- const { value } = unwrapUrlOwner(decode(valueFromUrl));
14004
+ // The ownership stamp decides nothing on read (the login redirect
14005
+ // does that, see `useStripForeignIdentityHash`); it is kept only to
14006
+ // seed the URL dedupe with what the URL is currently stamped with.
14007
+ const { value, owner } = unwrapUrlOwner(decode(valueFromUrl));
14002
14008
  urlState = validateState(transformFromUrlValue ? transformFromUrlValue(value) : value);
14009
+ urlStamp = urlState === undefined ? undefined : { owner };
14003
14010
  }
14004
14011
  catch {
14005
14012
  urlState = undefined;
@@ -14021,13 +14028,14 @@ const usePersistedState = ({ key, validate, toUrlValue, fromUrlValue, localStora
14021
14028
  else {
14022
14029
  hydrated = urlState;
14023
14030
  }
14024
- return { initialState: hydrated, storedState: stored };
14031
+ return { initialState: hydrated, storedState: stored, urlValue: urlStamp };
14025
14032
  }, [decode, mergeWithStorageOnRead, readStoredState]);
14033
+ // The shareable projection of `state`: what a URL write carries before
14034
+ // encoding. The URL dedupe compares this, not the whole state, so a change
14035
+ // confined to a field `toUrlValue` strips is not a URL change.
14036
+ const projectForUrl = react.useCallback((state) => (toUrlValueRef.current ? toUrlValueRef.current(state) : state), []);
14026
14037
  // Every URL write goes through here so the ownership stamp is applied uniformly.
14027
- const encodeForUrl = react.useCallback((state) => {
14028
- const value = toUrlValueRef.current ? toUrlValueRef.current(state) : state;
14029
- return encode(currentUrlOwner === undefined ? value : wrapUrlOwner(value, currentUrlOwner));
14030
- }, [encode, currentUrlOwner]);
14038
+ const encodeUrlWrite = react.useCallback(({ projected, owner }) => encode(owner === undefined ? projected : wrapUrlOwner(projected, owner)), [encode]);
14031
14039
  // `readPersistedState` changes identity exactly when an input that changes
14032
14040
  // what a read returns does (storage key, storage/merge options), so the
14033
14041
  // snapshot is re-read then and only then — synchronously, in the render
@@ -14043,20 +14051,33 @@ const usePersistedState = ({ key, validate, toUrlValue, fromUrlValue, localStora
14043
14051
  setSnapshot(readSnapshot());
14044
14052
  }
14045
14053
  const { initialState, storedState } = snapshot;
14046
- // Last value written to each slot, for deduping. The URL side starts as the
14047
- // hydrated value, so a consumer that echoes `initialState` on mount writes
14048
- // nothing: a layout read from storage must not grow a URL entry (GLU-2396).
14054
+ // Last value written to each slot, for deduping. Two refs, not one, because
14055
+ // a consumer may schedule the two writes differently (storage at once, URL
14056
+ // debounced). Both are re-seeded whenever the snapshot is re-read so the
14057
+ // first write to a newly scoped key is never skipped.
14058
+ //
14049
14059
  // The storage side starts as what storage held, so persisting the hydrated
14050
14060
  // state after opening a link writes only when the URL changed something.
14051
- // Both are re-seeded whenever the snapshot is re-read so the first write to
14052
- // a newly scoped key is never skipped. Two refs, not one, because a consumer
14053
- // may schedule the two writes differently (storage at once, URL debounced).
14061
+ //
14062
+ // The URL side holds what a write would put in the URL: the shareable
14063
+ // projection plus the owner stamp. It starts as the projection of the
14064
+ // hydrated value, so a consumer that echoes `initialState` on mount, or
14065
+ // changes only a field the projection strips, writes nothing: a layout read
14066
+ // from storage must not grow a URL entry (GLU-2396). Its owner starts as the
14067
+ // stamp the URL carries (or the current owner when the URL is empty), so an
14068
+ // equal state persisted after the identity resolves still rewrites the URL
14069
+ // with its stamp.
14070
+ const urlOwnerSeed = snapshot.urlValue ? snapshot.urlValue.owner : currentUrlOwner;
14071
+ const seedUrlWrite = react.useCallback((project) => initialState === undefined ? undefined : { projected: project(initialState), owner: urlOwnerSeed }, [initialState, urlOwnerSeed]);
14054
14072
  const lastStoredRef = react.useRef(storedState);
14055
- const lastUrlRef = react.useRef(initialState);
14073
+ // The first seed is computed during render, so it projects with the
14074
+ // `toUrlValue` prop rather than reading `toUrlValueRef` (refs are not read
14075
+ // in render); the projection is cheap, and later seeds come from the effect.
14076
+ const lastUrlRef = react.useRef(seedUrlWrite(state => (toUrlValue ? toUrlValue(state) : state)));
14056
14077
  react.useEffect(() => {
14057
14078
  lastStoredRef.current = storedState;
14058
- lastUrlRef.current = initialState;
14059
- }, [initialState, storedState]);
14079
+ lastUrlRef.current = seedUrlWrite(projectForUrl);
14080
+ }, [seedUrlWrite, projectForUrl, storedState]);
14060
14081
  const persistToStorage = react.useCallback((state) => {
14061
14082
  if (!localStorageEnabled) {
14062
14083
  return;
@@ -14078,12 +14099,24 @@ const usePersistedState = ({ key, validate, toUrlValue, fromUrlValue, localStora
14078
14099
  }
14079
14100
  }, [localStorageEnabled, storageKey]);
14080
14101
  const persistToUrl = react.useCallback((state) => {
14081
- if (state !== undefined && dequal.dequal(lastUrlRef.current, state)) {
14102
+ if (state === undefined) {
14103
+ lastUrlRef.current = undefined;
14104
+ updateUrlValue(undefined);
14105
+ return;
14106
+ }
14107
+ const write = { projected: projectForUrl(state), owner: currentUrlOwner };
14108
+ const last = lastUrlRef.current;
14109
+ // Same projection: write only to add or change a stamp. An unresolved
14110
+ // identity never rewrites a stamped URL just to strip the stamp; it is
14111
+ // re-stamped once the identity resolves.
14112
+ if (last !== undefined &&
14113
+ dequal.dequal(last.projected, write.projected) &&
14114
+ (last.owner === write.owner || write.owner === undefined)) {
14082
14115
  return;
14083
14116
  }
14084
- lastUrlRef.current = state;
14085
- updateUrlValue(state === undefined ? undefined : encodeForUrl(state));
14086
- }, [encodeForUrl, updateUrlValue]);
14117
+ lastUrlRef.current = write;
14118
+ updateUrlValue(encodeUrlWrite(write));
14119
+ }, [currentUrlOwner, encodeUrlWrite, projectForUrl, updateUrlValue]);
14087
14120
  const persistState = react.useCallback((state) => {
14088
14121
  persistToStorage(state);
14089
14122
  persistToUrl(state);
package/index.esm.js CHANGED
@@ -13930,11 +13930,15 @@ const rawLocalStorage = {
13930
13930
  *
13931
13931
  * `persistState` writes to localStorage (`storageSerializer`) and to the URL
13932
13932
  * (via `toUrlValue` + encoding).
13933
- * Each slot skips a value deep-equal to its last write. The URL side starts
13934
- * from the hydrated state, so echoing `initialState` writes no URL entry. The
13935
- * storage side starts from what localStorage held, so a consumer that persists
13936
- * `initialState` after opening a link saves that link's state for the account
13937
- * that opened it, and a reload of the account's own link writes nothing.
13933
+ * Each slot skips a value equal to its last write. The URL side compares what
13934
+ * would land in the URL (the `toUrlValue` projection and the identity stamp),
13935
+ * starting from the projection of the hydrated state and the stamp the URL
13936
+ * carries: echoing `initialState`, or changing only a field the projection
13937
+ * strips, writes no URL entry, while an equal state persisted after the
13938
+ * identity resolves rewrites the URL with its stamp. The storage side starts
13939
+ * from what localStorage held, so a consumer that persists `initialState`
13940
+ * after opening a link saves that link's state for the account that opened
13941
+ * it, and a reload of the account's own link writes nothing.
13938
13942
  * `persistToStorage` and `persistToUrl` are the same two halves exposed
13939
13943
  * separately for consumers that schedule them differently; passing `undefined`
13940
13944
  * removes the entry rather than writing an empty value.
@@ -13991,14 +13995,17 @@ const usePersistedState = ({ key, validate, toUrlValue, fromUrlValue, localStora
13991
13995
  return undefined;
13992
13996
  }
13993
13997
  }, [localStorageEnabled, storageKey]);
13994
- const readPersistedState = useCallback(({ valueFromUrl, validateState, transformFromUrlValue }) => {
13998
+ const readPersistedState = useCallback(({ valueFromUrl, validateState, transformFromUrlValue, }) => {
13995
13999
  let urlState;
14000
+ let urlStamp;
13996
14001
  if (valueFromUrl) {
13997
14002
  try {
13998
- // The ownership stamp is only read at the login redirect (see
13999
- // `useStripForeignIdentityHash`); here it is unwrapped and discarded.
14000
- const { value } = unwrapUrlOwner(decode(valueFromUrl));
14003
+ // The ownership stamp decides nothing on read (the login redirect
14004
+ // does that, see `useStripForeignIdentityHash`); it is kept only to
14005
+ // seed the URL dedupe with what the URL is currently stamped with.
14006
+ const { value, owner } = unwrapUrlOwner(decode(valueFromUrl));
14001
14007
  urlState = validateState(transformFromUrlValue ? transformFromUrlValue(value) : value);
14008
+ urlStamp = urlState === undefined ? undefined : { owner };
14002
14009
  }
14003
14010
  catch {
14004
14011
  urlState = undefined;
@@ -14020,13 +14027,14 @@ const usePersistedState = ({ key, validate, toUrlValue, fromUrlValue, localStora
14020
14027
  else {
14021
14028
  hydrated = urlState;
14022
14029
  }
14023
- return { initialState: hydrated, storedState: stored };
14030
+ return { initialState: hydrated, storedState: stored, urlValue: urlStamp };
14024
14031
  }, [decode, mergeWithStorageOnRead, readStoredState]);
14032
+ // The shareable projection of `state`: what a URL write carries before
14033
+ // encoding. The URL dedupe compares this, not the whole state, so a change
14034
+ // confined to a field `toUrlValue` strips is not a URL change.
14035
+ const projectForUrl = useCallback((state) => (toUrlValueRef.current ? toUrlValueRef.current(state) : state), []);
14025
14036
  // Every URL write goes through here so the ownership stamp is applied uniformly.
14026
- const encodeForUrl = useCallback((state) => {
14027
- const value = toUrlValueRef.current ? toUrlValueRef.current(state) : state;
14028
- return encode(currentUrlOwner === undefined ? value : wrapUrlOwner(value, currentUrlOwner));
14029
- }, [encode, currentUrlOwner]);
14037
+ const encodeUrlWrite = useCallback(({ projected, owner }) => encode(owner === undefined ? projected : wrapUrlOwner(projected, owner)), [encode]);
14030
14038
  // `readPersistedState` changes identity exactly when an input that changes
14031
14039
  // what a read returns does (storage key, storage/merge options), so the
14032
14040
  // snapshot is re-read then and only then — synchronously, in the render
@@ -14042,20 +14050,33 @@ const usePersistedState = ({ key, validate, toUrlValue, fromUrlValue, localStora
14042
14050
  setSnapshot(readSnapshot());
14043
14051
  }
14044
14052
  const { initialState, storedState } = snapshot;
14045
- // Last value written to each slot, for deduping. The URL side starts as the
14046
- // hydrated value, so a consumer that echoes `initialState` on mount writes
14047
- // nothing: a layout read from storage must not grow a URL entry (GLU-2396).
14053
+ // Last value written to each slot, for deduping. Two refs, not one, because
14054
+ // a consumer may schedule the two writes differently (storage at once, URL
14055
+ // debounced). Both are re-seeded whenever the snapshot is re-read so the
14056
+ // first write to a newly scoped key is never skipped.
14057
+ //
14048
14058
  // The storage side starts as what storage held, so persisting the hydrated
14049
14059
  // state after opening a link writes only when the URL changed something.
14050
- // Both are re-seeded whenever the snapshot is re-read so the first write to
14051
- // a newly scoped key is never skipped. Two refs, not one, because a consumer
14052
- // may schedule the two writes differently (storage at once, URL debounced).
14060
+ //
14061
+ // The URL side holds what a write would put in the URL: the shareable
14062
+ // projection plus the owner stamp. It starts as the projection of the
14063
+ // hydrated value, so a consumer that echoes `initialState` on mount, or
14064
+ // changes only a field the projection strips, writes nothing: a layout read
14065
+ // from storage must not grow a URL entry (GLU-2396). Its owner starts as the
14066
+ // stamp the URL carries (or the current owner when the URL is empty), so an
14067
+ // equal state persisted after the identity resolves still rewrites the URL
14068
+ // with its stamp.
14069
+ const urlOwnerSeed = snapshot.urlValue ? snapshot.urlValue.owner : currentUrlOwner;
14070
+ const seedUrlWrite = useCallback((project) => initialState === undefined ? undefined : { projected: project(initialState), owner: urlOwnerSeed }, [initialState, urlOwnerSeed]);
14053
14071
  const lastStoredRef = useRef(storedState);
14054
- const lastUrlRef = useRef(initialState);
14072
+ // The first seed is computed during render, so it projects with the
14073
+ // `toUrlValue` prop rather than reading `toUrlValueRef` (refs are not read
14074
+ // in render); the projection is cheap, and later seeds come from the effect.
14075
+ const lastUrlRef = useRef(seedUrlWrite(state => (toUrlValue ? toUrlValue(state) : state)));
14055
14076
  useEffect(() => {
14056
14077
  lastStoredRef.current = storedState;
14057
- lastUrlRef.current = initialState;
14058
- }, [initialState, storedState]);
14078
+ lastUrlRef.current = seedUrlWrite(projectForUrl);
14079
+ }, [seedUrlWrite, projectForUrl, storedState]);
14059
14080
  const persistToStorage = useCallback((state) => {
14060
14081
  if (!localStorageEnabled) {
14061
14082
  return;
@@ -14077,12 +14098,24 @@ const usePersistedState = ({ key, validate, toUrlValue, fromUrlValue, localStora
14077
14098
  }
14078
14099
  }, [localStorageEnabled, storageKey]);
14079
14100
  const persistToUrl = useCallback((state) => {
14080
- if (state !== undefined && dequal(lastUrlRef.current, state)) {
14101
+ if (state === undefined) {
14102
+ lastUrlRef.current = undefined;
14103
+ updateUrlValue(undefined);
14104
+ return;
14105
+ }
14106
+ const write = { projected: projectForUrl(state), owner: currentUrlOwner };
14107
+ const last = lastUrlRef.current;
14108
+ // Same projection: write only to add or change a stamp. An unresolved
14109
+ // identity never rewrites a stamped URL just to strip the stamp; it is
14110
+ // re-stamped once the identity resolves.
14111
+ if (last !== undefined &&
14112
+ dequal(last.projected, write.projected) &&
14113
+ (last.owner === write.owner || write.owner === undefined)) {
14081
14114
  return;
14082
14115
  }
14083
- lastUrlRef.current = state;
14084
- updateUrlValue(state === undefined ? undefined : encodeForUrl(state));
14085
- }, [encodeForUrl, updateUrlValue]);
14116
+ lastUrlRef.current = write;
14117
+ updateUrlValue(encodeUrlWrite(write));
14118
+ }, [currentUrlOwner, encodeUrlWrite, projectForUrl, updateUrlValue]);
14086
14119
  const persistState = useCallback((state) => {
14087
14120
  persistToStorage(state);
14088
14121
  persistToUrl(state);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-components",
3
- "version": "3.8.2-alpha-775ca1816d6.0",
3
+ "version": "3.8.3-alpha-e1e6a0f33ce.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.18.5-alpha-775ca1816d6.0",
18
- "@trackunit/css-class-variance-utilities": "2.3.5-alpha-775ca1816d6.0",
19
- "@trackunit/shared-utils": "1.19.5-alpha-775ca1816d6.0",
20
- "@trackunit/ui-icons": "1.17.5-alpha-775ca1816d6.0",
17
+ "@trackunit/ui-design-tokens": "1.18.6-alpha-e1e6a0f33ce.0",
18
+ "@trackunit/css-class-variance-utilities": "2.3.6-alpha-e1e6a0f33ce.0",
19
+ "@trackunit/shared-utils": "1.19.6-alpha-e1e6a0f33ce.0",
20
+ "@trackunit/ui-icons": "1.17.6-alpha-e1e6a0f33ce.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.8.5-alpha-775ca1816d6.0"
26
+ "@trackunit/i18n-library-translation": "2.8.6-alpha-e1e6a0f33ce.0"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "react": "^19.0.0",
@@ -81,11 +81,15 @@ type UsePersistedStateReturn<TState> = {
81
81
  *
82
82
  * `persistState` writes to localStorage (`storageSerializer`) and to the URL
83
83
  * (via `toUrlValue` + encoding).
84
- * Each slot skips a value deep-equal to its last write. The URL side starts
85
- * from the hydrated state, so echoing `initialState` writes no URL entry. The
86
- * storage side starts from what localStorage held, so a consumer that persists
87
- * `initialState` after opening a link saves that link's state for the account
88
- * that opened it, and a reload of the account's own link writes nothing.
84
+ * Each slot skips a value equal to its last write. The URL side compares what
85
+ * would land in the URL (the `toUrlValue` projection and the identity stamp),
86
+ * starting from the projection of the hydrated state and the stamp the URL
87
+ * carries: echoing `initialState`, or changing only a field the projection
88
+ * strips, writes no URL entry, while an equal state persisted after the
89
+ * identity resolves rewrites the URL with its stamp. The storage side starts
90
+ * from what localStorage held, so a consumer that persists `initialState`
91
+ * after opening a link saves that link's state for the account that opened
92
+ * it, and a reload of the account's own link writes nothing.
89
93
  * `persistToStorage` and `persistToUrl` are the same two halves exposed
90
94
  * separately for consumers that schedule them differently; passing `undefined`
91
95
  * removes the entry rather than writing an empty value.