@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 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 ? "translateY(0)" : "translateY(100%)",
7833
- transition: suppressTransition
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
- : isDragging
7836
- ? "none"
7837
- : autoHeight || !isOpen
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: dockedContent !== undefined,
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, dockedContent !== undefined, state.effectiveDockedHeight);
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.jsx("div", { className: cvaSheetScrollArea({ fillHeight: state.sizingMode !== "fit" }), "data-sheet-scroll-area": true, ref: setScrollAreaEl, children: state.sizingMode === "docked" ? dockedContent : children })] }));
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
- const compressed = fflate.gzipSync(textInput);
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, useSearch, useNavigate } from '@tanstack/react-router';
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 ? "translateY(0)" : "translateY(100%)",
7831
- transition: suppressTransition
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
- : isDragging
7834
- ? "none"
7835
- : autoHeight || !isOpen
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: dockedContent !== undefined,
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, dockedContent !== undefined, state.effectiveDockedHeight);
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 }), jsx("div", { className: cvaSheetScrollArea({ fillHeight: state.sizingMode !== "fit" }), "data-sheet-scroll-area": true, ref: setScrollAreaEl, children: state.sizingMode === "docked" ? dockedContent : children })] }));
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
- const compressed = gzipSync(textInput);
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.15",
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.23.8"
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";