diff --git a/apps/web-tss/src/components/ui/layouts/AppSplitter/AppSplitter.stories.tsx b/apps/web-tss/src/components/ui/layouts/AppSplitter/AppSplitter.stories.tsx index 777291698..2a23050e7 100644 --- a/apps/web-tss/src/components/ui/layouts/AppSplitter/AppSplitter.stories.tsx +++ b/apps/web-tss/src/components/ui/layouts/AppSplitter/AppSplitter.stories.tsx @@ -77,7 +77,9 @@ export const LeftPanelPreserved: Story = { rightChildren: , 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: , autoSaveId: 'percentage-sizing', defaultLayout: ['30%', 'auto'], + leftPanelMinSize: '200px', preserveSide: 'left', }, }; diff --git a/apps/web-tss/src/components/ui/layouts/AppSplitter/AppSplitter.tsx b/apps/web-tss/src/components/ui/layouts/AppSplitter/AppSplitter.tsx index 48cfa935f..f90506e50 100644 --- a/apps/web-tss/src/components/ui/layouts/AppSplitter/AppSplitter.tsx +++ b/apps/web-tss/src/components/ui/layouts/AppSplitter/AppSplitter.tsx @@ -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( ({ autoSaveId, style, className, split = 'vertical', ...props }, componentRef) => { const containerRef = useRef(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(splitterAutoSaveId, { defaultValue, - initialValue: initialLayout ?? undefined, - bustStorageOnInit: bustStorageOnInitSplitter, + initialValue, }); // ================================ diff --git a/apps/web-tss/src/hooks/useCookieState.tsx b/apps/web-tss/src/hooks/useCookieState.tsx index b95d98ddc..cd9d19e4a 100644 --- a/apps/web-tss/src/hooks/useCookieState.tsx +++ b/apps/web-tss/src/hooks/useCookieState.tsx @@ -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 { 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( serializer = JSON.stringify, deserializer = JSON.parse, onError, - bustStorageOnInit = false, expirationTime = DEFAULT_EXPIRATION_TIME, cookieOptions = {}, } = options || {}; @@ -85,14 +83,9 @@ export function useCookieState( // 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( // 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( 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 = { - 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) => { 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 = { + value: JSON.parse(serializer(newState)), + timestamp: Date.now(), + }; + setCookie(key, JSON.stringify(storageData), expirationTime, cookieOptions); + } + + return newState; + }); } catch (error) { onError?.(error); } diff --git a/apps/web/src/layouts/ChatLayout/ChatLayout/ChatLayout.tsx b/apps/web/src/layouts/ChatLayout/ChatLayout/ChatLayout.tsx index d9b0553d6..b678e8740 100644 --- a/apps/web/src/layouts/ChatLayout/ChatLayout/ChatLayout.tsx +++ b/apps/web/src/layouts/ChatLayout/ChatLayout/ChatLayout.tsx @@ -51,8 +51,8 @@ export const ChatLayout: React.FC = ({ children }) => { } - rightChildren={mounted && {children}} + leftChildren={mounted && renderLeftPanel && } + rightChildren={mounted && renderRightPanel && {children}} autoSaveId={autoSaveId} defaultLayout={defaultSplitterLayout} allowResize={selectedLayout === 'both'} @@ -61,8 +61,6 @@ export const ChatLayout: React.FC = ({ children }) => { leftPanelMaxSize={leftPanelMaxSize} rightPanelMinSize={rightPanelMinSize} rightPanelMaxSize={rightPanelMaxSize} - renderLeftPanel={renderLeftPanel} - renderRightPanel={renderRightPanel} bustStorageOnInit={bustStorageOnInit} /> diff --git a/apps/web/src/layouts/ChatLayout/FileContainer/FileContainer.tsx b/apps/web/src/layouts/ChatLayout/FileContainer/FileContainer.tsx index a73fac0d2..649c37922 100644 --- a/apps/web/src/layouts/ChatLayout/FileContainer/FileContainer.tsx +++ b/apps/web/src/layouts/ChatLayout/FileContainer/FileContainer.tsx @@ -70,7 +70,7 @@ export const FileContainer: React.FC = ({ children }) => { } }); - const rightChildren = useMemo(() => { + const rightChildren: React.ReactNode = useMemo(() => { if (!debouncedSelectedFileViewSecondary) { return null; }