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
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>
);
}
105 changes: 105 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,105 @@
.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);
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 @@ -188,6 +194,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 @@ -221,7 +230,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 @@ -230,6 +239,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 @@ -243,6 +253,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
10 changes: 10 additions & 0 deletions apps/ui/src/components/site-overview-view/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,13 @@ 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 { 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 @@ -230,6 +233,7 @@ 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.
Expand Down Expand Up @@ -261,6 +265,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>;
}
46 changes: 44 additions & 2 deletions apps/ui/src/components/site-overview-view/style.module.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
.root {
--overview-content-width: 1120px;
--overview-card-column-width: 320px;

/* Anchor for the footer toolbar + its progressive blur. */
position: relative;
display: flex;
Expand Down Expand Up @@ -62,11 +65,21 @@

.tabsBarInner {
min-width: 0;
max-width: 760px;
max-width: var(--overview-content-width);
}

.panel {
outline: none;
display: grid;
grid-template-columns: minmax(0, var(--overview-card-column-width)) minmax(0, 1fr);
align-items: start;
gap: var(--wpds-dimension-padding-3xl);
width: 100%;
min-width: 0;
}

.panel > :not(.cardColumn, .actionsColumn) {
grid-column: 1 / -1;
}

.scroll {
Expand Down Expand Up @@ -109,9 +122,12 @@
}

.content {
container-type: inline-size;
container-name: overview;

box-sizing: border-box;
width: 100%;
max-width: 760px;
max-width: var(--overview-content-width);
/* Extra bottom padding keeps the last content clear of the overlaid
footer toolbar. */
padding: var(--wpds-dimension-padding-xl) var(--wpds-dimension-padding-xl)
Expand All @@ -126,6 +142,32 @@
color: var(--wpds-color-fg-content-neutral);
}

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

@container overview (max-width: 760px) {
.panel {
grid-template-columns: minmax(0, 1fr);
gap: var(--wpds-dimension-padding-2xl);
}

.panel > :not(.cardColumn, .actionsColumn) {
order: 0;
}

.cardColumn {
order: 2;
}

.actionsColumn {
order: 1;
}
}

/* Each section is a full-width row (Customize, Open in…, Manage), stacked. */
.actionsColumn {
display: flex;
Expand Down
Loading