Skip to content
Merged
77 changes: 77 additions & 0 deletions apps/ui/src/components/site-overview-view/about-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { __, sprintf } from '@wordpress/i18n';
import { external, Icon } from '@wordpress/icons';
import { useConnector } from '@/data/core';
import { useSiteThumbnail } from '@/data/queries/use-site-thumbnail';
import { useIsSiteStarting, useStartSite } from '@/data/queries/use-sites';
import styles from './cards.module.css';
import { CardSection } from './overview-card';
import type { SiteDetails } from '@/data/core';

export function AboutSection( { site, wpVersion }: { site: SiteDetails; wpVersion?: string } ) {
const connector = useConnector();
const startSite = useStartSite();
const isStarting = useIsSiteStarting( site.id );
const themeName = site.themeDetails?.name || site.themeDetails?.slug || '—';
const thumbnail = useSiteThumbnail( site.id );
const wpLabel = wpVersion
? sprintf(
/* translators: %s: WordPress version number */
__( 'WP v%s' ),
wpVersion
)
: __( 'WP —' );
const phpLabel = sprintf(
/* translators: %s: PHP version number */
__( 'PHP v%s' ),
site.phpVersion
);
const openSiteInBrowser = async () => {
try {
if ( ! site.running ) {
await startSite.mutateAsync( site.id );
}
await connector.openSiteUrl( site.id, '/', { autoLogin: false } );
} catch ( error ) {
console.error( 'Failed to open site in browser:', error );
}
};

return (
<CardSection>
<div className={ styles.themeSummary }>
<button
type="button"
className={ styles.thumbnail }
data-empty={ thumbnail.data ? undefined : true }
disabled={ isStarting }
aria-label={ __( 'Open site in browser' ) }
onClick={ () => void openSiteInBrowser() }
>
{ thumbnail.data ? (
<img
src={ thumbnail.data }
alt={ sprintf(
/* translators: %s: site name */
__( 'Screenshot of %s' ),
site.name
) }
/>
) : null }
<span className={ styles.thumbnailOverlay } aria-hidden="true">
{ __( 'Open site' ) }
<Icon icon={ external } size={ 16 } />
</span>
</button>
<div className={ styles.themeDetails }>
<span className={ styles.tileLabel }>{ __( 'Theme' ) }</span>
<span className={ styles.themeValue }>{ themeName }</span>
<span className={ styles.themeMeta }>
<span>{ wpLabel }</span>
<span aria-hidden="true">•</span>
<span>{ phpLabel }</span>
</span>
</div>
</div>
</CardSection>
);
}
106 changes: 106 additions & 0 deletions apps/ui/src/components/site-overview-view/cards.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
.card {
display: flex;
flex-direction: column;
box-sizing: border-box;
border: var(--wpds-border-width-xs) solid var(--wpds-color-stroke-surface-neutral);
border-radius: var(--wpds-border-radius-md);
background: var(--wpds-color-bg-surface-neutral);
min-width: 0;
}

.cardSection {
display: flex;
flex-direction: column;
gap: var(--wpds-dimension-padding-md);
padding: var(--wpds-dimension-padding-lg);
min-width: 0;
}

.themeSummary {
display: flex;
align-items: center;
gap: var(--wpds-dimension-padding-md);
min-width: 0;
}

.thumbnail {
position: relative;
appearance: none;
width: 88px;
aspect-ratio: 4 / 3;
overflow: hidden;
flex: 0 0 auto;
padding: 0;
border: var(--wpds-border-width-xs) solid var(--wpds-color-stroke-surface-neutral);
border-radius: var(--wpds-border-radius-sm);
background: var(--wpds-color-bg-surface-neutral-strong);
cursor: var(--wpds-cursor-control);
}

.thumbnail img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}

.thumbnailOverlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
gap: var(--wpds-dimension-padding-xs);
background: var(--wpds-color-bg-interactive-neutral-weak-active);
color: var(--wpds-color-fg-content-neutral);
fill: currentColor;
font-size: var(--wpds-typography-font-size-xs);
font-weight: var(--wpds-typography-font-weight-medium);
opacity: 0;
transition: opacity 120ms ease;
}

.thumbnail:hover .thumbnailOverlay,
.thumbnail:focus-visible .thumbnailOverlay,
.thumbnail[data-empty] .thumbnailOverlay {
opacity: 0.94;
}

.thumbnail:focus-visible {
outline: var(--wpds-border-width-focus) solid var(--wpds-color-stroke-focus-brand);
outline-offset: 2px;
}

.thumbnail:disabled {
cursor: not-allowed;
}

.themeDetails {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}

.themeValue {
overflow-wrap: anywhere;
font-size: var(--wpds-typography-font-size-sm);
line-height: var(--wpds-typography-line-height-sm);
color: var(--wpds-color-fg-content-neutral);
}

.themeMeta {
display: flex;
align-items: center;
flex-wrap: wrap;
column-gap: var(--wpds-dimension-padding-xs);
font-size: var(--wpds-typography-font-size-xs);
line-height: var(--wpds-typography-line-height-xs);
color: var(--wpds-color-fg-content-neutral-weak);
}

.tileLabel {
font-size: var(--wpds-typography-font-size-xs);
line-height: var(--wpds-typography-line-height-xs);
color: var(--wpds-color-fg-content-neutral-weak);
}
43 changes: 42 additions & 1 deletion apps/ui/src/components/site-overview-view/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useConnector } from '@/data/core';
import { useAgenticFeatures } from '@/data/queries/use-agentic-features';
import { useLogin } from '@/data/queries/use-auth-user';
import { useExistingCustomDomains } from '@/data/queries/use-create-site-helpers';
import { useSiteThumbnail } from '@/data/queries/use-site-thumbnail';
import {
useCopySite,
useExportDatabase,
Expand Down Expand Up @@ -94,6 +95,10 @@ vi.mock( '@/data/queries/use-sites', () => ( {
useXdebugEnabledSite: vi.fn(),
} ) );

vi.mock( '@/data/queries/use-site-thumbnail', () => ( {
useSiteThumbnail: vi.fn(),
} ) );

vi.mock( '@/data/queries/use-user-preferences', () => ( {
useUserPreferences: vi.fn(),
} ) );
Expand Down Expand Up @@ -124,6 +129,7 @@ const useExportDatabaseMock = vi.mocked( useExportDatabase, { partial: true } );
const useExportFullSiteMock = vi.mocked( useExportFullSite, { partial: true } );
const useIsSiteStartingMock = vi.mocked( useIsSiteStarting );
const useIsSiteStoppingMock = vi.mocked( useIsSiteStopping );
const useSiteThumbnailMock = vi.mocked( useSiteThumbnail, { partial: true } );
const useSitesMock = vi.mocked( useSites, { partial: true } );
const useStartSiteMock = vi.mocked( useStartSite, { partial: true } );
const useUpdateSiteMock = vi.mocked( useUpdateSite, { partial: true } );
Expand Down Expand Up @@ -190,6 +196,9 @@ describe( 'SiteOverviewView', () => {
data: [ createSite( { running: true } ) ],
isLoading: false,
} );
useSiteThumbnailMock.mockReturnValue( {
data: 'data:image/png;base64,site-thumbnail',
} );
useIsSiteStartingMock.mockReturnValue( false );
useIsSiteStoppingMock.mockReturnValue( false );
useStartSiteMock.mockReturnValue( {
Expand Down Expand Up @@ -223,7 +232,7 @@ describe( 'SiteOverviewView', () => {
);
}

it( 'renders the tab strip with the customize and manage sections', () => {
it( 'renders the tab strip with the about, customize, and manage sections', () => {
renderView();

expect( siteDropdownMock ).toHaveBeenCalledWith(
Expand All @@ -232,6 +241,7 @@ describe( 'SiteOverviewView', () => {
expect( screen.getByRole( 'tab', { name: 'Overview' } ) ).toBeVisible();
expect( screen.getByRole( 'tab', { name: 'Settings' } ) ).toBeVisible();
expect( screen.getByRole( 'tab', { name: 'Debugging' } ) ).toBeVisible();
expect( screen.getByRole( 'heading', { name: 'About' } ) ).toBeVisible();
expect( screen.getByRole( 'heading', { name: 'Customize' } ) ).toBeVisible();
expect( screen.getByRole( 'heading', { name: 'Manage' } ) ).toBeVisible();
expect( screen.getByText( 'Site Editor' ) ).toBeVisible();
Expand All @@ -245,6 +255,37 @@ describe( 'SiteOverviewView', () => {
expect( screen.queryByDisplayValue( 'Demo Site' ) ).not.toBeInTheDocument();
} );

it( 'summarizes the site theme and runtime versions', async () => {
useWpVersionMock.mockReturnValue( { data: '6.8.2' } );

renderView();

expect( screen.getByText( 'Theme' ) ).toBeVisible();
expect( screen.getByText( 'Twenty Twenty-Six' ) ).toBeVisible();
expect( screen.getByText( 'WP v6.8.2' ) ).toBeVisible();
expect( screen.getByText( 'PHP v8.4' ) ).toBeVisible();
expect( await screen.findByRole( 'img', { name: 'Screenshot of Demo Site' } ) ).toHaveAttribute(
'src',
'data:image/png;base64,site-thumbnail'
);

fireEvent.click( screen.getByRole( 'button', { name: 'Open site in browser' } ) );
await waitFor( () =>
expect( openSiteUrl ).toHaveBeenCalledWith( 'site-1', '/', { autoLogin: false } )
);
} );

it( 'keeps the browser action available without a cached thumbnail', () => {
useSiteThumbnailMock.mockReturnValue( { data: null } );

renderView();

expect( screen.getByRole( 'button', { name: 'Open site in browser' } ) ).toBeVisible();
expect(
screen.queryByRole( 'img', { name: 'Screenshot of Demo Site' } )
).not.toBeInTheDocument();
} );

it( 'offsets the site menu below macOS traffic lights when the sidebar is collapsed', () => {
useSidebarCollapsedMock.mockReturnValue( true );
useTrafficLightSpaceMock.mockReturnValue( { start: true, end: false } );
Expand Down
12 changes: 11 additions & 1 deletion apps/ui/src/components/site-overview-view/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,14 @@ import * as Tabs from '@/components/tabs';
import { useConnector } from '@/data/core';
import { useIsSiteStarting, useIsSiteStopping, useSites } from '@/data/queries/use-sites';
import { useUserPreferences } from '@/data/queries/use-user-preferences';
import { useWpVersion } from '@/data/queries/use-wordpress-versions';
import { useOpenSiteUrl } from '@/hooks/use-open-site-url';
import { useSidebarCollapsed } from '@/hooks/use-sidebar-collapsed';
import { useSiteManagementActions } from '@/hooks/use-site-management-actions';
import { useTrafficLightSpace } from '@/hooks/use-traffic-light-space';
import { databaseLogo } from '@/lib/logos';
import { AboutSection } from './about-section';
import { OverviewCard } from './overview-card';
import styles from './style.module.css';
import type { SiteSettingsTabId } from '@/components/site-settings-view';
import type { SiteDetails } from '@/data/core';
Expand Down Expand Up @@ -226,6 +229,7 @@ function SiteOverviewBody( {
onTabChange: ( tab: SiteSettingsTabId ) => void;
} ) {
const navigate = useNavigate();
const connector = useConnector();
const isStarting = useIsSiteStarting( site.id );
const isStopping = useIsSiteStopping( site.id );
const [ deleteOpen, setDeleteOpen ] = useState( false );
Expand All @@ -236,12 +240,12 @@ function SiteOverviewBody( {
const busy = isStarting || isStopping;
const themeDetails = site.themeDetails;
const isBlockTheme = themeDetails?.isBlockTheme === true;
const { data: wpVersion } = useWpVersion( site.id );

// Opens WordPress screens in the in-app preview panel (starting the site
// first when needed) rather than the external browser.
const openSiteUrl = useOpenSiteUrl( site );

const connector = useConnector();
const openCustomize = ( url: string, entryPoint: TracksCustomizeEntryPoint ) => {
// The agentic UI opens customize screens in its in-app preview panel, not the OS browser.
void connector.trackEvent( TRACKS_EVENTS.SITE_OPEN_CUSTOMIZE, {
Expand Down Expand Up @@ -277,6 +281,12 @@ function SiteOverviewBody( {
<Tabs.Panel tabId="overview" className={ styles.panel }>
<OfflineBanner />
<AgenticSigninBanner />
<div className={ styles.cardColumn }>
<h2 className={ styles.columnHeading }>{ __( 'About' ) }</h2>
<OverviewCard>
<AboutSection site={ site } wpVersion={ wpVersion } />
</OverviewCard>
</div>
<div className={ styles.actionsColumn }>
<ButtonSection title={ __( 'Customize' ) }>
{ isBlockTheme ? (
Expand Down
10 changes: 10 additions & 0 deletions apps/ui/src/components/site-overview-view/overview-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import styles from './cards.module.css';
import type { ReactNode } from 'react';

export function OverviewCard( { children }: { children: ReactNode } ) {
return <div className={ styles.card }>{ children }</div>;
}

export function CardSection( { children }: { children: ReactNode } ) {
return <section className={ styles.cardSection }>{ children }</section>;
}
Loading