Skip to content
Draft
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
2 changes: 1 addition & 1 deletion apps/studio/src/components/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export default function App() {
<MainSidebar
className={ cx(
'h-full transition-all duration-500',
isSidebarVisible ? 'basis-52 flex-shrink-0' : 'basis-0 !min-w-[10px]'
isSidebarVisible ? 'basis-[248px] flex-shrink-0' : 'basis-0 !min-w-[10px]'
) }
/>
<main
Expand Down
4 changes: 1 addition & 3 deletions apps/studio/src/components/main-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ export default function MainSidebar( { className }: MainSidebarProps ) {
data-testid="main-sidebar"
className={ cx(
'text-chrome-inverted relative',
isMac() && 'pt-[10px]',
! isMac() && 'pt-[38px]',
className
) }
>
Expand All @@ -32,7 +30,7 @@ export default function MainSidebar( { className }: MainSidebarProps ) {
<div
className={ cx(
'flex-1 overflow-y-auto sites-scrollbar app-no-drag-region',
isMac() ? 'ms-4' : 'ms-3'
''
) }
>
<SiteMenu />
Expand Down
66 changes: 66 additions & 0 deletions apps/studio/src/components/site-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { WordPressLogoCircle } from 'src/components/wordpress-logo-circle';
import { useThemeDetails } from 'src/hooks/use-theme-details';
import { cx } from 'src/lib/cx';

const SITE_ICON_COLORS = [
'#5B8A72', // sage
'#7B6B8A', // lavender
'#8A7B5B', // khaki
'#5B7B8A', // steel
'#8A5B6B', // mauve
'#6B8A5B', // moss
'#5B6B8A', // slate
'#8A6B5B', // clay
];

/**
* Simple hash of a string to a number, used for deterministic
* color assignment for sites without an explicit iconColorIndex.
*/
function hashStringToIndex( str: string ): number {
let hash = 0;
for ( let i = 0; i < str.length; i++ ) {
hash = ( hash * 31 + str.charCodeAt( i ) ) | 0;
}
return Math.abs( hash );
}

export function SiteIcon( {
siteId,
iconColorIndex,
size = 16,
className,
}: {
siteId: string;
iconColorIndex?: number;
size?: number;
className?: string;
} ) {
const { siteIcons } = useThemeDetails();
const iconData = siteIcons[ siteId ];

if ( iconData ) {
return (
<img
src={ iconData }
width={ size }
height={ size }
alt=""
className={ cx( 'rounded-sm flex-shrink-0', className ) }
/>
);
}

const colorIndex =
typeof iconColorIndex === 'number' ? iconColorIndex : hashStringToIndex( siteId );
const bgColor = SITE_ICON_COLORS[ colorIndex % SITE_ICON_COLORS.length ];

return (
<div
className={ cx( 'rounded flex-shrink-0 flex items-center justify-center', className ) }
style={ { width: size, height: size, backgroundColor: bgColor } }
>
<WordPressLogoCircle size={ Math.round( size * 0.55 ) } color="currentColor" />
</div>
);
}
13 changes: 7 additions & 6 deletions apps/studio/src/components/site-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { Spinner } from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { useEffect, useState } from 'react';
import { XDebugIcon } from 'src/components/icons/xdebug-icon';
import { SiteIcon } from 'src/components/site-icon';
import { Tooltip } from 'src/components/tooltip';
import { useSyncSites } from 'src/hooks/sync-sites';
import { useContentTabs } from 'src/hooks/use-content-tabs';
import { useDeleteSite } from 'src/hooks/use-delete-site';
import { useImportExport } from 'src/hooks/use-import-export';
import { useSiteDetails } from 'src/hooks/use-site-details';
import { isMac, isWindows } from 'src/lib/app-globals';
import { isWindows } from 'src/lib/app-globals';
import { cx } from 'src/lib/cx';
import { getIpcApi } from 'src/lib/get-ipc-api';
import { supportedEditorConfig } from 'src/modules/user-settings/lib/editor';
Expand Down Expand Up @@ -208,8 +209,7 @@ function SiteItem( {
return (
<li
className={ cx(
'flex flex-row min-w-[168px] h-8 hover:bg-[#ffffff0C] rounded transition-all ms-1 items-center',
isMac() ? 'me-5' : 'me-4',
'flex flex-row min-w-[168px] pe-1 hover:bg-[#ffffff0C] rounded transition-all items-center',
isSelected && 'bg-[#ffffff19] hover:bg-[#ffffff19]',
isDragOver && 'bg-[#ffffff26]'
) }
Expand All @@ -222,11 +222,12 @@ function SiteItem( {
>
<button
type="button"
className="p-2 text-xs rounded-tl rounded-bl whitespace-nowrap overflow-hidden text-ellipsis w-full text-left rtl:text-right focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-a8c-blue-50"
className="flex flex-row items-center gap-2.5 p-2 text-xs rounded-tl rounded-bl whitespace-nowrap overflow-hidden text-ellipsis w-full text-left rtl:text-right focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-a8c-blue-50"
onClick={ () => {
setSelectedSiteId( site.id );
} }
>
<SiteIcon siteId={ site.id } iconColorIndex={ site.iconColorIndex } size={ 28 } />
{ site.name }
</button>
{ showSpinner ? (
Expand Down Expand Up @@ -386,11 +387,11 @@ export default function SiteMenu( { className }: SiteMenuProps ) {
scrollbarGutter: 'stable',
} }
className={ cx(
'w-full overflow-y-auto overflow-x-hidden flex flex-col gap-0.5 pb-4',
'w-full overflow-y-auto overflow-x-hidden flex flex-col gap-1 px-2 pb-4',
className
) }
>
<ul className="pt-px">
<ul className="pt-px flex flex-col gap-0.5">
{ sites.map( ( site, index ) => (
<SiteItem
key={ site.id }
Expand Down
13 changes: 11 additions & 2 deletions apps/studio/src/components/tests/main-sidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,17 @@ const siteDetailsMocked = {
stopServer: vi.fn(),
isSiteDeleting: vi.fn( () => false ),
};
vi.mock( 'src/hooks/use-site-details', () => ( {
useSiteDetails: () => ( { ...siteDetailsMocked } ),
vi.mock( 'src/hooks/use-site-details', async ( importOriginal ) => {
const actual = await importOriginal< typeof import('src/hooks/use-site-details') >();
return {
...actual,
useSiteDetails: () => ( { ...siteDetailsMocked } ),
};
} );

vi.mock( 'src/hooks/use-theme-details', () => ( {
useThemeDetails: () => ( { siteIcons: {} } ),
ThemeDetailsContext: { Provider: ( { children }: { children: React.ReactNode } ) => children },
} ) );

const renderWithProvider = ( children: React.ReactElement ) => {
Expand Down
2 changes: 1 addition & 1 deletion apps/studio/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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_WIDTH = 248;
export const MAIN_MIN_WIDTH = DEFAULT_WIDTH - SIDEBAR_WIDTH + 20;
export const APP_CHROME_SPACING = 10;
export const MIN_WIDTH_CLASS_TO_MEASURE = 'app-measure-tabs-width';
Expand Down
2 changes: 2 additions & 0 deletions apps/studio/src/hooks/use-site-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ export function SiteDetailsProvider( { children }: SiteDetailsProviderProps ) {
running: false,
isAddingSite: true,
phpVersion: '',
iconColorIndex: prevData.length % 8,
},
] )
);
Expand Down Expand Up @@ -524,6 +525,7 @@ export function SiteDetailsProvider( { children }: SiteDetailsProviderProps ) {
running: false,
isAddingSite: true,
phpVersion: sourceSite.phpVersion,
iconColorIndex: prevData.length % 8,
},
] )
);
Expand Down
38 changes: 32 additions & 6 deletions apps/studio/src/hooks/use-theme-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ import { getIpcApi } from 'src/lib/get-ipc-api';

type ThemeDetailsType = SiteDetails[ 'themeDetails' ] | undefined;
type ThumbnailType = string | undefined;
type SiteIconType = string | undefined;

interface ThemeDetailsContextType {
loadingThemeDetails: Record< string, boolean >;
loadingThumbnails: Record< string, boolean >;
themeDetails: Record< string, ThemeDetailsType >;
thumbnails: Record< string, ThumbnailType >;
siteIcons: Record< string, SiteIconType >;
initialLoading: boolean;
selectedThemeDetails: ThemeDetailsType;
selectedThumbnail: ThumbnailType;
selectedSiteIcon: SiteIconType;
selectedLoadingThemeDetails: boolean;
selectedLoadingThumbnails: boolean;
}
Expand All @@ -24,9 +27,11 @@ export const ThemeDetailsContext = createContext< ThemeDetailsContextType >( {
loadingThumbnails: {},
themeDetails: {},
thumbnails: {},
siteIcons: {},
initialLoading: false,
selectedThemeDetails: undefined,
selectedThumbnail: undefined,
selectedSiteIcon: undefined,
selectedLoadingThemeDetails: false,
selectedLoadingThumbnails: false,
} );
Expand All @@ -44,6 +49,7 @@ export const ThemeDetailsProvider: React.FC< ThemeDetailsProviderProps > = ( { c
{}
);
const [ loadingThumbnails, setLoadingThumbnails ] = useState< Record< string, boolean > >( {} );
const [ siteIcons, setSiteIcons ] = useState< Record< string, SiteIconType > >( {} );

useIpcListener( 'theme-details-loading', ( _evt, { id } ) => {
setLoadingThemeDetails( ( loadingThemeDetails ) => {
Expand Down Expand Up @@ -81,31 +87,48 @@ export const ThemeDetailsProvider: React.FC< ThemeDetailsProviderProps > = ( { c
} );
} );

useIpcListener( 'site-icon-loaded', ( _evt, { id, imageData } ) => {
setSiteIcons( ( siteIcons ) => {
return { ...siteIcons, [ id ]: imageData ?? undefined };
} );
} );

useWindowListener( 'focus', async () => {
// When the window is focused, we need to kick off a request to refetch the theme details, if server is running.
if ( ! selectedSite?.id || selectedSite.running === false ) {
return;
}
await getIpcApi().loadThemeDetails( selectedSite.id, false );
// When the window is focused, refetch theme details for all running sites.
const runningSites = sites.filter( ( site ) => site.running );
await Promise.all(
runningSites.map( ( site ) => getIpcApi().loadThemeDetails( site.id, false ) )
);
} );

useEffect( () => {
// When the selected site changes, refresh its theme details (including icon).
if ( selectedSite?.id && selectedSite.running ) {
void getIpcApi().loadThemeDetails( selectedSite.id, false );
}
}, [ selectedSite?.id ] ); // eslint-disable-line react-hooks/exhaustive-deps

useEffect( () => {
let isCurrent = true;
// Initial load. Prefetch all the thumbnails for the sites.
const run = async () => {
const newThemeDetails = { ...themeDetails };
const newThumbnailData = { ...thumbnails };
const newSiteIcons = { ...siteIcons };
for ( const site of sites ) {
if ( site.themeDetails ) {
newThemeDetails[ site.id ] = { ...site.themeDetails };
const thumbnailData = await getIpcApi().getThumbnailData( site.id );
newThumbnailData[ site.id ] = thumbnailData ?? undefined;
}
const iconData = await getIpcApi().getSiteIconData( site.id );
newSiteIcons[ site.id ] = iconData ?? undefined;
}
if ( isCurrent ) {
setInitialLoad( true );
setThemeDetails( newThemeDetails );
setThumbnails( newThumbnailData );
setSiteIcons( newSiteIcons );
}
};
if ( sites.length > 0 && ! loadingSites && ! initialLoad && isCurrent ) {
Expand All @@ -114,17 +137,19 @@ export const ThemeDetailsProvider: React.FC< ThemeDetailsProviderProps > = ( { c
return () => {
isCurrent = false;
};
}, [ initialLoad, loadingSites, sites, themeDetails, thumbnails ] );
}, [ initialLoad, loadingSites, sites, themeDetails, thumbnails, siteIcons ] );

const contextValue = useMemo( () => {
return {
thumbnails,
themeDetails,
siteIcons,
loadingThemeDetails,
loadingThumbnails,
initialLoading: ! initialLoad,
selectedThemeDetails: themeDetails[ selectedSite?.id ?? '' ],
selectedThumbnail: thumbnails[ selectedSite?.id ?? '' ],
selectedSiteIcon: siteIcons[ selectedSite?.id ?? '' ],
selectedLoadingThemeDetails: loadingThemeDetails[ selectedSite?.id ?? '' ],
selectedLoadingThumbnails: loadingThumbnails[ selectedSite?.id ?? '' ],
};
Expand All @@ -133,6 +158,7 @@ export const ThemeDetailsProvider: React.FC< ThemeDetailsProviderProps > = ( { c
loadingThemeDetails,
loadingThumbnails,
selectedSite?.id,
siteIcons,
themeDetails,
thumbnails,
] );
Expand Down
Loading