@trackunit/react-components 1.21.17 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.cjs.js CHANGED
@@ -9128,10 +9128,11 @@ const useSheetMotionOverflow = ({ panelEl, isDragging, scrollAreaEl, separatorEl
9128
9128
  * CSS transitions on transform; the component stays mounted during the
9129
9129
  * close animation and unmounts after the transition completes.
9130
9130
  */
9131
- 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, }) => {
9132
9132
  const isFirstRender = useIsFirstRender();
9133
9133
  const skipEntryAnimation = entryAnimation === "never" || (entryAnimation === "subsequent" && isFirstRender);
9134
9134
  const effectiveSnapping = resizable && snapping;
9135
+ const dockingEnabled = dockedContent !== undefined || persistentContent !== undefined;
9135
9136
  const [animState, animDispatch] = react.useReducer(sheetAnimationReducer, INITIAL_ANIMATION_STATE);
9136
9137
  if (isOpen !== animState.prevIsOpen) {
9137
9138
  animDispatch({
@@ -9143,7 +9144,7 @@ const Sheet = ({ isOpen, state, snap, onGeometryChange, onSnap, onClickHandle, o
9143
9144
  const measurements = useSheetMeasurements({
9144
9145
  shouldRender: animState.shouldRender,
9145
9146
  state,
9146
- dockingEnabled: dockedContent !== undefined,
9147
+ dockingEnabled,
9147
9148
  snapping: effectiveSnapping,
9148
9149
  externalRef: ref,
9149
9150
  onGeometryChange,
@@ -9218,7 +9219,7 @@ const Sheet = ({ isOpen, state, snap, onGeometryChange, onSnap, onClickHandle, o
9218
9219
  ? `${state.effectiveDockedHeight}px`
9219
9220
  : fitHeightMeasured
9220
9221
  ? `${cappedFitHeight}px`
9221
- : getSnapLevelCssHeight(state.level, state.totalLevels, dockedContent !== undefined, state.effectiveDockedHeight);
9222
+ : getSnapLevelCssHeight(state.level, state.totalLevels, dockingEnabled, state.effectiveDockedHeight);
9222
9223
  const fitMaxHeight = state.sizingMode === "fit" ? `calc(100cqh - ${FULL_HEIGHT_TOP_MARGIN_PX}px)` : undefined;
9223
9224
  if (!animState.shouldRender)
9224
9225
  return null;
@@ -9242,7 +9243,7 @@ const Sheet = ({ isOpen, state, snap, onGeometryChange, onSnap, onClickHandle, o
9242
9243
  snapHeight: snapHeightCss,
9243
9244
  suppressTransition: skipEntryAnimation && animState.visuallyOpen,
9244
9245
  variant,
9245
- }), 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] })] }));
9246
9247
  return (jsxRuntime.jsx(Portal, { root: container, children: jsxRuntime.jsxs("div", { className: cvaSheetContainer({
9247
9248
  docked: state.sizingMode === "docked",
9248
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)] }) }));
@@ -11047,7 +11048,7 @@ const useSearchParamSync = ({ key, enabled = true, onExternalChange, replace: re
11047
11048
  const urlBase = location.href.length - (location.searchStr.length || 0) + (location.hash.length || 0);
11048
11049
  return urlBase + otherParamsLength + 1 + paramKey.length + 1 + paramValue.length;
11049
11050
  }, [location]);
11050
- const updateSearchParam = react.useCallback((encodedValue) => {
11051
+ const updateSearchParam = react.useCallback((encodedValue, options) => {
11051
11052
  if (!enabled) {
11052
11053
  return;
11053
11054
  }
@@ -11059,7 +11060,7 @@ const useSearchParamSync = ({ key, enabled = true, onExternalChange, replace: re
11059
11060
  return;
11060
11061
  }
11061
11062
  requestAnimationFrame(() => {
11062
- const shouldReplace = replaceOption ?? !Boolean(currentSearchValue);
11063
+ const shouldReplace = options?.replace ?? replaceOption ?? !Boolean(currentSearchValue);
11063
11064
  void navigate({
11064
11065
  to: ".",
11065
11066
  search: (prev) => {
@@ -11140,14 +11141,14 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
11140
11141
  react.useEffect(() => {
11141
11142
  serializeRef.current = serialize;
11142
11143
  }, [serialize]);
11143
- const [initialState] = react.useState(() => {
11144
+ const [{ initialState, loadedFromStorage }] = react.useState(() => {
11144
11145
  if (enabled && searchValue) {
11145
11146
  try {
11146
11147
  const decoded = decode(searchValue);
11147
11148
  const transformed = fromUrlValue ? fromUrlValue(decoded) : decoded;
11148
11149
  const validated = validate(transformed);
11149
11150
  if (validated !== undefined) {
11150
- return validated;
11151
+ return { initialState: validated, loadedFromStorage: false };
11151
11152
  }
11152
11153
  }
11153
11154
  catch {
@@ -11158,15 +11159,28 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
11158
11159
  const raw = localStorage.getItem(storageKey);
11159
11160
  if (raw) {
11160
11161
  const parsed = storageSerializer.deserialize(raw);
11161
- return validate(parsed);
11162
+ const validated = validate(parsed);
11163
+ return { initialState: validated, loadedFromStorage: validated !== undefined };
11162
11164
  }
11163
11165
  }
11164
11166
  catch {
11165
11167
  // no valid stored state
11166
11168
  }
11167
- return undefined;
11169
+ return { initialState: undefined, loadedFromStorage: false };
11168
11170
  });
11169
11171
  const lastPersistedRef = react.useRef(initialState);
11172
+ const hasRestoredUrlRef = react.useRef(false);
11173
+ react.useEffect(() => {
11174
+ if (hasRestoredUrlRef.current) {
11175
+ return;
11176
+ }
11177
+ if (!enabled || !loadedFromStorage || initialState === undefined || searchValue !== undefined) {
11178
+ return;
11179
+ }
11180
+ hasRestoredUrlRef.current = true;
11181
+ const urlValue = toUrlValueRef.current ? toUrlValueRef.current(initialState) : initialState;
11182
+ updateSearchParam(encode(urlValue), { replace: true });
11183
+ }, [enabled, loadedFromStorage, initialState, searchValue, updateSearchParam, encode]);
11170
11184
  const persistState = react.useCallback((state) => {
11171
11185
  if (dequal.dequal(lastPersistedRef.current, state)) {
11172
11186
  return;
package/index.esm.js CHANGED
@@ -9126,10 +9126,11 @@ const useSheetMotionOverflow = ({ panelEl, isDragging, scrollAreaEl, separatorEl
9126
9126
  * CSS transitions on transform; the component stays mounted during the
9127
9127
  * close animation and unmounts after the transition completes.
9128
9128
  */
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, }) => {
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, }) => {
9130
9130
  const isFirstRender = useIsFirstRender();
9131
9131
  const skipEntryAnimation = entryAnimation === "never" || (entryAnimation === "subsequent" && isFirstRender);
9132
9132
  const effectiveSnapping = resizable && snapping;
9133
+ const dockingEnabled = dockedContent !== undefined || persistentContent !== undefined;
9133
9134
  const [animState, animDispatch] = useReducer(sheetAnimationReducer, INITIAL_ANIMATION_STATE);
9134
9135
  if (isOpen !== animState.prevIsOpen) {
9135
9136
  animDispatch({
@@ -9141,7 +9142,7 @@ const Sheet = ({ isOpen, state, snap, onGeometryChange, onSnap, onClickHandle, o
9141
9142
  const measurements = useSheetMeasurements({
9142
9143
  shouldRender: animState.shouldRender,
9143
9144
  state,
9144
- dockingEnabled: dockedContent !== undefined,
9145
+ dockingEnabled,
9145
9146
  snapping: effectiveSnapping,
9146
9147
  externalRef: ref,
9147
9148
  onGeometryChange,
@@ -9216,7 +9217,7 @@ const Sheet = ({ isOpen, state, snap, onGeometryChange, onSnap, onClickHandle, o
9216
9217
  ? `${state.effectiveDockedHeight}px`
9217
9218
  : fitHeightMeasured
9218
9219
  ? `${cappedFitHeight}px`
9219
- : getSnapLevelCssHeight(state.level, state.totalLevels, dockedContent !== undefined, state.effectiveDockedHeight);
9220
+ : getSnapLevelCssHeight(state.level, state.totalLevels, dockingEnabled, state.effectiveDockedHeight);
9220
9221
  const fitMaxHeight = state.sizingMode === "fit" ? `calc(100cqh - ${FULL_HEIGHT_TOP_MARGIN_PX}px)` : undefined;
9221
9222
  if (!animState.shouldRender)
9222
9223
  return null;
@@ -9240,7 +9241,7 @@ const Sheet = ({ isOpen, state, snap, onGeometryChange, onSnap, onClickHandle, o
9240
9241
  snapHeight: snapHeightCss,
9241
9242
  suppressTransition: skipEntryAnimation && animState.visuallyOpen,
9242
9243
  variant,
9243
- }), 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] })] }));
9244
9245
  return (jsx(Portal, { root: container, children: jsxs("div", { className: cvaSheetContainer({
9245
9246
  docked: state.sizingMode === "docked",
9246
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)] }) }));
@@ -11045,7 +11046,7 @@ const useSearchParamSync = ({ key, enabled = true, onExternalChange, replace: re
11045
11046
  const urlBase = location.href.length - (location.searchStr.length || 0) + (location.hash.length || 0);
11046
11047
  return urlBase + otherParamsLength + 1 + paramKey.length + 1 + paramValue.length;
11047
11048
  }, [location]);
11048
- const updateSearchParam = useCallback((encodedValue) => {
11049
+ const updateSearchParam = useCallback((encodedValue, options) => {
11049
11050
  if (!enabled) {
11050
11051
  return;
11051
11052
  }
@@ -11057,7 +11058,7 @@ const useSearchParamSync = ({ key, enabled = true, onExternalChange, replace: re
11057
11058
  return;
11058
11059
  }
11059
11060
  requestAnimationFrame(() => {
11060
- const shouldReplace = replaceOption ?? !Boolean(currentSearchValue);
11061
+ const shouldReplace = options?.replace ?? replaceOption ?? !Boolean(currentSearchValue);
11061
11062
  void navigate({
11062
11063
  to: ".",
11063
11064
  search: (prev) => {
@@ -11138,14 +11139,14 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
11138
11139
  useEffect(() => {
11139
11140
  serializeRef.current = serialize;
11140
11141
  }, [serialize]);
11141
- const [initialState] = useState(() => {
11142
+ const [{ initialState, loadedFromStorage }] = useState(() => {
11142
11143
  if (enabled && searchValue) {
11143
11144
  try {
11144
11145
  const decoded = decode(searchValue);
11145
11146
  const transformed = fromUrlValue ? fromUrlValue(decoded) : decoded;
11146
11147
  const validated = validate(transformed);
11147
11148
  if (validated !== undefined) {
11148
- return validated;
11149
+ return { initialState: validated, loadedFromStorage: false };
11149
11150
  }
11150
11151
  }
11151
11152
  catch {
@@ -11156,15 +11157,28 @@ const usePersistedState = ({ key, validate, serialize, toUrlValue, fromUrlValue,
11156
11157
  const raw = localStorage.getItem(storageKey);
11157
11158
  if (raw) {
11158
11159
  const parsed = storageSerializer.deserialize(raw);
11159
- return validate(parsed);
11160
+ const validated = validate(parsed);
11161
+ return { initialState: validated, loadedFromStorage: validated !== undefined };
11160
11162
  }
11161
11163
  }
11162
11164
  catch {
11163
11165
  // no valid stored state
11164
11166
  }
11165
- return undefined;
11167
+ return { initialState: undefined, loadedFromStorage: false };
11166
11168
  });
11167
11169
  const lastPersistedRef = useRef(initialState);
11170
+ const hasRestoredUrlRef = useRef(false);
11171
+ useEffect(() => {
11172
+ if (hasRestoredUrlRef.current) {
11173
+ return;
11174
+ }
11175
+ if (!enabled || !loadedFromStorage || initialState === undefined || searchValue !== undefined) {
11176
+ return;
11177
+ }
11178
+ hasRestoredUrlRef.current = true;
11179
+ const urlValue = toUrlValueRef.current ? toUrlValueRef.current(initialState) : initialState;
11180
+ updateSearchParam(encode(urlValue), { replace: true });
11181
+ }, [enabled, loadedFromStorage, initialState, searchValue, updateSearchParam, encode]);
11168
11182
  const persistState = useCallback((state) => {
11169
11183
  if (dequal(lastPersistedRef.current, state)) {
11170
11184
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/react-components",
3
- "version": "1.21.17",
3
+ "version": "1.22.0",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
@@ -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. */
@@ -7,7 +7,9 @@ type UseSearchParamSyncOptions = {
7
7
  };
8
8
  type UseSearchParamSyncReturn = {
9
9
  readonly searchValue: string | undefined;
10
- readonly updateSearchParam: (encodedValue: string | undefined) => void;
10
+ readonly updateSearchParam: (encodedValue: string | undefined, options?: {
11
+ readonly replace?: boolean;
12
+ }) => void;
11
13
  };
12
14
  /**
13
15
  * Syncs an encoded string value with a URL search parameter via Tanstack Router.