min size updates

This commit is contained in:
Nate Kelley 2025-08-13 11:25:41 -06:00
parent 01f3b63424
commit d725be5505
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
5 changed files with 93 additions and 69 deletions

View File

@ -77,7 +77,9 @@ export const LeftPanelPreserved: Story = {
rightChildren: <RightContent title="Right Panel (Auto)" />,
autoSaveId: 'left-preserved',
defaultLayout: ['300px', 'auto'],
initialLayout: ['100px', 'auto'],
preserveSide: 'left',
leftPanelMinSize: 200,
},
};
@ -89,6 +91,7 @@ export const RightPanelPreserved: Story = {
autoSaveId: 'right-preserved',
defaultLayout: ['auto', '200px'],
preserveSide: 'right',
rightPanelMinSize: 200,
},
};
@ -216,6 +219,7 @@ export const PercentageBasedSizing: Story = {
rightChildren: <RightContent title="70% Width Panel" />,
autoSaveId: 'percentage-sizing',
defaultLayout: ['30%', 'auto'],
leftPanelMinSize: '200px',
preserveSide: 'left',
},
};

View File

@ -40,13 +40,13 @@ interface IAppSplitterProps {
/**
* Initial preserved-side size from cookies (in pixels)
*/
initialLayout?: number | null;
initialLayout?: (`${number}px` | `${number}%` | 'auto' | number)[];
/**
* Default layout configuration as [left, right] sizes
* Can be numbers (pixels), percentages (strings like "50%"), or "auto"
*/
defaultLayout: (`${number}px` | `${number}&` | 'auto' | number)[];
defaultLayout: (`${number}px` | `${number}%` | 'auto' | number)[];
/**
* Minimum size for the left panel
@ -218,7 +218,12 @@ const AppSplitterWrapper = forwardRef<AppSplitterRef, IAppSplitterProps>(
({ autoSaveId, style, className, split = 'vertical', ...props }, componentRef) => {
const containerRef = useRef<HTMLDivElement>(null);
const isVertical = split === 'vertical';
const [mounted, setMounted] = useState(!props.bustStorageOnInit);
const [mounted, setMounted] = useState(
!props.leftPanelMinSize &&
!props.rightPanelMinSize &&
!props.leftPanelMaxSize &&
!props.rightPanelMaxSize
);
const splitterAutoSaveId = createAutoSaveId(autoSaveId);
const { splitterAutoSaveId: parentSplitterAutoSaveId } = useAppSplitterContext();
@ -293,7 +298,6 @@ const AppSplitterBase = forwardRef<
isVertical,
splitterAutoSaveId,
containerRef,
bustStorageOnInit,
split = 'vertical',
},
ref
@ -322,23 +326,7 @@ const AppSplitterBase = forwardRef<
// STORAGE MANAGEMENT
// ================================
const bustStorageOnInitSplitter = (preservedSideValue: number | null) => {
const refSize =
split === 'vertical'
? containerRef.current?.offsetWidth
: containerRef.current?.offsetHeight;
// Don't bust storage if container hasn't been sized yet
if (!refSize || refSize === 0) {
// console.warn('AppSplitter: container not sized yet');
return false;
}
return typeof bustStorageOnInit === 'function'
? bustStorageOnInit(preservedSideValue, refSize)
: !!bustStorageOnInit;
};
const defaultValue = () => {
const defaultValue = useCallback(() => {
const [leftValue, rightValue] = defaultLayout;
const containerSize =
split === 'vertical'
@ -354,13 +342,65 @@ const AppSplitterBase = forwardRef<
const preserveValue = preserveSide === 'left' ? leftValue : rightValue;
const result = sizeToPixels(preserveValue, containerSize);
return result;
};
}, [defaultLayout, split, preserveSide]);
const initialValue = useCallback(() => {
if (initialLayout) {
const [leftValue, rightValue] = initialLayout;
const containerSize =
split === 'vertical'
? (containerRef.current?.offsetWidth ?? 0)
: (containerRef.current?.offsetHeight ?? 0);
if (preserveSide === 'left' && leftValue === 'auto') {
return containerSize;
}
if (preserveSide === 'right' && rightValue === 'auto') {
return containerSize;
}
const preserveValue = preserveSide === 'left' ? leftValue : rightValue;
const result = sizeToPixels(preserveValue, containerSize);
// Check if the result is within min/max bounds
if (containerSize > 0) {
const minSize =
preserveSide === 'left'
? sizeToPixels(leftPanelMinSize, containerSize)
: sizeToPixels(rightPanelMinSize, containerSize);
const maxSize =
preserveSide === 'left'
? leftPanelMaxSize
? sizeToPixels(leftPanelMaxSize, containerSize)
: containerSize
: rightPanelMaxSize
? sizeToPixels(rightPanelMaxSize, containerSize)
: containerSize;
// If the result is outside the min/max bounds, use the default value
if (result < minSize || result > maxSize) {
return defaultValue();
}
}
return result;
}
return defaultValue();
}, [
initialLayout,
split,
preserveSide,
leftPanelMinSize,
rightPanelMinSize,
leftPanelMaxSize,
rightPanelMaxSize,
defaultValue,
]);
// Load saved layout from cookies
const [savedLayout, setSavedLayout] = useCookieState<number | null>(splitterAutoSaveId, {
defaultValue,
initialValue: initialLayout ?? undefined,
bustStorageOnInit: bustStorageOnInitSplitter,
initialValue,
});
// ================================

View File

@ -2,7 +2,7 @@
import { isServer } from '@tanstack/react-query';
import cookies from 'js-cookie';
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { useMemoizedFn } from './useMemoizedFn';
import { useMount } from './useMount';
@ -25,11 +25,10 @@ interface CookieOptions {
interface Options<T> {
defaultValue?: T | (() => T);
initialValue?: T;
initialValue?: T | (() => T);
serializer?: (value: T) => string;
deserializer?: (value: string) => T;
onError?: (error: unknown) => void;
bustStorageOnInit?: boolean | ((layout: T) => boolean);
expirationTime?: number;
cookieOptions?: CookieOptions;
}
@ -73,7 +72,6 @@ export function useCookieState<T>(
serializer = JSON.stringify,
deserializer = JSON.parse,
onError,
bustStorageOnInit = false,
expirationTime = DEFAULT_EXPIRATION_TIME,
cookieOptions = {},
} = options || {};
@ -85,14 +83,9 @@ export function useCookieState<T>(
// Get initial value from cookies or use default
const getInitialValue = useMemoizedFn((): T | undefined => {
// If bustStorageOnInit is true, ignore cookies and use default value
if (bustStorageOnInit === true) {
return executeBustStorage();
}
// Prefer explicitly provided initialValue if present
if (initialValue !== undefined) {
return initialValue;
return typeof initialValue === 'function' ? (initialValue as () => T)() : initialValue;
}
try {
@ -128,10 +121,6 @@ export function useCookieState<T>(
// Data is still valid, deserialize and return the value
const deserializedValue = deserializer(JSON.stringify(storageData.value));
if (typeof bustStorageOnInit === 'function' && bustStorageOnInit(deserializedValue)) {
return executeBustStorage();
}
return deserializedValue;
} catch (error) {
onError?.(error);
@ -146,35 +135,28 @@ export function useCookieState<T>(
setState(getInitialValue());
});
// Update cookies when state changes
useEffect(() => {
try {
if (state === undefined && !isServer) {
removeCookie(key, cookieOptions);
} else {
// Create storage data with current timestamp
const storageData: StorageData<T> = {
value: JSON.parse(serializer(state)),
timestamp: Date.now(),
};
setCookie(key, JSON.stringify(storageData), expirationTime, cookieOptions);
}
} catch (error) {
onError?.(error);
}
}, [key, state, serializer, onError, expirationTime, cookieOptions]);
// Setter function that handles both direct values and function updates
const setStoredState = useMemoizedFn((value?: SetState<T>) => {
try {
if (typeof value === 'function') {
setState((prevState) => {
const newState = (value as (prevState?: T) => T)(prevState);
return newState;
});
} else {
setState(value);
}
setState((prevState) => {
// Calculate the new state value
const newState =
typeof value === 'function' ? (value as (prevState?: T) => T)(prevState) : value;
// Update cookie with the new state
if (newState === undefined && !isServer) {
removeCookie(key, cookieOptions);
} else {
// Create storage data with current timestamp
const storageData: StorageData<T> = {
value: JSON.parse(serializer(newState)),
timestamp: Date.now(),
};
setCookie(key, JSON.stringify(storageData), expirationTime, cookieOptions);
}
return newState;
});
} catch (error) {
onError?.(error);
}

View File

@ -51,8 +51,8 @@ export const ChatLayout: React.FC<ChatSplitterProps> = ({ children }) => {
<ChatContextProvider>
<AppSplitter
ref={appSplitterRef}
leftChildren={mounted && <ChatContainer />}
rightChildren={mounted && <FileContainer>{children}</FileContainer>}
leftChildren={mounted && renderLeftPanel && <ChatContainer />}
rightChildren={mounted && renderRightPanel && <FileContainer>{children}</FileContainer>}
autoSaveId={autoSaveId}
defaultLayout={defaultSplitterLayout}
allowResize={selectedLayout === 'both'}
@ -61,8 +61,6 @@ export const ChatLayout: React.FC<ChatSplitterProps> = ({ children }) => {
leftPanelMaxSize={leftPanelMaxSize}
rightPanelMinSize={rightPanelMinSize}
rightPanelMaxSize={rightPanelMaxSize}
renderLeftPanel={renderLeftPanel}
renderRightPanel={renderRightPanel}
bustStorageOnInit={bustStorageOnInit}
/>
</ChatContextProvider>

View File

@ -70,7 +70,7 @@ export const FileContainer: React.FC<FileContainerProps> = ({ children }) => {
}
});
const rightChildren = useMemo(() => {
const rightChildren: React.ReactNode = useMemo(() => {
if (!debouncedSelectedFileViewSecondary) {
return null;
}