Repository: Automattic/wp-calypso
Pull request #103414: Create `Callout` component for displaying splash over the Deployments route
Author: login: p-jackson
Created: 2025-05-14T10:10:02Z | Merged: 2025-05-23T05:13:33Z

<!--
Link a related Github/Linear issue to this PR. If the PR does not immediately resolve the issue,
for example, it requires a separate deployment to production, avoid
using the "fixes" keyword. Consider using "part of" instead.
-->

DES-52

## Proposed Changes

Adds the `<Callout>` component to `dashboard/components/`.
dhW7zYBamXJIeJIE3wy5f4-fi-3510_117161

Also adds a callout to the Deployments page on the /v2 dashboard when the site doesn't have the necessary plan.

This PR only implements the callout features necessary for the hosting dashboard callouts. The design system also has a full-bleed image variation, but none of the dashboard mockups use it.
Seems best to implement things once we have a use case for them.

The blurred background (`<CalloutOverlay>`) is kept separate from `<Callout>`. I think this is best because the exact way we need to obscure the page content depends heavily on the specifics of our app's page layout. Other users of the callout design pattern may be embedding it directly within content without any overlay. It's just presented as a generic card in the DS spec.

![CleanShot 2025-05-21 at 21 07 14@2x](https://github.com/user-attachments/assets/e693c270-27c8-4271-82de-202994a9a4f2)


![CleanShot 2025-05-14 at 22 34 04@2x](https://github.com/user-attachments/assets/f1f4bc02-f081-4cd5-8422-b3c9affacbf1)

<img src="https://private-user-images.githubusercontent.com/1500769/446015812-5d2d419e-6ebb-4219-83d2-cbcbd24f460f.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NDc4MjEwNzEsIm5iZiI6MTc0NzgyMDc3MSwicGF0aCI6Ii8xNTAwNzY5LzQ0NjAxNTgxMi01ZDJkNDE5ZS02ZWJiLTQyMTktODNkMi1jYmNiZDI0ZjQ2MGYucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI1MDUyMSUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTA1MjFUMDk0NjExWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9MWQ4YzA4MmQzZmJiOTVjMTFmZTBiYWY0YjEwM2EyYzUxMTFjZTU0NGMxMzMxYzJlOWJkMGNhY2IxNjI2NTI1NyZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.Vby2pZLAlh8PISDLXlmxL_dk6OQmIS9d2aBkTzsOXbA" alt="CleanShot 2025-05-21 at 21 45 49@2x" style="max-width: 100%;" width="50%">

Since the deployment page is empty as of now, the easiest way to test focus management is in The `CalloutOverlay` storybook. The reason I haven't reused the `<Modal>` component is because this isn't really a modal. You can tab from the nav bars into the callout and back again.


https://github.com/user-attachments/assets/6dfa2fc0-5362-43c6-a380-a4e82dceaa1b



## Why are these changes being made?
<!--
It's easy to see what a PR does but much harder to find out why it was made,
particularly when researching old changes in history. Record an explanation of
the motivation behind this change and how it will help.
-->

Rather than hiding menus when a site doesn't have the necessary plan, this call-out pattern gives the user some impression of what the feature looks like (behind a blurred background) and provides instructions on next steps if they're interested.

## Testing Instructions

<!--
Add as many details as possible to help others reproduce the issue and test the fix.
"Before / After" screenshots can also be very helpful when the change is visual.
-->

To check storybook run `yarn storybook:start` in the wp-calypso directory.

To test the dashboard
- `calypso.localhost:3000/v2`
- in the site grid choose a free site (one that doesn't have the necessary plan for GitHub deployments)
- drill in to the deployments screen
- observe the call-out in action

When testing focus management, you should open the accessibility tree in the dev tools and confirm that the content under the overlay disappears from the a11y tree when it is covered.

## Pre-merge Checklist

<!--
Complete applicable items on this checklist **before** merging into trunk. Inapplicable items can be left unchecked.

Both the PR author and reviewer are responsible for ensuring the checklist is completed.
-->

- [ ] Has the general commit checklist been followed? (PCYsg-hS-p2)
- [ ] [Have you written new tests](https://wpcalypso.wordpress.com/devdocs/docs/testing/index.md) for your changes?
- [ ] Have you tested the feature in Simple (P9HQHe-k8-p2), Atomic (P9HQHe-jW-p2), and self-hosted Jetpack sites (PCYsg-g6b-p2)?
- [ ] Have you checked for TypeScript, React or other console errors?
- [ ] Have you used memoizing on expensive computations? More info in [Memoizing with create-selector](https://github.com/Automattic/wp-calypso/blob/trunk/packages/state-utils/src/create-selector/README.md) and [Using memoizing selectors](https://react-redux.js.org/api/hooks#using-memoizing-selectors) and [Our Approach to Data](https://github.com/Automattic/wp-calypso/blob/trunk/docs/our-approach-to-data.md)
- [ ] Have we added the "[Status] String Freeze" label as soon as any new strings were ready for translation (p4TIVU-5Jq-p2)?
    - [ ] For UI changes, have we tested the change in various languages (for example, ES, PT, FR, or DE)? The length of text and words vary significantly between languages. 
- [ ] For changes affecting Jetpack: Have we added the "[Status] Needs Privacy Updates" label if this pull request changes what data or activity we track or use (p4TIVU-aUh-p2)?


Diff:
diff --git a/.eslintrc.js b/.eslintrc.js
index 77b8c2d19af1..5480d3123490 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -507,6 +507,8 @@ module.exports = {
 					'__experimentalHStack',
 					'__experimentalVStack',
 					'__experimentalSpacer',
+					'__experimentalText',
+					'__experimentalHeading',
 					'__experimentalItem',
 					'__experimentalItemGroup',
 					'__experimentalNavigationBackButton',
diff --git a/client/dashboard/components/callout-overlay/index.stories.tsx b/client/dashboard/components/callout-overlay/index.stories.tsx
new file mode 100644
index 000000000000..a5c20fd43cc4
--- /dev/null
+++ b/client/dashboard/components/callout-overlay/index.stories.tsx
@@ -0,0 +1,62 @@
+import { Meta, StoryObj } from '@storybook/react';
+import { Button } from '@wordpress/components';
+import { Callout } from '../callout';
+import { PageHeader } from '../page-header';
+import PageLayout from '../page-layout';
+import { CalloutOverlay } from './index';
+
+const meta = {
+	title: 'client/dashboard/CalloutOverlay',
+	component: CalloutOverlay,
+	tags: [ 'autodocs' ],
+	decorators: [
+		( Story ) => (
+			<>
+				<p>You can focus between these form elements and the callout content</p>
+				<label htmlFor="name">Name</label>
+				<input type="text" id="name" />
+				<button style={ { display: 'block', border: '1px solid black' } }>Click Me</button>
+				<p>Things below this line can not be focused or clicked</p>
+				<hr />
+				<PageLayout header={ <PageHeader title="Inert content" /> }>
+					<Story />
+				</PageLayout>
+			</>
+		),
+	],
+} satisfies Meta< typeof CalloutOverlay >;
+
+export default meta;
+type Story = StoryObj< typeof meta >;
+
+export const Default: Story = {
+	args: {
+		showCallout: true,
+		callout: (
+			<Callout
+				title="Let our WordPress.com experts build your site!"
+				description={
+					<p>
+						Hire our dedicated experts to build a handcrafted, personalized website. Share some
+						details about what you’re looking for, and we’ll make it happen.
+					</p>
+				}
+				actions={
+					<Button __next40pxDefaultSize variant="primary">
+						Get started
+					</Button>
+				}
+				imageSrc="https://live.staticflickr.com/3277/2938134470_c807dc3e47_b.jpg"
+				imageAlt="Sweet eyed kitty"
+			/>
+		),
+		main: (
+			<>
+				<p>You can not focus or click these form elements</p>
+				<label htmlFor="name">Name</label>
+				<input type="text" id="name" />
+				<button style={ { display: 'block', border: '1px solid black' } }>Click Me</button>
+			</>
+		),
+	},
+};
diff --git a/client/dashboard/components/callout-overlay/index.tsx b/client/dashboard/components/callout-overlay/index.tsx
new file mode 100644
index 000000000000..657ea6026158
--- /dev/null
+++ b/client/dashboard/components/callout-overlay/index.tsx
@@ -0,0 +1,25 @@
+import { __experimentalVStack as VStack } from '@wordpress/components';
+import type { ReactNode } from 'react';
+import './style.scss';
+
+interface CalloutOverlayProps {
+	showCallout: boolean;
+	callout: ReactNode;
+	main: ReactNode;
+}
+
+export function CalloutOverlay( { showCallout, callout, main }: CalloutOverlayProps ) {
+	if ( ! showCallout ) {
+		return main;
+	}
+
+	return (
+		<>
+			<VStack className="dashboard-callout-overlay" alignment="center">
+				{ callout }
+			</VStack>
+			{ /* The inert attribute is too new for our version of React to understand */ }
+			<div ref={ ( el ) => el?.setAttribute( 'inert', '' ) }>{ main }</div>
+		</>
+	);
+}
diff --git a/client/dashboard/components/callout-overlay/style.scss b/client/dashboard/components/callout-overlay/style.scss
new file mode 100644
index 000000000000..29f67c925bc8
--- /dev/null
+++ b/client/dashboard/components/callout-overlay/style.scss
@@ -0,0 +1,13 @@
+@import '@wordpress/base-styles/variables';
+
+.dashboard-callout-overlay {
+	position: absolute;
+	min-height: calc( 100% - 2 * 161px ) !important; // Overriding VStack style
+	width: calc( 100% - 2 * $grid-unit-20 );
+	top: 0;
+	left: 0;
+	padding: 161px $grid-unit-20;
+	background: linear-gradient(180deg, rgba(252, 252, 252, 0.65) 0%, #FCFCFC 100%);
+	backdrop-filter: blur(3px);
+	z-index: 1;
+}
diff --git a/client/dashboard/components/callout/index.stories.tsx b/client/dashboard/components/callout/index.stories.tsx
new file mode 100644
index 000000000000..71c003d78405
--- /dev/null
+++ b/client/dashboard/components/callout/index.stories.tsx
@@ -0,0 +1,31 @@
+import { Meta, StoryObj } from '@storybook/react';
+import { Button, __experimentalText as Text } from '@wordpress/components';
+import { Callout } from './';
+
+const meta: Meta< typeof Callout > = {
+	title: 'client/dashboard/Callout',
+	component: Callout,
+	tags: [ 'autodocs' ],
+};
+
+export default meta;
+type Story = StoryObj< typeof Callout >;
+
+export const Default: Story = {
+	args: {
+		title: 'Let our WordPress.com experts build your site!',
+		description: (
+			<Text as="p" variant="muted">
+				Hire our dedicated experts to build a handcrafted, personalized website. Share some details
+				about what you’re looking for, and we’ll make it happen.
+			</Text>
+		),
+		actions: (
+			<Button __next40pxDefaultSize variant="primary">
+				Get started
+			</Button>
+		),
+		imageSrc: 'https://live.staticflickr.com/3277/2938134470_c807dc3e47_b.jpg',
+		imageAlt: 'Sweet eyed kitty',
+	},
+};
diff --git a/client/dashboard/components/callout/index.tsx b/client/dashboard/components/callout/index.tsx
new file mode 100644
index 000000000000..198535e02d5b
--- /dev/null
+++ b/client/dashboard/components/callout/index.tsx
@@ -0,0 +1,58 @@
+import {
+	Card,
+	__experimentalVStack as VStack,
+	__experimentalHStack as HStack,
+	__experimentalHeading as Heading,
+	Icon,
+} from '@wordpress/components';
+import { forwardRef } from 'react';
+import type { CalloutProps } from './types';
+import './styles.scss';
+
+function UnforwardedCallout(
+	{
+		title,
+		icon,
+		imageSrc,
+		imageAlt,
+		description,
+		actions,
+		headingLevel = 6,
+		variant = 'default',
+	}: CalloutProps,
+	ref: React.ForwardedRef< HTMLElement >
+) {
+	return (
+		<Card ref={ ref } className={ `dashboard-callout is-${ variant }` }>
+			<HStack
+				className="dashboard-callout__h-container"
+				spacing="6"
+				alignment="stretch"
+				justify="stretch"
+				expanded={ false }
+			>
+				<VStack justify="flex-start" alignment="flex-start" spacing="4">
+					{ icon && <Icon icon={ icon } /> }
+					<Heading level={ headingLevel } className="dashboard-callout__title">
+						{ title }
+					</Heading>
+					{ description }
+					{ actions }
+				</VStack>
+				{ imageSrc && (
+					<VStack justify="stretch" alignment="stretch" className="dashboard-callout__image">
+						<img src={ imageSrc } alt={ imageAlt } />
+					</VStack>
+				) }
+			</HStack>
+		</Card>
+	);
+}
+
+/**
+ * The Callout component is a visually prominent layout element used to
+ * highlight a key feature, message, or promotional content. It typically
+ * includes a title, supporting description, image and/or icon, and a
+ * single call-to-action (CTA) button or link.
+ */
+export const Callout = forwardRef( UnforwardedCallout );
diff --git a/client/dashboard/components/callout/styles.scss b/client/dashboard/components/callout/styles.scss
new file mode 100644
index 000000000000..18ef4d515326
--- /dev/null
+++ b/client/dashboard/components/callout/styles.scss
@@ -0,0 +1,39 @@
+@import "@wordpress/base-styles/colors";
+@import "@wordpress/base-styles/variables";
+@import "@wordpress/base-styles/mixins";
+
+.dashboard-callout {
+	max-width: 600px;
+
+	&.is-highlight {
+		background: rgba(56, 88, 233, 0.04);
+	}
+}
+
+.dashboard-callout__h-container {
+	padding: $grid-unit-40;
+	@include body-medium;
+	color: $gray-700;
+
+	> * {
+		flex: 1 0;
+	}
+}
+
+.dashboard-callout__title {
+	@include heading-large;
+	color: $gray-900;
+}
+
+.dashboard-callout__image {
+	position: relative;
+
+	img {
+		object-fit: cover;
+		height: 100%;
+		width: 100%;
+		position: absolute;
+		border-radius: $radius-medium;
+	}
+}
+
diff --git a/client/dashboard/components/callout/test/index.tsx b/client/dashboard/components/callout/test/index.tsx
new file mode 100644
index 000000000000..272b6584262b
--- /dev/null
+++ b/client/dashboard/components/callout/test/index.tsx
@@ -0,0 +1,33 @@
+/**
+ * @jest-environment jsdom
+ */
+import { render, screen } from '@testing-library/react';
+import React from 'react';
+import { Callout } from '../index';
+
+describe( 'Callout', () => {
+	test( 'renders title and description', () => {
+		render( <Callout title="Test Title" description={ <p>Helpful content</p> } /> );
+		expect(
+			screen.getByRole( 'heading', {
+				name: 'Test Title',
+			} )
+		).toBeInTheDocument();
+		expect( screen.getByRole( 'paragraph' ) ).toHaveTextContent( 'Helpful content' );
+	} );
+
+	test( 'renders img element using imageSrc', () => {
+		render(
+			<Callout
+				title="Test Title"
+				description={ <p>Helpful content</p> }
+				imageSrc="https://example.com/illustration.png"
+				imageAlt="Illustration"
+			/>
+		);
+		expect( screen.getByRole( 'img', { name: 'Illustration' } ) ).toHaveAttribute(
+			'src',
+			'https://example.com/illustration.png'
+		);
+	} );
+} );
diff --git a/client/dashboard/components/callout/types.ts b/client/dashboard/components/callout/types.ts
new file mode 100644
index 000000000000..b4478749102a
--- /dev/null
+++ b/client/dashboard/components/callout/types.ts
@@ -0,0 +1,43 @@
+import type { Icon, __experimentalHeading as Heading } from '@wordpress/components';
+import type { ComponentProps, ReactNode } from 'react';
+
+export interface CalloutProps {
+	/**
+	 * A concise headline that clearly communicates the main message or feature being highlighted.
+	 */
+	title: string;
+	/**
+	 * The level of the heading to be used for the title.
+	 * @default 6
+	 */
+	headingLevel?: ComponentProps< typeof Heading >[ 'level' ];
+	/**
+	 * A short paragraph providing supporting context or details to reinforce the title.
+	 * Elements will be arranged in a VStack with consistent spacing.
+	 */
+	description: ReactNode;
+	/**
+	 * Callout's style variant.
+	 * - default: Display the Callout in a card.
+	 * - highlight: Gives additional prominence to the Callout by increasing the heading size, adjusting some colors, and utilizing a primary button for the cta.
+	 * @default 'default'
+	 */
+	variant?: 'default' | 'highlight';
+	/**
+	 * An optional small graphic used to visually reinforce the message or category of the Callout.
+	 */
+	icon?: ComponentProps< typeof Icon >[ 'icon' ];
+	/**
+	 * An optional larger visual or graphic that enhances the Callout’s impact and draws attention.
+	 */
+	imageSrc?: string;
+	/**
+	 * Alt text to accompany the image.
+	 */
+	imageAlt?: string;
+	/**
+	 * Typically a single button serving as a call to action.
+	 * Elements will be arranged in a VStack with consistent spacing.
+	 */
+	actions?: ReactNode;
+}
diff --git a/client/dashboard/components/page-layout/style.scss b/client/dashboard/components/page-layout/style.scss
index 00536a196ccc..cee7465a6c96 100644
--- a/client/dashboard/components/page-layout/style.scss
+++ b/client/dashboard/components/page-layout/style.scss
@@ -3,4 +3,10 @@
 .dashboard-page-layout {
 	margin: auto;
 	padding: $grid-unit-60 $grid-unit-20;
+
+	// Creates a positioning context for CalloutOverlay components. It needs to
+	// be on the PageLayout so that the blurred overlay can cover the entire
+	// PageLayout, including the padding.
+	position: relative;
 }
+
diff --git a/client/dashboard/sites/deployments/deployments-callout-illustration.svg b/client/dashboard/sites/deployments/deployments-callout-illustration.svg
new file mode 100644
index 000000000000..a7b4ac38c60f
--- /dev/null
+++ b/client/dashboard/sites/deployments/deployments-callout-illustration.svg
@@ -0,0 +1,38 @@
+<svg width="256" height="264" viewBox="0 0 256 264" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g clip-path="url(#clip0_3398_27448)">
+<rect width="256" height="264" fill="#101517"/>
+<rect opacity="0.1" width="256" height="264" fill="url(#pattern0_3398_27448)"/>
+<line x1="129" y1="264" x2="129" y2="-1.94929e-08" stroke="url(#paint0_linear_3398_27448)"/>
+<circle cx="128.5" cy="204" r="42" fill="#101517"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M148.638 235.929C148.246 236.177 147.775 235.793 147.926 235.355L159.018 203.281C161.17 197.89 161.887 193.586 161.887 189.76C161.887 189.35 161.878 188.947 161.863 188.553C161.84 187.999 162.607 187.774 162.837 188.277C165.03 193.06 166.257 198.385 166.257 203.991C166.257 217.44 159.225 229.239 148.638 235.929ZM133.707 186.303C133.703 186.295 133.709 186.286 133.718 186.285C135.981 186.165 138.018 185.927 138.018 185.927C140.05 185.687 139.811 182.692 137.779 182.812C137.779 182.812 131.667 183.291 127.715 183.291C124.001 183.291 117.77 182.812 117.77 182.812C115.729 182.692 115.499 185.807 117.53 185.927C117.53 185.927 119.257 186.141 121.164 186.266C121.363 186.279 121.536 186.407 121.604 186.595L127.313 202.244C127.352 202.351 127.354 202.467 127.318 202.574L119.582 225.774C119.43 226.229 118.787 226.23 118.634 225.775L105.567 186.91C105.462 186.597 105.685 186.269 106.015 186.248C108.015 186.122 109.677 185.927 109.677 185.927C111.709 185.687 111.47 182.692 109.438 182.812C109.438 182.812 103.326 183.291 99.3732 183.291C98.9091 183.291 98.3901 183.284 97.8377 183.271C97.446 183.263 97.2202 182.826 97.4437 182.505C104.268 172.679 115.624 166.236 128.5 166.236C138.159 166.236 146.961 169.86 153.637 175.827C153.76 175.936 153.677 176.12 153.512 176.12V176.12C149.799 176.12 147.17 179.355 147.17 182.82C147.17 185.935 148.962 188.562 150.883 191.677C152.317 194.193 153.999 197.419 153.999 202.092C153.999 205.326 153.043 209.391 151.122 214.303L147.808 225.383C147.669 225.85 147.011 225.861 146.855 225.399L133.707 186.303V186.303ZM128.5 241.755C124.978 241.755 121.572 241.261 118.333 240.359C118.058 240.283 117.907 239.99 118 239.72L128.698 208.628C128.852 208.183 129.479 208.177 129.641 208.619L140.767 239.094V239.094C140.881 239.374 140.769 239.712 140.482 239.807C136.715 241.064 132.689 241.755 128.5 241.755ZM90.7341 203.991C90.7341 198.953 91.7312 194.153 93.5193 189.759C93.6887 189.343 94.2733 189.361 94.4274 189.783L111.592 236.812C111.746 237.234 111.311 237.624 110.913 237.415C98.912 231.092 90.7341 218.501 90.7341 203.991ZM128.5 162C105.34 162 86.5 180.844 86.5 204C86.5 227.156 105.34 246 128.5 246C151.66 246 170.5 227.156 170.5 204C170.5 180.844 151.66 162 128.5 162Z" stroke="url(#paint1_linear_3398_27448)"/>
+<ellipse cx="129.5" cy="61" rx="42" ry="43" fill="#111618"/>
+<path d="M153.213 96.5056C139.279 107.326 119.736 107.022 105.773 96.4146" stroke="url(#paint2_linear_3398_27448)"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M129.535 18C106.291 18 87.5 37.2499 87.5 61.0647C87.5 80.1011 99.5398 96.215 116.242 101.918C118.33 102.347 119.095 100.992 119.095 99.8515C119.095 98.9114 119.034 95.8219 119.027 92.4872C119.026 92.1679 118.724 91.9322 118.41 91.993C107.247 94.1606 104.898 86.7318 104.898 86.7318C103.147 82.0803 100.61 80.6497 100.272 80.4767C100.247 80.4636 100.221 80.4487 100.198 80.4324C96.4346 77.82 100.514 77.82 100.514 77.82C104.759 78.1052 106.987 82.2405 106.987 82.2405C110.621 88.5847 116.405 87.0586 118.984 85.9193C119.141 85.8499 119.247 85.7016 119.272 85.5317C119.57 83.4913 120.285 81.931 121.123 80.8508C121.388 80.5096 121.199 79.9546 120.77 79.8983C111.792 78.7182 102.741 74.6697 102.741 58.7827C102.741 54.1967 104.327 50.4235 106.855 47.463C106.977 47.3206 107.012 47.1253 106.95 46.9486C106.453 45.5353 105.373 41.4283 107.388 36.049C107.441 35.9078 107.556 35.7992 107.704 35.7758C108.599 35.6355 112.125 35.4998 118.83 40.1095C118.952 40.1936 119.108 40.2221 119.252 40.1836C122.607 39.2815 126.063 38.8227 129.535 38.8188C133.004 38.8188 136.539 39.2961 139.817 40.183C139.96 40.2219 140.117 40.1934 140.24 40.1092C146.945 35.4998 150.471 35.6355 151.365 35.7758C151.514 35.7992 151.629 35.9078 151.681 36.049C153.695 41.423 152.618 45.5273 152.12 46.9445C152.057 47.1234 152.094 47.3212 152.219 47.4639C154.808 50.4243 156.328 54.1972 156.328 58.7827C156.328 74.6713 147.276 78.6528 138.235 79.8908C137.806 79.9496 137.621 80.5064 137.891 80.8446C139.07 82.3186 139.973 84.655 139.973 88.0155C139.973 93.7904 139.904 98.4253 139.904 99.8507C139.904 100.992 140.67 102.347 142.758 101.919C159.46 96.2142 171.5 80.1011 171.5 61.0647C171.569 37.2499 152.709 18 129.535 18Z" stroke="url(#paint3_linear_3398_27448)"/>
+<circle cx="129" cy="133" r="4.25" fill="white" stroke="#3858E9" stroke-width="1.5"/>
+</g>
+<defs>
+<pattern id="pattern0_3398_27448" patternContentUnits="objectBoundingBox" width="0.0234375" height="0.0227273">
+<use xlink:href="#image0_3398_27448" transform="scale(0.00195312 0.00189394)"/>
+</pattern>
+<linearGradient id="paint0_linear_3398_27448" x1="130" y1="33.446" x2="190.148" y2="217.098" gradientUnits="userSpaceOnUse">
+<stop stop-color="#3858E9"/>
+<stop offset="1" stop-color="#069E08"/>
+</linearGradient>
+<linearGradient id="paint1_linear_3398_27448" x1="129" y1="162" x2="129" y2="246" gradientUnits="userSpaceOnUse">
+<stop stop-color="#3858E9"/>
+<stop offset="1" stop-color="#069E08"/>
+</linearGradient>
+<linearGradient id="paint2_linear_3398_27448" x1="108.306" y1="97.1912" x2="110.572" y2="110.98" gradientUnits="userSpaceOnUse">
+<stop stop-color="#3858E9"/>
+<stop offset="1" stop-color="#069E08"/>
+</linearGradient>
+<linearGradient id="paint3_linear_3398_27448" x1="132.5" y1="28" x2="130" y2="122.5" gradientUnits="userSpaceOnUse">
+<stop stop-color="#3858E9"/>
+<stop offset="1" stop-color="#069E08"/>
+</linearGradient>
+<clipPath id="clip0_3398_27448">
+<rect width="256" height="264" fill="white"/>
+</clipPath>
+<image id="image0_3398_27448" width="12" height="12" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAYAAABWdVznAAAAHklEQVR4nGNgGAUI8P/////YxJnwKcaliSQbhgMAAKzQD/NUeFzJAAAAAElFTkSuQmCC"/>
+</defs>
+</svg>
diff --git a/client/dashboard/sites/deployments/gh-icon.svg b/client/dashboard/sites/deployments/gh-icon.svg
new file mode 100644
index 000000000000..943ebbd839c9
--- /dev/null
+++ b/client/dashboard/sites/deployments/gh-icon.svg
@@ -0,0 +1,14 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Clip path group">
+<mask id="mask0_3398_27432" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="18" height="18">
+<g id="clip0_2014_1339">
+<path id="Vector" d="M18 0H0V17.6326H18V0Z" fill="white"/>
+</g>
+</mask>
+<g mask="url(#mask0_3398_27432)">
+<g id="Group">
+<path id="Vector_2" fill-rule="evenodd" clip-rule="evenodd" d="M8.97318 0C4.01124 0 0 4.04082 0 9.03986C0 13.0359 2.57014 16.4184 6.13561 17.6156C6.58139 17.7056 6.74468 17.4211 6.74468 17.1818C6.74468 16.9722 6.72998 16.2538 6.72998 15.5054C4.23386 16.0443 3.71406 14.4278 3.71406 14.4278C3.31292 13.3801 2.71855 13.1108 2.71855 13.1108C1.90157 12.557 2.77806 12.557 2.77806 12.557C3.68431 12.6169 4.15984 13.4849 4.15984 13.4849C4.96194 14.8617 6.25445 14.4728 6.77443 14.2333C6.84863 13.6495 7.08649 13.2454 7.33905 13.021C5.34821 12.8115 3.25359 12.0332 3.25359 8.56084C3.25359 7.57305 3.60992 6.76487 4.17453 6.13634C4.08545 5.9119 3.77339 4.98379 4.26379 3.74161C4.26379 3.74161 5.02145 3.5021 6.72979 4.66953C7.4612 4.47166 8.21548 4.37099 8.97318 4.37014C9.7308 4.37014 10.5032 4.47502 11.2164 4.66953C12.9249 3.5021 13.6826 3.74161 13.6826 3.74161C14.173 4.98379 13.8608 5.9119 13.7716 6.13634C14.3511 6.76487 14.6927 7.57305 14.6927 8.56084C14.6927 12.0332 12.5982 12.7964 10.5924 13.021C10.9194 13.3053 11.2015 13.844 11.2015 14.6972C11.2015 15.9094 11.1868 16.8824 11.1868 17.1816C11.1868 17.4211 11.3503 17.7056 11.7959 17.6158C15.3614 16.4182 17.9315 13.0359 17.9315 9.03986C17.9462 4.04082 13.9203 0 8.97318 0Z" fill="#24292F"/>
+</g>
+</g>
+</g>
+</svg>
diff --git a/client/dashboard/sites/deployments/index.tsx b/client/dashboard/sites/deployments/index.tsx
index 06ffb7ce48d9..5f24f3a66f42 100644
--- a/client/dashboard/sites/deployments/index.tsx
+++ b/client/dashboard/sites/deployments/index.tsx
@@ -1,16 +1,68 @@
+import { useQuery } from '@tanstack/react-query';
+import { __experimentalText as Text } from '@wordpress/components';
 import { __ } from '@wordpress/i18n';
+import { siteQuery } from '../../app/queries';
+import { siteRoute } from '../../app/router';
+import { Callout } from '../../components/callout';
+import { CalloutOverlay } from '../../components/callout-overlay';
 import DataViewsCard from '../../components/dataviews-card';
 import { PageHeader } from '../../components/page-header';
 import PageLayout from '../../components/page-layout';
+import RouterLinkButton from '../../components/router-link-button';
+import { SitePlan } from '../../data/types';
+import illustrationUrl from './deployments-callout-illustration.svg';
+import ghIconUrl from './gh-icon.svg';
 
 function SiteDeployments() {
+	const { siteSlug } = siteRoute.useParams();
+	const { data: site } = useQuery( siteQuery( siteSlug ) );
+
+	if ( ! site ) {
+		return;
+	}
+
+	const showIneligiblePlanCallout = ! site.plan || ! hasDeploymentsFeature( site.plan );
+
 	return (
 		<PageLayout header={ <PageHeader title={ __( 'Deployments' ) } /> }>
-			<DataViewsCard>
-				<></>
-			</DataViewsCard>
+			<CalloutOverlay
+				showCallout={ showIneligiblePlanCallout }
+				callout={
+					<Callout
+						icon={ <img src={ ghIconUrl } alt={ __( 'GitHub logo' ) } /> }
+						title={ __( 'Deploy from GitHub' ) }
+						imageSrc={ illustrationUrl }
+						description={
+							<>
+								<Text as="p" variant="muted">
+									{ __(
+										'Connect your GitHub repo directly to your WordPress.com site—with seamless integration, straightforward version control, and automated workflows.'
+									) }
+								</Text>
+								<Text as="p" variant="muted">
+									{ __( 'Available on the WordPress.com Business and Commerce plans.' ) }
+								</Text>
+							</>
+						}
+						actions={
+							<RouterLinkButton __next40pxDefaultSize variant="primary" to="#">
+								{ __( 'Upgrade plan' ) }
+							</RouterLinkButton>
+						}
+					/>
+				}
+				main={
+					<DataViewsCard>
+						<></>
+					</DataViewsCard>
+				}
+			/>
 		</PageLayout>
 	);
 }
 
+function hasDeploymentsFeature( plan: SitePlan ) {
+	return plan.features.active.includes( 'atomic' );
+}
+
 export default SiteDeployments;


Reviews:
user: login: p-jackson
body: @jameskoster @jasmussen I'm starting to implement DES-52. This PR is super early, but I definitely have some component-library type questions already.

Lego's focus is definitely getting the hosting dashboard off the ground, not necessarily fully implementing every permutation of the `CallOut`. Specifically the hosting dashboard doesn't have a need for the "full bleed" or "highlighted" variants right now. But I still want to make sure what I do implement is good enough to go in the blessed set of `@automattic/` components.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T10:59:24Z
user: login: p-jackson
body: @youknowriad This PR is still early, but I just wanted to point out to you where I was placing the `CallOut` in the React tree.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T11:08:04Z
user: login: aduth
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T13:25:34Z
user: login: youknowriad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T13:33:17Z
user: login: jameskoster
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T14:37:38Z
user: login: youknowriad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T14:42:15Z
user: login: ntsekouras
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T14:58:32Z
user: login: tyxla
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T15:04:14Z
user: login: jameskoster
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T15:16:38Z
user: login: p-jackson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T21:46:36Z
user: login: p-jackson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T22:00:02Z
user: login: p-jackson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T22:00:12Z
user: login: p-jackson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T22:10:55Z
user: login: p-jackson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T22:53:24Z
user: login: StevenDufresne
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T22:54:18Z
user: login: p-jackson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-14T23:34:44Z
user: login: aduth
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T13:43:06Z
user: login: aduth
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T13:46:17Z
user: login: arthur791004
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T14:17:42Z
user: login: fushar
body: Forgive me for this very nitpicky comment, feel free to ignore, but is it `Callout` or `CallOut`? I believe we have been inconsistently using both terms in discussions (Figma and GH)
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T15:36:29Z
user: login: p-jackson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T05:48:35Z
user: login: p-jackson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T05:51:04Z
user: login: p-jackson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T06:02:23Z
user: login: p-jackson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T06:05:15Z
user: login: jasmussen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-16T08:31:04Z
user: login: fushar
body: On smaller screen, should we stack the text and the illustration vertically?

<img width="234" alt="image" src="https://github.com/user-attachments/assets/ba7b9db7-74ee-435d-b51f-5495dcca6c81" />

state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T03:30:50Z
user: login: fushar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T03:59:54Z
user: login: mirka
body: Great work here!

We just discussed in the Design Systems call last week that we'd like to defer adding components to `@automattic/components` itself until it really has an actual use case outside of the initial local use case. This is to prevent spending too much time on premature abstractions, and to avoid the huge increase in technical maintenance cost once it lands in the package. The rest of the process will remain largely the same, such as involving the DS team from the early stages, and keeping it visible as a proposed addition to the DS. We just want to keep the implementation local until it really needs to be elevated to the actual package. Would that be the case here?
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-19T11:14:26Z
user: login: p-jackson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T05:51:10Z
user: login: p-jackson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T05:53:41Z
user: login: p-jackson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T06:00:02Z
user: login: mirka
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T10:22:19Z
user: login: mirka
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-20T10:23:24Z
user: login: p-jackson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T04:33:31Z
user: login: p-jackson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T04:34:26Z
user: login: p-jackson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T04:36:23Z
user: login: p-jackson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T04:58:15Z
user: login: fushar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T15:28:53Z
user: login: p-jackson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T23:03:47Z
user: login: p-jackson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T23:15:23Z
user: login: fushar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T02:08:03Z
user: login: fushar
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T02:11:44Z
user: login: fushar
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T04:14:29Z
user: login: youknowriad
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T15:28:37Z
user: login: taipeicoder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T00:35:27Z
user: login: taipeicoder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-27T01:17:23Z

Review comments:
diff_hunk: @@ -0,0 +1,33 @@
+import type { Icon } from '@wordpress/components';
+import type { ComponentProps, ReactNode } from 'react';
+
+export interface CallOutProps {
+	/**
+	 * A concise headline that clearly communicates the main message or feature being highlighted.
+	 */
+	title: string;
+	/**
+	 * A short paragraph providing supporting context or details to reinforce the title.
+	 */
+	children?: ReactNode;
path: packages/components/src/call-out/types.ts
user: login: p-jackson
body: I originally had separate props for `description`, `actions`, etc. This works for the example used in the `CallOut` design spec.

However the dashboard mockups use two paragraphs.

![CleanShot 2025-05-14 at 22 36 05@2x](https://github.com/user-attachments/assets/108aed99-d72d-4180-b37c-6602f20fff48)

So that means there are two `<Text>` elements.

Since all elements on the left are vertically stacked, with the same `gap` between each, does it make more sense to just take an arbitrary `children` prop, and let the consumer use `<Text>` and `<Button>` elements as they please? The `VStack` makes sure they'll be arranged correctly.
created_at: 2025-05-14T10:39:17Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,33 @@
+import type { Icon } from '@wordpress/components';
+import type { ComponentProps, ReactNode } from 'react';
+
+export interface CallOutProps {
+	/**
+	 * A concise headline that clearly communicates the main message or feature being highlighted.
+	 */
+	title: string;
+	/**
+	 * A short paragraph providing supporting context or details to reinforce the title.
+	 */
+	children?: ReactNode;
+	/**
+	 * CallOut's style variant.
+	 * - default: Display the CallOut in a card.
+	 * - highlight: Gives additional prominence to the CallOut by increasing the heading size, adjusting some colors, and utilizing a primary button for the cta.
+	 * - borderless: Display the CallOut with no card, no border, no highlight.
+	 * @default 'default'
+	 */
+	variant?: 'default' | 'highlight' | 'borderless';
path: packages/components/src/call-out/types.ts
user: login: p-jackson
body: The design spec has "card" and "highlighted" as two separate props. But it shouldn't be possible to have a non-card that is also highlighted. So I think they're better modeled as mutually exclusive.
created_at: 2025-05-14T10:41:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,34 @@
+@import "@wordpress/base-styles/colors";
+@import "@wordpress/base-styles/mixins";
+@import "@wordpress/base-styles/variables";
+
+.call-out {
+	max-width: 600px;
+
+	.call-out-h-container {
+		padding: $grid-unit-40;
+		@include body-medium;
+		color: $gray-700;
+
+		> * {
+			flex: 1 0;
path: packages/components/src/call-out/styles.scss
user: login: p-jackson
body: I've newish to `HStack` and `VStack`. I'm confused why there aren't props for setting the `flex-grow` and `flex-basis` of the children. Am I missing something?
created_at: 2025-05-14T10:42:09Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,43 @@
+import { Meta, StoryObj } from '@storybook/react';
+import { Button, __experimentalText as Text } from '@wordpress/components';
+import { CallOut, CallOutIllustration } from './';
+
+const meta: Meta< typeof CallOut > = {
+	title: 'CallOut',
+	component: CallOut,
+	tags: [ 'autodocs' ],
+	parameters: {
+		actions: { argTypesRegex: '^on.*' },
+		// Prevent flickering + automatic menu closing in compact mode.
+		layout: 'fullscreen',
+	},
+	decorators: [
+		( Story ) => (
+			<div style={ { paddingInlineStart: '1rem', paddingBlockStart: '1rem' } }>
+				<Story />
+			</div>
+		),
+	],
+};
+
+export default meta;
+type Story = StoryObj< typeof CallOut >;
+
+export const Default: Story = {
+	args: {
+		title: 'Let our WordPress.com experts build your site!',
+		children: (
+			<Text as="p" variant="muted">
+				Hire our dedicated experts to build a handcrafted, personalized website. Share some details
+				about what you’re looking for, and we’ll make it happen.
+			</Text>
+		),
+		illustration: (
+			<CallOutIllustration
+				src="https://live.staticflickr.com/3277/2938134470_c807dc3e47_b.jpg"
+				alt="Sweet eyed kitty"
+			/>
path: packages/components/src/call-out/index.stories.tsx
user: login: p-jackson
body: By using a special `CallOutIllustration` component we're able to set the necessary `object-fit` rules that allow the illustration to always be perfectly vertically aligned with the content on the left.

I'm not planning on implementing the "full bleed" version of the illustration, but having a special component should give us the power we need to implement this too when the time comes.

What do you think?
created_at: 2025-05-14T10:45:48Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,67 @@
+import {
+	Card,
+	__experimentalVStack as VStack,
+	__experimentalHStack as HStack,
+	__experimentalHeading as Heading,
+	Icon,
+} from '@wordpress/components';
+import clsx from 'clsx';
+import { forwardRef } from 'react';
+import type { CallOutProps } from './types';
+import './styles.scss';
+
+function UnforwardedCallOut(
+	{ title, icon, illustration, actions, children }: CallOutProps,
+	ref: React.ForwardedRef< HTMLElement >
+) {
+	return (
+		<Card ref={ ref } className="call-out">
+			<HStack
+				ref={ ref }
+				className="call-out-h-container"
+				spacing="6"
+				alignment="stretch"
+				justify="stretch"
+				expanded={ false }
+			>
+				<VStack justify="flex-start" alignment="flex-start" spacing="4">
+					{ icon && <Icon icon={ icon } /> }
+					<Heading level="6" className="call-out-title">
path: packages/components/src/call-out/index.tsx
user: login: p-jackson
body: Choosing heading level feels fraught. How will I know in the context which level is correct?
On the other hand, having screen readers treat it as regular paragraph text also doesn't seem right.
created_at: 2025-05-14T10:47:09Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,67 @@
+import {
+	Card,
+	__experimentalVStack as VStack,
+	__experimentalHStack as HStack,
+	__experimentalHeading as Heading,
+	Icon,
+} from '@wordpress/components';
+import clsx from 'clsx';
+import { forwardRef } from 'react';
+import type { CallOutProps } from './types';
+import './styles.scss';
+
+function UnforwardedCallOut(
+	{ title, icon, illustration, actions, children }: CallOutProps,
+	ref: React.ForwardedRef< HTMLElement >
+) {
+	return (
+		<Card ref={ ref } className="call-out">
+			<HStack
+				ref={ ref }
+				className="call-out-h-container"
+				spacing="6"
+				alignment="stretch"
+				justify="stretch"
+				expanded={ false }
+			>
+				<VStack justify="flex-start" alignment="flex-start" spacing="4">
+					{ icon && <Icon icon={ icon } /> }
+					<Heading level="6" className="call-out-title">
+						{ title }
+					</Heading>
+					{ children }
+					{ actions }
+				</VStack>
+				{ illustration && (
+					<VStack justify="stretch" alignment="stretch" className="call-out-illustration-container">
+						{ illustration }
+					</VStack>
+				) }
+			</HStack>
+		</Card>
+	);
+}
+
+function UnforwardedCallOutIllustration(
+	{ className, alt, ...rest }: React.ImgHTMLAttributes< HTMLImageElement >,
+	ref: React.ForwardedRef< HTMLImageElement >
+) {
+	return (
+		<img
+			ref={ ref }
+			className={ clsx( 'call-out-illustration', className ) }
+			alt={ alt } // Forwarded explicitly to silence warnings
+			{ ...rest }
+		/>
+	);
+}
+
+/**
+ * The CallOut component is a visually prominent layout element used to
+ * highlight a key feature, message, or promotional content. It typically
+ * includes a title, supporting description, illustration and/or icon,
+ * and a single call-to-action (CTA) button or link.
+ */
+export const CallOut = forwardRef( UnforwardedCallOut );
path: packages/components/src/call-out/index.tsx
user: login: p-jackson
body: `Breadcrumbs` and `SummaryButton` also forward the ref, so I thought I should too.

Although is it wise to do this when we haven't got a use for it? I'm simply forwarding it to the parent element, how do I know if that's the best element the consumer would want a ref to?
created_at: 2025-05-14T10:48:57Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,34 @@
+@import "@wordpress/base-styles/colors";
+@import "@wordpress/base-styles/mixins";
+@import "@wordpress/base-styles/variables";
+
+.call-out {
+	max-width: 600px;
path: packages/components/src/call-out/styles.scss
user: login: p-jackson
body: The design spec has guidance for showing 4 or 6 column wide call outs. Are these column widths encoded anywhere yet?

Should `CallOut` have a `cols` prop which sets a max width?
created_at: 2025-05-14T10:58:57Z
author_association: MEMBER
diff_hunk: @@ -21,15 +21,18 @@ function PageLayout( {
 	actions,
 	children,
 	size = 'large',
+	callOut,
 }: {
 	title: string;
 	description?: React.ReactNode;
 	actions?: React.ReactNode;
 	children?: React.ReactNode;
 	size?: 'large' | 'small';
+	callOut?: React.ReactNode;
 } ) {
 	return (
 		<VStack spacing={ 8 } className="dashboard-page-layout" style={ sizes[ size ] }>
+			{ callOut && <CallOutParent>{ callOut }</CallOutParent> }
path: client/dashboard/components/page-layout/index.tsx
user: login: p-jackson
body: I've added the `CallOut` directly to the `PageLayout`. I'd prefer to experiment with call outs before wrapping them into any sort of official layout. However it needs the `CallOut` needs to render over the page heading, and this is where the heading gets rendered.
created_at: 2025-05-14T11:04:34Z
author_association: MEMBER
diff_hunk: @@ -50,4 +53,12 @@ function PageLayout( {
 	);
 }
 
+function CallOutParent( { children }: { children: React.ReactNode } ) {
path: client/dashboard/components/page-layout/index.tsx
user: login: p-jackson
body: I'm not using a modal to implement the "sneeze guard". It looks like a modal, but it's a bit different. For example pressing escape doesn't navigate you away from the page.

This sort of call out will need special focus management though, to ensure the user doesn't tab into the content behind the "sneeze guard". I'll see if there's anything I can commandeer from core for this.
created_at: 2025-05-14T11:06:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,67 @@
+import {
+	Card,
+	__experimentalVStack as VStack,
+	__experimentalHStack as HStack,
+	__experimentalHeading as Heading,
path: packages/components/src/call-out/index.tsx
user: login: aduth
body: I notice this produces a warning from ESLint about using experimental APIs, unlike VStack and HStack. If we're okay to use it, should we [exempt it as we do with the others](https://github.com/Automattic/wp-calypso/blob/e6301fae236e282495b092b1e64dc71bce406ba1/.eslintrc.js#L505-L523)?
created_at: 2025-05-14T12:47:22Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,67 @@
+import {
+	Card,
+	__experimentalVStack as VStack,
+	__experimentalHStack as HStack,
+	__experimentalHeading as Heading,
+	Icon,
+} from '@wordpress/components';
+import clsx from 'clsx';
+import { forwardRef } from 'react';
+import type { CallOutProps } from './types';
+import './styles.scss';
+
+function UnforwardedCallOut(
+	{ title, icon, illustration, actions, children }: CallOutProps,
+	ref: React.ForwardedRef< HTMLElement >
+) {
+	return (
+		<Card ref={ ref } className="call-out">
+			<HStack
+				ref={ ref }
+				className="call-out-h-container"
+				spacing="6"
+				alignment="stretch"
+				justify="stretch"
+				expanded={ false }
+			>
+				<VStack justify="flex-start" alignment="flex-start" spacing="4">
+					{ icon && <Icon icon={ icon } /> }
+					<Heading level="6" className="call-out-title">
path: packages/components/src/call-out/index.tsx
user: login: aduth
body: > Choosing heading level feels fraught. How will I know in the context which level is correct? On the other hand, having screen readers treat it as regular paragraph text also doesn't seem right.

Maybe this should be a required prop provided when rendered by a consumer? (e.g. `headingLevel`)

Though on the semantics of how a call-out would be used, I wonder if it's something where the content isn't necessarily part of the document structure, but rather more like an `<aside>`, that can associate the callout heading using `aria-labelledby`. I don't feel entirely confident that this wouldn't still be most appropriate to mark up as a heading.

Separately, I wonder if the `<Heading>` component from `@wordpress/components` could manage React context so that we could assign a heading level relative to the closest heading (e.g. `+ 1` from the current ancestor heading level). I see that it [internally does have some context behaviors](https://github.com/WordPress/gutenberg/blob/e5c93c9d8d24bcd6b1b9e76b51557809afb62a67/packages/components/src/heading/component.tsx#L35), so maybe it is possible? (or at least could be extended to support)
created_at: 2025-05-14T13:07:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,67 @@
+import {
+	Card,
+	__experimentalVStack as VStack,
+	__experimentalHStack as HStack,
+	__experimentalHeading as Heading,
+	Icon,
+} from '@wordpress/components';
+import clsx from 'clsx';
+import { forwardRef } from 'react';
+import type { CallOutProps } from './types';
+import './styles.scss';
+
+function UnforwardedCallOut(
+	{ title, icon, illustration, actions, children }: CallOutProps,
+	ref: React.ForwardedRef< HTMLElement >
+) {
+	return (
+		<Card ref={ ref } className="call-out">
+			<HStack
+				ref={ ref }
+				className="call-out-h-container"
+				spacing="6"
+				alignment="stretch"
+				justify="stretch"
+				expanded={ false }
+			>
+				<VStack justify="flex-start" alignment="flex-start" spacing="4">
+					{ icon && <Icon icon={ icon } /> }
+					<Heading level="6" className="call-out-title">
+						{ title }
+					</Heading>
+					{ children }
+					{ actions }
+				</VStack>
+				{ illustration && (
+					<VStack justify="stretch" alignment="stretch" className="call-out-illustration-container">
+						{ illustration }
+					</VStack>
+				) }
+			</HStack>
+		</Card>
+	);
+}
+
+function UnforwardedCallOutIllustration(
+	{ className, alt, ...rest }: React.ImgHTMLAttributes< HTMLImageElement >,
+	ref: React.ForwardedRef< HTMLImageElement >
+) {
+	return (
+		<img
+			ref={ ref }
+			className={ clsx( 'call-out-illustration', className ) }
+			alt={ alt } // Forwarded explicitly to silence warnings
+			{ ...rest }
+		/>
+	);
+}
+
+/**
+ * The CallOut component is a visually prominent layout element used to
+ * highlight a key feature, message, or promotional content. It typically
+ * includes a title, supporting description, illustration and/or icon,
+ * and a single call-to-action (CTA) button or link.
+ */
+export const CallOut = forwardRef( UnforwardedCallOut );
path: packages/components/src/call-out/index.tsx
user: login: aduth
body: > `Breadcrumbs` and `SummaryButton` also forward the ref, so I thought I should too.
> 
> Although is it wise to do this when we haven't got a use for it? I'm simply forwarding it to the parent element, how do I know if that's the best element the consumer would want a ref to?

I feel like we should have a consistent stance on this, to avoid confusion both for component developers and component consumers. My instinct is that we could always accept the `ref` and always assign it to the top-most wrapping element, but maybe that misses some use-cases I'm not thinking about?
created_at: 2025-05-14T13:10:48Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,67 @@
+import {
+	Card,
+	__experimentalVStack as VStack,
+	__experimentalHStack as HStack,
+	__experimentalHeading as Heading,
+	Icon,
+} from '@wordpress/components';
+import clsx from 'clsx';
+import { forwardRef } from 'react';
+import type { CallOutProps } from './types';
+import './styles.scss';
+
+function UnforwardedCallOut(
+	{ title, icon, illustration, actions, children }: CallOutProps,
+	ref: React.ForwardedRef< HTMLElement >
+) {
+	return (
+		<Card ref={ ref } className="call-out">
+			<HStack
+				ref={ ref }
path: packages/components/src/call-out/index.tsx
user: login: aduth
body: On the previous `ref` point, is it valid to be assigning it to both elements like this?
created_at: 2025-05-14T13:11:03Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,33 @@
+import type { Icon } from '@wordpress/components';
+import type { ComponentProps, ReactNode } from 'react';
+
+export interface CallOutProps {
+	/**
+	 * A concise headline that clearly communicates the main message or feature being highlighted.
+	 */
+	title: string;
+	/**
+	 * A short paragraph providing supporting context or details to reinforce the title.
+	 */
+	children?: ReactNode;
path: packages/components/src/call-out/types.ts
user: login: aduth
body: I think using `children` this way seems like a reasonable choice, especially with wanting to reinforce a consistent vertical gap.

A couple alternatives that came to mind, but don't seem like great fits either:

- `description` prop and expecting the consumer to render their own `HStack` if needing multiple paragraphs. Hard to enforce consistency.
- `description` accepting an array of either `<Text>` elements or strings. Arrays of elements aren't very ergonomic since they require `key`s, and arrays of strings might not support the type of flexibility we want in content (links, emphasis, etc.)
created_at: 2025-05-14T13:16:06Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,43 @@
+import { Meta, StoryObj } from '@storybook/react';
+import { Button, __experimentalText as Text } from '@wordpress/components';
+import { CallOut, CallOutIllustration } from './';
+
+const meta: Meta< typeof CallOut > = {
+	title: 'CallOut',
+	component: CallOut,
+	tags: [ 'autodocs' ],
+	parameters: {
+		actions: { argTypesRegex: '^on.*' },
+		// Prevent flickering + automatic menu closing in compact mode.
+		layout: 'fullscreen',
+	},
+	decorators: [
+		( Story ) => (
+			<div style={ { paddingInlineStart: '1rem', paddingBlockStart: '1rem' } }>
+				<Story />
+			</div>
+		),
+	],
+};
+
+export default meta;
+type Story = StoryObj< typeof CallOut >;
+
+export const Default: Story = {
+	args: {
+		title: 'Let our WordPress.com experts build your site!',
+		children: (
+			<Text as="p" variant="muted">
+				Hire our dedicated experts to build a handcrafted, personalized website. Share some details
+				about what you’re looking for, and we’ll make it happen.
+			</Text>
+		),
+		illustration: (
+			<CallOutIllustration
+				src="https://live.staticflickr.com/3277/2938134470_c807dc3e47_b.jpg"
+				alt="Sweet eyed kitty"
+			/>
path: packages/components/src/call-out/index.stories.tsx
user: login: aduth
body: Could we apply the additional styles by adding child-selectors assuming this would be passed as an `img` ?

e.g.

```scss
.call-out-illustration img {
  // ...
}
```
created_at: 2025-05-14T13:21:39Z
author_association: MEMBER
diff_hunk: @@ -1,8 +1,32 @@
+import { useQuery } from '@tanstack/react-query';
 import { __ } from '@wordpress/i18n';
+import { siteQuery } from '../../app/queries';
+import { siteRoute } from '../../app/router';
 import PageLayout from '../../components/page-layout';
+import { SitePlan } from '../../data/types';
+import { DeploymentsCallOut } from './deployments-call-out';
 
 function SiteDeployments() {
-	return <PageLayout title={ __( 'Deployments' ) } />;
+	const { siteSlug } = siteRoute.useParams();
+	const { data } = useQuery( siteQuery( siteSlug ) );
+
+	if ( ! data ) {
+		return;
+	}
+
+	const showCallOut =
+		( ! data.site.plan || ! hasDeploymentsFeature( data.site.plan ) ) &&
+		// Show to all atomic sites regardless of plan status e.g. an atomic site in the revert queue
+		// after a plan downgrade will continue to see Deployments until the revert is complete
+		! data.site.options?.is_wpcom_atomic;
+
+	return (
+		<PageLayout title={ __( 'Deployments' ) } callOut={ showCallOut && <DeploymentsCallOut /> } />
+	);
path: client/dashboard/sites/deployments/index.tsx
user: login: aduth
body: With how we're using the call-out as a modal-like element not part of the layout's `VStack`, could we render it adjacent to the page layout rather than inside it? To limit prop overload on `PageLayout`.

e.g.

```suggestion
	return (
	    <>
		  <PageLayout title={ __( 'Deployments' ) } />
		  { showCallOut && <DeploymentsCallOut /> }
		</>
	);
```
created_at: 2025-05-14T13:25:22Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,67 @@
+import {
+	Card,
+	__experimentalVStack as VStack,
+	__experimentalHStack as HStack,
+	__experimentalHeading as Heading,
path: packages/components/src/call-out/index.tsx
user: login: youknowriad
body: Same for Text. We should probably reexport most of these from `@automattic/components` as stable to clarify things better.
created_at: 2025-05-14T13:33:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,8 +1,32 @@
+import { useQuery } from '@tanstack/react-query';
 import { __ } from '@wordpress/i18n';
+import { siteQuery } from '../../app/queries';
+import { siteRoute } from '../../app/router';
 import PageLayout from '../../components/page-layout';
+import { SitePlan } from '../../data/types';
+import { DeploymentsCallOut } from './deployments-call-out';
 
 function SiteDeployments() {
-	return <PageLayout title={ __( 'Deployments' ) } />;
+	const { siteSlug } = siteRoute.useParams();
+	const { data } = useQuery( siteQuery( siteSlug ) );
+
+	if ( ! data ) {
+		return;
+	}
+
+	const showCallOut =
+		( ! data.site.plan || ! hasDeploymentsFeature( data.site.plan ) ) &&
+		// Show to all atomic sites regardless of plan status e.g. an atomic site in the revert queue
+		// after a plan downgrade will continue to see Deployments until the revert is complete
+		! data.site.options?.is_wpcom_atomic;
+
+	return (
+		<PageLayout title={ __( 'Deployments' ) } callOut={ showCallOut && <DeploymentsCallOut /> } />
+	);
path: client/dashboard/sites/deployments/index.tsx
user: login: jameskoster
body: From an a11y perspective I suppose the `Callout` will need to be outside because `PageLayout` contents will need to be `inert` or `aria-hidden="true"`.

I guess it will also need to trap focus inside the 'modal' too, but still provide access back to the main nav.

Alternatively we could just use an actual modal dialog. I suspect this would be much simpler 😅
created_at: 2025-05-14T14:37:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,8 +1,32 @@
+import { useQuery } from '@tanstack/react-query';
 import { __ } from '@wordpress/i18n';
+import { siteQuery } from '../../app/queries';
+import { siteRoute } from '../../app/router';
 import PageLayout from '../../components/page-layout';
+import { SitePlan } from '../../data/types';
+import { DeploymentsCallOut } from './deployments-call-out';
 
 function SiteDeployments() {
-	return <PageLayout title={ __( 'Deployments' ) } />;
+	const { siteSlug } = siteRoute.useParams();
+	const { data } = useQuery( siteQuery( siteSlug ) );
+
+	if ( ! data ) {
+		return;
+	}
+
+	const showCallOut =
+		( ! data.site.plan || ! hasDeploymentsFeature( data.site.plan ) ) &&
+		// Show to all atomic sites regardless of plan status e.g. an atomic site in the revert queue
+		// after a plan downgrade will continue to see Deployments until the revert is complete
+		! data.site.options?.is_wpcom_atomic;
+
+	return (
+		<PageLayout title={ __( 'Deployments' ) } callOut={ showCallOut && <DeploymentsCallOut /> } />
+	);
path: client/dashboard/sites/deployments/index.tsx
user: login: youknowriad
body: I'm actually not sure we'll have any content outside the callout, most endpoints ... will fail unless the user actually has the right plans... so the content outside the callout probably needs to be "fake". We might need a system for that.
created_at: 2025-05-14T14:42:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -21,15 +21,18 @@ function PageLayout( {
 	actions,
 	children,
 	size = 'large',
+	callOut,
 }: {
 	title: string;
 	description?: React.ReactNode;
 	actions?: React.ReactNode;
 	children?: React.ReactNode;
 	size?: 'large' | 'small';
+	callOut?: React.ReactNode;
path: client/dashboard/components/page-layout/index.tsx
user: login: tyxla
body: Why did we prefer to expose the callout through the `PageLayout` API? Isn't it more flexible if we just keep it outside of the `PageLayout` component, and render it as one of the `children`?
created_at: 2025-05-14T14:56:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,33 @@
+import type { Icon } from '@wordpress/components';
+import type { ComponentProps, ReactNode } from 'react';
+
+export interface CallOutProps {
+	/**
+	 * A concise headline that clearly communicates the main message or feature being highlighted.
+	 */
+	title: string;
+	/**
+	 * A short paragraph providing supporting context or details to reinforce the title.
+	 */
+	children?: ReactNode;
path: packages/components/src/call-out/types.ts
user: login: ntsekouras
body: Why not have `description` with `ReactNode`? The consumer could still pass multiple items with `<>` and get the spacing from the internal VStack, and it's a bit more specific to where the content goes IMO.
created_at: 2025-05-14T14:58:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,67 @@
+import {
path: packages/components/src/call-out/index.tsx
user: login: tyxla
body: Is this component intended to be part of the design system? cc @jasmussen @jameskoster 

I didn't see it in the list of components we're building these days:
![Screenshot 2025-05-14 at 18 02 32](https://github.com/user-attachments/assets/f288aecf-94ad-4142-80ae-a54d16e4e725)
(source - puPv3-pw6-p2)

With that in mind, should we move it outside of the package for now?
created_at: 2025-05-14T15:04:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,67 @@
+import {
path: packages/components/src/call-out/index.tsx
user: login: jameskoster
body: I think that's an accidental oversight, it is meant to be part of the system. 

I don't have the full details at hand but I believe @nuriapenya and @<!-- linear:userMention:7383b9d7-dc3f-4d14-9568-ee3afc303109 -->marina.matijaca1 have a need for Callout in projects adjacent to the host dashboard.
created_at: 2025-05-14T15:16:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,67 @@
+import {
+	Card,
+	__experimentalVStack as VStack,
+	__experimentalHStack as HStack,
+	__experimentalHeading as Heading,
+	Icon,
+} from '@wordpress/components';
+import clsx from 'clsx';
+import { forwardRef } from 'react';
+import type { CallOutProps } from './types';
+import './styles.scss';
+
+function UnforwardedCallOut(
+	{ title, icon, illustration, actions, children }: CallOutProps,
+	ref: React.ForwardedRef< HTMLElement >
+) {
+	return (
+		<Card ref={ ref } className="call-out">
+			<HStack
+				ref={ ref }
path: packages/components/src/call-out/index.tsx
user: login: p-jackson
body: Oops :)

created_at: 2025-05-14T21:46:36Z
author_association: MEMBER
diff_hunk: @@ -1,8 +1,32 @@
+import { useQuery } from '@tanstack/react-query';
 import { __ } from '@wordpress/i18n';
+import { siteQuery } from '../../app/queries';
+import { siteRoute } from '../../app/router';
 import PageLayout from '../../components/page-layout';
+import { SitePlan } from '../../data/types';
+import { DeploymentsCallOut } from './deployments-call-out';
 
 function SiteDeployments() {
-	return <PageLayout title={ __( 'Deployments' ) } />;
+	const { siteSlug } = siteRoute.useParams();
+	const { data } = useQuery( siteQuery( siteSlug ) );
+
+	if ( ! data ) {
+		return;
+	}
+
+	const showCallOut =
+		( ! data.site.plan || ! hasDeploymentsFeature( data.site.plan ) ) &&
+		// Show to all atomic sites regardless of plan status e.g. an atomic site in the revert queue
+		// after a plan downgrade will continue to see Deployments until the revert is complete
+		! data.site.options?.is_wpcom_atomic;
+
+	return (
+		<PageLayout title={ __( 'Deployments' ) } callOut={ showCallOut && <DeploymentsCallOut /> } />
+	);
path: client/dashboard/sites/deployments/index.tsx
user: login: p-jackson
body: That's a good point, I prefer this.
The downside is the call-out will no longer have access to the page layout's height, which it can use to ensure all the components on the page get covered, without resorting to `position: fixed`.
created_at: 2025-05-14T22:00:02Z
author_association: MEMBER
diff_hunk: @@ -21,15 +21,18 @@ function PageLayout( {
 	actions,
 	children,
 	size = 'large',
+	callOut,
 }: {
 	title: string;
 	description?: React.ReactNode;
 	actions?: React.ReactNode;
 	children?: React.ReactNode;
 	size?: 'large' | 'small';
+	callOut?: React.ReactNode;
path: client/dashboard/components/page-layout/index.tsx
user: login: p-jackson
body: Of course! I'll try that out. It depends if I'm able to get the tab/focus management working without explicit control over where the DOM elements are placed.
created_at: 2025-05-14T22:00:12Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,33 @@
+import type { Icon } from '@wordpress/components';
+import type { ComponentProps, ReactNode } from 'react';
+
+export interface CallOutProps {
+	/**
+	 * A concise headline that clearly communicates the main message or feature being highlighted.
+	 */
+	title: string;
+	/**
+	 * A short paragraph providing supporting context or details to reinforce the title.
+	 */
+	children?: ReactNode;
path: packages/components/src/call-out/types.ts
user: login: p-jackson
body: > Why not have `description` with `ReactNode`?

IMO that basically is a `children` prop, except with a `children` you don't need to wrap in `<>` when you've got multiple.

Perhaps an explicit prop name is better for enforcing consistent usage of the component though?
created_at: 2025-05-14T22:10:54Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,67 @@
+import {
+	Card,
+	__experimentalVStack as VStack,
+	__experimentalHStack as HStack,
+	__experimentalHeading as Heading,
+	Icon,
+} from '@wordpress/components';
+import clsx from 'clsx';
+import { forwardRef } from 'react';
+import type { CallOutProps } from './types';
+import './styles.scss';
+
+function UnforwardedCallOut(
+	{ title, icon, illustration, actions, children }: CallOutProps,
+	ref: React.ForwardedRef< HTMLElement >
+) {
+	return (
+		<Card ref={ ref } className="call-out">
+			<HStack
+				ref={ ref }
+				className="call-out-h-container"
+				spacing="6"
+				alignment="stretch"
+				justify="stretch"
+				expanded={ false }
+			>
+				<VStack justify="flex-start" alignment="flex-start" spacing="4">
+					{ icon && <Icon icon={ icon } /> }
+					<Heading level="6" className="call-out-title">
+						{ title }
+					</Heading>
+					{ children }
+					{ actions }
+				</VStack>
+				{ illustration && (
+					<VStack justify="stretch" alignment="stretch" className="call-out-illustration-container">
+						{ illustration }
+					</VStack>
+				) }
+			</HStack>
+		</Card>
+	);
+}
+
+function UnforwardedCallOutIllustration(
+	{ className, alt, ...rest }: React.ImgHTMLAttributes< HTMLImageElement >,
+	ref: React.ForwardedRef< HTMLImageElement >
+) {
+	return (
+		<img
+			ref={ ref }
+			className={ clsx( 'call-out-illustration', className ) }
+			alt={ alt } // Forwarded explicitly to silence warnings
+			{ ...rest }
+		/>
+	);
+}
+
+/**
+ * The CallOut component is a visually prominent layout element used to
+ * highlight a key feature, message, or promotional content. It typically
+ * includes a title, supporting description, illustration and/or icon,
+ * and a single call-to-action (CTA) button or link.
+ */
+export const CallOut = forwardRef( UnforwardedCallOut );
path: packages/components/src/call-out/index.tsx
user: login: p-jackson
body: Without know the use case I think the most wrapping element is the most obvious candidate. It's most likely to be useful for something like collision detection.
created_at: 2025-05-14T22:53:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,67 @@
+import {
+	Card,
+	__experimentalVStack as VStack,
+	__experimentalHStack as HStack,
+	__experimentalHeading as Heading,
+	Icon,
+} from '@wordpress/components';
+import clsx from 'clsx';
+import { forwardRef } from 'react';
+import type { CallOutProps } from './types';
+import './styles.scss';
+
+function UnforwardedCallOut(
+	{ title, icon, illustration, actions, children }: CallOutProps,
+	ref: React.ForwardedRef< HTMLElement >
+) {
+	return (
+		<Card ref={ ref } className="call-out">
+			<HStack
+				ref={ ref }
+				className="call-out-h-container"
+				spacing="6"
+				alignment="stretch"
+				justify="stretch"
+				expanded={ false }
+			>
+				<VStack justify="flex-start" alignment="flex-start" spacing="4">
+					{ icon && <Icon icon={ icon } /> }
+					<Heading level="6" className="call-out-title">
+						{ title }
+					</Heading>
+					{ children }
+					{ actions }
+				</VStack>
+				{ illustration && (
+					<VStack justify="stretch" alignment="stretch" className="call-out-illustration-container">
+						{ illustration }
+					</VStack>
+				) }
+			</HStack>
+		</Card>
+	);
+}
+
+function UnforwardedCallOutIllustration(
+	{ className, alt, ...rest }: React.ImgHTMLAttributes< HTMLImageElement >,
+	ref: React.ForwardedRef< HTMLImageElement >
+) {
+	return (
+		<img
+			ref={ ref }
+			className={ clsx( 'call-out-illustration', className ) }
+			alt={ alt } // Forwarded explicitly to silence warnings
+			{ ...rest }
+		/>
+	);
+}
+
+/**
+ * The CallOut component is a visually prominent layout element used to
+ * highlight a key feature, message, or promotional content. It typically
+ * includes a title, supporting description, illustration and/or icon,
+ * and a single call-to-action (CTA) button or link.
+ */
+export const CallOut = forwardRef( UnforwardedCallOut );
path: packages/components/src/call-out/index.tsx
user: login: StevenDufresne
body: Noting that [forwardRef will be deprecated](https://react.dev/blog/2024/12/05/react-19#improvements-in-react-19) in the near future.
created_at: 2025-05-14T22:54:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -21,15 +21,18 @@ function PageLayout( {
 	actions,
 	children,
 	size = 'large',
+	callOut,
 }: {
 	title: string;
 	description?: React.ReactNode;
 	actions?: React.ReactNode;
 	children?: React.ReactNode;
 	size?: 'large' | 'small';
+	callOut?: React.ReactNode;
path: client/dashboard/components/page-layout/index.tsx
user: login: p-jackson
body: This is moot now, the `<PageLayout>` has been refactored to be more composition focused 😃 
created_at: 2025-05-14T23:34:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,67 @@
+import {
+	Card,
+	__experimentalVStack as VStack,
+	__experimentalHStack as HStack,
+	__experimentalHeading as Heading,
+	Icon,
+} from '@wordpress/components';
+import clsx from 'clsx';
+import { forwardRef } from 'react';
+import type { CallOutProps } from './types';
+import './styles.scss';
+
+function UnforwardedCallOut(
+	{ title, icon, illustration, actions, children }: CallOutProps,
+	ref: React.ForwardedRef< HTMLElement >
+) {
+	return (
+		<Card ref={ ref } className="call-out">
+			<HStack
+				ref={ ref }
+				className="call-out-h-container"
+				spacing="6"
+				alignment="stretch"
+				justify="stretch"
+				expanded={ false }
+			>
+				<VStack justify="flex-start" alignment="flex-start" spacing="4">
+					{ icon && <Icon icon={ icon } /> }
+					<Heading level="6" className="call-out-title">
+						{ title }
+					</Heading>
+					{ children }
+					{ actions }
+				</VStack>
+				{ illustration && (
+					<VStack justify="stretch" alignment="stretch" className="call-out-illustration-container">
+						{ illustration }
+					</VStack>
+				) }
+			</HStack>
+		</Card>
+	);
+}
+
+function UnforwardedCallOutIllustration(
+	{ className, alt, ...rest }: React.ImgHTMLAttributes< HTMLImageElement >,
+	ref: React.ForwardedRef< HTMLImageElement >
+) {
+	return (
+		<img
+			ref={ ref }
+			className={ clsx( 'call-out-illustration', className ) }
+			alt={ alt } // Forwarded explicitly to silence warnings
+			{ ...rest }
+		/>
+	);
+}
+
+/**
+ * The CallOut component is a visually prominent layout element used to
+ * highlight a key feature, message, or promotional content. It typically
+ * includes a title, supporting description, illustration and/or icon,
+ * and a single call-to-action (CTA) button or link.
+ */
+export const CallOut = forwardRef( UnforwardedCallOut );
path: packages/components/src/call-out/index.tsx
user: login: aduth
body: As far as I understand, `forwardRef` is going away, but only because components receive the `ref` by default. But that doesn't impact what's being discussed here, which is if/what to do with that `ref`.
created_at: 2025-05-15T13:43:06Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,33 @@
+import type { Icon } from '@wordpress/components';
+import type { ComponentProps, ReactNode } from 'react';
+
+export interface CallOutProps {
+	/**
+	 * A concise headline that clearly communicates the main message or feature being highlighted.
+	 */
+	title: string;
+	/**
+	 * A short paragraph providing supporting context or details to reinforce the title.
+	 */
+	children?: ReactNode;
path: packages/components/src/call-out/types.ts
user: login: aduth
body: I think to @ntsekouras 's point, when using `children` it should be obvious where those children would be rendered, and I'm not sure it's inherently obvious that it'd be rendered below the heading and above the actions.

Although I think it's the obvious place if you think about it for more than a few seconds, just not _immediately_ obvious. So a soft opinion from me on `children` vs. `description`, anyways.
created_at: 2025-05-15T13:46:17Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,34 @@
+@import "@wordpress/base-styles/colors";
+@import "@wordpress/base-styles/mixins";
+@import "@wordpress/base-styles/variables";
+
+.call-out {
+	max-width: 600px;
+
+	.call-out-h-container {
+		padding: $grid-unit-40;
+		@include body-medium;
+		color: $gray-700;
+
+		> * {
+			flex: 1 0;
path: packages/components/src/call-out/styles.scss
user: login: arthur791004
body: I guess you can use `expanded` for `flow-grow`
created_at: 2025-05-15T14:17:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,34 @@
+@import "@wordpress/base-styles/colors";
+@import "@wordpress/base-styles/mixins";
+@import "@wordpress/base-styles/variables";
+
+.call-out {
+	max-width: 600px;
+
+	.call-out-h-container {
+		padding: $grid-unit-40;
+		@include body-medium;
+		color: $gray-700;
+
+		> * {
+			flex: 1 0;
path: packages/components/src/call-out/styles.scss
user: login: p-jackson
body: The problem is I definitely want to set `flex-basis` to `0` too. But TIL that `expanded` is there for the most usual case
created_at: 2025-05-16T05:48:34Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,33 @@
+import type { Icon } from '@wordpress/components';
+import type { ComponentProps, ReactNode } from 'react';
+
+export interface CallOutProps {
+	/**
+	 * A concise headline that clearly communicates the main message or feature being highlighted.
+	 */
+	title: string;
+	/**
+	 * A short paragraph providing supporting context or details to reinforce the title.
+	 */
+	children?: ReactNode;
path: packages/components/src/call-out/types.ts
user: login: p-jackson
body: > it should be obvious where those children would be rendered

I think that's a fair point. I'll try adding two separate `description` and `actions` for the sake of making the props explanatory.
created_at: 2025-05-16T05:51:04Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,43 @@
+import { Meta, StoryObj } from '@storybook/react';
+import { Button, __experimentalText as Text } from '@wordpress/components';
+import { CallOut, CallOutIllustration } from './';
+
+const meta: Meta< typeof CallOut > = {
+	title: 'CallOut',
+	component: CallOut,
+	tags: [ 'autodocs' ],
+	parameters: {
+		actions: { argTypesRegex: '^on.*' },
+		// Prevent flickering + automatic menu closing in compact mode.
+		layout: 'fullscreen',
+	},
+	decorators: [
+		( Story ) => (
+			<div style={ { paddingInlineStart: '1rem', paddingBlockStart: '1rem' } }>
+				<Story />
+			</div>
+		),
+	],
+};
+
+export default meta;
+type Story = StoryObj< typeof CallOut >;
+
+export const Default: Story = {
+	args: {
+		title: 'Let our WordPress.com experts build your site!',
+		children: (
+			<Text as="p" variant="muted">
+				Hire our dedicated experts to build a handcrafted, personalized website. Share some details
+				about what you’re looking for, and we’ll make it happen.
+			</Text>
+		),
+		illustration: (
+			<CallOutIllustration
+				src="https://live.staticflickr.com/3277/2938134470_c807dc3e47_b.jpg"
+				alt="Sweet eyed kitty"
+			/>
path: packages/components/src/call-out/index.stories.tsx
user: login: p-jackson
body: At first I thought, how do I know the user is passing in an `<img>`? Maybe they should be able to pass in a `svg`? But I think that clued me into the fact I was over abstracting. We don't need callers to be able to pass in arbitrary elements, it has to be an image since the layout depends on `object-fit: cover`. So no need to give them a powerful API.

I've switched to a simple `illustrationSrc` prop. That gives us the most power over positioning when it comes to full bleed. I think we should start with something simple like this and then see how long it actually takes for a use case to come up that needs more.
created_at: 2025-05-16T06:02:23Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,67 @@
+import {
+	Card,
+	__experimentalVStack as VStack,
+	__experimentalHStack as HStack,
+	__experimentalHeading as Heading,
+	Icon,
+} from '@wordpress/components';
+import clsx from 'clsx';
+import { forwardRef } from 'react';
+import type { CallOutProps } from './types';
+import './styles.scss';
+
+function UnforwardedCallOut(
+	{ title, icon, illustration, actions, children }: CallOutProps,
+	ref: React.ForwardedRef< HTMLElement >
+) {
+	return (
+		<Card ref={ ref } className="call-out">
+			<HStack
+				ref={ ref }
+				className="call-out-h-container"
+				spacing="6"
+				alignment="stretch"
+				justify="stretch"
+				expanded={ false }
+			>
+				<VStack justify="flex-start" alignment="flex-start" spacing="4">
+					{ icon && <Icon icon={ icon } /> }
+					<Heading level="6" className="call-out-title">
path: packages/components/src/call-out/index.tsx
user: login: p-jackson
body: I thought about the `<aside>` and `aria-labelledby` idea, but I think semantically heading is still correct. In the case of the v2 dashboard, when the user focuses in to an area covered by a call-out, it should just appear to be that the call-out message is the main/only content of the area. You wouldn't expect to have an `<aside>` be the main content, just a heading and a paragraph telling you that this nav area needs a purchase.

It'd be cool if internally `<Heading>` could automatically set the correct level. Without that I think `h6` is probably a good bet and I've added an optional `headingLevel` prop if a particularly assiduous developer would like to set a more correct one.
created_at: 2025-05-16T06:05:15Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,67 @@
+import {
path: packages/components/src/call-out/index.tsx
user: login: jasmussen
body: > I didn't see it in the list of components we're building these days:

Noting that the screenshot you shared there is specific for the forms & settings sub-set of componentry. That doesn't mean you can't use a Callout in a settings page, just that this particular aspect was a presentation for a subset of the componentry.
created_at: 2025-05-16T08:31:04Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,62 @@
+import {
+	Card,
+	__experimentalVStack as VStack,
+	__experimentalHStack as HStack,
+	__experimentalHeading as Heading,
+	Icon,
+} from '@wordpress/components';
+import { forwardRef } from 'react';
+import type { CallOutProps } from './types';
+import './styles.scss';
+
+function UnforwardedCallOut(
+	{
+		title,
+		icon,
+		illustrationSrc,
+		illustrationAlt,
+		description,
+		actions,
+		headingLevel = 6,
+		variant = 'default',
+	}: CallOutProps,
+	ref: React.ForwardedRef< HTMLElement >
+) {
+	return (
+		<Card ref={ ref } className={ `call-out call-out--${ variant }` }>
path: packages/components/src/call-out/index.tsx
user: login: fushar
body: Based on my understanding reading various DS PRs, I think we're supposed to prefix the classname with `a8c-components-`. See e.g. https://github.com/Automattic/wp-calypso/pull/102998#discussion_r2075443401
created_at: 2025-05-19T03:59:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,38 @@
+@import "@wordpress/base-styles/colors";
+@import "@wordpress/base-styles/mixins";
+@import "@wordpress/base-styles/variables";
+
+.call-out {
+	max-width: 600px;
+
+	&.call-out--highlight {
+		background: rgba(56, 88, 233, 0.04);
+	}
+
+	.call-out-h-container {
path: packages/components/src/call-out/styles.scss
user: login: mirka
body: We'll likely be moving to CSS Modules soon, but in the meantime we need to adhere to [Calypso/WP BEM naming](https://developer.wordpress.org/block-editor/contributors/code/coding-guidelines/#naming). Also see this note about [unnecessary nesting](https://github.com/Automattic/wp-calypso/blob/19540e2edad18db7ca17db55b105708fef832090/docs/coding-guidelines/css.md?plain=1#L178).
created_at: 2025-05-19T10:11:42Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,38 @@
+@import "@wordpress/base-styles/colors";
+@import "@wordpress/base-styles/mixins";
+@import "@wordpress/base-styles/variables";
path: packages/components/src/call-out/styles.scss
user: login: mirka
body: Let's switch these to `@use` (#101983).
created_at: 2025-05-19T10:12:36Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,43 @@
+import type { Icon, __experimentalHeading as Heading } from '@wordpress/components';
+import type { ComponentProps, ReactNode } from 'react';
+
+export interface CallOutProps {
+	/**
+	 * A concise headline that clearly communicates the main message or feature being highlighted.
+	 */
+	title: string;
+	/**
+	 * The level of the heading to be used for the title.
+	 * @default 6
+	 */
+	headingLevel?: ComponentProps< typeof Heading >[ 'level' ];
path: packages/components/src/call-out/types.ts
user: login: mirka
body: Note that `level` also changes the semantic heading level as well as the visual, so simply exposing this prop without the ability to override the semantic level is problematic. If anything you probably want to hardcode the `level` and expose the `as` prop so only the semantics can be changed.
created_at: 2025-05-19T10:21:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,62 @@
+import {
+	Card,
+	__experimentalVStack as VStack,
+	__experimentalHStack as HStack,
+	__experimentalHeading as Heading,
+	Icon,
+} from '@wordpress/components';
+import { forwardRef } from 'react';
+import type { CallOutProps } from './types';
+import './styles.scss';
+
+function UnforwardedCallOut(
+	{
+		title,
+		icon,
+		illustrationSrc,
+		illustrationAlt,
+		description,
+		actions,
+		headingLevel = 6,
+		variant = 'default',
+	}: CallOutProps,
+	ref: React.ForwardedRef< HTMLElement >
+) {
+	return (
+		<Card ref={ ref } className={ `call-out call-out--${ variant }` }>
+			<HStack
+				className="call-out-h-container"
+				spacing="6"
+				alignment="stretch"
+				justify="stretch"
+				expanded={ false }
+			>
+				<VStack justify="flex-start" alignment="flex-start" spacing="4">
+					{ icon && <Icon icon={ icon } /> }
+					<Heading level={ headingLevel } className="call-out-title">
+						{ title }
+					</Heading>
+					{ description }
+					{ actions }
+				</VStack>
+				{ illustrationSrc && (
+					<VStack justify="stretch" alignment="stretch" className="call-out-illustration-container">
+						<img
+							src={ illustrationSrc }
+							alt={ illustrationAlt }
+							className="call-out-illustration"
+						/>
+					</VStack>
+				) }
path: packages/components/src/call-out/index.tsx
user: login: mirka
body: You may want to consider exposing this as a single `image` prop that accepts a subcomponent (e.g. `Callout.Image`) that takes all `img` element props, which ensures that other desired props can be passed down without prop bloat at the top level.

I would also consider renaming it from `illustration` to `image`, as it is more neutral and reflects the underlying element.
created_at: 2025-05-19T10:38:14Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,39 @@
+import { Meta, StoryObj } from '@storybook/react';
+import { Button, __experimentalText as Text } from '@wordpress/components';
+import { CallOut } from './';
+
+const meta: Meta< typeof CallOut > = {
+	title: 'CallOut',
+	component: CallOut,
+	tags: [ 'autodocs' ],
+	parameters: {
+		actions: { argTypesRegex: '^on.*' },
+		// Prevent flickering + automatic menu closing in compact mode.
+		layout: 'fullscreen',
path: packages/components/src/call-out/index.stories.tsx
user: login: mirka
body: Copypasta?
created_at: 2025-05-19T10:39:05Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,39 @@
+import { Meta, StoryObj } from '@storybook/react';
+import { Button, __experimentalText as Text } from '@wordpress/components';
+import { CallOut } from './';
+
+const meta: Meta< typeof CallOut > = {
+	title: 'CallOut',
+	component: CallOut,
+	tags: [ 'autodocs' ],
+	parameters: {
+		actions: { argTypesRegex: '^on.*' },
+		// Prevent flickering + automatic menu closing in compact mode.
+		layout: 'fullscreen',
+	},
+	decorators: [
+		( Story ) => (
+			<div style={ { paddingInlineStart: '1rem', paddingBlockStart: '1rem' } }>
+				<Story />
+			</div>
+		),
+	],
path: packages/components/src/call-out/index.stories.tsx
user: login: mirka
body: Is this necessary?
created_at: 2025-05-19T10:39:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,62 @@
+import {
+	Card,
+	__experimentalVStack as VStack,
+	__experimentalHStack as HStack,
+	__experimentalHeading as Heading,
+	Icon,
+} from '@wordpress/components';
+import { forwardRef } from 'react';
+import type { CallOutProps } from './types';
+import './styles.scss';
+
+function UnforwardedCallOut(
path: packages/components/src/call-out/index.tsx
user: login: mirka
body: We were discussing whether it's `Callout` or `CallOut`, and based on the noun/verb rule I'm thinking it's `Callout`.

> I would group it with words like "setup" and "login", where it's two words when used as a verb and one word when used as a noun.

p1747641021190659-slack-CNGQYA3B9
created_at: 2025-05-19T10:45:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,114 @@
+/**
+ * @jest-environment jsdom
+ */
+import { fireEvent, render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import '@testing-library/user-event';
+import { Button, __experimentalText as Text } from '@wordpress/components';
+import React from 'react';
+import { CallOut } from '../index';
+
+describe( 'CallOut', () => {
+	test( 'renders title and description', () => {
+		render(
+			<CallOut
+				title="Test Title"
+				description={
+					<Text as="p" variant="muted">
+						Helpful content
+					</Text>
+				}
+			/>
+		);
+		expect(
+			screen.getByRole( 'heading', {
+				name: 'Test Title',
+			} )
+		).toBeInTheDocument();
+		expect( screen.getByRole( 'paragraph' ) ).toHaveTextContent( 'Helpful content' );
+	} );
+
+	test( 'variant prop applies correct class', () => {
+		const { container, rerender } = render(
+			<CallOut
+				title="Test Title"
+				description={
+					<Text as="p" variant="muted">
+						Helpful content
+					</Text>
+				}
+				variant="highlight"
+			/>
+		);
+		expect( container.firstChild ).toHaveClass( 'call-out--highlight' );
path: packages/components/src/call-out/test/index.tsx
user: login: mirka
body: We've generally found these kinds of CSS class name tests to not be worth the maintenance cost, since it's just testing an implementation detail (which is prone to change) and doesn't really guarantee that the visual result is correct.
created_at: 2025-05-19T10:51:46Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,114 @@
+/**
+ * @jest-environment jsdom
+ */
+import { fireEvent, render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import '@testing-library/user-event';
+import { Button, __experimentalText as Text } from '@wordpress/components';
+import React from 'react';
+import { CallOut } from '../index';
+
+describe( 'CallOut', () => {
+	test( 'renders title and description', () => {
+		render(
+			<CallOut
+				title="Test Title"
+				description={
+					<Text as="p" variant="muted">
+						Helpful content
+					</Text>
+				}
+			/>
+		);
+		expect(
+			screen.getByRole( 'heading', {
+				name: 'Test Title',
+			} )
+		).toBeInTheDocument();
+		expect( screen.getByRole( 'paragraph' ) ).toHaveTextContent( 'Helpful content' );
+	} );
+
+	test( 'variant prop applies correct class', () => {
+		const { container, rerender } = render(
+			<CallOut
+				title="Test Title"
+				description={
+					<Text as="p" variant="muted">
+						Helpful content
+					</Text>
+				}
+				variant="highlight"
+			/>
+		);
+		expect( container.firstChild ).toHaveClass( 'call-out--highlight' );
+
+		rerender(
+			<CallOut
+				title="Test Title"
+				description={
+					<Text as="p" variant="muted">
+						Helpful content
+					</Text>
+				}
+				variant="default"
+			/>
+		);
+		expect( container.firstChild ).toHaveClass( 'call-out--default' );
+	} );
+
+	test( 'action buttons are clickable', () => {
path: packages/components/src/call-out/test/index.tsx
user: login: mirka
body: It seems that this is testing `Button` and not `Callout`. At most, I would only test that what is passed into `actions` is `.toBeVisible`.

Relatedly, it can be a maintenance burden to use unnecessary wp-components (`Text`, `Button`) in these tests, as the tests may need to be updated when the upstream component changes. Plain HTML elements are preferable when the functionality is unrelated to the actual component being tested.
created_at: 2025-05-19T11:01:11Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,38 @@
+@import "@wordpress/base-styles/colors";
+@import "@wordpress/base-styles/mixins";
+@import "@wordpress/base-styles/variables";
path: packages/components/src/call-out/styles.scss
user: login: p-jackson
body: It looks like there's some extra work that's necessary before we can start using `@use` with packages. Afaics the existing uses in the repo are `@use "sass:math"` and relative module lookup. `@use "@wordpress/base-styles/variables"` isn't importing the expected variables into scope, so I'm guessing something needs to be done for `@wordpress/` lookup to work?
created_at: 2025-05-20T05:51:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,43 @@
+import type { Icon, __experimentalHeading as Heading } from '@wordpress/components';
+import type { ComponentProps, ReactNode } from 'react';
+
+export interface CallOutProps {
+	/**
+	 * A concise headline that clearly communicates the main message or feature being highlighted.
+	 */
+	title: string;
+	/**
+	 * The level of the heading to be used for the title.
+	 * @default 6
+	 */
+	headingLevel?: ComponentProps< typeof Heading >[ 'level' ];
path: packages/components/src/call-out/types.ts
user: login: p-jackson
body: > exposing this prop without the ability to override the semantic level is problematic

I don't understand, the reason I'm exposing this prop is specially to allow the level to be set semantically. The `<Heading>` component doesn't change visually, so this is just for the semantics.
We could expose an `as`, but it feels appropriate that we force the title to be a heading.
created_at: 2025-05-20T05:53:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,62 @@
+import {
+	Card,
+	__experimentalVStack as VStack,
+	__experimentalHStack as HStack,
+	__experimentalHeading as Heading,
+	Icon,
+} from '@wordpress/components';
+import { forwardRef } from 'react';
+import type { CallOutProps } from './types';
+import './styles.scss';
+
+function UnforwardedCallOut(
+	{
+		title,
+		icon,
+		illustrationSrc,
+		illustrationAlt,
+		description,
+		actions,
+		headingLevel = 6,
+		variant = 'default',
+	}: CallOutProps,
+	ref: React.ForwardedRef< HTMLElement >
+) {
+	return (
+		<Card ref={ ref } className={ `call-out call-out--${ variant }` }>
+			<HStack
+				className="call-out-h-container"
+				spacing="6"
+				alignment="stretch"
+				justify="stretch"
+				expanded={ false }
+			>
+				<VStack justify="flex-start" alignment="flex-start" spacing="4">
+					{ icon && <Icon icon={ icon } /> }
+					<Heading level={ headingLevel } className="call-out-title">
+						{ title }
+					</Heading>
+					{ description }
+					{ actions }
+				</VStack>
+				{ illustrationSrc && (
+					<VStack justify="stretch" alignment="stretch" className="call-out-illustration-container">
+						<img
+							src={ illustrationSrc }
+							alt={ illustrationAlt }
+							className="call-out-illustration"
+						/>
+					</VStack>
+				) }
path: packages/components/src/call-out/index.tsx
user: login: p-jackson
body: That was actually my original implementation, see: https://github.com/Automattic/wp-calypso/pull/103414#discussion_r2088644782

I like the idea of KISS for now, and just exposing a src prop. Which I think especially makes sense if I'm going to move this out of the `@automattic/components` package and directly into the app where we're using it.

But if it's staying in the component package, I'm open to revisit this.

> I would also consider renaming it from `illustration` to `image`, as it is more neutral and reflects the underlying element.

Sounds good to me. I went with how it was being described in the design spec, but if the component library is more likely to call this sort of thing `image` I will too.
created_at: 2025-05-20T06:00:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,43 @@
+import type { Icon, __experimentalHeading as Heading } from '@wordpress/components';
+import type { ComponentProps, ReactNode } from 'react';
+
+export interface CallOutProps {
+	/**
+	 * A concise headline that clearly communicates the main message or feature being highlighted.
+	 */
+	title: string;
+	/**
+	 * The level of the heading to be used for the title.
+	 * @default 6
+	 */
+	headingLevel?: ComponentProps< typeof Heading >[ 'level' ];
path: packages/components/src/call-out/types.ts
user: login: mirka
body: Oh, I see you're overriding the font size in CSS. The `Heading` component was apparently intended to provide the typography sizing scale ("`Heading` renders headings and titles using the library's typography system"), but since that scale is neither aligned with the current WP sizing scale nor are we even trying to leverage here, there's probably no reason to use that component. In fact we are actively discouraging style overrides like this.

Perhaps we might as well directly accept `h2` thru `h6` elements for the title? The default `h6` is quite arbitrary and I can see consumers forgetting to change it.
created_at: 2025-05-20T10:22:19Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,38 @@
+@import "@wordpress/base-styles/colors";
+@import "@wordpress/base-styles/mixins";
+@import "@wordpress/base-styles/variables";
path: packages/components/src/call-out/styles.scss
user: login: mirka
body: Gotcha. We're getting that issue addressed upstream in the base-styles package right now, so hopefully we'll get it working soon.
created_at: 2025-05-20T10:23:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,39 @@
+import { Meta, StoryObj } from '@storybook/react';
+import { Button, __experimentalText as Text } from '@wordpress/components';
+import { CallOut } from './';
+
+const meta: Meta< typeof CallOut > = {
+	title: 'CallOut',
+	component: CallOut,
+	tags: [ 'autodocs' ],
+	parameters: {
+		actions: { argTypesRegex: '^on.*' },
+		// Prevent flickering + automatic menu closing in compact mode.
+		layout: 'fullscreen',
path: packages/components/src/call-out/index.stories.tsx
user: login: p-jackson
body: Yes sorry 😆 
created_at: 2025-05-21T04:33:31Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,39 @@
+import { Meta, StoryObj } from '@storybook/react';
+import { Button, __experimentalText as Text } from '@wordpress/components';
+import { CallOut } from './';
+
+const meta: Meta< typeof CallOut > = {
+	title: 'CallOut',
+	component: CallOut,
+	tags: [ 'autodocs' ],
+	parameters: {
+		actions: { argTypesRegex: '^on.*' },
+		// Prevent flickering + automatic menu closing in compact mode.
+		layout: 'fullscreen',
+	},
+	decorators: [
+		( Story ) => (
+			<div style={ { paddingInlineStart: '1rem', paddingBlockStart: '1rem' } }>
+				<Story />
+			</div>
+		),
+	],
path: packages/components/src/call-out/index.stories.tsx
user: login: p-jackson
body: I thought i'd loose all padding from my story if I removed this. Turns out I don't 🤷 
created_at: 2025-05-21T04:34:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,62 @@
+import {
+	Card,
+	__experimentalVStack as VStack,
+	__experimentalHStack as HStack,
+	__experimentalHeading as Heading,
+	Icon,
+} from '@wordpress/components';
+import { forwardRef } from 'react';
+import type { CallOutProps } from './types';
+import './styles.scss';
+
+function UnforwardedCallOut(
path: packages/components/src/call-out/index.tsx
user: login: p-jackson
body: Looks like `Callout` is the winner. @jameskoster has already updated the design spec :) dhW7zYBamXJIeJIE3wy5f4-fi-3510_117161
created_at: 2025-05-21T04:36:23Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,114 @@
+/**
+ * @jest-environment jsdom
+ */
+import { fireEvent, render, screen } from '@testing-library/react';
+import '@testing-library/jest-dom';
+import '@testing-library/user-event';
+import { Button, __experimentalText as Text } from '@wordpress/components';
+import React from 'react';
+import { CallOut } from '../index';
+
+describe( 'CallOut', () => {
+	test( 'renders title and description', () => {
+		render(
+			<CallOut
+				title="Test Title"
+				description={
+					<Text as="p" variant="muted">
+						Helpful content
+					</Text>
+				}
+			/>
+		);
+		expect(
+			screen.getByRole( 'heading', {
+				name: 'Test Title',
+			} )
+		).toBeInTheDocument();
+		expect( screen.getByRole( 'paragraph' ) ).toHaveTextContent( 'Helpful content' );
+	} );
+
+	test( 'variant prop applies correct class', () => {
+		const { container, rerender } = render(
+			<CallOut
+				title="Test Title"
+				description={
+					<Text as="p" variant="muted">
+						Helpful content
+					</Text>
+				}
+				variant="highlight"
+			/>
+		);
+		expect( container.firstChild ).toHaveClass( 'call-out--highlight' );
path: packages/components/src/call-out/test/index.tsx
user: login: p-jackson
body: That's fair. What I really want is an assertion that tests what the user perceives, i.e. asserting whether the background is blue.

In general these tests are a bit low-effort. I wanted to have some sort of coverage for a shared component. But given this is pretty much a static component I can get by without trying to test user interactions.

That's a good tip about using standard elements rather than passing other DS components as props.
created_at: 2025-05-21T04:58:14Z
author_association: MEMBER
diff_hunk: @@ -1,16 +1,72 @@
+import { useQuery } from '@tanstack/react-query';
+import { __experimentalText as Text } from '@wordpress/components';
 import { __ } from '@wordpress/i18n';
+import { siteQuery } from '../../app/queries';
+import { siteRoute } from '../../app/router';
+import { Callout } from '../../components/callout';
+import { CalloutOverlay } from '../../components/callout-overlay';
 import DataViewsCard from '../../components/dataviews-card';
 import { PageHeader } from '../../components/page-header';
 import PageLayout from '../../components/page-layout';
+import RouterLinkButton from '../../components/router-link-button';
+import { SitePlan } from '../../data/types';
+import illustrationUrl from './deployments-callout-illustration.svg';
+import ghIconUrl from './gh-icon.svg';
 
 function SiteDeployments() {
+	const { siteSlug } = siteRoute.useParams();
+	const { data: site } = useQuery( siteQuery( siteSlug ) );
+
+	if ( ! site ) {
+		return;
+	}
+
+	const showCallout =
+		( ! site.plan || ! hasDeploymentsFeature( site.plan ) ) &&
+		// Show to all atomic sites regardless of plan status e.g. an atomic site in the revert queue
+		// after a plan downgrade will continue to see Deployments until the revert is complete
+		! site.is_wpcom_atomic;
path: client/dashboard/sites/deployments/index.tsx
user: login: fushar
body: The condition seems to be different from the original logic in v1:

https://github.com/Automattic/wp-calypso/blob/9eca4fcfea8d0fadd390c7573f9540a12e23d6e7/client/sites/components/site-preview-pane/dotcom-preview-pane.tsx#L67

`showCallout` should be the inverse of the above condition. 🤔 

created_at: 2025-05-22T15:24:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,16 +1,72 @@
+import { useQuery } from '@tanstack/react-query';
+import { __experimentalText as Text } from '@wordpress/components';
 import { __ } from '@wordpress/i18n';
+import { siteQuery } from '../../app/queries';
+import { siteRoute } from '../../app/router';
+import { Callout } from '../../components/callout';
+import { CalloutOverlay } from '../../components/callout-overlay';
 import DataViewsCard from '../../components/dataviews-card';
 import { PageHeader } from '../../components/page-header';
 import PageLayout from '../../components/page-layout';
+import RouterLinkButton from '../../components/router-link-button';
+import { SitePlan } from '../../data/types';
+import illustrationUrl from './deployments-callout-illustration.svg';
+import ghIconUrl from './gh-icon.svg';
 
 function SiteDeployments() {
+	const { siteSlug } = siteRoute.useParams();
+	const { data: site } = useQuery( siteQuery( siteSlug ) );
+
+	if ( ! site ) {
+		return;
+	}
+
+	const showCallout =
+		( ! site.plan || ! hasDeploymentsFeature( site.plan ) ) &&
+		// Show to all atomic sites regardless of plan status e.g. an atomic site in the revert queue
+		// after a plan downgrade will continue to see Deployments until the revert is complete
+		! site.is_wpcom_atomic;
+
 	return (
 		<PageLayout header={ <PageHeader title={ __( 'Deployments' ) } /> }>
-			<DataViewsCard>
-				<></>
-			</DataViewsCard>
+			<CalloutOverlay
+				showCallout={ showCallout }
+				callout={
+					<Callout
+						icon={ <img src={ ghIconUrl } alt={ __( 'GitHub logo' ) } /> }
+						title={ __( 'Deploy from GitHub' ) }
+						imageSrc={ illustrationUrl }
+						description={
+							<>
+								<Text as="p" variant="muted">
+									{ __(
+										'Connect your GitHub repo directly to your WordPress.com site—with seamless integration, straightforward version control, and automated workflows.'
+									) }
+								</Text>
+								<Text as="p" variant="muted">
+									{ __( 'Available on the WordPress.com Business and Commerce plans.' ) }
+								</Text>
+							</>
+						}
+						actions={
+							<RouterLinkButton __next40pxDefaultSize variant="primary" to="#">
+								{ __( 'Upgrade plan' ) }
+							</RouterLinkButton>
+						}
+					/>
+				}
+				main={
+					<DataViewsCard>
+						<></>
+					</DataViewsCard>
path: client/dashboard/sites/deployments/index.tsx
user: login: fushar
body: I'm not sure how this works. Shouldn't the content of `main` depends on `showCallout`? As in, if `showCallout` is true, we should render a blurred placeholder content here; otherwise, we render the real component (the real Deployments page). How would we handle that?
created_at: 2025-05-22T15:28:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,16 +1,72 @@
+import { useQuery } from '@tanstack/react-query';
+import { __experimentalText as Text } from '@wordpress/components';
 import { __ } from '@wordpress/i18n';
+import { siteQuery } from '../../app/queries';
+import { siteRoute } from '../../app/router';
+import { Callout } from '../../components/callout';
+import { CalloutOverlay } from '../../components/callout-overlay';
 import DataViewsCard from '../../components/dataviews-card';
 import { PageHeader } from '../../components/page-header';
 import PageLayout from '../../components/page-layout';
+import RouterLinkButton from '../../components/router-link-button';
+import { SitePlan } from '../../data/types';
+import illustrationUrl from './deployments-callout-illustration.svg';
+import ghIconUrl from './gh-icon.svg';
 
 function SiteDeployments() {
+	const { siteSlug } = siteRoute.useParams();
+	const { data: site } = useQuery( siteQuery( siteSlug ) );
+
+	if ( ! site ) {
+		return;
+	}
+
+	const showCallout =
+		( ! site.plan || ! hasDeploymentsFeature( site.plan ) ) &&
+		// Show to all atomic sites regardless of plan status e.g. an atomic site in the revert queue
+		// after a plan downgrade will continue to see Deployments until the revert is complete
+		! site.is_wpcom_atomic;
+
 	return (
 		<PageLayout header={ <PageHeader title={ __( 'Deployments' ) } /> }>
-			<DataViewsCard>
-				<></>
-			</DataViewsCard>
+			<CalloutOverlay
+				showCallout={ showCallout }
+				callout={
+					<Callout
+						icon={ <img src={ ghIconUrl } alt={ __( 'GitHub logo' ) } /> }
+						title={ __( 'Deploy from GitHub' ) }
+						imageSrc={ illustrationUrl }
+						description={
+							<>
+								<Text as="p" variant="muted">
+									{ __(
+										'Connect your GitHub repo directly to your WordPress.com site—with seamless integration, straightforward version control, and automated workflows.'
+									) }
+								</Text>
+								<Text as="p" variant="muted">
+									{ __( 'Available on the WordPress.com Business and Commerce plans.' ) }
+								</Text>
+							</>
+						}
+						actions={
+							<RouterLinkButton __next40pxDefaultSize variant="primary" to="#">
+								{ __( 'Upgrade plan' ) }
+							</RouterLinkButton>
+						}
+					/>
+				}
+				main={
+					<DataViewsCard>
+						<></>
+					</DataViewsCard>
path: client/dashboard/sites/deployments/index.tsx
user: login: p-jackson
body: Oooh that's an interesting point. I was imagining the placeholder content would use the same elements as the real page, and it would be the data that was swapped out for dummy data. But I see now that the placeholder content could be totally different elements.

I like the way that in this situation the placeholder is using the same elements as the live data. But shall we wait until we have some placeholder components before we settle on what the right abstraction is? The main thing about this PR is that the `CalloutOverlay` needs to own the placeholder elements so that it can correctly blur them.
created_at: 2025-05-22T23:03:47Z
author_association: MEMBER
diff_hunk: @@ -1,16 +1,72 @@
+import { useQuery } from '@tanstack/react-query';
+import { __experimentalText as Text } from '@wordpress/components';
 import { __ } from '@wordpress/i18n';
+import { siteQuery } from '../../app/queries';
+import { siteRoute } from '../../app/router';
+import { Callout } from '../../components/callout';
+import { CalloutOverlay } from '../../components/callout-overlay';
 import DataViewsCard from '../../components/dataviews-card';
 import { PageHeader } from '../../components/page-header';
 import PageLayout from '../../components/page-layout';
+import RouterLinkButton from '../../components/router-link-button';
+import { SitePlan } from '../../data/types';
+import illustrationUrl from './deployments-callout-illustration.svg';
+import ghIconUrl from './gh-icon.svg';
 
 function SiteDeployments() {
+	const { siteSlug } = siteRoute.useParams();
+	const { data: site } = useQuery( siteQuery( siteSlug ) );
+
+	if ( ! site ) {
+		return;
+	}
+
+	const showCallout =
+		( ! site.plan || ! hasDeploymentsFeature( site.plan ) ) &&
+		// Show to all atomic sites regardless of plan status e.g. an atomic site in the revert queue
+		// after a plan downgrade will continue to see Deployments until the revert is complete
+		! site.is_wpcom_atomic;
path: client/dashboard/sites/deployments/index.tsx
user: login: p-jackson
body: Interesting, I think it's sort of annoying when an atomic site can't see the atomic features. But that might just be a dev thing 😃 It makes sense to copy v1 business logic unless making an explicit proposal to change it.
created_at: 2025-05-22T23:15:23Z
author_association: MEMBER
diff_hunk: @@ -1,16 +1,72 @@
+import { useQuery } from '@tanstack/react-query';
+import { __experimentalText as Text } from '@wordpress/components';
 import { __ } from '@wordpress/i18n';
+import { siteQuery } from '../../app/queries';
+import { siteRoute } from '../../app/router';
+import { Callout } from '../../components/callout';
+import { CalloutOverlay } from '../../components/callout-overlay';
 import DataViewsCard from '../../components/dataviews-card';
 import { PageHeader } from '../../components/page-header';
 import PageLayout from '../../components/page-layout';
+import RouterLinkButton from '../../components/router-link-button';
+import { SitePlan } from '../../data/types';
+import illustrationUrl from './deployments-callout-illustration.svg';
+import ghIconUrl from './gh-icon.svg';
 
 function SiteDeployments() {
+	const { siteSlug } = siteRoute.useParams();
+	const { data: site } = useQuery( siteQuery( siteSlug ) );
+
+	if ( ! site ) {
+		return;
+	}
+
+	const showCallout =
+		( ! site.plan || ! hasDeploymentsFeature( site.plan ) ) &&
+		// Show to all atomic sites regardless of plan status e.g. an atomic site in the revert queue
+		// after a plan downgrade will continue to see Deployments until the revert is complete
+		! site.is_wpcom_atomic;
+
 	return (
 		<PageLayout header={ <PageHeader title={ __( 'Deployments' ) } /> }>
-			<DataViewsCard>
-				<></>
-			</DataViewsCard>
+			<CalloutOverlay
+				showCallout={ showCallout }
+				callout={
+					<Callout
+						icon={ <img src={ ghIconUrl } alt={ __( 'GitHub logo' ) } /> }
+						title={ __( 'Deploy from GitHub' ) }
+						imageSrc={ illustrationUrl }
+						description={
+							<>
+								<Text as="p" variant="muted">
+									{ __(
+										'Connect your GitHub repo directly to your WordPress.com site—with seamless integration, straightforward version control, and automated workflows.'
+									) }
+								</Text>
+								<Text as="p" variant="muted">
+									{ __( 'Available on the WordPress.com Business and Commerce plans.' ) }
+								</Text>
+							</>
+						}
+						actions={
+							<RouterLinkButton __next40pxDefaultSize variant="primary" to="#">
+								{ __( 'Upgrade plan' ) }
+							</RouterLinkButton>
+						}
+					/>
+				}
+				main={
+					<DataViewsCard>
+						<></>
+					</DataViewsCard>
path: client/dashboard/sites/deployments/index.tsx
user: login: fushar
body: > I see now that the placeholder content could be totally different elements.

Yeah, this is what I imagine, too:

- Deployments renders deployment list with many rows of fake data
- Monitoring renders fake graphs

> placeholder content would use the same elements as the real page, and it would be the data that was swapped out for dummy data.

Not sure but I *think* it's harder to pass around the `showCallout` state to various downstream components, instead of just coming up with a fake static content in the first place.

---

Not a blocker though, we can change the logic later, just wanted to call out (pun slightly intended)!
created_at: 2025-05-23T02:08:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,16 +1,72 @@
+import { useQuery } from '@tanstack/react-query';
+import { __experimentalText as Text } from '@wordpress/components';
 import { __ } from '@wordpress/i18n';
+import { siteQuery } from '../../app/queries';
+import { siteRoute } from '../../app/router';
+import { Callout } from '../../components/callout';
+import { CalloutOverlay } from '../../components/callout-overlay';
 import DataViewsCard from '../../components/dataviews-card';
 import { PageHeader } from '../../components/page-header';
 import PageLayout from '../../components/page-layout';
+import RouterLinkButton from '../../components/router-link-button';
+import { SitePlan } from '../../data/types';
+import illustrationUrl from './deployments-callout-illustration.svg';
+import ghIconUrl from './gh-icon.svg';
 
 function SiteDeployments() {
+	const { siteSlug } = siteRoute.useParams();
+	const { data: site } = useQuery( siteQuery( siteSlug ) );
+
+	if ( ! site ) {
+		return;
+	}
+
+	const showCallout =
+		( ! site.plan || ! hasDeploymentsFeature( site.plan ) ) &&
+		// Show to all atomic sites regardless of plan status e.g. an atomic site in the revert queue
+		// after a plan downgrade will continue to see Deployments until the revert is complete
+		! site.is_wpcom_atomic;
path: client/dashboard/sites/deployments/index.tsx
user: login: fushar
body: IIRC the "features" in the wpcom backend code is tied to plans, not the atomic state.

We can iterate on it later when we actually implement the Deployments screen.
created_at: 2025-05-23T02:11:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,62 @@
+import {
+	Card,
+	__experimentalVStack as VStack,
+	__experimentalHStack as HStack,
+	__experimentalHeading as Heading,
+	Icon,
+} from '@wordpress/components';
+import { forwardRef } from 'react';
+import type { CallOutProps } from './types';
+import './styles.scss';
+
+function UnforwardedCallOut(
+	{
+		title,
+		icon,
+		illustrationSrc,
+		illustrationAlt,
+		description,
+		actions,
+		headingLevel = 6,
+		variant = 'default',
+	}: CallOutProps,
+	ref: React.ForwardedRef< HTMLElement >
+) {
+	return (
+		<Card ref={ ref } className={ `call-out call-out--${ variant }` }>
+			<HStack
+				className="call-out-h-container"
+				spacing="6"
+				alignment="stretch"
+				justify="stretch"
+				expanded={ false }
+			>
+				<VStack justify="flex-start" alignment="flex-start" spacing="4">
+					{ icon && <Icon icon={ icon } /> }
+					<Heading level={ headingLevel } className="call-out-title">
+						{ title }
+					</Heading>
+					{ description }
+					{ actions }
+				</VStack>
+				{ illustrationSrc && (
+					<VStack justify="stretch" alignment="stretch" className="call-out-illustration-container">
+						<img
+							src={ illustrationSrc }
+							alt={ illustrationAlt }
+							className="call-out-illustration"
+						/>
+					</VStack>
+				) }
path: packages/components/src/call-out/index.tsx
user: login: youknowriad
body: Funny I made the opposite suggestion here https://github.com/Automattic/wp-calypso/pull/103702#discussion_r2107069808

My thinking is that naming an API shouldn't consider how the component works internally. If this is supposed to be an "illustration", then "illustration" might be a good name for the prop.

Now, "image" is fine too but I don't like "imageSrc" feels too implementation specific.
created_at: 2025-05-26T15:28:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,62 @@
+import {
+	Card,
+	__experimentalVStack as VStack,
+	__experimentalHStack as HStack,
+	__experimentalHeading as Heading,
+	Icon,
+} from '@wordpress/components';
+import { forwardRef } from 'react';
+import type { CallOutProps } from './types';
+import './styles.scss';
+
+function UnforwardedCallOut(
+	{
+		title,
+		icon,
+		illustrationSrc,
+		illustrationAlt,
+		description,
+		actions,
+		headingLevel = 6,
+		variant = 'default',
+	}: CallOutProps,
+	ref: React.ForwardedRef< HTMLElement >
+) {
+	return (
+		<Card ref={ ref } className={ `call-out call-out--${ variant }` }>
+			<HStack
+				className="call-out-h-container"
+				spacing="6"
+				alignment="stretch"
+				justify="stretch"
+				expanded={ false }
+			>
+				<VStack justify="flex-start" alignment="flex-start" spacing="4">
+					{ icon && <Icon icon={ icon } /> }
+					<Heading level={ headingLevel } className="call-out-title">
+						{ title }
+					</Heading>
+					{ description }
+					{ actions }
+				</VStack>
+				{ illustrationSrc && (
+					<VStack justify="stretch" alignment="stretch" className="call-out-illustration-container">
+						<img
+							src={ illustrationSrc }
+							alt={ illustrationAlt }
+							className="call-out-illustration"
+						/>
+					</VStack>
+				) }
path: packages/components/src/call-out/index.tsx
user: login: taipeicoder
body: I'll rename the prop from "imageSrc" to "image" in https://github.com/Automattic/wp-calypso/pull/103702. EDIT: On second thought, I'll create a separate PR for it.
created_at: 2025-05-27T00:35:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,62 @@
+import {
+	Card,
+	__experimentalVStack as VStack,
+	__experimentalHStack as HStack,
+	__experimentalHeading as Heading,
+	Icon,
+} from '@wordpress/components';
+import { forwardRef } from 'react';
+import type { CallOutProps } from './types';
+import './styles.scss';
+
+function UnforwardedCallOut(
+	{
+		title,
+		icon,
+		illustrationSrc,
+		illustrationAlt,
+		description,
+		actions,
+		headingLevel = 6,
+		variant = 'default',
+	}: CallOutProps,
+	ref: React.ForwardedRef< HTMLElement >
+) {
+	return (
+		<Card ref={ ref } className={ `call-out call-out--${ variant }` }>
+			<HStack
+				className="call-out-h-container"
+				spacing="6"
+				alignment="stretch"
+				justify="stretch"
+				expanded={ false }
+			>
+				<VStack justify="flex-start" alignment="flex-start" spacing="4">
+					{ icon && <Icon icon={ icon } /> }
+					<Heading level={ headingLevel } className="call-out-title">
+						{ title }
+					</Heading>
+					{ description }
+					{ actions }
+				</VStack>
+				{ illustrationSrc && (
+					<VStack justify="stretch" alignment="stretch" className="call-out-illustration-container">
+						<img
+							src={ illustrationSrc }
+							alt={ illustrationAlt }
+							className="call-out-illustration"
+						/>
+					</VStack>
+				) }
path: packages/components/src/call-out/index.tsx
user: login: taipeicoder
body: Created follow up https://github.com/Automattic/wp-calypso/pull/103725
created_at: 2025-05-27T01:17:23Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-14T10:10:16Z
body: <details>
<summary>Calypso Live <a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-145470">(direct link)</a></summary>
<table>
<tr>
<td>
<img src="https://chart.googleapis.com/chart?chs=150x150&cht=qr&chl=https%3A%2F%2Fcalypso.live%3Fimage%3Dregistry.a8c.com%2Fcalypso%2Fapp%3Abuild-145470%26flags%3Doauth&choe=UTF-8" />
</td>
<td>
<a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-145470">https://calypso.live?image=registry.a8c.com/calypso/app:build-145470</a>
</td>
</tr>
</table>
</details>
<details>
<summary>Jetpack Cloud live <a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-145470&env=jetpack">(direct link)</a></summary>
<table>
<tr>
<td>
<img src="https://chart.googleapis.com/chart?chs=150x150&cht=qr&chl=https%3A%2F%2Fcalypso.live%3Fimage%3Dregistry.a8c.com%2Fcalypso%2Fapp%3Abuild-145470%26env%3Djetpack%26flags%3Doauth&choe=UTF-8" />
</td>
<td>
<a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-145470&env=jetpack">https://calypso.live?image=registry.a8c.com/calypso/app:build-145470&env=jetpack</a>
</td>
</tr>
</table>
</details>
<details>
<summary>Automattic for Agencies live <a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-145470&env=a8c-for-agencies">(direct link)</a></summary>
<table>
<tr>
<td>
<img src="https://chart.googleapis.com/chart?chs=150x150&cht=qr&chl=https%3A%2F%2Fcalypso.live%3Fimage%3Dregistry.a8c.com%2Fcalypso%2Fapp%3Abuild-145470%26env%3Da8c-for-agencies%26flags%3Doauth&choe=UTF-8" />
</td>
<td>
<a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-145470&env=a8c-for-agencies">https://calypso.live?image=registry.a8c.com/calypso/app:build-145470&env=a8c-for-agencies</a>
</td>
</tr>
</table>
</details>
<!--calypso-live-watermark:apr@v1-->
author_association: NONE
user: login: matticbot
created_at: 2025-05-14T10:20:10Z
body: <!-- icfy-watermark: c52822 -->
Here is how your PR affects size of JS and CSS bundles shipped to the user's browser:

**App Entrypoints** (~169 bytes added 📈 [gzipped])
<details>

```
name                    parsed_size           gzip_size
entry-dashboard-dotcom       +359 B  (+0.0%)     +143 B  (+0.0%)
entry-dashboard-a4a          +359 B  (+0.0%)     +143 B  (+0.0%)
entry-subscriptions          +296 B  (+0.0%)     +117 B  (+0.0%)
entry-stepper                +296 B  (+0.0%)     +117 B  (+0.0%)
entry-reauth-required        +296 B  (+0.0%)     +117 B  (+0.0%)
entry-main                   +296 B  (+0.0%)     +117 B  (+0.0%)
entry-login                  +296 B  (+0.0%)     +117 B  (+0.0%)
entry-domains-landing        +296 B  (+0.0%)     +117 B  (+0.1%)
entry-browsehappy            +296 B  (+0.1%)     +117 B  (+0.2%)
```

Common code that is always downloaded and parsed every time the app is loaded, no matter which route is used.
</details>

**Legend**
<details>
<summary>What is parsed and gzip size?</summary>

**Parsed Size:** Uncompressed size of the JS and CSS files. This much code needs to be parsed and stored in memory.
**Gzip Size:** Compressed size of the JS and CSS files. This much data needs to be downloaded over network.
</details>

Generated by performance advisor bot at [iscalypsofastyet.com](http://iscalypsofastyet.com).
author_association: CONTRIBUTOR
user: login: jasmussen
created_at: 2025-05-14T11:16:26Z
body: > Specifically the hosting dashboard doesn't have a need for the "full bleed" or "highlighted" variants right now

Sounds good to me, I like to start with as little as possible and grow in variants/densities either when it becomes necessary or when there's a clearly identified future need. 
author_association: MEMBER
user: login: p-jackson
created_at: 2025-05-16T03:42:07Z
body: <!-- linear:replyTo:2844200904 -->

The DS figma uses `CallOut` so I've been trying to do the same. I'm using `call-out` for lowercase file paths and trying to use "call out" or "call-out" in English conversations like this. It can be a bit cumbersome since callout just rolls off the tongue 🙂 

`CallOut` is good though IMO. "Callout" reminds me of a verb. Like a `.callout()` method nae. But "Call Out" feels like a noun.
author_association: MEMBER
user: login: matticbot
created_at: 2025-05-16T06:35:31Z
body: **This PR modifies the release build for the following Calypso Apps:**

_For info about this notification, see here: PCYsg-OT6-p2_

* help-center
* notifications
* wpcom-block-editor

To test WordPress.com changes, run `install-plugin.sh $pluginSlug add/call-out-component-deployments` on your sandbox.
<!--calypso-app-artifacts-watermark:apr@v1-->
author_association: CONTRIBUTOR
user: login: jasmussen
created_at: 2025-05-16T08:32:11Z
body: No strong opinions on Callout or CallOut, though I found this:

> In design systems and UI libraries (like Material UI, Fluent, Carbon, etc.), component names typically use PascalCase (e.g., Button, Alert, Card, Callout). In PascalCase, each word is capitalized, but compound words like "Callout" are usually written as a single word, not split.
author_association: MEMBER
user: login: jameskoster
created_at: 2025-05-19T11:06:19Z
body: <!-- linear:replyTo:2849294289 -->

Yes I think so. Can it be built into the component as a container query, using the `$break-mobile` breakpoint (`480px`)? 
author_association: CONTRIBUTOR
user: login: jameskoster
created_at: 2025-05-19T11:46:48Z
body: <!-- linear:replyTo:2850215631 -->

I don't have the full context or a sense of urgency, but I've had conversations with designers about using this component outside of the host dashboard project. cc @nuriapenya and @<!-- linear:userMention:7383b9d7-dc3f-4d14-9568-ee3afc303109 -->marina.matijaca1 for input. Where are y'all planning to use `Callout`, is it needed soon?
author_association: CONTRIBUTOR
user: login: p-jackson
created_at: 2025-05-21T09:27:38Z
body: @fushar @jameskoster when the content is stacked vertically and the image takes on it's natural height it just dominates.

![CleanShot 2025-05-23 at 10 56 14](https://github.com/user-attachments/assets/a9f186ae-650f-499f-b6de-38a44e0a3440)


The design spec actually calls for the content to stay presented horizontally, but it's demo content is much shorter. The two paragraphs worth of copy really makes it look wrong presented horizontally.

I looks a bit dry, but I think the fix on small screens might just be to hide the illustration.

![CleanShot 2025-05-23 at 10 56 30](https://github.com/user-attachments/assets/97c88061-d8e7-4c9d-b8ce-f1a8ac3c6427)

author_association: MEMBER
user: login: p-jackson
created_at: 2025-05-21T10:25:48Z
body: @ellatrix I've just rebased your fancy `view-transitions.scss` changes into this PR. Transitions appear to mess with the blur effect that should be obscuring the page header. Check PR description for what it's supposed to look like.

After rebasing the `<h1>` looks crisp instead of blurry.

![CleanShot 2025-05-21 at 22 20 29@2x](https://github.com/user-attachments/assets/fb764c88-a4b7-4ddc-bd60-4647df3b6a0d)

It makes sense that transitions wouldn't be compatible with overlapping elements. Does it make sense to you that we should disable the transitions when the page the user is coming from or going to has an overlay? Have you got any ideas for how we could add a test to the selectors in `view-transitions.scss` that could detect whether an overlay was visible?
author_association: MEMBER
user: login: jameskoster
created_at: 2025-05-21T14:40:19Z
body: <!-- linear:replyTo:2850215631 -->

> when the content is stacked vertically and the image takes on it's natural height it just dominates

Yeah that does look quite heavy. Maybe it's okay for the layout to remain horizontal. What do you think @matt-west?

I'm going to cc @annchichi too since you've been looking at this component recently; did you have any feelings about how the layout would adapt as the container shrinks?
author_association: CONTRIBUTOR
user: login: matt-west
created_at: 2025-05-21T15:29:26Z
body: I'm tempted to drop the graphic on mobile. In the hosting dashboard, the graphic is complimentary to the main content, but not really essential. That might not always be the case though if the graphic is used to show a specific feature/animation.

author_association: CONTRIBUTOR
user: login: p-jackson
created_at: 2025-05-21T23:38:54Z
body: @jameskoster @matt-west  This is what it looks like horizontal on an iPhone 12 Pro if I also decrease the card's padding and column gap to `grid-20` on `$break-small` screens.

(ignore misalignment along the right, the dashboard appears to have a layout bug on small screens)

![CleanShot 2025-05-23 at 10 55 10](https://github.com/user-attachments/assets/2d10dc5a-56b1-4ca4-8715-080cf28fbe5c)


I don't think it looks _that_ bad. Although this graphic just happens to crop really well at this aspect ratio. The staging site illustration wouldn't work as well

![CleanShot 2025-05-23 at 10 55 31](https://github.com/user-attachments/assets/e60d2bd6-5d37-4dfd-be75-d9fe5c88661a)

author_association: MEMBER
user: login: jasmussen
created_at: 2025-05-22T07:07:26Z
body: > I'm tempted to drop the graphic on mobile. In the hosting dashboard, the graphic is complimentary to the main content, but not really essential. That might not always be the case though if the graphic is used to show a specific feature/animation.

I'm skeptical of that, not because the idea is bad, but because ideally we think in generic components, meaning if we decide to drop the graphic from mobile, we're essentially doing that for all callouts going forward. We just need to be sure that's what we want.
author_association: MEMBER
user: login: matt-west
created_at: 2025-05-22T08:41:42Z
body: @jasmussen That's fair. 
author_association: CONTRIBUTOR
user: login: fushar
created_at: 2025-05-22T15:11:32Z
body: @ellatrix could you help with this comment? https://github.com/Automattic/wp-calypso/pull/103414#issuecomment-2897434989 Thanks!
author_association: CONTRIBUTOR
user: login: fushar
created_at: 2025-05-22T15:12:50Z
body: @p-jackson I think the CleanShot images are broken in your comments

<img width="683" alt="image" src="https://github.com/user-attachments/assets/a6e1ef40-bf89-458c-bccb-9f699e9d6846" />

author_association: CONTRIBUTOR
user: login: jameskoster
created_at: 2025-05-22T15:34:23Z
body: <!-- linear:replyTo:2850215631 -->

Maybe we introduce a property to allow consumers to hide the illustration at a certain size?
author_association: CONTRIBUTOR
user: login: p-jackson
created_at: 2025-05-23T04:24:20Z
body: I think this PR is in a good place to merge.

- [There's an conversation about the callout mobile layout](https://github.com/Automattic/wp-calypso/pull/103414#issuecomment-2900150227). I'm not a fan of adding a prop to decide whether the image should be hidden on mobile, I'd rather we decide to always design copy and images in such a way that they'll look good on narrow screens to. But for the purposes of this PR I'm happy implementing it to the spec. And then continuing the conversation about whether the spec should change 😃 
- [The abstraction for how placeholder components are rendered might not be right](https://github.com/Automattic/wp-calypso/pull/103414#discussion_r2102845705). But I think we need a concrete example we can work with before we settle on the correct abstraction.
- [Page transitions have introduced an issued a bug in the blurred background](https://github.com/Automattic/wp-calypso/pull/103414#issuecomment-2897434989). We can address that in a follow up. 
author_association: MEMBER
user: login: p-jackson
created_at: 2025-05-23T05:28:21Z
body: I've summarised the mobile layout issues in DES-52
author_association: MEMBER
user: login: a8ci18n
created_at: 2025-05-23T07:20:54Z
body: This Pull Request is now available for translation here: https://translate.wordpress.com/deliverables/17505574

Some locales (Hebrew) have been temporarily machine-translated due to translator availability. All other translations are usually ready within a few days. Untranslated and machine-translated strings will be sent for translation next Monday and are expected to be completed by the following Friday.

Thank you @p-jackson for including a screenshot in the description! This is really helpful for our translators.
author_association: NONE