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

View File

@ -40,13 +40,13 @@ interface IAppSplitterProps {
/** /**
* Initial preserved-side size from cookies (in pixels) * Initial preserved-side size from cookies (in pixels)
*/ */
initialLayout?: number | null; initialLayout?: (`${number}px` | `${number}%` | 'auto' | number)[];
/** /**
* Default layout configuration as [left, right] sizes * Default layout configuration as [left, right] sizes
* Can be numbers (pixels), percentages (strings like "50%"), or "auto" * 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 * Minimum size for the left panel
@ -218,7 +218,12 @@ const AppSplitterWrapper = forwardRef<AppSplitterRef, IAppSplitterProps>(
({ autoSaveId, style, className, split = 'vertical', ...props }, componentRef) => { ({ autoSaveId, style, className, split = 'vertical', ...props }, componentRef) => {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const isVertical = split === 'vertical'; 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 = createAutoSaveId(autoSaveId);
const { splitterAutoSaveId: parentSplitterAutoSaveId } = useAppSplitterContext(); const { splitterAutoSaveId: parentSplitterAutoSaveId } = useAppSplitterContext();
@ -293,7 +298,6 @@ const AppSplitterBase = forwardRef<
isVertical, isVertical,
splitterAutoSaveId, splitterAutoSaveId,
containerRef, containerRef,
bustStorageOnInit,
split = 'vertical', split = 'vertical',
}, },
ref ref
@ -322,23 +326,7 @@ const AppSplitterBase = forwardRef<
// STORAGE MANAGEMENT // STORAGE MANAGEMENT
// ================================ // ================================
const bustStorageOnInitSplitter = (preservedSideValue: number | null) => { const defaultValue = useCallback(() => {
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 [leftValue, rightValue] = defaultLayout; const [leftValue, rightValue] = defaultLayout;
const containerSize = const containerSize =
split === 'vertical' split === 'vertical'
@ -354,13 +342,65 @@ const AppSplitterBase = forwardRef<
const preserveValue = preserveSide === 'left' ? leftValue : rightValue; const preserveValue = preserveSide === 'left' ? leftValue : rightValue;
const result = sizeToPixels(preserveValue, containerSize); const result = sizeToPixels(preserveValue, containerSize);
return result; 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 // Load saved layout from cookies
const [savedLayout, setSavedLayout] = useCookieState<number | null>(splitterAutoSaveId, { const [savedLayout, setSavedLayout] = useCookieState<number | null>(splitterAutoSaveId, {
defaultValue, defaultValue,
initialValue: initialLayout ?? undefined, initialValue,
bustStorageOnInit: bustStorageOnInitSplitter,
}); });
// ================================ // ================================

View File

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

View File

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

View File

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