Skip to content
15 changes: 15 additions & 0 deletions apps/ui/src/components/site-dropdown/main-view.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,21 @@
word-break: break-word;
}

.xdebugBadge {
display: inline-flex;
align-items: center;
color: var(--studio-color-status-running);
}

.xdebugBadge_stopped {
color: var(--wpds-color-fg-content-neutral-weak);
}

.xdebugGlyph {
width: 16px;
height: 16px;
}

.rowActions {
display: flex;
align-items: center;
Expand Down
15 changes: 13 additions & 2 deletions apps/ui/src/components/site-dropdown/main-view.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ const site: SiteDetails = {
phpVersion: '8.3',
};

function renderMainView() {
function renderMainView( siteOverrides: Partial< SiteDetails > = {} ) {
return render(
<MainView
site={ site }
site={ { ...site, ...siteOverrides } }
activity={ null }
onSetupClick={ vi.fn() }
onDisconnectClick={ vi.fn() }
Expand All @@ -91,6 +91,17 @@ describe( 'MainView', () => {
connectedSites.splice( 0, connectedSites.length );
} );

it( 'shows an Xdebug badge on the Studio row only when Xdebug is enabled', () => {
const { unmount } = renderMainView( { enableXdebug: true } );

expect( screen.getByRole( 'img', { name: 'Xdebug enabled' } ) ).toBeInTheDocument();

unmount();
renderMainView();

expect( screen.queryByRole( 'img', { name: 'Xdebug enabled' } ) ).not.toBeInTheDocument();
} );

it( 'handles preview URL copy failures', async () => {
const error = new Error( 'Clipboard denied' );
const consoleError = vi.spyOn( console, 'error' ).mockImplementation( () => undefined );
Expand Down
33 changes: 32 additions & 1 deletion apps/ui/src/components/site-dropdown/main-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Button, IconButton, Tooltip } from '@wordpress/ui';
import { clsx } from 'clsx';
import { useMemo } from 'react';
import * as Menu from '@/components/menu';
import { XdebugIcon } from '@/components/xdebug-icon';
import { useConnector } from '@/data/core';
import { useAgenticFeatures } from '@/data/queries/use-agentic-features';
import { useLogin } from '@/data/queries/use-auth-user';
Expand Down Expand Up @@ -204,7 +205,16 @@ export function MainView( { site, activity, onSetupClick, onDisconnectClick }: P
{ activity?.kind === 'error' ? <SyncActivityError activity={ activity } /> : null }

<PopoverRow
label={ __( 'Studio' ) }
label={
site.enableXdebug ? (
<>
{ __( 'Studio' ) }
<XdebugBadge running={ site.running } />
</>
) : (
__( 'Studio' )
)
}
sublabel={
canOpenLocalSite
? renderUrlLink( {
Expand Down Expand Up @@ -355,6 +365,27 @@ export function MainView( { site, activity, onSetupClick, onDisconnectClick }: P
);
}

function XdebugBadge( { running }: { running: boolean } ) {
const label = __( 'Xdebug enabled' );

return (
<Tooltip.Root>
<Tooltip.Trigger
render={
<span
className={ clsx( styles.xdebugBadge, ! running && styles.xdebugBadge_stopped ) }
role="img"
aria-label={ label }
/>
}
>
<XdebugIcon className={ styles.xdebugGlyph } />
</Tooltip.Trigger>
<Tooltip.Popup positioner={ <Tooltip.Positioner side="top" /> }>{ label }</Tooltip.Popup>
</Tooltip.Root>
);
}

function SyncActivityError( {
activity,
}: {
Expand Down
66 changes: 66 additions & 0 deletions apps/ui/src/components/site-list/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,72 @@ describe( 'SiteList', () => {
expect( runningSiteClassName ).not.toContain( 'siteNameStopped' );
} );

it( 'replaces the status dot with the Xdebug glyph on the Xdebug-enabled site', () => {
useSitesMock.mockReturnValue( {
data: [
createSite( {
id: 'xdebug-site',
name: 'Xdebug Site',
path: '/Users/example/Studio/xdebug-site',
running: true,
enableXdebug: true,
} ),
createSite( {
id: 'plain-site',
name: 'Plain Site',
path: '/Users/example/Studio/plain-site',
running: true,
} ),
],
isLoading: false,
} );

render( <SiteList /> );

const xdebugButton = screen.getByRole( 'button', {
name: 'Site status: Running. Xdebug enabled. Stop site',
} );
const xdebugGlyph = xdebugButton.querySelector( 'svg:first-of-type' );
const plainButton = screen.getByRole( 'button', {
name: 'Site status: Running. Stop site',
} );

expect( xdebugGlyph ).toHaveAttribute( 'viewBox', '0 0 24 24' );
expect( xdebugGlyph?.querySelector( 'rect' ) ).not.toBeInTheDocument();
expect( plainButton ).not.toHaveAttribute( 'data-xdebug' );
expect( plainButton.querySelector( 'svg:first-of-type rect' ) ).toBeInTheDocument();

fireEvent.click( xdebugButton );
expect( stopSite ).toHaveBeenCalledWith( 'xdebug-site' );
} );

it( 'keeps the greyed Xdebug glyph visible while the site is stopped', () => {
useSitesMock.mockReturnValue( {
data: [
createSite( {
id: 'xdebug-site',
name: 'Xdebug Site',
path: '/Users/example/Studio/xdebug-site',
running: false,
enableXdebug: true,
} ),
],
isLoading: false,
} );

render( <SiteList /> );

const button = screen.getByRole( 'button', {
name: 'Site status: Stopped. Xdebug enabled. Start site',
} );

// The stopped-row CSS hides the status button unless `data-xdebug` is
// set alongside `data-state`; assert that DOM contract.
expect( button ).toHaveAttribute( 'data-state', 'stopped' );
expect( button ).toHaveAttribute( 'data-xdebug' );
expect( button.querySelector( 'svg:first-of-type' ) ).toHaveAttribute( 'viewBox', '0 0 24 24' );
} );

it( 'marks the site row as current for the active chat', () => {
paramsMock = { sessionId: 'stopped-chat' };
pathnameMock = '/sessions/stopped-chat';
Expand Down
37 changes: 24 additions & 13 deletions apps/ui/src/components/site-list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import * as Menu from '@/components/menu';
import { ReorderableList } from '@/components/reorderable-list';
import { SidebarButton } from '@/components/sidebar-button';
import { deriveSiteStatus } from '@/components/site-dropdown/utils';
import { XdebugIcon } from '@/components/xdebug-icon';
import { useConnector } from '@/data/core';
import { useSiteAgentActivity, type SiteAgentActivity } from '@/data/queries/use-agent-run';
import { useAgenticFeatures } from '@/data/queries/use-agentic-features';
Expand Down Expand Up @@ -244,7 +245,10 @@ function SiteStatusButton( {
? __( 'Stopping' )
: __( 'Starting' )
: __( 'Stopped' );
const tooltipLabel = sprintf( __( 'Site status: %s' ), statusName );
const xdebug = Boolean( site.enableXdebug );
const tooltipLabel = xdebug
? sprintf( __( 'Site status: %s. Xdebug enabled' ), statusName )
: sprintf( __( 'Site status: %s' ), statusName );
const actionLabel = site.running ? __( 'Stop site' ) : __( 'Start site' );
const label = busy ? tooltipLabel : sprintf( __( '%1$s. %2$s' ), tooltipLabel, actionLabel );
const handleClick = ( event: MouseEvent< HTMLButtonElement > ) => {
Expand All @@ -270,20 +274,27 @@ function SiteStatusButton( {
aria-busy={ busy || undefined }
aria-disabled={ busy || undefined }
data-state={ status }
data-xdebug={ xdebug || undefined }
onClick={ handleClick }
>
<svg
className={ styles.siteStatusGlyph }
viewBox={ status === 'stopped' ? '0 0 10 10' : '0 0 8 8' }
aria-hidden="true"
focusable="false"
>
{ status === 'stopped' ? (
<path className={ styles.siteStatusPlayShape } d="M2.5 1 L9 5 L2.5 9 Z" />
) : (
<rect className={ styles.siteStatusShape } x="0" y="0" width="8" height="8" />
) }
</svg>
{ xdebug ? (
<XdebugIcon
className={ clsx( styles.siteStatusGlyph, styles.siteStatusXdebugGlyph ) }
/>
) : (
<svg
className={ styles.siteStatusGlyph }
viewBox={ status === 'stopped' ? '0 0 10 10' : '0 0 8 8' }
aria-hidden="true"
focusable="false"
>
{ status === 'stopped' ? (
<path className={ styles.siteStatusPlayShape } d="M2.5 1 L9 5 L2.5 9 Z" />
) : (
<rect className={ styles.siteStatusShape } x="0" y="0" width="8" height="8" />
) }
</svg>
) }
{ ! busy ? (
site.running ? (
<span className={ styles.siteStatusActionGlyph } aria-hidden="true">
Expand Down
32 changes: 32 additions & 0 deletions apps/ui/src/components/site-list/style.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,12 @@
opacity: 1;
}

/* Xdebug is a persistent single-site setting worth spotting at a glance, so
its (greyed) glyph stays visible even while the site is stopped. */
.siteStatus[data-xdebug][data-state='stopped'] {
opacity: 1;
}

.siteStatus:not([aria-disabled='true']):is(:hover, :focus-visible) {
background-color: var(--wpds-color-bg-interactive-neutral-weak-active);
}
Expand All @@ -337,6 +343,28 @@
transition: opacity 100ms ease;
}

/* Classic's Xdebug bug replaces the status dot; its color tracks the same
state palette the dot uses. */
.siteStatus .siteStatusXdebugGlyph.siteStatusXdebugGlyph {
inline-size: 20px;
block-size: 20px;
width: 20px;
height: 20px;
}

.siteStatus[data-state='running'] .siteStatusXdebugGlyph {
color: var(--studio-color-status-running);
}

.siteStatus[data-state='transitioning'] .siteStatusXdebugGlyph {
color: var(--studio-color-status-transitioning);
animation: siteStatusBlink 1s ease-in-out infinite;
}

.siteStatus[data-state='stopped'] .siteStatusXdebugGlyph {
color: var(--wpds-color-fg-content-neutral-weak);
}

/* Hovering the status button itself (not the row) crossfades the status dot
into the action it triggers: play when stopped, stop when running. The
transitioning state keeps the blinking dot — there's no action to take. */
Expand Down Expand Up @@ -479,6 +507,10 @@
transition: none;
}

.siteStatusXdebugGlyph {
animation: none;
}

.siteAgentActivitySlot {
transition: none;
}
Expand Down
12 changes: 12 additions & 0 deletions apps/ui/src/components/xdebug-icon/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Same glyph as Classic's `XDebugIcon` (apps/studio/src/components/icons/
// xdebug-icon.tsx), recolored via `currentColor` for the wpds theme.
export function XdebugIcon( { className }: { className?: string } ) {
return (
<svg className={ className } viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path
d="M14.5002 10.4214C14.9651 10.4215 15.3419 10.7983 15.342 11.2632V11.7075L16.6741 11.1333C16.9089 11.0319 17.1821 11.1397 17.2834 11.3745C17.3848 11.6094 17.2761 11.8825 17.0413 11.9839L15.342 12.7163V14.6313L16.97 15.5581C17.1923 15.6846 17.2703 15.9676 17.1438 16.1899C17.0173 16.4121 16.7342 16.4902 16.512 16.3638L15.2219 15.6294C14.8413 16.9968 13.5886 18.0005 12.0999 18.0005C10.6204 18.0004 9.37355 17.0092 8.98462 15.6548L7.7395 16.3638C7.51719 16.4902 7.23416 16.4122 7.10767 16.1899C6.9812 15.9676 7.05914 15.6846 7.28149 15.5581L8.85767 14.6616V12.6802L7.24243 11.9839C7.00757 11.8825 6.89985 11.6094 7.00122 11.3745C7.10268 11.1398 7.37485 11.032 7.60962 11.1333L8.85767 11.6714V11.2632C8.85782 10.7984 9.23464 10.4216 9.69946 10.4214H14.5002ZM13.7483 6.18896C13.8999 5.98302 14.1897 5.93884 14.3958 6.09033C14.6016 6.24192 14.6458 6.53181 14.4944 6.73779L13.7874 7.69873C14.1762 8.09284 14.4163 8.62971 14.4163 9.24268C14.4161 9.48774 14.1995 9.66352 13.9543 9.66357H10.2454C10.0003 9.66345 9.78462 9.48769 9.78442 9.24268C9.78442 8.65527 10.0041 8.13759 10.3645 7.74854L9.62134 6.73779C9.46987 6.53179 9.5141 6.24192 9.71997 6.09033C9.92601 5.93883 10.2159 5.98299 10.3674 6.18896L11.1213 7.21338C11.4188 7.08356 11.7499 7.01026 12.0999 7.01025C12.4248 7.01025 12.7342 7.07232 13.0149 7.18506L13.7483 6.18896Z"
fill="currentColor"
/>
</svg>
);
}
Loading