@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 +60 -27
- package/index.esm.js +60 -27
- package/package.json +6 -6
- package/src/hooks/persistence/usePersistedState.d.ts +9 -5
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
|
|
13935
|
-
*
|
|
13936
|
-
*
|
|
13937
|
-
* `initialState
|
|
13938
|
-
*
|
|
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
|
|
14000
|
-
// `useStripForeignIdentityHash`);
|
|
14001
|
-
|
|
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
|
|
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.
|
|
14047
|
-
//
|
|
14048
|
-
//
|
|
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
|
-
//
|
|
14052
|
-
//
|
|
14053
|
-
//
|
|
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
|
-
|
|
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 =
|
|
14059
|
-
}, [
|
|
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
|
|
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 =
|
|
14085
|
-
updateUrlValue(
|
|
14086
|
-
}, [
|
|
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
|
|
13934
|
-
*
|
|
13935
|
-
*
|
|
13936
|
-
* `initialState
|
|
13937
|
-
*
|
|
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
|
|
13999
|
-
// `useStripForeignIdentityHash`);
|
|
14000
|
-
|
|
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
|
|
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.
|
|
14046
|
-
//
|
|
14047
|
-
//
|
|
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
|
-
//
|
|
14051
|
-
//
|
|
14052
|
-
//
|
|
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
|
-
|
|
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 =
|
|
14058
|
-
}, [
|
|
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
|
|
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 =
|
|
14084
|
-
updateUrlValue(
|
|
14085
|
-
}, [
|
|
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.
|
|
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.
|
|
18
|
-
"@trackunit/css-class-variance-utilities": "2.3.
|
|
19
|
-
"@trackunit/shared-utils": "1.19.
|
|
20
|
-
"@trackunit/ui-icons": "1.17.
|
|
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.
|
|
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
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* `initialState
|
|
88
|
-
*
|
|
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.
|