From 2b3a1cc9c1ee17fe51a478a7e74d386df5382e6f Mon Sep 17 00:00:00 2001 From: Shaun Andrews Date: Thu, 6 Aug 2026 14:14:01 -0400 Subject: [PATCH 1/7] Add Open in actions to the site overview --- .../site-overview-view/index.test.tsx | 66 ++++++++++++++++++- .../components/site-overview-view/index.tsx | 66 ++++++++++++++++++- .../site-overview-view/style.module.css | 6 +- apps/ui/src/data/core/index.ts | 1 + 4 files changed, 135 insertions(+), 4 deletions(-) diff --git a/apps/ui/src/components/site-overview-view/index.test.tsx b/apps/ui/src/components/site-overview-view/index.test.tsx index 3837e8e6be..aaae92b8e0 100644 --- a/apps/ui/src/components/site-overview-view/index.test.tsx +++ b/apps/ui/src/components/site-overview-view/index.test.tsx @@ -16,11 +16,17 @@ import { useUpdateSite, useXdebugEnabledSite, } from '@/data/queries/use-sites'; +import { useUserPreferences } from '@/data/queries/use-user-preferences'; import { useWordPressVersions, useWpVersion } from '@/data/queries/use-wordpress-versions'; import { useOffline } from '@/hooks/use-offline'; import styles from './style.module.css'; import { SiteOverviewView } from './index'; -import type { SiteDetails } from '@/data/core'; +import type { + ConnectorCapabilities, + SiteDetails, + SupportedEditor, + UserPreferences, +} from '@/data/core'; const navigateMock = vi.fn(); const siteDropdownMock = vi.hoisted( () => vi.fn() ); @@ -88,6 +94,10 @@ vi.mock( '@/data/queries/use-sites', () => ( { useXdebugEnabledSite: vi.fn(), } ) ); +vi.mock( '@/data/queries/use-user-preferences', () => ( { + useUserPreferences: vi.fn(), +} ) ); + vi.mock( '@/data/queries/use-wordpress-versions', () => ( { useWordPressVersions: vi.fn(), useWpVersion: vi.fn(), @@ -118,18 +128,33 @@ const useSitesMock = vi.mocked( useSites, { partial: true } ); const useStartSiteMock = vi.mocked( useStartSite, { partial: true } ); const useUpdateSiteMock = vi.mocked( useUpdateSite, { partial: true } ); const useOfflineMock = vi.mocked( useOffline ); +const useUserPreferencesMock = vi.mocked( useUserPreferences, { partial: true } ); const useWordPressVersionsMock = vi.mocked( useWordPressVersions, { partial: true } ); const useWpVersionMock = vi.mocked( useWpVersion, { partial: true } ); const useXdebugEnabledSiteMock = vi.mocked( useXdebugEnabledSite, { partial: true } ); describe( 'SiteOverviewView', () => { const openSiteUrl = vi.fn().mockResolvedValue( undefined ); + const openSiteFolder = vi.fn().mockResolvedValue( undefined ); + const openSiteInEditor = vi.fn().mockResolvedValue( undefined ); + const openSiteInTerminal = vi.fn().mockResolvedValue( undefined ); const startSite = vi.fn().mockResolvedValue( undefined ); const copySite = vi.fn(); const exportFullSite = vi.fn(); const exportDatabase = vi.fn(); const onTabChange = vi.fn(); + const connectorStub = ( openInOS = true ) => ( { + openSiteUrl, + openSiteFolder, + openSiteInEditor, + openSiteInTerminal, + capabilities: { openInOS } as ConnectorCapabilities, + } ); + + const preferencesStub = ( editor: SupportedEditor | null ) => + ( { editor, terminal: 'terminal' } ) as UserPreferences; + beforeEach( () => { vi.clearAllMocks(); useSidebarCollapsedMock.mockReturnValue( false ); @@ -149,7 +174,8 @@ describe( 'SiteOverviewView', () => { } ) ), } ); - useConnectorMock.mockReturnValue( { openSiteUrl } ); + useConnectorMock.mockReturnValue( connectorStub() ); + useUserPreferencesMock.mockReturnValue( { data: preferencesStub( 'vscode' ) } ); useAgenticFeaturesMock.mockReturnValue( { enabled: true, chatEnabled: true, @@ -476,6 +502,42 @@ describe( 'SiteOverviewView', () => { expect( screen.queryByText( 'Site Editor' ) ).not.toBeInTheDocument(); } ); + it( 'offers the configured apps and phpMyAdmin under Open in…', () => { + renderView(); + + expect( screen.getByRole( 'heading', { name: 'Open in…' } ) ).toBeVisible(); + expect( screen.getByText( 'Finder' ) ).toBeVisible(); + expect( screen.getByText( 'Visual Studio Code' ) ).toBeVisible(); + expect( screen.getByText( 'Terminal' ) ).toBeVisible(); + expect( screen.queryByText( 'Browser' ) ).not.toBeInTheDocument(); + + fireEvent.click( screen.getByText( 'Finder' ).closest( 'button' )! ); + expect( openSiteFolder ).toHaveBeenCalledWith( 'site-1' ); + + fireEvent.click( screen.getByText( 'phpMyAdmin' ).closest( 'button' )! ); + expect( openSiteUrl ).toHaveBeenCalledWith( + 'site-1', + '/phpmyadmin/index.php?route=/database/structure&db=wordpress' + ); + } ); + + it( 'hides the editor shortcut until an editor is configured', () => { + useUserPreferencesMock.mockReturnValue( { data: preferencesStub( null ) } ); + + renderView(); + + expect( screen.queryByText( 'Visual Studio Code' ) ).not.toBeInTheDocument(); + expect( screen.getByText( 'Finder' ) ).toBeVisible(); + } ); + + it( 'drops the Open in… section on hosts that cannot open local apps', () => { + useConnectorMock.mockReturnValue( connectorStub( false ) ); + + renderView(); + + expect( screen.queryByRole( 'heading', { name: 'Open in…' } ) ).not.toBeInTheDocument(); + } ); + // Rendered without a SessionUIProvider, so the open-site-url hook takes // its browser fallback path; inside the app these open the preview panel. it( 'routes shortcuts through the connector', async () => { diff --git a/apps/ui/src/components/site-overview-view/index.tsx b/apps/ui/src/components/site-overview-view/index.tsx index 96c242a8fe..d024975d44 100644 --- a/apps/ui/src/components/site-overview-view/index.tsx +++ b/apps/ui/src/components/site-overview-view/index.tsx @@ -2,6 +2,7 @@ import { useNavigate } from '@tanstack/react-router'; import { __ } from '@wordpress/i18n'; import { desktop, + grid, Icon, layout, media, @@ -17,12 +18,16 @@ import { useState } from 'react'; import { AgenticSigninBanner } from '@/components/agentic-signin-banner'; import { DeleteSiteDialog } from '@/components/delete-site-dialog'; import { OfflineBanner } from '@/components/offline-banner'; +import { useOpenInDestinations } from '@/components/open-in-menu/use-open-in-destinations'; import { PreviewToggleButton } from '@/components/preview-toggle-button'; import { ProgressiveBlur } from '@/components/progressive-blur'; import { SiteDropdown } from '@/components/site-dropdown'; +import { DATABASE_HOME_PATH } from '@/components/site-preview/address-bar'; import { isSiteSettingsTab, SiteSettingsForm } from '@/components/site-settings-view'; 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 { useOpenSiteUrl } from '@/hooks/use-open-site-url'; import { useSidebarCollapsed } from '@/hooks/use-sidebar-collapsed'; import { useSiteManagementActions } from '@/hooks/use-site-management-actions'; @@ -47,6 +52,7 @@ interface OverviewButtonProps { loading?: boolean; loadingAnnouncement?: string; className?: string; + brandIcon?: boolean; } function OverviewHeader( { @@ -86,6 +92,7 @@ function OverviewButton( { loading, loadingAnnouncement, className, + brandIcon, }: OverviewButtonProps ) { return ( ); } @@ -122,7 +124,7 @@ function OverviewButton( { function ButtonSection( { title, children }: { title: string; children: ReactNode } ) { return (
-

{ title }

+

{ title }

{ children }
); diff --git a/apps/ui/src/components/site-overview-view/style.module.css b/apps/ui/src/components/site-overview-view/style.module.css index 0c8e4c3030..22c513887e 100644 --- a/apps/ui/src/components/site-overview-view/style.module.css +++ b/apps/ui/src/components/site-overview-view/style.module.css @@ -118,58 +118,61 @@ calc(var(--wpds-dimension-padding-2xl) + 46px); } -.buttonSection h2 { - margin: 0 0 var(--wpds-dimension-padding-md); +.columnHeading { + margin: 0; font-size: var(--wpds-typography-font-size-sm); line-height: var(--wpds-typography-line-height-sm); font-weight: var(--wpds-typography-font-weight-medium); color: var(--wpds-color-fg-content-neutral); } -/* Each section is a full-width row (Customize, then Manage), stacked. */ +/* Each section is a full-width row (Customize, Open in…, Manage), stacked. */ .actionsColumn { display: flex; flex-direction: column; width: 100%; min-width: 0; - gap: var(--wpds-dimension-padding-xl); + gap: var(--wpds-dimension-padding-3xl); } .buttonSection { + display: flex; + flex-direction: column; + gap: var(--wpds-dimension-padding-md); width: 100%; } -/* Buttons flow across the row and wrap to the next line as the panel narrows. */ +/* Equal-width cells reflow as the panel narrows, keeping every section aligned. */ .buttonGrid { - display: flex; - flex-wrap: wrap; - gap: var(--wpds-dimension-padding-xs); + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: var(--wpds-dimension-padding-md); } .overviewButton { - --wp-ui-button-height: 32px; + --wp-ui-button-height: 36px; display: inline-flex; align-items: center; justify-content: flex-start; - /* Never squeeze a button until its label wraps — wrap the whole button to - the next row instead. */ - flex: 0 0 auto; - gap: var(--wpds-dimension-padding-xs); + width: 100%; + gap: var(--wpds-dimension-padding-sm); min-width: 0; - padding: 0 var(--wpds-dimension-padding-sm); - border: 0; - border-radius: 4px; + padding: 0 var(--wpds-dimension-padding-md); + border: var(--wpds-border-width-xs) solid var(--wpds-color-stroke-surface-neutral); + border-radius: var(--wpds-border-radius-sm); background: transparent; color: var(--wpds-color-fg-content-neutral); text-align: start; } .overviewButton:not([data-disabled]):is(:hover, :focus-visible) { - background: var( - --wpds-color-bg-interactive-neutral-weak-active, - var(--wpds-color-bg-surface-neutral-weak) - ); + border-color: var(--wpds-color-stroke-interactive-neutral); + background: var(--wpds-color-bg-interactive-neutral-weak-active); +} + +.overviewButton[data-disabled] { + border-color: var(--wpds-color-stroke-surface-neutral-weak); } .overviewButtonIcon { @@ -191,7 +194,9 @@ } .overviewButtonLabel { + overflow: hidden; white-space: nowrap; + text-overflow: ellipsis; font-size: var(--wpds-typography-font-size-sm); line-height: var(--wpds-typography-line-height-sm); color: var(--wpds-color-fg-content-neutral); @@ -199,7 +204,7 @@ .destructiveButton .overviewButtonIcon, .destructiveButton .overviewButtonLabel { - color: var(--wpds-color-fg-content-error, var(--wpds-color-fg-content-neutral)); + color: var(--wpds-color-fg-content-error); } @media (max-width: 760px) { From 6ed3e529f1c4af7a32aef13c9a33d95214220776 Mon Sep 17 00:00:00 2001 From: Shaun Andrews Date: Thu, 6 Aug 2026 15:39:08 -0400 Subject: [PATCH 4/7] Fix overview shortcut interaction states --- .../components/site-overview-view/style.module.css | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/ui/src/components/site-overview-view/style.module.css b/apps/ui/src/components/site-overview-view/style.module.css index 22c513887e..d75c84207c 100644 --- a/apps/ui/src/components/site-overview-view/style.module.css +++ b/apps/ui/src/components/site-overview-view/style.module.css @@ -151,6 +151,8 @@ .overviewButton { --wp-ui-button-height: 36px; + --wp-ui-button-border-color: var(--wpds-color-stroke-surface-neutral); + --wp-ui-button-border-color-active: var(--wpds-color-stroke-interactive-neutral); display: inline-flex; align-items: center; @@ -207,6 +209,17 @@ color: var(--wpds-color-fg-content-error); } +.destructiveButton { + --wp-ui-button-background-color-active: var(--wpds-color-bg-surface-error-weak); + --wp-ui-button-border-color-active: var(--wpds-color-stroke-surface-error); + --wp-ui-button-foreground-color-active: var(--wpds-color-fg-content-error); +} + +.destructiveButton:not([data-disabled]):is(:hover, :focus-visible, :active) { + border-color: var(--wpds-color-stroke-surface-error); + background: var(--wpds-color-bg-surface-error-weak); +} + @media (max-width: 760px) { .content { padding: var(--wpds-dimension-padding-xl) var(--wpds-dimension-padding-md) From 4d0e026914a5421d9945c887959a7522911a4de7 Mon Sep 17 00:00:00 2001 From: Shaun Andrews Date: Thu, 6 Aug 2026 15:50:41 -0400 Subject: [PATCH 5/7] Add site overview About summary --- .../site-overview-view/about-section.tsx | 29 ++++++++++++ .../site-overview-view/cards.module.css | 45 ++++++++++++++++++ .../site-overview-view/index.test.tsx | 16 ++++++- .../components/site-overview-view/index.tsx | 10 ++++ .../site-overview-view/overview-card.tsx | 10 ++++ .../site-overview-view/style.module.css | 46 ++++++++++++++++++- 6 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 apps/ui/src/components/site-overview-view/about-section.tsx create mode 100644 apps/ui/src/components/site-overview-view/cards.module.css create mode 100644 apps/ui/src/components/site-overview-view/overview-card.tsx diff --git a/apps/ui/src/components/site-overview-view/about-section.tsx b/apps/ui/src/components/site-overview-view/about-section.tsx new file mode 100644 index 0000000000..0cd0d83ec5 --- /dev/null +++ b/apps/ui/src/components/site-overview-view/about-section.tsx @@ -0,0 +1,29 @@ +import { __ } from '@wordpress/i18n'; +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 themeName = site.themeDetails?.name || site.themeDetails?.slug || '—'; + + return ( + +
+ + + +
+
+ ); +} + +function Tile( { label, value }: { label: string; value: string } ) { + return ( +
+ { label } + + { value } + +
+ ); +} diff --git a/apps/ui/src/components/site-overview-view/cards.module.css b/apps/ui/src/components/site-overview-view/cards.module.css new file mode 100644 index 0000000000..4d09e2a5da --- /dev/null +++ b/apps/ui/src/components/site-overview-view/cards.module.css @@ -0,0 +1,45 @@ +.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; +} + +.tiles { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(72px, 1fr)); + gap: var(--wpds-dimension-padding-md); +} + +.tile { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.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); +} + +.tileValue { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + font-size: var(--wpds-typography-font-size-sm); + line-height: var(--wpds-typography-line-height-sm); + color: var(--wpds-color-fg-content-neutral); +} diff --git a/apps/ui/src/components/site-overview-view/index.test.tsx b/apps/ui/src/components/site-overview-view/index.test.tsx index aaae92b8e0..7d47ff656e 100644 --- a/apps/ui/src/components/site-overview-view/index.test.tsx +++ b/apps/ui/src/components/site-overview-view/index.test.tsx @@ -221,7 +221,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( @@ -230,6 +230,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(); @@ -243,6 +244,19 @@ describe( 'SiteOverviewView', () => { expect( screen.queryByDisplayValue( 'Demo Site' ) ).not.toBeInTheDocument(); } ); + it( 'summarizes the site theme and runtime versions', () => { + useWpVersionMock.mockReturnValue( { data: '6.8.2' } ); + + renderView(); + + expect( screen.getByText( 'Theme' ) ).toBeVisible(); + expect( screen.getByText( 'Twenty Twenty-Six' ) ).toBeVisible(); + expect( screen.getByText( 'WordPress' ) ).toBeVisible(); + expect( screen.getByText( '6.8.2' ) ).toBeVisible(); + expect( screen.getByText( 'PHP' ) ).toBeVisible(); + expect( screen.getByText( '8.4' ) ).toBeVisible(); + } ); + it( 'offsets the site menu below macOS traffic lights when the sidebar is collapsed', () => { useSidebarCollapsedMock.mockReturnValue( true ); useTrafficLightSpaceMock.mockReturnValue( { start: true, end: false } ); diff --git a/apps/ui/src/components/site-overview-view/index.tsx b/apps/ui/src/components/site-overview-view/index.tsx index caceaff26e..27b237d273 100644 --- a/apps/ui/src/components/site-overview-view/index.tsx +++ b/apps/ui/src/components/site-overview-view/index.tsx @@ -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'; @@ -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. @@ -261,6 +265,12 @@ function SiteOverviewBody( { +
+

{ __( 'About' ) }

+ + + +
{ isBlockTheme ? ( diff --git a/apps/ui/src/components/site-overview-view/overview-card.tsx b/apps/ui/src/components/site-overview-view/overview-card.tsx new file mode 100644 index 0000000000..b88a6f2172 --- /dev/null +++ b/apps/ui/src/components/site-overview-view/overview-card.tsx @@ -0,0 +1,10 @@ +import styles from './cards.module.css'; +import type { ReactNode } from 'react'; + +export function OverviewCard( { children }: { children: ReactNode } ) { + return
{ children }
; +} + +export function CardSection( { children }: { children: ReactNode } ) { + return
{ children }
; +} diff --git a/apps/ui/src/components/site-overview-view/style.module.css b/apps/ui/src/components/site-overview-view/style.module.css index d75c84207c..1eafd5f4cf 100644 --- a/apps/ui/src/components/site-overview-view/style.module.css +++ b/apps/ui/src/components/site-overview-view/style.module.css @@ -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; @@ -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 { @@ -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) @@ -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; From 4b89ecf3edd133c3dd18d6e190f4de83b56b4edd Mon Sep 17 00:00:00 2001 From: Shaun Andrews Date: Thu, 6 Aug 2026 16:02:31 -0400 Subject: [PATCH 6/7] Refine About theme summary --- .../site-overview-view/about-section.tsx | 80 ++++++++++++++---- .../site-overview-view/cards.module.css | 84 ++++++++++++++++--- .../site-overview-view/index.test.tsx | 37 ++++++-- .../ui/src/data/queries/use-site-thumbnail.ts | 15 ++++ 4 files changed, 183 insertions(+), 33 deletions(-) create mode 100644 apps/ui/src/data/queries/use-site-thumbnail.ts diff --git a/apps/ui/src/components/site-overview-view/about-section.tsx b/apps/ui/src/components/site-overview-view/about-section.tsx index 0cd0d83ec5..ade97fbfaa 100644 --- a/apps/ui/src/components/site-overview-view/about-section.tsx +++ b/apps/ui/src/components/site-overview-view/about-section.tsx @@ -1,29 +1,77 @@ -import { __ } from '@wordpress/i18n'; +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 ( -
- - - +
+ +
+ { __( 'Theme' ) } + { themeName } + + { wpLabel } + + { phpLabel } + +
); } - -function Tile( { label, value }: { label: string; value: string } ) { - return ( -
- { label } - - { value } - -
- ); -} diff --git a/apps/ui/src/components/site-overview-view/cards.module.css b/apps/ui/src/components/site-overview-view/cards.module.css index 4d09e2a5da..6eae92f8f9 100644 --- a/apps/ui/src/components/site-overview-view/cards.module.css +++ b/apps/ui/src/components/site-overview-view/cards.module.css @@ -16,30 +16,90 @@ min-width: 0; } -.tiles { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(72px, 1fr)); +.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; } -.tile { +.thumbnail:disabled { + cursor: not-allowed; +} + +.themeDetails { display: flex; flex-direction: column; gap: 2px; min-width: 0; } -.tileLabel { +.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); } -.tileValue { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - font-size: var(--wpds-typography-font-size-sm); - line-height: var(--wpds-typography-line-height-sm); - color: var(--wpds-color-fg-content-neutral); +.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); } diff --git a/apps/ui/src/components/site-overview-view/index.test.tsx b/apps/ui/src/components/site-overview-view/index.test.tsx index 7d47ff656e..a9f1bd13e7 100644 --- a/apps/ui/src/components/site-overview-view/index.test.tsx +++ b/apps/ui/src/components/site-overview-view/index.test.tsx @@ -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, @@ -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(), } ) ); @@ -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 } ); @@ -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( { @@ -244,17 +253,35 @@ describe( 'SiteOverviewView', () => { expect( screen.queryByDisplayValue( 'Demo Site' ) ).not.toBeInTheDocument(); } ); - it( 'summarizes the site theme and runtime versions', () => { + 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( 'WordPress' ) ).toBeVisible(); - expect( screen.getByText( '6.8.2' ) ).toBeVisible(); - expect( screen.getByText( 'PHP' ) ).toBeVisible(); - expect( screen.getByText( '8.4' ) ).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', () => { diff --git a/apps/ui/src/data/queries/use-site-thumbnail.ts b/apps/ui/src/data/queries/use-site-thumbnail.ts new file mode 100644 index 0000000000..7276d2f6cb --- /dev/null +++ b/apps/ui/src/data/queries/use-site-thumbnail.ts @@ -0,0 +1,15 @@ +import { useQuery } from '@tanstack/react-query'; +import { useConnector } from '@/data/core'; + +export const siteThumbnailQueryKey = ( siteId: string ) => + [ 'site-preview-thumbnail', siteId ] as const; + +export function useSiteThumbnail( siteId: string ) { + const connector = useConnector(); + + return useQuery( { + queryKey: siteThumbnailQueryKey( siteId ), + queryFn: () => connector.getSiteThumbnail( siteId ), + meta: { persist: false }, + } ); +} From 8a8649102de3466ab15b61da6318be16d45dbf84 Mon Sep 17 00:00:00 2001 From: bcotrim Date: Mon, 10 Aug 2026 17:56:23 +0100 Subject: [PATCH 7/7] Match the thumbnail overlay icon color to its label --- apps/ui/src/components/site-overview-view/cards.module.css | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/ui/src/components/site-overview-view/cards.module.css b/apps/ui/src/components/site-overview-view/cards.module.css index 6eae92f8f9..1a9f69d25b 100644 --- a/apps/ui/src/components/site-overview-view/cards.module.css +++ b/apps/ui/src/components/site-overview-view/cards.module.css @@ -53,6 +53,7 @@ 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;