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
66 changes: 66 additions & 0 deletions apps/ui/src/components/app-message-cards/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import '@testing-library/jest-dom/vitest';
import { fireEvent, render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useConnector } from '@/data/core';
import { useAppGlobals } from '@/data/queries/use-app-globals';
import { REPORT_ISSUE_URL } from '@/lib/docs-links';
import { StudioBetaCard } from './index';

vi.mock( '@/data/core', () => ( {
useConnector: vi.fn(),
} ) );

vi.mock( '@/data/queries/use-app-globals', () => ( {
useAppGlobals: vi.fn(),
} ) );

const useConnectorMock = vi.mocked( useConnector, { partial: true } );
const useAppGlobalsMock = vi.mocked( useAppGlobals, { partial: true } );

describe( 'StudioBetaCard', () => {
const disableAgenticUi = vi.fn().mockResolvedValue( undefined );
const openExternalUrl = vi.fn().mockResolvedValue( undefined );

beforeEach( () => {
vi.clearAllMocks();
useConnectorMock.mockReturnValue( {
capabilities: { switchToClassicUi: true },
disableAgenticUi,
openExternalUrl,
} as never );
useAppGlobalsMock.mockReturnValue( {
data: { platform: 'darwin', isWindowsStore: false, appVersion: '1.18.0-beta1' },
} as never );
} );

it( 'offers feedback and classic Studio actions from its menu', async () => {
render( <StudioBetaCard /> );

expect( screen.queryByRole( 'menuitem', { name: 'Send feedback' } ) ).not.toBeInTheDocument();
fireEvent.click( screen.getByRole( 'button', { name: 'Studio Beta options' } ) );
expect( await screen.findByText( 'Studio 1.18.0-beta1' ) ).toBeVisible();

fireEvent.click( await screen.findByRole( 'menuitem', { name: 'Switch to Studio Classic' } ) );
expect( disableAgenticUi ).toHaveBeenCalledTimes( 1 );

fireEvent.click( screen.getByRole( 'button', { name: 'Studio Beta options' } ) );
fireEvent.click( await screen.findByRole( 'menuitem', { name: 'Send feedback' } ) );
expect( openExternalUrl ).toHaveBeenCalledWith( REPORT_ISSUE_URL );
} );

it( 'hides the classic action when the host cannot switch experiences', async () => {
useConnectorMock.mockReturnValue( {
capabilities: { switchToClassicUi: false },
disableAgenticUi,
openExternalUrl,
} as never );

render( <StudioBetaCard /> );
fireEvent.click( screen.getByRole( 'button', { name: 'Studio Beta options' } ) );

expect( await screen.findByRole( 'menuitem', { name: 'Send feedback' } ) ).toBeVisible();
expect(
screen.queryByRole( 'menuitem', { name: 'Switch to Studio Classic' } )
).not.toBeInTheDocument();
} );
} );
51 changes: 50 additions & 1 deletion apps/ui/src/components/app-message-cards/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,59 @@
import { __ } from '@wordpress/i18n';
import { __, sprintf } from '@wordpress/i18n';
import { Button, Notice } from '@wordpress/ui';
import { clsx } from 'clsx';
import toastStyles from '@/components/app-toasts/style.module.css';
import * as Menu from '@/components/menu';
import { useConnector } from '@/data/core';
import { useAppGlobals } from '@/data/queries/use-app-globals';
import { useActivePersistentMessages } from '@/data/queries/use-app-messages';
import { REPORT_ISSUE_URL } from '@/lib/docs-links';
import styles from './style.module.css';

export function StudioBetaCard( { className }: { className?: string } ) {
const connector = useConnector();
const { data: appGlobals } = useAppGlobals();

return (
<Menu.Root>
<div className={ clsx( styles.stack, className ) }>
<div className={ styles.cell }>
<Menu.Trigger
render={
<button
type="button"
className={ styles.betaCardButton }
aria-label={ __( 'Studio Beta options' ) }
/>
}
>
<span className={ styles.betaLabel }>{ __( 'Beta' ) }</span>
</Menu.Trigger>
</div>
</div>
<Menu.Popup side="top" align="start">
{ connector.capabilities.switchToClassicUi ? (
<Menu.Item onClick={ () => void connector.disableAgenticUi() }>
{ __( 'Switch to Studio Classic' ) }
</Menu.Item>
) : null }
<Menu.Item onClick={ () => void connector.openExternalUrl( REPORT_ISSUE_URL ) }>
{ __( 'Send feedback' ) }
</Menu.Item>
{ appGlobals?.appVersion ? (
<>
<Menu.Separator />
<Menu.Group>
<Menu.GroupLabel>
{ sprintf( __( 'Studio %s' ), appGlobals.appVersion ) }
</Menu.GroupLabel>
</Menu.Group>
</>
) : null }
</Menu.Popup>
</Menu.Root>
);
}

export function AppMessageCards( { className }: { className?: string } ) {
const { messages, dismiss } = useActivePersistentMessages();

Expand Down
54 changes: 54 additions & 0 deletions apps/ui/src/components/app-message-cards/style.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,60 @@
animation: card-enter 160ms ease-out;
}

.betaCardButton {
--beta-bubble-bg: var(--wpds-color-bg-interactive-brand-strong);
--beta-bubble-border: color-mix(
in srgb,
var(--wpds-color-fg-content-neutral) 35%,
transparent
);
--wp-ui-button-background-color: var(--beta-bubble-bg);
--wp-ui-button-border-color: var(--beta-bubble-border);

display: flex;
align-items: center;
width: max-content;
max-width: 100%;
box-sizing: border-box;
padding: var(--wpds-dimension-padding-sm) var(--wpds-dimension-padding-md);
appearance: none;
background-color: var(--beta-bubble-bg);
border: var(--wpds-border-width-xs) solid var(--beta-bubble-border);
border-radius: 14px 14px 14px 4px;
box-shadow: var(--wpds-elevation-md);
color: var(--wpds-color-fg-content-neutral);
font: inherit;
font-size: 11px;
line-height: var(--wpds-typography-line-height-xs);
cursor: var(--wpds-cursor-control);
outline: none;
animation: card-enter 160ms ease-out;
}

.betaCardButton:hover {
--beta-bubble-bg: color-mix(
in srgb,
var(--wpds-color-bg-interactive-brand-strong) 88%,
var(--wpds-color-fg-content-neutral) 12%
);
}

.betaCardButton:active,
.betaCardButton[data-popup-open] {
background-color: var(--wpds-color-bg-interactive-brand-strong);
border-color: var(--beta-bubble-border);
}

.betaCardButton:focus-visible {
outline: var(--wpds-border-width-xs) solid var(--wpds-color-fg-content-neutral);
outline-offset: 2px;
}

.betaLabel {
text-transform: uppercase;
letter-spacing: 0.04em;
}

@keyframes card-enter {
from {
opacity: 0;
Expand Down
1 change: 1 addition & 0 deletions apps/ui/src/components/sidebar-layout/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ vi.mock( '@/components/sidebar-header', () => ( {
vi.mock( '@/components/app-message-cards', () => ( {
AppMessageCards: () => null,
AppMessageCardsDot: () => null,
StudioBetaCard: () => null,
} ) );

vi.mock( '@/components/site-list', () => ( {
Expand Down
9 changes: 8 additions & 1 deletion apps/ui/src/components/sidebar-layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { privateApis } from '@wordpress/theme';
import { IconButton } from '@wordpress/ui';
import { clsx } from 'clsx';
import { useCallback, useEffect, useState } from 'react';
import { AppMessageCards, AppMessageCardsDot } from '@/components/app-message-cards';
import {
AppMessageCards,
AppMessageCardsDot,
StudioBetaCard,
} from '@/components/app-message-cards';
import { AppToasts } from '@/components/app-toasts';
import { ResizeHandle, ResizeOverlay } from '@/components/resize-handle';
import { SidebarHeader } from '@/components/sidebar-header';
Expand Down Expand Up @@ -98,6 +102,9 @@ export function SidebarLayout( {
{ ! effectiveCollapsed ? (
<AppMessageCards className={ styles.sidebarCards } />
) : null }
{ ! effectiveCollapsed ? (
<StudioBetaCard className={ styles.sidebarBeta } />
) : null }
<UserMenu onToggleSidebar={ toggleSidebar } />
</div>
</div>
Expand Down
9 changes: 9 additions & 0 deletions apps/ui/src/components/sidebar-layout/style.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@
padding: var(--wpds-dimension-padding-sm) calc(var(--wpds-dimension-padding-lg) - var(--wpds-dimension-padding-sm)) 0;
}

/* The beta affordance stays in normal footer flow, below transient/persistent
messages and above Settings, so it never overlays a toast or checklist. */
.sidebarBeta {
width: fit-content;
max-width: calc(100% - 2 * var(--wpds-dimension-padding-lg));
padding: var(--wpds-dimension-padding-sm) var(--wpds-dimension-padding-lg)
var(--wpds-dimension-padding-xs);
}

/* Sidebar-specific indicator treatment: match the content card's vertical
extent instead of running through the frame gaps above/below it (keep the
12px in sync with --panel-frame-gap in preview-split-frame). The handle is
Expand Down
3 changes: 3 additions & 0 deletions apps/ui/src/data/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,9 @@ export interface UserPreferences {
export interface AppGlobals {
platform: string;
isWindowsStore: boolean;
// Supplied by the desktop host; browser targets do not have an installed
// Studio app version to report.
appVersion?: string;
}

// Subset of UserPreferences that callers can actually mutate. `locale` is
Expand Down
Loading