Repository: Automattic/wp-calypso
Pull request #103750: Components: Organize some highlight components
Author: login: tyxla
Labels: name: Components
name: Janitorial
Created: 2025-05-27T15:12:26Z | Merged: 2025-05-29T07:48:14Z

Stacked on #103749 which cleans up some unused components.

## Proposed Changes

Move some highlight card components around so they're in their own files and ready to be moved outside. 

## Why are these changes being made?

I'm planning to move all these components outside of `@automattic/components` because they're not part of the DS and are used only in stats, but first, I'm moving some code around. 

As a next step, I'll move the entire directory of components outside of `@automattic/components`.

## Testing Instructions

* All checks should be green.


Diff:
diff --git a/client/my-sites/stats/stats-tabs/tab.jsx b/client/my-sites/stats/stats-tabs/tab.jsx
index 0c86497491aa..04bd02ef3ac7 100644
--- a/client/my-sites/stats/stats-tabs/tab.jsx
+++ b/client/my-sites/stats/stats-tabs/tab.jsx
@@ -1,5 +1,4 @@
-import { LoadingPlaceholder } from '@automattic/components';
-import { TooltipContent } from '@automattic/components/src/highlight-cards/count-card';
+import { LoadingPlaceholder, TooltipContent } from '@automattic/components';
 import { TrendComparison } from '@automattic/components/src/highlight-cards/count-comparison-card';
 import Popover from '@automattic/components/src/popover';
 import { formatNumberCompact, formatNumber } from '@automattic/number-formatters';
diff --git a/packages/components/src/highlight-cards/annual-highlight-cards.tsx b/packages/components/src/highlight-cards/annual-highlight-cards.tsx
index 1cc9957b282f..96b7553bb6cc 100644
--- a/packages/components/src/highlight-cards/annual-highlight-cards.tsx
+++ b/packages/components/src/highlight-cards/annual-highlight-cards.tsx
@@ -3,7 +3,6 @@ import clsx from 'clsx';
 import { translate, useTranslate } from 'i18n-calypso';
 import ComponentSwapper from '../component-swapper';
 import CountCard from './count-card';
-import HighlightCardsHeading from './highlight-cards-heading';
 import MobileHighlightCardListing from './mobile-highlight-cards';
 
 import './style.scss';
@@ -58,6 +57,18 @@ function AnnualHighlightsStandard( { counts }: AnnualHighlightsProps ) {
 	);
 }
 
+function HighlightCardsHeading( { children }: { children: React.ReactNode } ) {
+	const translate = useTranslate();
+	return (
+		<div className="highlight-cards-heading__wrapper">
+			<h3 className="highlight-cards-heading">{ children }</h3>
+			<div className="highlight-cards-heading__update-frequency">
+				<span>{ translate( 'Updates every 30 minutes' ) }</span>
+			</div>
+		</div>
+	);
+}
+
 export default function AnnualHighlightCards( {
 	className,
 	counts,
diff --git a/packages/components/src/highlight-cards/count-card.tsx b/packages/components/src/highlight-cards/count-card.tsx
index a16a62d825b4..540379e432d1 100644
--- a/packages/components/src/highlight-cards/count-card.tsx
+++ b/packages/components/src/highlight-cards/count-card.tsx
@@ -1,10 +1,9 @@
-import { formatNumber as formatNumberI18n } from '@automattic/number-formatters';
-import { arrowDown, arrowUp, Icon } from '@wordpress/icons';
 import clsx from 'clsx';
 import { useRef, useState } from 'react';
 import { Card } from '../';
 import Popover from '../popover';
-import { subtract, formatNumber } from './lib/numbers';
+import { formatNumber } from './lib/numbers';
+import TooltipContent from './tooltip-content';
 
 interface CountCardProps {
 	heading?: React.ReactNode;
@@ -16,39 +15,6 @@ interface CountCardProps {
 	previousValue?: number | null;
 }
 
-export function TooltipContent( { value, label, note, previousValue }: CountCardProps ) {
-	const difference = subtract( value, previousValue );
-
-	let trendClass = 'highlight-card-tooltip-count-difference-positive';
-	let trendIcon = arrowUp;
-	if ( difference !== null && difference < 0 ) {
-		trendClass = 'highlight-card-tooltip-count-difference-negative';
-		trendIcon = arrowDown;
-	}
-
-	/**
-	 * TODO clk - We are currently in the process of unifying numberFormat from i18n-calypso with the one from number-formatters.
-	 * Once settled, we should consider where to place the "-" default for null values.
-	 */
-	const tooltipCount = value !== null ? formatNumberI18n( value ) : '—';
-
-	return (
-		<div className="highlight-card-tooltip-content">
-			<span className="highlight-card-tooltip-counts">
-				{ tooltipCount }
-				{ label && ` ${ label }` }
-			</span>
-			{ difference !== null && difference !== 0 && (
-				<span className={ trendClass }>
-					<Icon size={ 18 } icon={ trendIcon } />
-					{ formatNumberI18n( Math.abs( difference ) ) }
-				</span>
-			) }
-			{ note && <div className="highlight-card-tooltip-note">{ note }</div> }
-		</div>
-	);
-}
-
 export default function CountCard( {
 	heading,
 	icon,
diff --git a/packages/components/src/highlight-cards/highlight-cards-heading.tsx b/packages/components/src/highlight-cards/highlight-cards-heading.tsx
deleted file mode 100644
index f4c6408df867..000000000000
--- a/packages/components/src/highlight-cards/highlight-cards-heading.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { useTranslate } from 'i18n-calypso';
-
-export default function HighlightCardsHeading( { children }: { children: React.ReactNode } ) {
-	const translate = useTranslate();
-	return (
-		<div className="highlight-cards-heading__wrapper">
-			<h3 className="highlight-cards-heading">{ children }</h3>
-			<div className="highlight-cards-heading__update-frequency">
-				<span>{ translate( 'Updates every 30 minutes' ) }</span>
-			</div>
-		</div>
-	);
-}
diff --git a/packages/components/src/highlight-cards/tooltip-content.tsx b/packages/components/src/highlight-cards/tooltip-content.tsx
new file mode 100644
index 000000000000..d9ad4cbea219
--- /dev/null
+++ b/packages/components/src/highlight-cards/tooltip-content.tsx
@@ -0,0 +1,48 @@
+import { formatNumber as formatNumberI18n } from '@automattic/number-formatters';
+import { arrowDown, arrowUp, Icon } from '@wordpress/icons';
+import { subtract } from './lib/numbers';
+
+interface TooltipContentProps {
+	label?: string;
+	note?: string;
+	value: number | null;
+	previousValue?: number | null;
+}
+
+export default function TooltipContent( {
+	value,
+	label,
+	note,
+	previousValue,
+}: TooltipContentProps ) {
+	const difference = subtract( value, previousValue );
+
+	let trendClass = 'highlight-card-tooltip-count-difference-positive';
+	let trendIcon = arrowUp;
+	if ( difference !== null && difference < 0 ) {
+		trendClass = 'highlight-card-tooltip-count-difference-negative';
+		trendIcon = arrowDown;
+	}
+
+	/**
+	 * TODO clk - We are currently in the process of unifying numberFormat from i18n-calypso with the one from number-formatters.
+	 * Once settled, we should consider where to place the "-" default for null values.
+	 */
+	const tooltipCount = value !== null ? formatNumberI18n( value ) : '—';
+
+	return (
+		<div className="highlight-card-tooltip-content">
+			<span className="highlight-card-tooltip-counts">
+				{ tooltipCount }
+				{ label && ` ${ label }` }
+			</span>
+			{ difference !== null && difference !== 0 && (
+				<span className={ trendClass }>
+					<Icon size={ 18 } icon={ trendIcon } />
+					{ formatNumberI18n( Math.abs( difference ) ) }
+				</span>
+			) }
+			{ note && <div className="highlight-card-tooltip-note">{ note }</div> }
+		</div>
+	);
+}
diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts
index dd3e812c9707..54cec5083480 100644
--- a/packages/components/src/index.ts
+++ b/packages/components/src/index.ts
@@ -45,6 +45,7 @@ export { default as CountCard } from './highlight-cards/count-card';
 export { default as AnnualHighlightCards } from './highlight-cards/annual-highlight-cards';
 export { default as NoticeBanner } from './notice-banner';
 export { default as MobileHighlightCardListing } from './highlight-cards/mobile-highlight-cards';
+export { default as TooltipContent } from './highlight-cards/tooltip-content';
 export { default as ConfettiAnimation } from './confetti';
 export { default as ComponentSwapper } from './component-swapper';
 export { default as SubmenuPopover, useSubmenuPopoverProps } from './submenu-popover';


Reviews:
user: login: aduth
body: LGTM
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-28T16:50:40Z
user: login: tyxla
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-29T07:34:30Z
user: login: tyxla
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-29T07:35:17Z
user: login: tyxla
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-29T08:28:36Z

Review comments:
diff_hunk: @@ -58,6 +57,18 @@ function AnnualHighlightsStandard( { counts }: AnnualHighlightsProps ) {
 	);
 }
 
+function HighlightCardsHeading( { children }: { children: React.ReactNode } ) {
path: packages/components/src/highlight-cards/annual-highlight-cards.tsx
user: login: aduth
body: What's the rubric we're using to determine when to consolidate components into a file, vs. splitting out to a separate file? Since we're doing a bit of both in this pull request with `HighlightCardsHeading` and `TooltipContent`.

I'm also coming from a personal convention of strict one-component-per-file 😄 
created_at: 2025-05-28T16:49:19Z
author_association: MEMBER
diff_hunk: @@ -45,6 +45,7 @@ export { default as CountCard } from './highlight-cards/count-card';
 export { default as AnnualHighlightCards } from './highlight-cards/annual-highlight-cards';
 export { default as NoticeBanner } from './notice-banner';
 export { default as MobileHighlightCardListing } from './highlight-cards/mobile-highlight-cards';
+export { default as TooltipContent } from './highlight-cards/tooltip-content';
path: packages/components/src/index.ts
user: login: aduth
body: Recognizing that this is likely temporary, I wonder if we should add a qualifier to clarify that this is tooltips specific to highlight cards? Or not expose it at all?
created_at: 2025-05-28T16:50:35Z
author_association: MEMBER
diff_hunk: @@ -58,6 +57,18 @@ function AnnualHighlightsStandard( { counts }: AnnualHighlightsProps ) {
 	);
 }
 
+function HighlightCardsHeading( { children }: { children: React.ReactNode } ) {
path: packages/components/src/highlight-cards/annual-highlight-cards.tsx
user: login: tyxla
body: Usually, a component per file makes sense. This just felt too small to be its own file 😉 Can totally be separate though, I wouldn't be opposed. 
created_at: 2025-05-29T07:34:29Z
author_association: MEMBER
diff_hunk: @@ -45,6 +45,7 @@ export { default as CountCard } from './highlight-cards/count-card';
 export { default as AnnualHighlightCards } from './highlight-cards/annual-highlight-cards';
 export { default as NoticeBanner } from './notice-banner';
 export { default as MobileHighlightCardListing } from './highlight-cards/mobile-highlight-cards';
+export { default as TooltipContent } from './highlight-cards/tooltip-content';
path: packages/components/src/index.ts
user: login: tyxla
body: 🤷 I don't expect any issues, TBH, I'm hoping to clean this up in a follow-up PR soon after merging, so it will never really be published as part of the package.
created_at: 2025-05-29T07:35:17Z
author_association: MEMBER
diff_hunk: @@ -45,6 +45,7 @@ export { default as CountCard } from './highlight-cards/count-card';
 export { default as AnnualHighlightCards } from './highlight-cards/annual-highlight-cards';
 export { default as NoticeBanner } from './notice-banner';
 export { default as MobileHighlightCardListing } from './highlight-cards/mobile-highlight-cards';
+export { default as TooltipContent } from './highlight-cards/tooltip-content';
path: packages/components/src/index.ts
user: login: tyxla
body: OK, here's the last bit that moves them out: #103799
created_at: 2025-05-29T08:28:36Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-27T15:12:39Z
body: <details>
<summary>Calypso Live <a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-146134">(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-146134%26flags%3Doauth&choe=UTF-8" />
</td>
<td>
<a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-146134">https://calypso.live?image=registry.a8c.com/calypso/app:build-146134</a>
</td>
</tr>
</table>
</details>
<details>
<summary>Jetpack Cloud live <a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-146134&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-146134%26env%3Djetpack%26flags%3Doauth&choe=UTF-8" />
</td>
<td>
<a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-146134&env=jetpack">https://calypso.live?image=registry.a8c.com/calypso/app:build-146134&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-146134&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-146134%26env%3Da8c-for-agencies%26flags%3Doauth&choe=UTF-8" />
</td>
<td>
<a href="https://calypso.live?image=registry.a8c.com/calypso/app:build-146134&env=a8c-for-agencies">https://calypso.live?image=registry.a8c.com/calypso/app:build-146134&env=a8c-for-agencies</a>
</td>
</tr>
</table>
</details>
<!--calypso-live-watermark:apr@v1-->
author_association: NONE
user: login: matticbot
created_at: 2025-05-27T15:13:41Z
body: <!-- icfy-watermark: c52822 -->
Here is how your PR affects size of JS and CSS bundles shipped to the user's browser:

**Sections** (~210 bytes removed 📉 [gzipped])
<details>

```
name   parsed_size           gzip_size
stats       -930 B  (-0.1%)     -239 B  (-0.1%)
earn        +323 B  (+0.0%)      +29 B  (+0.0%)
```

Sections contain code specific for a given set of routes. Is downloaded and parsed only when a particular route is navigated to.
</details>

**Async-loaded Components** (~603 bytes added 📈 [gzipped])
<details>

```
name                                                 parsed_size           gzip_size
async-load-calypso-my-sites-stats-wordads                +1258 B  (+4.6%)     +235 B  (+2.9%)
async-load-calypso-my-sites-stats-pages-subscribers      +1258 B  (+0.7%)     +298 B  (+0.5%)
async-load-calypso-my-sites-stats-pages-insights         +1254 B  (+2.3%)     +272 B  (+1.7%)
async-load-store-app-store-stats                          -930 B  (-0.3%)     -202 B  (-0.2%)
```

React components that are loaded lazily, when a certain part of UI is displayed for the first time.
</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: matticbot
created_at: 2025-05-29T07:40:57Z
body: **This PR modifies the release build for the following Calypso Apps:**

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

* help-center
* odyssey-stats

To test WordPress.com changes, run `install-plugin.sh $pluginSlug move/highlight-cards-components` on your sandbox.
<!--calypso-app-artifacts-watermark:apr@v1-->
author_association: CONTRIBUTOR