Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions apps/studio/src/components/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import WindowsTitlebar from 'src/components/windows-titlebar';
import { useListenDeepLinkConnection } from 'src/hooks/sync-sites/use-listen-deep-link-connection';
import { useAuth } from 'src/hooks/use-auth';
import { useLocalizationSupport } from 'src/hooks/use-localization-support';
import { useSidebarResize } from 'src/hooks/use-sidebar-resize';
import { useSidebarVisibility } from 'src/hooks/use-sidebar-visibility';
import { useSiteDetails } from 'src/hooks/use-site-details';
import { isWindows } from 'src/lib/app-globals';
Expand All @@ -31,6 +32,10 @@ export default function App() {
const { needsOnboarding } = useOnboarding();
const isOnboardingLoading = useRootSelector( selectOnboardingLoading );
const { isSidebarVisible, toggleSidebar } = useSidebarVisibility();
const { sidebarWidth, isDragging, handleMouseDown } = useSidebarResize(
isSidebarVisible,
toggleSidebar
);
const { showWhatsNew, closeWhatsNew } = useWhatsNew();
const { sites: localSites, loadingSites } = useSiteDetails();
const isEmpty = ! loadingSites && ! localSites.length;
Expand Down Expand Up @@ -87,10 +92,27 @@ export default function App() {
<HStack spacing="0" alignment="left" className="flex-grow">
<MainSidebar
className={ cx(
'h-full transition-all duration-500',
isSidebarVisible ? 'basis-52 flex-shrink-0' : 'basis-0 !min-w-[10px]'
'h-full flex-shrink-0',
! isDragging && 'transition-all duration-500',
! isSidebarVisible && 'basis-0 !min-w-[10px]'
) }
style={ isSidebarVisible ? { flexBasis: `${ sidebarWidth }px` } : undefined }
/>
{ /* Resize handle */ }
<div
className="group h-full w-3 cursor-col-resize flex-shrink-0 z-20 flex items-stretch justify-start -ml-1.5 -mr-1.5"
onMouseDown={ handleMouseDown }
>
<div
className={ cx(
'w-[3px] rounded-[2px] transition-opacity duration-150',
isDragging
? 'bg-[#3858e9] opacity-100'
: 'bg-[#3858e9] opacity-0 group-hover:opacity-100'
) }
/>
</div>
{ isDragging && <div className="fixed inset-0 z-50 cursor-col-resize" /> }
<main
data-testid="site-content"
className="bg-white h-full flex-grow rounded-chrome overflow-hidden z-10"
Expand Down
4 changes: 3 additions & 1 deletion apps/studio/src/components/main-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import AddSite from 'src/modules/add-site';

interface MainSidebarProps {
className?: string;
style?: React.CSSProperties;
}

export default function MainSidebar( { className }: MainSidebarProps ) {
export default function MainSidebar( { className, style }: MainSidebarProps ) {
const { sites: localSites } = useSiteDetails();

return (
Expand All @@ -22,6 +23,7 @@ export default function MainSidebar( { className }: MainSidebarProps ) {
! isMac() && 'pt-[38px]',
className
) }
style={ style }
>
{ ! localSites.length ? (
<div className="flex h-full px-[20px] justify-center items-center app-no-drag-region text-center text-[12px] text-a8c-gray-50">
Expand Down
2 changes: 1 addition & 1 deletion apps/studio/src/components/running-sites.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function RunningSites() {
}

return (
<div className="flex flex-row px-5 pb-1 justify-between align-center self-stretch opacity-70">
<div className="flex flex-row px-5 pb-1 justify-between align-center self-stretch opacity-70 whitespace-nowrap min-w-0">
<p className="text-xxs leading-4">
{ anyRunning
? sprintf(
Expand Down
4 changes: 4 additions & 0 deletions apps/studio/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { HOUR_MS } from '@studio/common/constants';
export const DEFAULT_WIDTH = 900;
export const MAIN_MIN_HEIGHT = 600;
export const SIDEBAR_WIDTH = 208;
export const SIDEBAR_MIN_WIDTH = 200;
export const SIDEBAR_MAX_WIDTH = 400;
export const SIDEBAR_SNAP_THRESHOLD = 100;
export const MAIN_MIN_WIDTH = DEFAULT_WIDTH - SIDEBAR_WIDTH + 20;
export const LOCAL_STORAGE_SIDEBAR_WIDTH_KEY = 'sidebar_width';
export const APP_CHROME_SPACING = 10;
export const MIN_WIDTH_CLASS_TO_MEASURE = 'app-measure-tabs-width';
export const MIN_WIDTH_SELECTOR_TO_MEASURE = `.${ MIN_WIDTH_CLASS_TO_MEASURE }`;
Expand Down
106 changes: 106 additions & 0 deletions apps/studio/src/hooks/use-sidebar-resize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { useState, useCallback, useRef, useEffect } from 'react';
import {
SIDEBAR_WIDTH,
SIDEBAR_MIN_WIDTH,
SIDEBAR_MAX_WIDTH,
SIDEBAR_SNAP_THRESHOLD,
LOCAL_STORAGE_SIDEBAR_WIDTH_KEY,
} from 'src/constants';

function loadSavedWidth(): number {
const saved = localStorage.getItem( LOCAL_STORAGE_SIDEBAR_WIDTH_KEY );
if ( saved ) {
const parsed = Number( saved );
if ( ! isNaN( parsed ) && parsed >= SIDEBAR_MIN_WIDTH && parsed <= SIDEBAR_MAX_WIDTH ) {
return parsed;
}
}
return SIDEBAR_WIDTH;
}

function saveWidth( width: number ) {
localStorage.setItem( LOCAL_STORAGE_SIDEBAR_WIDTH_KEY, String( width ) );
}

export function useSidebarResize( isSidebarVisible: boolean, toggleSidebar: () => void ) {
const [ sidebarWidth, setSidebarWidth ] = useState( loadSavedWidth );
const [ isDragging, setIsDragging ] = useState( false );
const dragStartX = useRef( 0 );
const dragStartWidth = useRef( 0 );

const handleMouseDown = useCallback(
( e: React.MouseEvent ) => {
e.preventDefault();
setIsDragging( true );
dragStartX.current = e.clientX;
dragStartWidth.current = isSidebarVisible ? sidebarWidth : 0;
},
[ sidebarWidth, isSidebarVisible ]
);

useEffect( () => {
if ( ! isDragging ) {
return;
}

let rafId: number;

const handleMouseMove = ( e: MouseEvent ) => {
cancelAnimationFrame( rafId );
rafId = requestAnimationFrame( () => {
const delta = e.clientX - dragStartX.current;
const newWidth = dragStartWidth.current + delta;

if ( newWidth < SIDEBAR_SNAP_THRESHOLD ) {
// Will snap closed on mouseup
setSidebarWidth( Math.max( 0, newWidth ) );
return;
}

setSidebarWidth( Math.min( SIDEBAR_MAX_WIDTH, Math.max( SIDEBAR_MIN_WIDTH, newWidth ) ) );
} );
};

const handleMouseUp = ( e: MouseEvent ) => {
setIsDragging( false );
cancelAnimationFrame( rafId );

const delta = e.clientX - dragStartX.current;
const finalWidth = dragStartWidth.current + delta;

if ( finalWidth < SIDEBAR_SNAP_THRESHOLD ) {
// Snap closed — restore the last good width for when it reopens
setSidebarWidth(
dragStartWidth.current > SIDEBAR_MIN_WIDTH ? dragStartWidth.current : SIDEBAR_WIDTH
);
if ( isSidebarVisible ) {
toggleSidebar();
}
return;
}

const clampedWidth = Math.min( SIDEBAR_MAX_WIDTH, Math.max( SIDEBAR_MIN_WIDTH, finalWidth ) );
setSidebarWidth( clampedWidth );
saveWidth( clampedWidth );

if ( ! isSidebarVisible ) {
toggleSidebar();
}
};

document.addEventListener( 'mousemove', handleMouseMove );
document.addEventListener( 'mouseup', handleMouseUp );

return () => {
cancelAnimationFrame( rafId );
document.removeEventListener( 'mousemove', handleMouseMove );
document.removeEventListener( 'mouseup', handleMouseUp );
};
}, [ isDragging, isSidebarVisible, toggleSidebar ] );

return {
sidebarWidth,
isDragging,
handleMouseDown,
};
}
16 changes: 14 additions & 2 deletions apps/studio/src/hooks/use-sidebar-visibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,21 @@ import {
SIDEBAR_WIDTH,
MIN_WIDTH_SELECTOR_TO_MEASURE,
APP_CHROME_SPACING,
LOCAL_STORAGE_SIDEBAR_WIDTH_KEY,
} from 'src/constants';
import { getIpcApi } from 'src/lib/get-ipc-api';

function getCurrentSidebarWidth(): number {
const saved = localStorage.getItem( LOCAL_STORAGE_SIDEBAR_WIDTH_KEY );
if ( saved ) {
const parsed = Number( saved );
if ( ! isNaN( parsed ) && parsed > 0 ) {
return parsed;
}
}
return SIDEBAR_WIDTH;
}

const SIDEBAR_BREAKPOINT = DEFAULT_WIDTH;

/**
Expand Down Expand Up @@ -35,7 +47,7 @@ export function useSidebarVisibility( elementSelector: string = MIN_WIDTH_SELECT
el?.clientWidth < el?.scrollWidth
) {
// The new breakpoint is the width of the element to measure plus the sidebar plus the right padding
setDynamicBreakPoint( el.clientWidth + SIDEBAR_WIDTH + APP_CHROME_SPACING );
setDynamicBreakPoint( el.clientWidth + getCurrentSidebarWidth() + APP_CHROME_SPACING );
}

setIsLowerThanBreakpoint( window.innerWidth < dynamicBreakPoint );
Expand All @@ -55,7 +67,7 @@ export function useSidebarVisibility( elementSelector: string = MIN_WIDTH_SELECT
}, [ isLowerThanBreakpoint ] );

const toggleSidebar = useCallback( () => {
void getIpcApi().toggleMinWindowWidth( isSidebarVisible );
void getIpcApi().toggleMinWindowWidth( isSidebarVisible, getCurrentSidebarWidth() );
setIsSidebarVisible( ! isSidebarVisible );
}, [ isSidebarVisible ] );

Expand Down
9 changes: 7 additions & 2 deletions apps/studio/src/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1223,15 +1223,20 @@ export function resetDefaultLocaleData( _event: IpcMainInvokeEvent ) {
defaultI18n.resetLocaleData();
}

export function toggleMinWindowWidth( event: IpcMainInvokeEvent, isSidebarVisible: boolean ) {
export function toggleMinWindowWidth(
event: IpcMainInvokeEvent,
isSidebarVisible: boolean,
currentSidebarWidth?: number
) {
const parentWindow = BrowserWindow.fromWebContents( event.sender );
if ( ! parentWindow || parentWindow.isDestroyed() || event.sender.isDestroyed() ) {
return;
}
const sidebarW = currentSidebarWidth ?? SIDEBAR_WIDTH;
const [ currentWidth, currentHeight ] = parentWindow.getSize();
const newWidth = Math.max(
MAIN_MIN_WIDTH,
isSidebarVisible ? currentWidth - SIDEBAR_WIDTH : currentWidth + SIDEBAR_WIDTH
isSidebarVisible ? currentWidth - sidebarW : currentWidth + sidebarW
);
parentWindow.setSize( newWidth, currentHeight, true );
}
Expand Down
4 changes: 2 additions & 2 deletions apps/studio/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ const api: IpcApi = {
ipcRendererInvoke( 'promptWindowsSpeedUpSites', ...args ),
setDefaultLocaleData: ( locale ) => ipcRendererInvoke( 'setDefaultLocaleData', locale ),
resetDefaultLocaleData: () => ipcRendererInvoke( 'resetDefaultLocaleData' ),
toggleMinWindowWidth: ( isSidebarVisible ) =>
ipcRendererInvoke( 'toggleMinWindowWidth', isSidebarVisible ),
toggleMinWindowWidth: ( isSidebarVisible, currentSidebarWidth? ) =>
ipcRendererInvoke( 'toggleMinWindowWidth', isSidebarVisible, currentSidebarWidth ),
getAbsolutePathFromSite: ( siteId, relativePath ) =>
ipcRendererInvoke( 'getAbsolutePathFromSite', siteId, relativePath ),
openFileInIDE: ( relativePath, siteId ) =>
Expand Down
Loading