@trackunit/react-components 1.21.15 → 1.21.18
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 +251 -44
- package/index.esm.js +248 -46
- package/package.json +3 -2
- package/src/components/Sheet/Sheet.d.ts +1 -1
- package/src/components/Sheet/sheet-types.d.ts +9 -0
- package/src/hooks/persistence/usePersistedState.d.ts +40 -0
- package/src/hooks/persistence/useSearchParamSync.d.ts +27 -0
- package/src/hooks/persistence/useStorageKey.d.ts +11 -0
- package/src/index.d.ts +4 -0
package/index.cjs.js
CHANGED
|
@@ -20,8 +20,9 @@ var reactVirtual = require('@tanstack/react-virtual');
|
|
|
20
20
|
var reactHelmetAsync = require('react-helmet-async');
|
|
21
21
|
var reactTabs = require('@radix-ui/react-tabs');
|
|
22
22
|
var fflate = require('fflate');
|
|
23
|
-
var zod = require('zod');
|
|
24
23
|
var superjson = require('superjson');
|
|
24
|
+
var zod = require('zod');
|
|
25
|
+
var dequal = require('dequal');
|
|
25
26
|
|
|
26
27
|
const cvaIcon = cssClassVarianceUtilities.cvaMerge(["aspect-square", "inline-grid", "relative", "shrink-0"], {
|
|
27
28
|
variants: {
|
|
@@ -7829,14 +7830,15 @@ const getSheetPanelStyle = ({ snapHeight, isOpen, closePhase, variant, autoHeigh
|
|
|
7829
7830
|
height: autoHeight ? "fit-content" : `var(--sheet-drag-height, ${snapHeight})`,
|
|
7830
7831
|
maxHeight: maxHeight ?? `calc(100cqh - ${FULL_HEIGHT_TOP_MARGIN_PX}px)`,
|
|
7831
7832
|
pointerEvents: "auto",
|
|
7832
|
-
transform: isOpen
|
|
7833
|
-
|
|
7833
|
+
transform: isOpen
|
|
7834
|
+
? "translateY(0) scale(var(--sheet-stack-scale, 1))"
|
|
7835
|
+
: "translateY(100%) scale(var(--sheet-stack-scale, 1))",
|
|
7836
|
+
transformOrigin: "bottom center",
|
|
7837
|
+
transition: isDragging
|
|
7834
7838
|
? "none"
|
|
7835
|
-
:
|
|
7836
|
-
?
|
|
7837
|
-
:
|
|
7838
|
-
? SHEET_TRANSFORM_TRANSITION
|
|
7839
|
-
: SHEET_OPEN_TRANSITION,
|
|
7839
|
+
: suppressTransition || autoHeight || !isOpen
|
|
7840
|
+
? SHEET_TRANSFORM_TRANSITION
|
|
7841
|
+
: SHEET_OPEN_TRANSITION,
|
|
7840
7842
|
};
|
|
7841
7843
|
};
|
|
7842
7844
|
|
|
@@ -9126,10 +9128,11 @@ const useSheetMotionOverflow = ({ panelEl, isDragging, scrollAreaEl, separatorEl
|
|
|
9126
9128
|
* CSS transitions on transform; the component stays mounted during the
|
|
9127
9129
|
* close animation and unmounts after the transition completes.
|
|
9128
9130
|
*/
|
|
9129
|
-
const Sheet = ({ isOpen, state, snap, onGeometryChange, onSnap, onClickHandle, onCloseGesture, floatingUi, ref, anchor = "center", snapping = true, resizable = true, variant = "default", trapFocus = true, container, dockedContent, className, "data-testid": dataTestId, onCloseComplete, entryAnimation = "subsequent", children, }) => {
|
|
9131
|
+
const Sheet = ({ isOpen, state, snap, onGeometryChange, onSnap, onClickHandle, onCloseGesture, floatingUi, ref, anchor = "center", snapping = true, resizable = true, variant = "default", trapFocus = true, container, dockedContent, persistentContent, className, "data-testid": dataTestId, onCloseComplete, entryAnimation = "subsequent", children, }) => {
|
|
9130
9132
|
const isFirstRender = useIsFirstRender();
|
|
9131
9133
|
const skipEntryAnimation = entryAnimation === "never" || (entryAnimation === "subsequent" && isFirstRender);
|
|
9132
9134
|
const effectiveSnapping = resizable && snapping;
|
|
9135
|
+
const dockingEnabled = dockedContent !== undefined || persistentContent !== undefined;
|
|
9133
9136
|
const [animState, animDispatch] = react.useReducer(sheetAnimationReducer, INITIAL_ANIMATION_STATE);
|
|
9134
9137
|
if (isOpen !== animState.prevIsOpen) {
|
|
9135
9138
|
animDispatch({
|
|
@@ -9141,7 +9144,7 @@ const Sheet = ({ isOpen, state, snap, onGeometryChange, onSnap, onClickHandle, o
|
|
|
9141
9144
|
const measurements = useSheetMeasurements({
|
|
9142
9145
|
shouldRender: animState.shouldRender,
|
|
9143
9146
|
state,
|
|
9144
|
-
dockingEnabled
|
|
9147
|
+
dockingEnabled,
|
|
9145
9148
|
snapping: effectiveSnapping,
|
|
9146
9149
|
externalRef: ref,
|
|
9147
9150
|
onGeometryChange,
|
|
@@ -9216,7 +9219,7 @@ const Sheet = ({ isOpen, state, snap, onGeometryChange, onSnap, onClickHandle, o
|
|
|
9216
9219
|
? `${state.effectiveDockedHeight}px`
|
|
9217
9220
|
: fitHeightMeasured
|
|
9218
9221
|
? `${cappedFitHeight}px`
|
|
9219
|
-
: getSnapLevelCssHeight(state.level, state.totalLevels,
|
|
9222
|
+
: getSnapLevelCssHeight(state.level, state.totalLevels, dockingEnabled, state.effectiveDockedHeight);
|
|
9220
9223
|
const fitMaxHeight = state.sizingMode === "fit" ? `calc(100cqh - ${FULL_HEIGHT_TOP_MARGIN_PX}px)` : undefined;
|
|
9221
9224
|
if (!animState.shouldRender)
|
|
9222
9225
|
return null;
|
|
@@ -9240,7 +9243,7 @@ const Sheet = ({ isOpen, state, snap, onGeometryChange, onSnap, onClickHandle, o
|
|
|
9240
9243
|
snapHeight: snapHeightCss,
|
|
9241
9244
|
suppressTransition: skipEntryAnimation && animState.visuallyOpen,
|
|
9242
9245
|
variant,
|
|
9243
|
-
}), children: [resizable ? (jsxRuntime.jsx(SheetHandle, { "data-testid": dataTestId !== undefined ? `${dataTestId}-handle` : undefined, isDragging: gestures.isDragging, onClick: handleClick, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, ...gestureHandlers })) : null, jsxRuntime.jsx("div", { className: "h-px shrink-0 bg-neutral-200 opacity-0 transition-opacity duration-200", ref: setSeparatorEl }), jsxRuntime.
|
|
9246
|
+
}), children: [resizable ? (jsxRuntime.jsx(SheetHandle, { "data-testid": dataTestId !== undefined ? `${dataTestId}-handle` : undefined, isDragging: gestures.isDragging, onClick: handleClick, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, ...gestureHandlers })) : null, jsxRuntime.jsx("div", { className: "h-px shrink-0 bg-neutral-200 opacity-0 transition-opacity duration-200", ref: setSeparatorEl }), jsxRuntime.jsxs("div", { className: cvaSheetScrollArea({ fillHeight: state.sizingMode !== "fit" }), "data-sheet-scroll-area": true, ref: setScrollAreaEl, children: [persistentContent, state.sizingMode === "docked" ? dockedContent : children] })] }));
|
|
9244
9247
|
return (jsxRuntime.jsx(Portal, { root: container, children: jsxRuntime.jsxs("div", { className: cvaSheetContainer({
|
|
9245
9248
|
docked: state.sizingMode === "docked",
|
|
9246
9249
|
}), "data-testid": dataTestId !== undefined ? `${dataTestId}-container` : undefined, ref: containerRef, children: [jsxRuntime.jsx(SheetOverlay, { "data-testid": dataTestId !== undefined ? `${dataTestId}-overlay` : undefined, visible: showOverlay }), shouldTrapFocus === true ? (jsxRuntime.jsx(react$1.FloatingFocusManager, { context: floatingUi.context, children: panel })) : (panel)] }) }));
|
|
@@ -10316,8 +10319,11 @@ const useCustomEncoding = () => {
|
|
|
10316
10319
|
// If it's already a string, use it directly; otherwise stringify the object
|
|
10317
10320
|
const json = typeof input === "string" ? input : JSON.stringify(input);
|
|
10318
10321
|
const textInput = new TextEncoder().encode(json);
|
|
10319
|
-
// Use fflate for synchronous gzip compression
|
|
10320
|
-
|
|
10322
|
+
// Use fflate for synchronous gzip compression.
|
|
10323
|
+
// mtime: 0 ensures deterministic output — without it the gzip header
|
|
10324
|
+
// includes the current timestamp, making the same input produce a
|
|
10325
|
+
// different encoded string on every call.
|
|
10326
|
+
const compressed = fflate.gzipSync(textInput, { mtime: 0 });
|
|
10321
10327
|
return b64urlEncode(compressed);
|
|
10322
10328
|
}
|
|
10323
10329
|
catch (_) {
|
|
@@ -10369,6 +10375,36 @@ const useCustomEncoding = () => {
|
|
|
10369
10375
|
return react.useMemo(() => ({ encode, decode }), [encode, decode]);
|
|
10370
10376
|
};
|
|
10371
10377
|
|
|
10378
|
+
/**
|
|
10379
|
+
* Internal envelope used to tag superjson-serialized data in web storage.
|
|
10380
|
+
*
|
|
10381
|
+
* `__serializer` is a **reserved internal key** — consumer state objects must
|
|
10382
|
+
* not include it as a top-level key. The double-underscore prefix is a
|
|
10383
|
+
* deliberate signal that this is a private implementation detail.
|
|
10384
|
+
*
|
|
10385
|
+
* A runtime warning is emitted (via `writeToStorage`) if reserved keys are
|
|
10386
|
+
* detected in the value being written.
|
|
10387
|
+
*/
|
|
10388
|
+
const taggedSuperjsonEnvelopeSchema = zod.z.object({
|
|
10389
|
+
__serializer: zod.z.literal("superjson"),
|
|
10390
|
+
json: zod.z.custom(),
|
|
10391
|
+
meta: zod.z.custom().optional(),
|
|
10392
|
+
});
|
|
10393
|
+
const storageSerializer = {
|
|
10394
|
+
serialize: (value) => {
|
|
10395
|
+
const serialized = superjson.serialize(value);
|
|
10396
|
+
return JSON.stringify({ __serializer: "superjson", ...serialized });
|
|
10397
|
+
},
|
|
10398
|
+
deserialize: (value) => {
|
|
10399
|
+
const parsed = JSON.parse(value);
|
|
10400
|
+
const result = taggedSuperjsonEnvelopeSchema.safeParse(parsed);
|
|
10401
|
+
if (result.success) {
|
|
10402
|
+
return superjson.deserialize({ json: result.data.json, meta: result.data.meta });
|
|
10403
|
+
}
|
|
10404
|
+
return parsed;
|
|
10405
|
+
},
|
|
10406
|
+
};
|
|
10407
|
+
|
|
10372
10408
|
/**
|
|
10373
10409
|
* Runs a sequential migration pipeline on the provided data, applying
|
|
10374
10410
|
* each migration whose version is in the range (fromVersion, toVersion].
|
|
@@ -10474,36 +10510,6 @@ const salvageState = (schema, rawData, defaultState) => {
|
|
|
10474
10510
|
}
|
|
10475
10511
|
};
|
|
10476
10512
|
|
|
10477
|
-
/**
|
|
10478
|
-
* Internal envelope used to tag superjson-serialized data in web storage.
|
|
10479
|
-
*
|
|
10480
|
-
* `__serializer` is a **reserved internal key** — consumer state objects must
|
|
10481
|
-
* not include it as a top-level key. The double-underscore prefix is a
|
|
10482
|
-
* deliberate signal that this is a private implementation detail.
|
|
10483
|
-
*
|
|
10484
|
-
* A runtime warning is emitted (via `writeToStorage`) if reserved keys are
|
|
10485
|
-
* detected in the value being written.
|
|
10486
|
-
*/
|
|
10487
|
-
const taggedSuperjsonEnvelopeSchema = zod.z.object({
|
|
10488
|
-
__serializer: zod.z.literal("superjson"),
|
|
10489
|
-
json: zod.z.custom(),
|
|
10490
|
-
meta: zod.z.custom().optional(),
|
|
10491
|
-
});
|
|
10492
|
-
const storageSerializer = {
|
|
10493
|
-
serialize: (value) => {
|
|
10494
|
-
const serialized = superjson.serialize(value);
|
|
10495
|
-
return JSON.stringify({ __serializer: "superjson", ...serialized });
|
|
10496
|
-
},
|
|
10497
|
-
deserialize: (value) => {
|
|
10498
|
-
const parsed = JSON.parse(value);
|
|
10499
|
-
const result = taggedSuperjsonEnvelopeSchema.safeParse(parsed);
|
|
10500
|
-
if (result.success) {
|
|
10501
|
-
return superjson.deserialize({ json: result.data.json, meta: result.data.meta });
|
|
10502
|
-
}
|
|
10503
|
-
return parsed;
|
|
10504
|
-
},
|
|
10505
|
-
};
|
|
10506
|
-
|
|
10507
10513
|
/**
|
|
10508
10514
|
* Internal envelope that pairs stored data with a schema version number.
|
|
10509
10515
|
*
|
|
@@ -10979,6 +10985,202 @@ const useSessionStorage = (options) => useWebStorage(globalThis.sessionStorage,
|
|
|
10979
10985
|
*/
|
|
10980
10986
|
const useSessionStorageReducer = (options) => useWebStorageReducer(globalThis.sessionStorage, options);
|
|
10981
10987
|
|
|
10988
|
+
const MAX_URL_LENGTH = 5000;
|
|
10989
|
+
/**
|
|
10990
|
+
* Syncs an encoded string value with a URL search parameter via Tanstack Router.
|
|
10991
|
+
*
|
|
10992
|
+
* Provides a write function that updates the URL through the application
|
|
10993
|
+
* router (preserving other search params), guards against exceeding
|
|
10994
|
+
* {@link MAX_URL_LENGTH}, and detects external URL changes (browser
|
|
10995
|
+
* back/forward, shared links) via an optional callback.
|
|
10996
|
+
*
|
|
10997
|
+
* @param options - Configuration for the search param sync.
|
|
10998
|
+
* @param options.key - The URL search parameter name.
|
|
10999
|
+
* @param options.enabled - Set to `false` to disable all URL interaction (default `true`).
|
|
11000
|
+
* @param options.onExternalChange - Called when the param changes externally.
|
|
11001
|
+
* @param options.replace - `true` to always use `replaceState`.
|
|
11002
|
+
*/
|
|
11003
|
+
const useSearchParamSync = ({ key, enabled = true, onExternalChange, replace: replaceOption, }) => {
|
|
11004
|
+
const navigate = reactRouter.useNavigate();
|
|
11005
|
+
const location = reactRouter.useLocation();
|
|
11006
|
+
const search = reactRouter.useSearch({ strict: false, shouldThrow: false });
|
|
11007
|
+
const lastWrittenRef = react.useRef(undefined);
|
|
11008
|
+
const onExternalChangeRef = react.useRef(onExternalChange);
|
|
11009
|
+
react.useEffect(() => {
|
|
11010
|
+
onExternalChangeRef.current = onExternalChange;
|
|
11011
|
+
}, [onExternalChange]);
|
|
11012
|
+
const currentSearchValue = react.useMemo(() => {
|
|
11013
|
+
if (!enabled) {
|
|
11014
|
+
return undefined;
|
|
11015
|
+
}
|
|
11016
|
+
const value = search?.[key];
|
|
11017
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
11018
|
+
return String(value);
|
|
11019
|
+
}
|
|
11020
|
+
return undefined;
|
|
11021
|
+
}, [enabled, search, key]);
|
|
11022
|
+
react.useEffect(() => {
|
|
11023
|
+
if (!enabled) {
|
|
11024
|
+
return;
|
|
11025
|
+
}
|
|
11026
|
+
if (currentSearchValue === undefined) {
|
|
11027
|
+
return;
|
|
11028
|
+
}
|
|
11029
|
+
if (currentSearchValue === lastWrittenRef.current) {
|
|
11030
|
+
return;
|
|
11031
|
+
}
|
|
11032
|
+
if (lastWrittenRef.current === undefined) {
|
|
11033
|
+
return;
|
|
11034
|
+
}
|
|
11035
|
+
requestAnimationFrame(() => {
|
|
11036
|
+
onExternalChangeRef.current?.();
|
|
11037
|
+
});
|
|
11038
|
+
}, [currentSearchValue, enabled]);
|
|
11039
|
+
const getUrlLengthWithSearchParam = react.useCallback((paramKey, paramValue, params) => {
|
|
11040
|
+
const otherParamsLength = sharedUtils.objectKeys(params)
|
|
11041
|
+
.filter(k => k !== paramKey)
|
|
11042
|
+
.reduce((totalLength, k) => {
|
|
11043
|
+
const kLen = encodeURIComponent(String(k)).length;
|
|
11044
|
+
const v = params[k];
|
|
11045
|
+
const vLen = v !== null && v !== undefined ? encodeURIComponent(String(v)).length : 0;
|
|
11046
|
+
return totalLength + kLen + vLen + (totalLength > 0 ? 2 : 1);
|
|
11047
|
+
}, 0);
|
|
11048
|
+
const urlBase = location.href.length - (location.searchStr.length || 0) + (location.hash.length || 0);
|
|
11049
|
+
return urlBase + otherParamsLength + 1 + paramKey.length + 1 + paramValue.length;
|
|
11050
|
+
}, [location]);
|
|
11051
|
+
const updateSearchParam = react.useCallback((encodedValue) => {
|
|
11052
|
+
if (!enabled) {
|
|
11053
|
+
return;
|
|
11054
|
+
}
|
|
11055
|
+
if (encodedValue !== undefined && encodedValue === lastWrittenRef.current) {
|
|
11056
|
+
return;
|
|
11057
|
+
}
|
|
11058
|
+
lastWrittenRef.current = encodedValue;
|
|
11059
|
+
if (currentSearchValue === encodedValue) {
|
|
11060
|
+
return;
|
|
11061
|
+
}
|
|
11062
|
+
requestAnimationFrame(() => {
|
|
11063
|
+
const shouldReplace = replaceOption ?? !Boolean(currentSearchValue);
|
|
11064
|
+
void navigate({
|
|
11065
|
+
to: ".",
|
|
11066
|
+
search: (prev) => {
|
|
11067
|
+
if (getUrlLengthWithSearchParam(key, encodedValue || "", prev) <= MAX_URL_LENGTH) {
|
|
11068
|
+
return { ...prev, [key]: encodedValue };
|
|
11069
|
+
}
|
|
11070
|
+
else {
|
|
11071
|
+
return { ...prev, [key]: undefined };
|
|
11072
|
+
}
|
|
11073
|
+
},
|
|
11074
|
+
hash: location.hash,
|
|
11075
|
+
replace: shouldReplace,
|
|
11076
|
+
});
|
|
11077
|
+
});
|
|
11078
|
+
}, [enabled, navigate, key, replaceOption, location.hash, getUrlLengthWithSearchParam, currentSearchValue]);
|
|
11079
|
+
return react.useMemo(() => ({ searchValue: currentSearchValue, updateSearchParam }), [currentSearchValue, updateSearchParam]);
|
|
11080
|
+
};
|
|
11081
|
+
|
|
11082
|
+
/**
|
|
11083
|
+
* Generates a localStorage key, optionally scoped to a specific user.
|
|
11084
|
+
*
|
|
11085
|
+
* When `userId` is provided the key is `"key-userId"`.
|
|
11086
|
+
* When omitted the key is returned as-is (unscoped).
|
|
11087
|
+
*
|
|
11088
|
+
* @param key - Unique persistence identifier.
|
|
11089
|
+
* @param userId - Client-side user id to scope storage per user.
|
|
11090
|
+
* @returns {string} The combined storage key.
|
|
11091
|
+
*/
|
|
11092
|
+
const useStorageKey = (key, userId) => {
|
|
11093
|
+
return react.useMemo(() => (userId ? `${key}-${userId}` : key), [key, userId]);
|
|
11094
|
+
};
|
|
11095
|
+
|
|
11096
|
+
/**
|
|
11097
|
+
* Generic persistence hook that loads state from URL search params (with
|
|
11098
|
+
* localStorage fallback) and writes changes to both.
|
|
11099
|
+
*
|
|
11100
|
+
* On mount the hook tries, in order:
|
|
11101
|
+
* 1. Decode the URL search param and pass it through `validate`.
|
|
11102
|
+
* 2. If that yields nothing, read localStorage and pass the parsed JSON through `validate`.
|
|
11103
|
+
*
|
|
11104
|
+
* `persistState` writes the state to localStorage (via `serialize`, default
|
|
11105
|
+
* `JSON.stringify`) and syncs to the URL (via `toUrlValue` + encoding).
|
|
11106
|
+
* Duplicate writes where the state has not changed (deep equality) are skipped.
|
|
11107
|
+
*
|
|
11108
|
+
* @param options - Configuration for the persisted state.
|
|
11109
|
+
* @param options.key - Unique identifier used for both the URL search param and the localStorage key.
|
|
11110
|
+
* @param options.validate - Called with the decoded/parsed value; must return `TState` or `undefined`.
|
|
11111
|
+
* @param options.serialize - Custom localStorage serialiser (default `storageSerializer.serialize`).
|
|
11112
|
+
* @param options.toUrlValue - Transform applied before URL encoding (default: encode state as-is).
|
|
11113
|
+
* @param options.fromUrlValue - Transform applied after URL decoding, before validation (default: identity).
|
|
11114
|
+
* @param options.enabled - When `false` the URL is neither read nor written (default `true`).
|
|
11115
|
+
* @param options.onExternalChange - Fired when the URL param changes externally (e.g. browser back).
|
|
11116
|
+
* @param options.replace - Forwarded to `useSearchParamSync`.
|
|
11117
|
+
* @param options.clientSideUserId - The user ID to use for the localStorage key.
|
|
11118
|
+
*/
|
|
11119
|
+
const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue, enabled = true, onExternalChange, replace, clientSideUserId, }) => {
|
|
11120
|
+
const { encode, decode } = useCustomEncoding();
|
|
11121
|
+
const { searchValue, updateSearchParam } = useSearchParamSync({
|
|
11122
|
+
key,
|
|
11123
|
+
enabled,
|
|
11124
|
+
onExternalChange,
|
|
11125
|
+
replace,
|
|
11126
|
+
});
|
|
11127
|
+
const storageKey = useStorageKey(key, clientSideUserId);
|
|
11128
|
+
const validateRef = react.useRef(validate);
|
|
11129
|
+
react.useEffect(() => {
|
|
11130
|
+
validateRef.current = validate;
|
|
11131
|
+
}, [validate]);
|
|
11132
|
+
const toUrlValueRef = react.useRef(toUrlValue);
|
|
11133
|
+
react.useEffect(() => {
|
|
11134
|
+
toUrlValueRef.current = toUrlValue;
|
|
11135
|
+
}, [toUrlValue]);
|
|
11136
|
+
const fromUrlValueRef = react.useRef(fromUrlValue);
|
|
11137
|
+
react.useEffect(() => {
|
|
11138
|
+
fromUrlValueRef.current = fromUrlValue;
|
|
11139
|
+
}, [fromUrlValue]);
|
|
11140
|
+
const serializeRef = react.useRef(serialize);
|
|
11141
|
+
react.useEffect(() => {
|
|
11142
|
+
serializeRef.current = serialize;
|
|
11143
|
+
}, [serialize]);
|
|
11144
|
+
const [initialState] = react.useState(() => {
|
|
11145
|
+
if (enabled && searchValue) {
|
|
11146
|
+
try {
|
|
11147
|
+
const decoded = decode(searchValue);
|
|
11148
|
+
const transformed = fromUrlValue ? fromUrlValue(decoded) : decoded;
|
|
11149
|
+
const validated = validate(transformed);
|
|
11150
|
+
if (validated !== undefined) {
|
|
11151
|
+
return validated;
|
|
11152
|
+
}
|
|
11153
|
+
}
|
|
11154
|
+
catch {
|
|
11155
|
+
// fall through to localStorage
|
|
11156
|
+
}
|
|
11157
|
+
}
|
|
11158
|
+
try {
|
|
11159
|
+
const raw = localStorage.getItem(storageKey);
|
|
11160
|
+
if (raw) {
|
|
11161
|
+
const parsed = storageSerializer.deserialize(raw);
|
|
11162
|
+
return validate(parsed);
|
|
11163
|
+
}
|
|
11164
|
+
}
|
|
11165
|
+
catch {
|
|
11166
|
+
// no valid stored state
|
|
11167
|
+
}
|
|
11168
|
+
return undefined;
|
|
11169
|
+
});
|
|
11170
|
+
const lastPersistedRef = react.useRef(initialState);
|
|
11171
|
+
const persistState = react.useCallback((state) => {
|
|
11172
|
+
if (dequal.dequal(lastPersistedRef.current, state)) {
|
|
11173
|
+
return;
|
|
11174
|
+
}
|
|
11175
|
+
lastPersistedRef.current = state;
|
|
11176
|
+
const serialized = serializeRef.current ? serializeRef.current(state) : storageSerializer.serialize(state);
|
|
11177
|
+
localStorage.setItem(storageKey, serialized);
|
|
11178
|
+
const urlValue = toUrlValueRef.current ? toUrlValueRef.current(state) : state;
|
|
11179
|
+
updateSearchParam(encode(urlValue));
|
|
11180
|
+
}, [storageKey, encode, updateSearchParam]);
|
|
11181
|
+
return react.useMemo(() => ({ initialState, persistState }), [initialState, persistState]);
|
|
11182
|
+
};
|
|
11183
|
+
|
|
10982
11184
|
const OVERSCAN = 10;
|
|
10983
11185
|
const DEFAULT_ROW_HEIGHT = 50;
|
|
10984
11186
|
/**
|
|
@@ -12154,6 +12356,7 @@ exports.KPICardSkeleton = KPICardSkeleton;
|
|
|
12154
12356
|
exports.KPISkeleton = KPISkeleton;
|
|
12155
12357
|
exports.List = List;
|
|
12156
12358
|
exports.ListItem = ListItem;
|
|
12359
|
+
exports.MAX_URL_LENGTH = MAX_URL_LENGTH;
|
|
12157
12360
|
exports.MenuDivider = MenuDivider;
|
|
12158
12361
|
exports.MenuItem = MenuItem;
|
|
12159
12362
|
exports.MenuList = MenuList;
|
|
@@ -12255,6 +12458,7 @@ exports.iconColorNames = iconColorNames;
|
|
|
12255
12458
|
exports.iconPalette = iconPalette;
|
|
12256
12459
|
exports.noPagination = noPagination;
|
|
12257
12460
|
exports.preferenceCardGrid = preferenceCardGrid;
|
|
12461
|
+
exports.storageSerializer = storageSerializer;
|
|
12258
12462
|
exports.useBidirectionalScroll = useBidirectionalScroll;
|
|
12259
12463
|
exports.useClickOutside = useClickOutside;
|
|
12260
12464
|
exports.useContainerBreakpoints = useContainerBreakpoints;
|
|
@@ -12282,6 +12486,7 @@ exports.useMergeRefs = useMergeRefs;
|
|
|
12282
12486
|
exports.useModifierKey = useModifierKey;
|
|
12283
12487
|
exports.useOverflowBorder = useOverflowBorder;
|
|
12284
12488
|
exports.useOverflowItems = useOverflowItems;
|
|
12489
|
+
exports.usePersistedState = usePersistedState;
|
|
12285
12490
|
exports.usePopoverContext = usePopoverContext;
|
|
12286
12491
|
exports.usePrevious = usePrevious;
|
|
12287
12492
|
exports.usePrompt = usePrompt;
|
|
@@ -12290,11 +12495,13 @@ exports.useRelayPagination = useRelayPagination;
|
|
|
12290
12495
|
exports.useResize = useResize;
|
|
12291
12496
|
exports.useScrollBlock = useScrollBlock;
|
|
12292
12497
|
exports.useScrollDetection = useScrollDetection;
|
|
12498
|
+
exports.useSearchParamSync = useSearchParamSync;
|
|
12293
12499
|
exports.useSelfUpdatingRef = useSelfUpdatingRef;
|
|
12294
12500
|
exports.useSessionStorage = useSessionStorage;
|
|
12295
12501
|
exports.useSessionStorageReducer = useSessionStorageReducer;
|
|
12296
12502
|
exports.useSheet = useSheet;
|
|
12297
12503
|
exports.useSheetSnap = useSheetSnap;
|
|
12504
|
+
exports.useStorageKey = useStorageKey;
|
|
12298
12505
|
exports.useTextSearch = useTextSearch;
|
|
12299
12506
|
exports.useTimeout = useTimeout;
|
|
12300
12507
|
exports.useViewportBreakpoints = useViewportBreakpoints;
|
package/index.esm.js
CHANGED
|
@@ -10,7 +10,7 @@ import IconSpriteSolid from '@trackunit/ui-icons/icons-sprite-solid.svg';
|
|
|
10
10
|
import { snakeCase, titleCase } from 'string-ts';
|
|
11
11
|
import { cvaMerge } from '@trackunit/css-class-variance-utilities';
|
|
12
12
|
import { Slot, Slottable } from '@radix-ui/react-slot';
|
|
13
|
-
import { Link, useBlocker,
|
|
13
|
+
import { Link, useBlocker, useNavigate, useLocation, useSearch } from '@tanstack/react-router';
|
|
14
14
|
import { isEqual, omit } from 'es-toolkit';
|
|
15
15
|
import { useFloating, offset, flip, shift, size, autoUpdate, useClick, useDismiss, useHover as useHover$1, safePolygon, useRole, useInteractions, FloatingPortal, useMergeRefs as useMergeRefs$1, FloatingFocusManager, arrow, useTransitionStatus, FloatingArrow } from '@floating-ui/react';
|
|
16
16
|
import { twMerge } from 'tailwind-merge';
|
|
@@ -18,8 +18,9 @@ import { useVirtualizer } from '@tanstack/react-virtual';
|
|
|
18
18
|
import { HelmetProvider, Helmet } from 'react-helmet-async';
|
|
19
19
|
import { Trigger, Content as Content$1, List as List$1, Root } from '@radix-ui/react-tabs';
|
|
20
20
|
import { gzipSync, gunzipSync } from 'fflate';
|
|
21
|
-
import { z } from 'zod';
|
|
22
21
|
import superjson from 'superjson';
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
import { dequal } from 'dequal';
|
|
23
24
|
|
|
24
25
|
const cvaIcon = cvaMerge(["aspect-square", "inline-grid", "relative", "shrink-0"], {
|
|
25
26
|
variants: {
|
|
@@ -7827,14 +7828,15 @@ const getSheetPanelStyle = ({ snapHeight, isOpen, closePhase, variant, autoHeigh
|
|
|
7827
7828
|
height: autoHeight ? "fit-content" : `var(--sheet-drag-height, ${snapHeight})`,
|
|
7828
7829
|
maxHeight: maxHeight ?? `calc(100cqh - ${FULL_HEIGHT_TOP_MARGIN_PX}px)`,
|
|
7829
7830
|
pointerEvents: "auto",
|
|
7830
|
-
transform: isOpen
|
|
7831
|
-
|
|
7831
|
+
transform: isOpen
|
|
7832
|
+
? "translateY(0) scale(var(--sheet-stack-scale, 1))"
|
|
7833
|
+
: "translateY(100%) scale(var(--sheet-stack-scale, 1))",
|
|
7834
|
+
transformOrigin: "bottom center",
|
|
7835
|
+
transition: isDragging
|
|
7832
7836
|
? "none"
|
|
7833
|
-
:
|
|
7834
|
-
?
|
|
7835
|
-
:
|
|
7836
|
-
? SHEET_TRANSFORM_TRANSITION
|
|
7837
|
-
: SHEET_OPEN_TRANSITION,
|
|
7837
|
+
: suppressTransition || autoHeight || !isOpen
|
|
7838
|
+
? SHEET_TRANSFORM_TRANSITION
|
|
7839
|
+
: SHEET_OPEN_TRANSITION,
|
|
7838
7840
|
};
|
|
7839
7841
|
};
|
|
7840
7842
|
|
|
@@ -9124,10 +9126,11 @@ const useSheetMotionOverflow = ({ panelEl, isDragging, scrollAreaEl, separatorEl
|
|
|
9124
9126
|
* CSS transitions on transform; the component stays mounted during the
|
|
9125
9127
|
* close animation and unmounts after the transition completes.
|
|
9126
9128
|
*/
|
|
9127
|
-
const Sheet = ({ isOpen, state, snap, onGeometryChange, onSnap, onClickHandle, onCloseGesture, floatingUi, ref, anchor = "center", snapping = true, resizable = true, variant = "default", trapFocus = true, container, dockedContent, className, "data-testid": dataTestId, onCloseComplete, entryAnimation = "subsequent", children, }) => {
|
|
9129
|
+
const Sheet = ({ isOpen, state, snap, onGeometryChange, onSnap, onClickHandle, onCloseGesture, floatingUi, ref, anchor = "center", snapping = true, resizable = true, variant = "default", trapFocus = true, container, dockedContent, persistentContent, className, "data-testid": dataTestId, onCloseComplete, entryAnimation = "subsequent", children, }) => {
|
|
9128
9130
|
const isFirstRender = useIsFirstRender();
|
|
9129
9131
|
const skipEntryAnimation = entryAnimation === "never" || (entryAnimation === "subsequent" && isFirstRender);
|
|
9130
9132
|
const effectiveSnapping = resizable && snapping;
|
|
9133
|
+
const dockingEnabled = dockedContent !== undefined || persistentContent !== undefined;
|
|
9131
9134
|
const [animState, animDispatch] = useReducer(sheetAnimationReducer, INITIAL_ANIMATION_STATE);
|
|
9132
9135
|
if (isOpen !== animState.prevIsOpen) {
|
|
9133
9136
|
animDispatch({
|
|
@@ -9139,7 +9142,7 @@ const Sheet = ({ isOpen, state, snap, onGeometryChange, onSnap, onClickHandle, o
|
|
|
9139
9142
|
const measurements = useSheetMeasurements({
|
|
9140
9143
|
shouldRender: animState.shouldRender,
|
|
9141
9144
|
state,
|
|
9142
|
-
dockingEnabled
|
|
9145
|
+
dockingEnabled,
|
|
9143
9146
|
snapping: effectiveSnapping,
|
|
9144
9147
|
externalRef: ref,
|
|
9145
9148
|
onGeometryChange,
|
|
@@ -9214,7 +9217,7 @@ const Sheet = ({ isOpen, state, snap, onGeometryChange, onSnap, onClickHandle, o
|
|
|
9214
9217
|
? `${state.effectiveDockedHeight}px`
|
|
9215
9218
|
: fitHeightMeasured
|
|
9216
9219
|
? `${cappedFitHeight}px`
|
|
9217
|
-
: getSnapLevelCssHeight(state.level, state.totalLevels,
|
|
9220
|
+
: getSnapLevelCssHeight(state.level, state.totalLevels, dockingEnabled, state.effectiveDockedHeight);
|
|
9218
9221
|
const fitMaxHeight = state.sizingMode === "fit" ? `calc(100cqh - ${FULL_HEIGHT_TOP_MARGIN_PX}px)` : undefined;
|
|
9219
9222
|
if (!animState.shouldRender)
|
|
9220
9223
|
return null;
|
|
@@ -9238,7 +9241,7 @@ const Sheet = ({ isOpen, state, snap, onGeometryChange, onSnap, onClickHandle, o
|
|
|
9238
9241
|
snapHeight: snapHeightCss,
|
|
9239
9242
|
suppressTransition: skipEntryAnimation && animState.visuallyOpen,
|
|
9240
9243
|
variant,
|
|
9241
|
-
}), children: [resizable ? (jsx(SheetHandle, { "data-testid": dataTestId !== undefined ? `${dataTestId}-handle` : undefined, isDragging: gestures.isDragging, onClick: handleClick, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, ...gestureHandlers })) : null, jsx("div", { className: "h-px shrink-0 bg-neutral-200 opacity-0 transition-opacity duration-200", ref: setSeparatorEl }),
|
|
9244
|
+
}), children: [resizable ? (jsx(SheetHandle, { "data-testid": dataTestId !== undefined ? `${dataTestId}-handle` : undefined, isDragging: gestures.isDragging, onClick: handleClick, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, ...gestureHandlers })) : null, jsx("div", { className: "h-px shrink-0 bg-neutral-200 opacity-0 transition-opacity duration-200", ref: setSeparatorEl }), jsxs("div", { className: cvaSheetScrollArea({ fillHeight: state.sizingMode !== "fit" }), "data-sheet-scroll-area": true, ref: setScrollAreaEl, children: [persistentContent, state.sizingMode === "docked" ? dockedContent : children] })] }));
|
|
9242
9245
|
return (jsx(Portal, { root: container, children: jsxs("div", { className: cvaSheetContainer({
|
|
9243
9246
|
docked: state.sizingMode === "docked",
|
|
9244
9247
|
}), "data-testid": dataTestId !== undefined ? `${dataTestId}-container` : undefined, ref: containerRef, children: [jsx(SheetOverlay, { "data-testid": dataTestId !== undefined ? `${dataTestId}-overlay` : undefined, visible: showOverlay }), shouldTrapFocus === true ? (jsx(FloatingFocusManager, { context: floatingUi.context, children: panel })) : (panel)] }) }));
|
|
@@ -10314,8 +10317,11 @@ const useCustomEncoding = () => {
|
|
|
10314
10317
|
// If it's already a string, use it directly; otherwise stringify the object
|
|
10315
10318
|
const json = typeof input === "string" ? input : JSON.stringify(input);
|
|
10316
10319
|
const textInput = new TextEncoder().encode(json);
|
|
10317
|
-
// Use fflate for synchronous gzip compression
|
|
10318
|
-
|
|
10320
|
+
// Use fflate for synchronous gzip compression.
|
|
10321
|
+
// mtime: 0 ensures deterministic output — without it the gzip header
|
|
10322
|
+
// includes the current timestamp, making the same input produce a
|
|
10323
|
+
// different encoded string on every call.
|
|
10324
|
+
const compressed = gzipSync(textInput, { mtime: 0 });
|
|
10319
10325
|
return b64urlEncode(compressed);
|
|
10320
10326
|
}
|
|
10321
10327
|
catch (_) {
|
|
@@ -10367,6 +10373,36 @@ const useCustomEncoding = () => {
|
|
|
10367
10373
|
return useMemo(() => ({ encode, decode }), [encode, decode]);
|
|
10368
10374
|
};
|
|
10369
10375
|
|
|
10376
|
+
/**
|
|
10377
|
+
* Internal envelope used to tag superjson-serialized data in web storage.
|
|
10378
|
+
*
|
|
10379
|
+
* `__serializer` is a **reserved internal key** — consumer state objects must
|
|
10380
|
+
* not include it as a top-level key. The double-underscore prefix is a
|
|
10381
|
+
* deliberate signal that this is a private implementation detail.
|
|
10382
|
+
*
|
|
10383
|
+
* A runtime warning is emitted (via `writeToStorage`) if reserved keys are
|
|
10384
|
+
* detected in the value being written.
|
|
10385
|
+
*/
|
|
10386
|
+
const taggedSuperjsonEnvelopeSchema = z.object({
|
|
10387
|
+
__serializer: z.literal("superjson"),
|
|
10388
|
+
json: z.custom(),
|
|
10389
|
+
meta: z.custom().optional(),
|
|
10390
|
+
});
|
|
10391
|
+
const storageSerializer = {
|
|
10392
|
+
serialize: (value) => {
|
|
10393
|
+
const serialized = superjson.serialize(value);
|
|
10394
|
+
return JSON.stringify({ __serializer: "superjson", ...serialized });
|
|
10395
|
+
},
|
|
10396
|
+
deserialize: (value) => {
|
|
10397
|
+
const parsed = JSON.parse(value);
|
|
10398
|
+
const result = taggedSuperjsonEnvelopeSchema.safeParse(parsed);
|
|
10399
|
+
if (result.success) {
|
|
10400
|
+
return superjson.deserialize({ json: result.data.json, meta: result.data.meta });
|
|
10401
|
+
}
|
|
10402
|
+
return parsed;
|
|
10403
|
+
},
|
|
10404
|
+
};
|
|
10405
|
+
|
|
10370
10406
|
/**
|
|
10371
10407
|
* Runs a sequential migration pipeline on the provided data, applying
|
|
10372
10408
|
* each migration whose version is in the range (fromVersion, toVersion].
|
|
@@ -10472,36 +10508,6 @@ const salvageState = (schema, rawData, defaultState) => {
|
|
|
10472
10508
|
}
|
|
10473
10509
|
};
|
|
10474
10510
|
|
|
10475
|
-
/**
|
|
10476
|
-
* Internal envelope used to tag superjson-serialized data in web storage.
|
|
10477
|
-
*
|
|
10478
|
-
* `__serializer` is a **reserved internal key** — consumer state objects must
|
|
10479
|
-
* not include it as a top-level key. The double-underscore prefix is a
|
|
10480
|
-
* deliberate signal that this is a private implementation detail.
|
|
10481
|
-
*
|
|
10482
|
-
* A runtime warning is emitted (via `writeToStorage`) if reserved keys are
|
|
10483
|
-
* detected in the value being written.
|
|
10484
|
-
*/
|
|
10485
|
-
const taggedSuperjsonEnvelopeSchema = z.object({
|
|
10486
|
-
__serializer: z.literal("superjson"),
|
|
10487
|
-
json: z.custom(),
|
|
10488
|
-
meta: z.custom().optional(),
|
|
10489
|
-
});
|
|
10490
|
-
const storageSerializer = {
|
|
10491
|
-
serialize: (value) => {
|
|
10492
|
-
const serialized = superjson.serialize(value);
|
|
10493
|
-
return JSON.stringify({ __serializer: "superjson", ...serialized });
|
|
10494
|
-
},
|
|
10495
|
-
deserialize: (value) => {
|
|
10496
|
-
const parsed = JSON.parse(value);
|
|
10497
|
-
const result = taggedSuperjsonEnvelopeSchema.safeParse(parsed);
|
|
10498
|
-
if (result.success) {
|
|
10499
|
-
return superjson.deserialize({ json: result.data.json, meta: result.data.meta });
|
|
10500
|
-
}
|
|
10501
|
-
return parsed;
|
|
10502
|
-
},
|
|
10503
|
-
};
|
|
10504
|
-
|
|
10505
10511
|
/**
|
|
10506
10512
|
* Internal envelope that pairs stored data with a schema version number.
|
|
10507
10513
|
*
|
|
@@ -10977,6 +10983,202 @@ const useSessionStorage = (options) => useWebStorage(globalThis.sessionStorage,
|
|
|
10977
10983
|
*/
|
|
10978
10984
|
const useSessionStorageReducer = (options) => useWebStorageReducer(globalThis.sessionStorage, options);
|
|
10979
10985
|
|
|
10986
|
+
const MAX_URL_LENGTH = 5000;
|
|
10987
|
+
/**
|
|
10988
|
+
* Syncs an encoded string value with a URL search parameter via Tanstack Router.
|
|
10989
|
+
*
|
|
10990
|
+
* Provides a write function that updates the URL through the application
|
|
10991
|
+
* router (preserving other search params), guards against exceeding
|
|
10992
|
+
* {@link MAX_URL_LENGTH}, and detects external URL changes (browser
|
|
10993
|
+
* back/forward, shared links) via an optional callback.
|
|
10994
|
+
*
|
|
10995
|
+
* @param options - Configuration for the search param sync.
|
|
10996
|
+
* @param options.key - The URL search parameter name.
|
|
10997
|
+
* @param options.enabled - Set to `false` to disable all URL interaction (default `true`).
|
|
10998
|
+
* @param options.onExternalChange - Called when the param changes externally.
|
|
10999
|
+
* @param options.replace - `true` to always use `replaceState`.
|
|
11000
|
+
*/
|
|
11001
|
+
const useSearchParamSync = ({ key, enabled = true, onExternalChange, replace: replaceOption, }) => {
|
|
11002
|
+
const navigate = useNavigate();
|
|
11003
|
+
const location = useLocation();
|
|
11004
|
+
const search = useSearch({ strict: false, shouldThrow: false });
|
|
11005
|
+
const lastWrittenRef = useRef(undefined);
|
|
11006
|
+
const onExternalChangeRef = useRef(onExternalChange);
|
|
11007
|
+
useEffect(() => {
|
|
11008
|
+
onExternalChangeRef.current = onExternalChange;
|
|
11009
|
+
}, [onExternalChange]);
|
|
11010
|
+
const currentSearchValue = useMemo(() => {
|
|
11011
|
+
if (!enabled) {
|
|
11012
|
+
return undefined;
|
|
11013
|
+
}
|
|
11014
|
+
const value = search?.[key];
|
|
11015
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
11016
|
+
return String(value);
|
|
11017
|
+
}
|
|
11018
|
+
return undefined;
|
|
11019
|
+
}, [enabled, search, key]);
|
|
11020
|
+
useEffect(() => {
|
|
11021
|
+
if (!enabled) {
|
|
11022
|
+
return;
|
|
11023
|
+
}
|
|
11024
|
+
if (currentSearchValue === undefined) {
|
|
11025
|
+
return;
|
|
11026
|
+
}
|
|
11027
|
+
if (currentSearchValue === lastWrittenRef.current) {
|
|
11028
|
+
return;
|
|
11029
|
+
}
|
|
11030
|
+
if (lastWrittenRef.current === undefined) {
|
|
11031
|
+
return;
|
|
11032
|
+
}
|
|
11033
|
+
requestAnimationFrame(() => {
|
|
11034
|
+
onExternalChangeRef.current?.();
|
|
11035
|
+
});
|
|
11036
|
+
}, [currentSearchValue, enabled]);
|
|
11037
|
+
const getUrlLengthWithSearchParam = useCallback((paramKey, paramValue, params) => {
|
|
11038
|
+
const otherParamsLength = objectKeys(params)
|
|
11039
|
+
.filter(k => k !== paramKey)
|
|
11040
|
+
.reduce((totalLength, k) => {
|
|
11041
|
+
const kLen = encodeURIComponent(String(k)).length;
|
|
11042
|
+
const v = params[k];
|
|
11043
|
+
const vLen = v !== null && v !== undefined ? encodeURIComponent(String(v)).length : 0;
|
|
11044
|
+
return totalLength + kLen + vLen + (totalLength > 0 ? 2 : 1);
|
|
11045
|
+
}, 0);
|
|
11046
|
+
const urlBase = location.href.length - (location.searchStr.length || 0) + (location.hash.length || 0);
|
|
11047
|
+
return urlBase + otherParamsLength + 1 + paramKey.length + 1 + paramValue.length;
|
|
11048
|
+
}, [location]);
|
|
11049
|
+
const updateSearchParam = useCallback((encodedValue) => {
|
|
11050
|
+
if (!enabled) {
|
|
11051
|
+
return;
|
|
11052
|
+
}
|
|
11053
|
+
if (encodedValue !== undefined && encodedValue === lastWrittenRef.current) {
|
|
11054
|
+
return;
|
|
11055
|
+
}
|
|
11056
|
+
lastWrittenRef.current = encodedValue;
|
|
11057
|
+
if (currentSearchValue === encodedValue) {
|
|
11058
|
+
return;
|
|
11059
|
+
}
|
|
11060
|
+
requestAnimationFrame(() => {
|
|
11061
|
+
const shouldReplace = replaceOption ?? !Boolean(currentSearchValue);
|
|
11062
|
+
void navigate({
|
|
11063
|
+
to: ".",
|
|
11064
|
+
search: (prev) => {
|
|
11065
|
+
if (getUrlLengthWithSearchParam(key, encodedValue || "", prev) <= MAX_URL_LENGTH) {
|
|
11066
|
+
return { ...prev, [key]: encodedValue };
|
|
11067
|
+
}
|
|
11068
|
+
else {
|
|
11069
|
+
return { ...prev, [key]: undefined };
|
|
11070
|
+
}
|
|
11071
|
+
},
|
|
11072
|
+
hash: location.hash,
|
|
11073
|
+
replace: shouldReplace,
|
|
11074
|
+
});
|
|
11075
|
+
});
|
|
11076
|
+
}, [enabled, navigate, key, replaceOption, location.hash, getUrlLengthWithSearchParam, currentSearchValue]);
|
|
11077
|
+
return useMemo(() => ({ searchValue: currentSearchValue, updateSearchParam }), [currentSearchValue, updateSearchParam]);
|
|
11078
|
+
};
|
|
11079
|
+
|
|
11080
|
+
/**
|
|
11081
|
+
* Generates a localStorage key, optionally scoped to a specific user.
|
|
11082
|
+
*
|
|
11083
|
+
* When `userId` is provided the key is `"key-userId"`.
|
|
11084
|
+
* When omitted the key is returned as-is (unscoped).
|
|
11085
|
+
*
|
|
11086
|
+
* @param key - Unique persistence identifier.
|
|
11087
|
+
* @param userId - Client-side user id to scope storage per user.
|
|
11088
|
+
* @returns {string} The combined storage key.
|
|
11089
|
+
*/
|
|
11090
|
+
const useStorageKey = (key, userId) => {
|
|
11091
|
+
return useMemo(() => (userId ? `${key}-${userId}` : key), [key, userId]);
|
|
11092
|
+
};
|
|
11093
|
+
|
|
11094
|
+
/**
|
|
11095
|
+
* Generic persistence hook that loads state from URL search params (with
|
|
11096
|
+
* localStorage fallback) and writes changes to both.
|
|
11097
|
+
*
|
|
11098
|
+
* On mount the hook tries, in order:
|
|
11099
|
+
* 1. Decode the URL search param and pass it through `validate`.
|
|
11100
|
+
* 2. If that yields nothing, read localStorage and pass the parsed JSON through `validate`.
|
|
11101
|
+
*
|
|
11102
|
+
* `persistState` writes the state to localStorage (via `serialize`, default
|
|
11103
|
+
* `JSON.stringify`) and syncs to the URL (via `toUrlValue` + encoding).
|
|
11104
|
+
* Duplicate writes where the state has not changed (deep equality) are skipped.
|
|
11105
|
+
*
|
|
11106
|
+
* @param options - Configuration for the persisted state.
|
|
11107
|
+
* @param options.key - Unique identifier used for both the URL search param and the localStorage key.
|
|
11108
|
+
* @param options.validate - Called with the decoded/parsed value; must return `TState` or `undefined`.
|
|
11109
|
+
* @param options.serialize - Custom localStorage serialiser (default `storageSerializer.serialize`).
|
|
11110
|
+
* @param options.toUrlValue - Transform applied before URL encoding (default: encode state as-is).
|
|
11111
|
+
* @param options.fromUrlValue - Transform applied after URL decoding, before validation (default: identity).
|
|
11112
|
+
* @param options.enabled - When `false` the URL is neither read nor written (default `true`).
|
|
11113
|
+
* @param options.onExternalChange - Fired when the URL param changes externally (e.g. browser back).
|
|
11114
|
+
* @param options.replace - Forwarded to `useSearchParamSync`.
|
|
11115
|
+
* @param options.clientSideUserId - The user ID to use for the localStorage key.
|
|
11116
|
+
*/
|
|
11117
|
+
const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue, enabled = true, onExternalChange, replace, clientSideUserId, }) => {
|
|
11118
|
+
const { encode, decode } = useCustomEncoding();
|
|
11119
|
+
const { searchValue, updateSearchParam } = useSearchParamSync({
|
|
11120
|
+
key,
|
|
11121
|
+
enabled,
|
|
11122
|
+
onExternalChange,
|
|
11123
|
+
replace,
|
|
11124
|
+
});
|
|
11125
|
+
const storageKey = useStorageKey(key, clientSideUserId);
|
|
11126
|
+
const validateRef = useRef(validate);
|
|
11127
|
+
useEffect(() => {
|
|
11128
|
+
validateRef.current = validate;
|
|
11129
|
+
}, [validate]);
|
|
11130
|
+
const toUrlValueRef = useRef(toUrlValue);
|
|
11131
|
+
useEffect(() => {
|
|
11132
|
+
toUrlValueRef.current = toUrlValue;
|
|
11133
|
+
}, [toUrlValue]);
|
|
11134
|
+
const fromUrlValueRef = useRef(fromUrlValue);
|
|
11135
|
+
useEffect(() => {
|
|
11136
|
+
fromUrlValueRef.current = fromUrlValue;
|
|
11137
|
+
}, [fromUrlValue]);
|
|
11138
|
+
const serializeRef = useRef(serialize);
|
|
11139
|
+
useEffect(() => {
|
|
11140
|
+
serializeRef.current = serialize;
|
|
11141
|
+
}, [serialize]);
|
|
11142
|
+
const [initialState] = useState(() => {
|
|
11143
|
+
if (enabled && searchValue) {
|
|
11144
|
+
try {
|
|
11145
|
+
const decoded = decode(searchValue);
|
|
11146
|
+
const transformed = fromUrlValue ? fromUrlValue(decoded) : decoded;
|
|
11147
|
+
const validated = validate(transformed);
|
|
11148
|
+
if (validated !== undefined) {
|
|
11149
|
+
return validated;
|
|
11150
|
+
}
|
|
11151
|
+
}
|
|
11152
|
+
catch {
|
|
11153
|
+
// fall through to localStorage
|
|
11154
|
+
}
|
|
11155
|
+
}
|
|
11156
|
+
try {
|
|
11157
|
+
const raw = localStorage.getItem(storageKey);
|
|
11158
|
+
if (raw) {
|
|
11159
|
+
const parsed = storageSerializer.deserialize(raw);
|
|
11160
|
+
return validate(parsed);
|
|
11161
|
+
}
|
|
11162
|
+
}
|
|
11163
|
+
catch {
|
|
11164
|
+
// no valid stored state
|
|
11165
|
+
}
|
|
11166
|
+
return undefined;
|
|
11167
|
+
});
|
|
11168
|
+
const lastPersistedRef = useRef(initialState);
|
|
11169
|
+
const persistState = useCallback((state) => {
|
|
11170
|
+
if (dequal(lastPersistedRef.current, state)) {
|
|
11171
|
+
return;
|
|
11172
|
+
}
|
|
11173
|
+
lastPersistedRef.current = state;
|
|
11174
|
+
const serialized = serializeRef.current ? serializeRef.current(state) : storageSerializer.serialize(state);
|
|
11175
|
+
localStorage.setItem(storageKey, serialized);
|
|
11176
|
+
const urlValue = toUrlValueRef.current ? toUrlValueRef.current(state) : state;
|
|
11177
|
+
updateSearchParam(encode(urlValue));
|
|
11178
|
+
}, [storageKey, encode, updateSearchParam]);
|
|
11179
|
+
return useMemo(() => ({ initialState, persistState }), [initialState, persistState]);
|
|
11180
|
+
};
|
|
11181
|
+
|
|
10980
11182
|
const OVERSCAN = 10;
|
|
10981
11183
|
const DEFAULT_ROW_HEIGHT = 50;
|
|
10982
11184
|
/**
|
|
@@ -12121,4 +12323,4 @@ const useWindowActivity = ({ onFocus, onBlur, skip = false } = { onBlur: undefin
|
|
|
12121
12323
|
return useMemo(() => ({ focused }), [focused]);
|
|
12122
12324
|
};
|
|
12123
12325
|
|
|
12124
|
-
export { ActionRenderer, Alert, Badge, Breadcrumb, BreadcrumbContainer, Button, Card, CardBody, CardFooter, CardHeader, Collapse, CompletionStatusIndicator, CopyableText, DEFAULT_SKELETON_PREFERENCE_CARD_PROPS, DetailsList, EmptyState, EmptyValue, ExternalLink, GridAreas, Heading, Highlight, HorizontalOverflowScroller, Icon, IconButton, Indicator, KPI, KPICard, KPICardSkeleton, KPISkeleton, List, ListItem, MenuDivider, MenuItem, MenuList, MoreMenu, Notice, PackageNameStoryComponent, Page, PageContent, PageHeader, PageHeaderKpiMetrics, PageHeaderSecondaryActions, PageHeaderTitle, Pagination, Polygon, Popover, PopoverContent, PopoverTitle, PopoverTrigger, Portal, PreferenceCard, PreferenceCardSkeleton, Prompt, ROLE_CARD, SHEET_TRANSITION_DURATION, SHEET_TRANSITION_DURATION_MS, SHEET_TRANSITION_EASING, SectionHeader, SegmentedValueBar, Sheet, Sidebar, SkeletonBlock, SkeletonLabel, SkeletonLines, Spacer, Spinner, StarButton, Tab, TabContent, TabList, Tabs, Tag, Text, ToggleGroup, Tooltip, TrendIndicator, TrendIndicators, ValueBar, ZStack, createGrid, cvaButton, cvaButtonPrefixSuffix, cvaButtonSpinner, cvaButtonSpinnerContainer, cvaClickable, cvaContainerStyles, cvaContentContainer, cvaContentWrapper, cvaDescriptionCard, cvaIconBackground, cvaIconButton, cvaImgStyles, cvaIndicator, cvaIndicatorIcon, cvaIndicatorIconBackground, cvaIndicatorLabel, cvaIndicatorPing, cvaInputContainer, cvaInteractableItem, cvaList, cvaListContainer, cvaListItem$1 as cvaListItem, cvaMenu, cvaMenuItem, cvaMenuItemLabel, cvaMenuItemPrefix, cvaMenuItemStyle, cvaMenuItemSuffix, cvaMenuList, cvaMenuListDivider, cvaMenuListItem, cvaMenuListMultiSelect, cvaPageHeader, cvaPageHeaderContainer, cvaPageHeaderHeading, cvaPreferenceCard, cvaTitleCard, cvaToggleGroup, cvaToggleGroupWithSlidingBackground, cvaToggleItem, cvaToggleItemContent, cvaToggleItemText, cvaZStackContainer, cvaZStackItem, defaultPageSize, docs, getDevicePixelRatio, getValueBarColorByValue, iconColorNames, iconPalette, noPagination, preferenceCardGrid, useBidirectionalScroll, useClickOutside, useContainerBreakpoints, useContinuousTimeout, useCopyToClipboard, useCursorUrlSync, useCustomEncoding, useDebounce, useDevicePixelRatio, useElevatedReducer, useElevatedState, useGridAreas, useHover, useInfiniteScroll, useIsFirstRender, useIsFullscreen, useIsTextTruncated, useKeyboardShortcut, useList, useListItemHeight, useLocalStorage, useLocalStorageReducer, useMeasure, useMergeRefs, useModifierKey, useOverflowBorder, useOverflowItems, usePopoverContext, usePrevious, usePrompt, useRandomCSSLengths, useRelayPagination, useResize, useScrollBlock, useScrollDetection, useSelfUpdatingRef, useSessionStorage, useSessionStorageReducer, useSheet, useSheetSnap, useTextSearch, useTimeout, useViewportBreakpoints, useWatch, useWindowActivity };
|
|
12326
|
+
export { ActionRenderer, Alert, Badge, Breadcrumb, BreadcrumbContainer, Button, Card, CardBody, CardFooter, CardHeader, Collapse, CompletionStatusIndicator, CopyableText, DEFAULT_SKELETON_PREFERENCE_CARD_PROPS, DetailsList, EmptyState, EmptyValue, ExternalLink, GridAreas, Heading, Highlight, HorizontalOverflowScroller, Icon, IconButton, Indicator, KPI, KPICard, KPICardSkeleton, KPISkeleton, List, ListItem, MAX_URL_LENGTH, MenuDivider, MenuItem, MenuList, MoreMenu, Notice, PackageNameStoryComponent, Page, PageContent, PageHeader, PageHeaderKpiMetrics, PageHeaderSecondaryActions, PageHeaderTitle, Pagination, Polygon, Popover, PopoverContent, PopoverTitle, PopoverTrigger, Portal, PreferenceCard, PreferenceCardSkeleton, Prompt, ROLE_CARD, SHEET_TRANSITION_DURATION, SHEET_TRANSITION_DURATION_MS, SHEET_TRANSITION_EASING, SectionHeader, SegmentedValueBar, Sheet, Sidebar, SkeletonBlock, SkeletonLabel, SkeletonLines, Spacer, Spinner, StarButton, Tab, TabContent, TabList, Tabs, Tag, Text, ToggleGroup, Tooltip, TrendIndicator, TrendIndicators, ValueBar, ZStack, createGrid, cvaButton, cvaButtonPrefixSuffix, cvaButtonSpinner, cvaButtonSpinnerContainer, cvaClickable, cvaContainerStyles, cvaContentContainer, cvaContentWrapper, cvaDescriptionCard, cvaIconBackground, cvaIconButton, cvaImgStyles, cvaIndicator, cvaIndicatorIcon, cvaIndicatorIconBackground, cvaIndicatorLabel, cvaIndicatorPing, cvaInputContainer, cvaInteractableItem, cvaList, cvaListContainer, cvaListItem$1 as cvaListItem, cvaMenu, cvaMenuItem, cvaMenuItemLabel, cvaMenuItemPrefix, cvaMenuItemStyle, cvaMenuItemSuffix, cvaMenuList, cvaMenuListDivider, cvaMenuListItem, cvaMenuListMultiSelect, cvaPageHeader, cvaPageHeaderContainer, cvaPageHeaderHeading, cvaPreferenceCard, cvaTitleCard, cvaToggleGroup, cvaToggleGroupWithSlidingBackground, cvaToggleItem, cvaToggleItemContent, cvaToggleItemText, cvaZStackContainer, cvaZStackItem, defaultPageSize, docs, getDevicePixelRatio, getValueBarColorByValue, iconColorNames, iconPalette, noPagination, preferenceCardGrid, storageSerializer, useBidirectionalScroll, useClickOutside, useContainerBreakpoints, useContinuousTimeout, useCopyToClipboard, useCursorUrlSync, useCustomEncoding, useDebounce, useDevicePixelRatio, useElevatedReducer, useElevatedState, useGridAreas, useHover, useInfiniteScroll, useIsFirstRender, useIsFullscreen, useIsTextTruncated, useKeyboardShortcut, useList, useListItemHeight, useLocalStorage, useLocalStorageReducer, useMeasure, useMergeRefs, useModifierKey, useOverflowBorder, useOverflowItems, usePersistedState, usePopoverContext, usePrevious, usePrompt, useRandomCSSLengths, useRelayPagination, useResize, useScrollBlock, useScrollDetection, useSearchParamSync, useSelfUpdatingRef, useSessionStorage, useSessionStorageReducer, useSheet, useSheetSnap, useStorageKey, useTextSearch, useTimeout, useViewportBreakpoints, useWatch, useWindowActivity };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-components",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.18",
|
|
4
4
|
"repository": "https://github.com/Trackunit/manager",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"engines": {
|
|
@@ -19,9 +19,10 @@
|
|
|
19
19
|
"@trackunit/ui-icons": "1.11.93",
|
|
20
20
|
"es-toolkit": "^1.39.10",
|
|
21
21
|
"@tanstack/react-virtual": "3.13.12",
|
|
22
|
+
"dequal": "^2.0.3",
|
|
22
23
|
"fflate": "^0.8.2",
|
|
23
24
|
"superjson": "^2.2.6",
|
|
24
|
-
"zod": "^3.
|
|
25
|
+
"zod": "^3.25.76"
|
|
25
26
|
},
|
|
26
27
|
"peerDependencies": {
|
|
27
28
|
"react": "^19.0.0",
|
|
@@ -26,4 +26,4 @@ import type { SheetProps } from "./sheet-types";
|
|
|
26
26
|
* CSS transitions on transform; the component stays mounted during the
|
|
27
27
|
* close animation and unmounts after the transition completes.
|
|
28
28
|
*/
|
|
29
|
-
export declare const Sheet: ({ isOpen, state, snap, onGeometryChange, onSnap, onClickHandle, onCloseGesture, floatingUi, ref, anchor, snapping, resizable, variant, trapFocus, container, dockedContent, className, "data-testid": dataTestId, onCloseComplete, entryAnimation, children, }: SheetProps) => ReactElement | null;
|
|
29
|
+
export declare const Sheet: ({ isOpen, state, snap, onGeometryChange, onSnap, onClickHandle, onCloseGesture, floatingUi, ref, anchor, snapping, resizable, variant, trapFocus, container, dockedContent, persistentContent, className, "data-testid": dataTestId, onCloseComplete, entryAnimation, children, }: SheetProps) => ReactElement | null;
|
|
@@ -187,6 +187,15 @@ export type SheetProps = {
|
|
|
187
187
|
readonly container: HTMLElement | null;
|
|
188
188
|
/** Content rendered when in docked mode. Presence enables docking behavior. */
|
|
189
189
|
readonly dockedContent?: ReactNode;
|
|
190
|
+
/**
|
|
191
|
+
* Content rendered above `dockedContent`/`children` in every sizing mode.
|
|
192
|
+
* Use this when the same subtree (e.g. a filter bar with an input) must
|
|
193
|
+
* stay mounted across snap transitions so its DOM — and any focused
|
|
194
|
+
* element inside it — is preserved. Presence alone also enables docking
|
|
195
|
+
* behavior even when `dockedContent` is omitted; in that case the docked
|
|
196
|
+
* mode shows only `persistentContent`.
|
|
197
|
+
*/
|
|
198
|
+
readonly persistentContent?: ReactNode;
|
|
190
199
|
/** Custom class name. */
|
|
191
200
|
readonly className?: string;
|
|
192
201
|
/** Test ID for the sheet. */
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
type UsePersistedStateOptions<TState> = {
|
|
2
|
+
readonly key: string;
|
|
3
|
+
readonly validate: (raw: unknown) => TState | undefined;
|
|
4
|
+
readonly serialize?: (state: TState) => string;
|
|
5
|
+
readonly toUrlValue?: (state: TState) => string | object | Array<object>;
|
|
6
|
+
readonly fromUrlValue?: (decoded: unknown) => unknown;
|
|
7
|
+
readonly enabled?: boolean;
|
|
8
|
+
readonly onExternalChange?: () => void;
|
|
9
|
+
readonly replace?: boolean;
|
|
10
|
+
readonly clientSideUserId?: string;
|
|
11
|
+
};
|
|
12
|
+
type UsePersistedStateReturn<TState> = {
|
|
13
|
+
readonly initialState: TState | undefined;
|
|
14
|
+
readonly persistState: (state: TState) => void;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Generic persistence hook that loads state from URL search params (with
|
|
18
|
+
* localStorage fallback) and writes changes to both.
|
|
19
|
+
*
|
|
20
|
+
* On mount the hook tries, in order:
|
|
21
|
+
* 1. Decode the URL search param and pass it through `validate`.
|
|
22
|
+
* 2. If that yields nothing, read localStorage and pass the parsed JSON through `validate`.
|
|
23
|
+
*
|
|
24
|
+
* `persistState` writes the state to localStorage (via `serialize`, default
|
|
25
|
+
* `JSON.stringify`) and syncs to the URL (via `toUrlValue` + encoding).
|
|
26
|
+
* Duplicate writes where the state has not changed (deep equality) are skipped.
|
|
27
|
+
*
|
|
28
|
+
* @param options - Configuration for the persisted state.
|
|
29
|
+
* @param options.key - Unique identifier used for both the URL search param and the localStorage key.
|
|
30
|
+
* @param options.validate - Called with the decoded/parsed value; must return `TState` or `undefined`.
|
|
31
|
+
* @param options.serialize - Custom localStorage serialiser (default `storageSerializer.serialize`).
|
|
32
|
+
* @param options.toUrlValue - Transform applied before URL encoding (default: encode state as-is).
|
|
33
|
+
* @param options.fromUrlValue - Transform applied after URL decoding, before validation (default: identity).
|
|
34
|
+
* @param options.enabled - When `false` the URL is neither read nor written (default `true`).
|
|
35
|
+
* @param options.onExternalChange - Fired when the URL param changes externally (e.g. browser back).
|
|
36
|
+
* @param options.replace - Forwarded to `useSearchParamSync`.
|
|
37
|
+
* @param options.clientSideUserId - The user ID to use for the localStorage key.
|
|
38
|
+
*/
|
|
39
|
+
export declare const usePersistedState: <TState extends object>({ key, validate, serialize, toUrlValue, fromUrlValue, enabled, onExternalChange, replace, clientSideUserId, }: UsePersistedStateOptions<TState>) => UsePersistedStateReturn<TState>;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare const MAX_URL_LENGTH = 5000;
|
|
2
|
+
type UseSearchParamSyncOptions = {
|
|
3
|
+
readonly key: string;
|
|
4
|
+
readonly enabled?: boolean;
|
|
5
|
+
readonly onExternalChange?: () => void;
|
|
6
|
+
readonly replace?: boolean;
|
|
7
|
+
};
|
|
8
|
+
type UseSearchParamSyncReturn = {
|
|
9
|
+
readonly searchValue: string | undefined;
|
|
10
|
+
readonly updateSearchParam: (encodedValue: string | undefined) => void;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Syncs an encoded string value with a URL search parameter via Tanstack Router.
|
|
14
|
+
*
|
|
15
|
+
* Provides a write function that updates the URL through the application
|
|
16
|
+
* router (preserving other search params), guards against exceeding
|
|
17
|
+
* {@link MAX_URL_LENGTH}, and detects external URL changes (browser
|
|
18
|
+
* back/forward, shared links) via an optional callback.
|
|
19
|
+
*
|
|
20
|
+
* @param options - Configuration for the search param sync.
|
|
21
|
+
* @param options.key - The URL search parameter name.
|
|
22
|
+
* @param options.enabled - Set to `false` to disable all URL interaction (default `true`).
|
|
23
|
+
* @param options.onExternalChange - Called when the param changes externally.
|
|
24
|
+
* @param options.replace - `true` to always use `replaceState`.
|
|
25
|
+
*/
|
|
26
|
+
export declare const useSearchParamSync: ({ key, enabled, onExternalChange, replace: replaceOption, }: UseSearchParamSyncOptions) => UseSearchParamSyncReturn;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a localStorage key, optionally scoped to a specific user.
|
|
3
|
+
*
|
|
4
|
+
* When `userId` is provided the key is `"key-userId"`.
|
|
5
|
+
* When omitted the key is returned as-is (unscoped).
|
|
6
|
+
*
|
|
7
|
+
* @param key - Unique persistence identifier.
|
|
8
|
+
* @param userId - Client-side user id to scope storage per user.
|
|
9
|
+
* @returns {string} The combined storage key.
|
|
10
|
+
*/
|
|
11
|
+
export declare const useStorageKey: (key: string, userId?: string) => string;
|
package/src/index.d.ts
CHANGED
|
@@ -110,12 +110,16 @@ export * from "./components/ValueBar/ValueBarTypes";
|
|
|
110
110
|
export * from "./components/ZStack/ZStack";
|
|
111
111
|
export * from "./components/ZStack/ZStack.variants";
|
|
112
112
|
export * from "./hooks/encoding/useCustomEncoding";
|
|
113
|
+
export * from "./hooks/localStorage/storageSerializer";
|
|
113
114
|
export * from "./hooks/localStorage/types";
|
|
114
115
|
export * from "./hooks/localStorage/useLocalStorage";
|
|
115
116
|
export * from "./hooks/localStorage/useLocalStorageReducer";
|
|
116
117
|
export * from "./hooks/localStorage/useSessionStorage";
|
|
117
118
|
export * from "./hooks/localStorage/useSessionStorageReducer";
|
|
118
119
|
export * from "./hooks/noPagination";
|
|
120
|
+
export * from "./hooks/persistence/usePersistedState";
|
|
121
|
+
export * from "./hooks/persistence/useSearchParamSync";
|
|
122
|
+
export * from "./hooks/persistence/useStorageKey";
|
|
119
123
|
export * from "./hooks/useBidirectionalScroll";
|
|
120
124
|
export * from "./hooks/useClickOutside";
|
|
121
125
|
export * from "./hooks/useContainerBreakpoints";
|