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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/cli/lib/import-export/export/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { z } from 'zod';
import { SiteData } from 'cli/lib/cli-config/core';
import type { ImportExportEventEmitter } from '../events';

Expand Down
24 changes: 22 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,25 @@ 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 bg-frame-theme',
isDragging ? 'opacity-100' : '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-frame text-frame-text 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,15 +8,17 @@ 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 (
<div
data-testid="main-sidebar"
className={ cx( 'text-chrome-inverted relative pt-[10px]', 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
3 changes: 3 additions & 0 deletions apps/studio/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ 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 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
75 changes: 75 additions & 0 deletions apps/studio/src/hooks/use-sidebar-resize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { useState, useCallback, useRef, useEffect } from 'react';
import {
SIDEBAR_MIN_WIDTH,
SIDEBAR_MAX_WIDTH,
LOCAL_STORAGE_SIDEBAR_WIDTH_KEY,
} from 'src/constants';
import { getSavedSidebarWidth } from 'src/lib/sidebar-utils';

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

export function useSidebarResize( isSidebarVisible: boolean, toggleSidebar: () => void ) {
const [ sidebarWidth, setSidebarWidth ] = useState( getSavedSidebarWidth );
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;
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;
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,
};
}
12 changes: 4 additions & 8 deletions apps/studio/src/hooks/use-sidebar-visibility.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
import {
DEFAULT_WIDTH,
SIDEBAR_WIDTH,
MIN_WIDTH_SELECTOR_TO_MEASURE,
APP_CHROME_SPACING,
} from 'src/constants';
import { DEFAULT_WIDTH, MIN_WIDTH_SELECTOR_TO_MEASURE, APP_CHROME_SPACING } from 'src/constants';
import { getIpcApi } from 'src/lib/get-ipc-api';
import { getSavedSidebarWidth } from 'src/lib/sidebar-utils';

const SIDEBAR_BREAKPOINT = DEFAULT_WIDTH;

Expand Down Expand Up @@ -35,7 +31,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 + getSavedSidebarWidth() + APP_CHROME_SPACING );
}

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

const toggleSidebar = useCallback( () => {
void getIpcApi().toggleMinWindowWidth( isSidebarVisible );
void getIpcApi().toggleMinWindowWidth( isSidebarVisible, getSavedSidebarWidth() );
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 @@ -1461,15 +1461,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
17 changes: 17 additions & 0 deletions apps/studio/src/lib/sidebar-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {
SIDEBAR_WIDTH,
SIDEBAR_MIN_WIDTH,
SIDEBAR_MAX_WIDTH,
LOCAL_STORAGE_SIDEBAR_WIDTH_KEY,
} from 'src/constants';

export function getSavedSidebarWidth(): number {
const saved = localStorage.getItem( LOCAL_STORAGE_SIDEBAR_WIDTH_KEY );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did we decide on a local storage approach?
We already have some UI properties (window size) living in the app.json config file.
I don't have anything against local storage, I just believe it should be consistent with our other properties.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have the answer specifically for that bit since it was not originally my PR but it makes sense to be consistent with our other properties. I will check how we can align our approach here 👀

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a chance to check this a bit further and I think the localStorage approach would be all right here. The sidebar width uses localStorage rather than app.json because it's renderer-side state: it's read synchronously at component init (allowing it to be passed directly as a useState initializer) and never needed by the Main process. windowBounds lives in app.json because Electron's Main process must restore the window size before the renderer exists. Moving sidebar width to app.json would introduce an async IPC to initialize a CSS value, requiring a loading state and added complexity.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes a lot of sense, thanks for the explanation! 👍

if ( saved ) {
const parsed = Number( saved );
if ( ! isNaN( parsed ) && parsed >= SIDEBAR_MIN_WIDTH && parsed <= SIDEBAR_MAX_WIDTH ) {
return parsed;
}
}
return SIDEBAR_WIDTH;
}
4 changes: 2 additions & 2 deletions apps/studio/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,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