From 08ecfd07e06b3d6ab9b805d9a550e48190a53801 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Thu, 18 Jun 2026 10:01:26 +0200 Subject: [PATCH 1/6] Premium Analytics: add Top posts & pages widget to Storybook Add a prop-driven TopPostsWidget and Storybook story under Packages/Premium Analytics/Widgets/TopPosts, mimicking the "Top posts & pages" widget from #49568 as a self-contained, mock-data-backed component. The widget renders the most-viewed posts/pages as an overlay leaderboard (each row links to the published content) and supports previous-period comparison with colored deltas, mirroring the overlay comparison mode of the toolkit's LeaderboardChart. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01CpN9Z3MTaLb4GxTNsGFaEQ --- .../changelog/add-top-posts-widget-storybook | 4 + .../packages/widgets/top-posts/index.ts | 2 + .../stories/top-posts-widget.stories.tsx | 223 ++++++++++++++++++ .../top-posts/top-posts-widget.module.css | 22 ++ .../widgets/top-posts/top-posts-widget.tsx | 145 ++++++++++++ .../packages/widgets/top-posts/types.ts | 27 +++ 6 files changed, 423 insertions(+) create mode 100644 projects/packages/premium-analytics/changelog/add-top-posts-widget-storybook create mode 100644 projects/packages/premium-analytics/packages/widgets/top-posts/index.ts create mode 100644 projects/packages/premium-analytics/packages/widgets/top-posts/stories/top-posts-widget.stories.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets/top-posts/top-posts-widget.module.css create mode 100644 projects/packages/premium-analytics/packages/widgets/top-posts/top-posts-widget.tsx create mode 100644 projects/packages/premium-analytics/packages/widgets/top-posts/types.ts diff --git a/projects/packages/premium-analytics/changelog/add-top-posts-widget-storybook b/projects/packages/premium-analytics/changelog/add-top-posts-widget-storybook new file mode 100644 index 000000000000..36a43081b6fc --- /dev/null +++ b/projects/packages/premium-analytics/changelog/add-top-posts-widget-storybook @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Storybook: Add a prop-driven Top posts & pages widget story under Packages/Premium Analytics/Widgets. diff --git a/projects/packages/premium-analytics/packages/widgets/top-posts/index.ts b/projects/packages/premium-analytics/packages/widgets/top-posts/index.ts new file mode 100644 index 000000000000..0abccc7e86cf --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets/top-posts/index.ts @@ -0,0 +1,2 @@ +export { TopPostsWidget, type TopPostsWidgetProps } from './top-posts-widget'; +export type { TopPostRow } from './types'; diff --git a/projects/packages/premium-analytics/packages/widgets/top-posts/stories/top-posts-widget.stories.tsx b/projects/packages/premium-analytics/packages/widgets/top-posts/stories/top-posts-widget.stories.tsx new file mode 100644 index 000000000000..ad7c4af81e29 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets/top-posts/stories/top-posts-widget.stories.tsx @@ -0,0 +1,223 @@ +/** + * External dependencies + */ +import { GlobalChartsProvider } from '@automattic/charts'; +import { useChartTheme } from '@jetpack-premium-analytics/widgets-toolkit'; +/** + * Internal dependencies + */ +import { TopPostsWidget } from '../top-posts-widget'; +import type { TopPostRow } from '../types'; +import type { Meta, StoryObj, Decorator } from '@storybook/react'; +import type { ReactNode } from 'react'; + +/** + * Seeds a `GlobalChartsProvider` with the Woo chart theme. In the app this + * provider lives at the top of the widget tree (inside `WidgetRoot`); stories + * render outside of it, so the leaderboard chart would otherwise throw + * "useGlobalChartsContext must be used within a GlobalChartsProvider". + * + * @param props - Component props. + * @param props.children - The subtree to render inside the provider. + * @return The themed chart provider wrapping `children`. + */ +const ChartThemeProvider = ( { children }: { children: ReactNode } ) => { + const theme = useChartTheme(); + + return { children }; +}; + +const withChartTheme: Decorator = Story => ( + + + +); + +const meta: Meta< typeof TopPostsWidget > = { + title: 'Packages/Premium Analytics/Widgets/TopPosts', + component: TopPostsWidget, + tags: [ 'autodocs' ], + parameters: { + docs: { + description: { + component: + 'The "Top posts & pages" widget. Renders the most-viewed posts and pages for the period as a leaderboard, with each row linking to the published content. This is a presentational component — it takes already-fetched rows via props and handles the loading, error, empty, and populated states.', + }, + }, + }, + decorators: [ withChartTheme ], +}; + +export default meta; + +type Story = StoryObj< typeof TopPostsWidget >; + +const mockRows: TopPostRow[] = [ + { + label: 'How we cut our build times in half', + value: 12840, + previousValue: 9870, + href: 'https://example.com/cut-build-times-in-half', + type: 'post', + }, + { + label: 'Pricing', + value: 9320, + previousValue: 10110, + href: 'https://example.com/pricing', + type: 'page', + }, + { + label: '10 lessons from scaling to a million users', + value: 7610, + previousValue: 5400, + href: 'https://example.com/lessons-scaling-million-users', + type: 'post', + }, + { + label: 'About us', + value: 4180, + previousValue: 4360, + href: 'https://example.com/about', + type: 'page', + }, + { + label: 'A practical guide to feature flags', + value: 2950, + previousValue: 0, + href: 'https://example.com/guide-to-feature-flags', + type: 'post', + }, +]; + +const mockLongLabelRows: TopPostRow[] = [ + { + label: + 'An exhaustively long, keyword-stuffed headline that almost certainly needs to be truncated before it overflows the row', + value: 8400, + href: 'https://example.com/very-long-headline-that-needs-truncation', + type: 'post', + }, + { + label: 'Frequently asked questions about billing, refunds, and account management', + value: 5120, + href: 'https://example.com/faq-billing-refunds-account-management', + type: 'page', + }, + { + label: 'Changelog', + value: 2010, + href: 'https://example.com/changelog', + type: 'page', + }, +]; + +/** + * Default populated state — a mix of posts and pages ranked by views. + */ +export const Default: Story = { + args: { + rows: mockRows, + }, +}; + +/** + * Comparison state — each value shows its change versus the previous period + * (green for gains, red for losses), driven by each row's `previousValue`. + * Mirrors the overlay comparison mode of the toolkit's `LeaderboardChart`. + */ +export const WithComparison: Story = { + args: { + rows: mockRows, + withComparison: true, + showLegend: true, + legendLabels: { + primary: 'Jun 1 – 18, 2026', + comparison: 'May 14 – 31, 2026', + }, + }, +}; + +/** + * Loading state — the chart renders its loading overlay while data is fetched. + */ +export const Loading: Story = { + args: { + rows: [], + isLoading: true, + }, +}; + +/** + * Empty state — no views were recorded for the selected period. + */ +export const NoViews: Story = { + args: { + rows: [], + }, +}; + +/** + * Error state — the report could not be loaded. + */ +export const ErrorState: Story = { + args: { + isError: true, + }, +}; + +/** + * Long titles are truncated with an ellipsis so rows stay single-line. + */ +export const LongLabels: Story = { + args: { + rows: mockLongLabelRows, + }, +}; + +/** + * Creates a decorator that wraps the story in a fixed-size container so the + * widget's responsiveness can be inspected at a given width. + * + * @param width - The container width (any CSS length). + * @param [height] - The container height; defaults to `auto`. + * @return A Storybook decorator. + */ +const createSizeDecorator = ( width: string, height = 'auto' ): Decorator => { + return Story => ( +
+ +
+ ); +}; + +/** + * Medium container (448px / md breakpoint). + */ +export const SizeMedium: Story = { + args: { + rows: mockRows, + }, + decorators: [ createSizeDecorator( '448px' ) ], +}; + +/** + * Large container (576px / xl breakpoint). + */ +export const SizeLarge: Story = { + args: { + rows: mockRows, + }, + decorators: [ createSizeDecorator( '576px' ) ], +}; diff --git a/projects/packages/premium-analytics/packages/widgets/top-posts/top-posts-widget.module.css b/projects/packages/premium-analytics/packages/widgets/top-posts/top-posts-widget.module.css new file mode 100644 index 000000000000..b1eeeb18f676 --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets/top-posts/top-posts-widget.module.css @@ -0,0 +1,22 @@ +.labelLink { + display: block; + + /* Vertical padding sets the overlay bar height — the bar fills the row, + whose height is driven by this label (there is no image to size it). */ + padding-block: var(--wpds-dimension-padding-md, 12px); + + /* Inset the text from the bar's rounded left edge. The chart applies this to + its default `.label` in overlay mode (`.is-overlay .label`), but a custom + label element bypasses that rule, so we mirror it here. */ + padding-inline-start: var(--wpds-dimension-padding-sm, 8px); + overflow: hidden; + color: inherit; + text-decoration: none; + text-overflow: ellipsis; + white-space: nowrap; +} + +.labelLink:hover, +.labelLink:focus { + text-decoration: underline; +} diff --git a/projects/packages/premium-analytics/packages/widgets/top-posts/top-posts-widget.tsx b/projects/packages/premium-analytics/packages/widgets/top-posts/top-posts-widget.tsx new file mode 100644 index 000000000000..7bbe01f81eef --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets/top-posts/top-posts-widget.tsx @@ -0,0 +1,145 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Text } from '@wordpress/ui'; +import { + LeaderboardChart, + calculateDelta, + type LeaderboardChartData, + type LegendLabels, +} from '@jetpack-premium-analytics/widgets-toolkit'; +/** + * Internal dependencies + */ +import styles from './top-posts-widget.module.css'; +import type { TopPostRow } from './types'; + +export type TopPostsWidgetProps = { + /** + * Normalized top-posts rows to render. When omitted, the empty state is shown + * (unless `isLoading` is set). + */ + rows?: TopPostRow[]; + /** + * When `true`, the chart renders its loading overlay instead of data. + */ + isLoading?: boolean; + /** + * When `true`, an error message is rendered in place of the chart. + */ + isError?: boolean; + /** + * When `true`, render the comparison (previous-period) delta next to each + * value, using `previousValue` from each row. Mirrors the overlay + * comparison mode of the toolkit's `LeaderboardChart`. + */ + withComparison?: boolean; + /** + * When `true`, show the period legend below the chart. Requires + * `legendLabels` to be meaningful. + */ + showLegend?: boolean; + /** + * Custom legend labels for the current/comparison periods. + */ + legendLabels?: LegendLabels; +}; + +/** + * Renders a post/page title as a link that opens in a new tab. The link fills + * its row so the leaderboard overlay bar gets its height from the label. + * + * @param props - Component props. + * @param props.label - The post/page title. + * @param props.href - The published URL of the post/page. + * @return The rendered label link. + */ +const TopPostLabel = ( { label, href }: { label: string; href: string } ) => ( + + { label } + +); + +/** + * Maps normalized top-posts rows onto the shape `LeaderboardChart` expects. + * Current shares are computed relative to the most-viewed row so the overlay + * bars are proportional. When `withComparison` is set, previous-period shares + * and per-row deltas are derived from each row's `previousValue`; otherwise + * the comparison fields are zeroed. + * + * @param rows - The normalized top-posts rows. + * @param withComparison - Whether to derive previous-period shares and deltas. + * @return The leaderboard chart data. + */ +function buildLeaderboardData( rows: TopPostRow[], withComparison: boolean ): LeaderboardChartData { + // `1` guards against division by zero when every value is 0. + const maxCurrentViews = Math.max( ...rows.map( row => row.value ), 1 ); + const maxPreviousViews = Math.max( ...rows.map( row => row.previousValue ?? 0 ), 1 ); + + return rows.map( ( row, index ) => { + const previousValue = row.previousValue ?? 0; + + return { + id: `${ index }-${ row.href }`, + label: , + currentValue: row.value, + currentShare: ( row.value / maxCurrentViews ) * 100, + previousValue, + previousShare: + withComparison && previousValue > 0 ? ( previousValue / maxPreviousViews ) * 100 : 0, + delta: withComparison ? calculateDelta( row.value, previousValue ) : 0, + }; + } ); +} + +/** + * "Top posts & pages" widget. Renders the most-viewed posts and pages for the + * period as a leaderboard, each row linking to the published content. + * + * This is a presentational component: it takes already-fetched rows via props + * and is responsible only for the loading, error, empty, and populated states. + * + * @param props - Component props. + * @param props.rows - Normalized top-posts rows to render. + * @param props.isLoading - Whether the chart should render its loading overlay. + * @param props.isError - Whether to render an error message in place of the chart. + * @param props.withComparison - Whether to render previous-period deltas. + * @param props.showLegend - Whether to show the period legend below the chart. + * @param props.legendLabels - Custom labels for the current/comparison periods. + * @return The rendered widget. + */ +export const TopPostsWidget = ( { + rows = [], + isLoading = false, + isError = false, + withComparison = false, + showLegend = false, + legendLabels, +}: TopPostsWidgetProps ) => { + if ( isError ) { + return { __( 'Unable to load top posts.', 'jetpack-premium-analytics' ) }; + } + + if ( ! isLoading && rows.length === 0 ) { + return { __( 'No views in this period.', 'jetpack-premium-analytics' ) }; + } + + return ( + + ); +}; diff --git a/projects/packages/premium-analytics/packages/widgets/top-posts/types.ts b/projects/packages/premium-analytics/packages/widgets/top-posts/types.ts new file mode 100644 index 000000000000..c7755895a72d --- /dev/null +++ b/projects/packages/premium-analytics/packages/widgets/top-posts/types.ts @@ -0,0 +1,27 @@ +/** + * A single normalized top-posts row, as produced by the Jetpack Stats + * "Top posts & pages" data layer (`useReportTopPosts` in `@jetpack-premium-analytics/data`). + */ +export type TopPostRow = { + /** + * Post or page title. + */ + label: string; + /** + * View count for the period. + */ + value: number; + /** + * View count for the comparison (previous) period. Only used when the widget + * is rendered with `withComparison`; omit it for single-period views. + */ + previousValue?: number; + /** + * URL of the published post/page. + */ + href: string; + /** + * Post type, e.g. `post` or `page`. + */ + type: string; +}; From 21d984346981aa0345a09b5d6f7740faeb250258 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Thu, 18 Jun 2026 10:19:33 +0200 Subject: [PATCH 2/6] Premium Analytics: reuse shared withChartTheme decorator in Top Posts story Replace the duplicated ChartThemeProvider + withChartTheme in the Top Posts widget story with the canonical decorator from the widgets-toolkit. Behavior is identical. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01CpN9Z3MTaLb4GxTNsGFaEQ --- .../stories/top-posts-widget.stories.tsx | 29 +------------------ 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/projects/packages/premium-analytics/packages/widgets/top-posts/stories/top-posts-widget.stories.tsx b/projects/packages/premium-analytics/packages/widgets/top-posts/stories/top-posts-widget.stories.tsx index ad7c4af81e29..407b146fab35 100644 --- a/projects/packages/premium-analytics/packages/widgets/top-posts/stories/top-posts-widget.stories.tsx +++ b/projects/packages/premium-analytics/packages/widgets/top-posts/stories/top-posts-widget.stories.tsx @@ -1,37 +1,10 @@ -/** - * External dependencies - */ -import { GlobalChartsProvider } from '@automattic/charts'; -import { useChartTheme } from '@jetpack-premium-analytics/widgets-toolkit'; /** * Internal dependencies */ +import { withChartTheme } from '../../../widgets-toolkit/src/stories/with-chart-theme'; import { TopPostsWidget } from '../top-posts-widget'; import type { TopPostRow } from '../types'; import type { Meta, StoryObj, Decorator } from '@storybook/react'; -import type { ReactNode } from 'react'; - -/** - * Seeds a `GlobalChartsProvider` with the Woo chart theme. In the app this - * provider lives at the top of the widget tree (inside `WidgetRoot`); stories - * render outside of it, so the leaderboard chart would otherwise throw - * "useGlobalChartsContext must be used within a GlobalChartsProvider". - * - * @param props - Component props. - * @param props.children - The subtree to render inside the provider. - * @return The themed chart provider wrapping `children`. - */ -const ChartThemeProvider = ( { children }: { children: ReactNode } ) => { - const theme = useChartTheme(); - - return { children }; -}; - -const withChartTheme: Decorator = Story => ( - - - -); const meta: Meta< typeof TopPostsWidget > = { title: 'Packages/Premium Analytics/Widgets/TopPosts', From 50c145c7bc9f952556e921d7cb569cd2c48ef161 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Thu, 18 Jun 2026 10:29:40 +0200 Subject: [PATCH 3/6] Premium Analytics: move widgets dir out of packages into the package root Relocate widgets/ from premium-analytics/packages/widgets to premium-analytics/widgets. Update the story's relative import to the toolkit decorator, add widgets/**/* to the package tsconfig include, and register the new path in the Storybook projects list so stories are still discovered. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01CpN9Z3MTaLb4GxTNsGFaEQ --- projects/js-packages/storybook/storybook/projects.js | 1 + projects/packages/premium-analytics/tsconfig.json | 2 +- .../premium-analytics/{packages => }/widgets/top-posts/index.ts | 0 .../widgets/top-posts/stories/top-posts-widget.stories.tsx | 2 +- .../widgets/top-posts/top-posts-widget.module.css | 0 .../{packages => }/widgets/top-posts/top-posts-widget.tsx | 0 .../premium-analytics/{packages => }/widgets/top-posts/types.ts | 0 7 files changed, 3 insertions(+), 2 deletions(-) rename projects/packages/premium-analytics/{packages => }/widgets/top-posts/index.ts (100%) rename projects/packages/premium-analytics/{packages => }/widgets/top-posts/stories/top-posts-widget.stories.tsx (97%) rename projects/packages/premium-analytics/{packages => }/widgets/top-posts/top-posts-widget.module.css (100%) rename projects/packages/premium-analytics/{packages => }/widgets/top-posts/top-posts-widget.tsx (100%) rename projects/packages/premium-analytics/{packages => }/widgets/top-posts/types.ts (100%) diff --git a/projects/js-packages/storybook/storybook/projects.js b/projects/js-packages/storybook/storybook/projects.js index 6601798f40a9..f5261e5205aa 100644 --- a/projects/js-packages/storybook/storybook/projects.js +++ b/projects/js-packages/storybook/storybook/projects.js @@ -14,6 +14,7 @@ export const projects = [ 'projects/js-packages/social-logos/src/react', 'projects/packages/my-jetpack/_inc/components', 'projects/packages/premium-analytics/packages', + 'projects/packages/premium-analytics/widgets', 'projects/packages/publicize/_inc/components', 'projects/packages/search/src/dashboard/components', 'projects/packages/videopress/src/client/admin/components', diff --git a/projects/packages/premium-analytics/tsconfig.json b/projects/packages/premium-analytics/tsconfig.json index 3619d8447603..202a51a70064 100644 --- a/projects/packages/premium-analytics/tsconfig.json +++ b/projects/packages/premium-analytics/tsconfig.json @@ -9,5 +9,5 @@ "@jetpack-premium-analytics/*": [ "./packages/*/src" ] } }, - "include": [ "routes/**/*", "packages/**/*", "types/**/*" ] + "include": [ "routes/**/*", "packages/**/*", "types/**/*", "widgets/**/*" ] } diff --git a/projects/packages/premium-analytics/packages/widgets/top-posts/index.ts b/projects/packages/premium-analytics/widgets/top-posts/index.ts similarity index 100% rename from projects/packages/premium-analytics/packages/widgets/top-posts/index.ts rename to projects/packages/premium-analytics/widgets/top-posts/index.ts diff --git a/projects/packages/premium-analytics/packages/widgets/top-posts/stories/top-posts-widget.stories.tsx b/projects/packages/premium-analytics/widgets/top-posts/stories/top-posts-widget.stories.tsx similarity index 97% rename from projects/packages/premium-analytics/packages/widgets/top-posts/stories/top-posts-widget.stories.tsx rename to projects/packages/premium-analytics/widgets/top-posts/stories/top-posts-widget.stories.tsx index 407b146fab35..e0f82b0c5c25 100644 --- a/projects/packages/premium-analytics/packages/widgets/top-posts/stories/top-posts-widget.stories.tsx +++ b/projects/packages/premium-analytics/widgets/top-posts/stories/top-posts-widget.stories.tsx @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { withChartTheme } from '../../../widgets-toolkit/src/stories/with-chart-theme'; +import { withChartTheme } from '../../../packages/widgets-toolkit/src/stories/with-chart-theme'; import { TopPostsWidget } from '../top-posts-widget'; import type { TopPostRow } from '../types'; import type { Meta, StoryObj, Decorator } from '@storybook/react'; diff --git a/projects/packages/premium-analytics/packages/widgets/top-posts/top-posts-widget.module.css b/projects/packages/premium-analytics/widgets/top-posts/top-posts-widget.module.css similarity index 100% rename from projects/packages/premium-analytics/packages/widgets/top-posts/top-posts-widget.module.css rename to projects/packages/premium-analytics/widgets/top-posts/top-posts-widget.module.css diff --git a/projects/packages/premium-analytics/packages/widgets/top-posts/top-posts-widget.tsx b/projects/packages/premium-analytics/widgets/top-posts/top-posts-widget.tsx similarity index 100% rename from projects/packages/premium-analytics/packages/widgets/top-posts/top-posts-widget.tsx rename to projects/packages/premium-analytics/widgets/top-posts/top-posts-widget.tsx diff --git a/projects/packages/premium-analytics/packages/widgets/top-posts/types.ts b/projects/packages/premium-analytics/widgets/top-posts/types.ts similarity index 100% rename from projects/packages/premium-analytics/packages/widgets/top-posts/types.ts rename to projects/packages/premium-analytics/widgets/top-posts/types.ts From 05eb44681fc64e04f23d030d48c14be630b42fc4 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Thu, 18 Jun 2026 10:30:27 +0200 Subject: [PATCH 4/6] Storybook: changelog for premium-analytics widgets discovery path Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01CpN9Z3MTaLb4GxTNsGFaEQ --- .../changelog/register-premium-analytics-widgets-path | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 projects/js-packages/storybook/changelog/register-premium-analytics-widgets-path diff --git a/projects/js-packages/storybook/changelog/register-premium-analytics-widgets-path b/projects/js-packages/storybook/changelog/register-premium-analytics-widgets-path new file mode 100644 index 000000000000..a29648a3733d --- /dev/null +++ b/projects/js-packages/storybook/changelog/register-premium-analytics-widgets-path @@ -0,0 +1,3 @@ +Significance: patch +Type: changed +Comment: Register the premium-analytics widgets directory as a story discovery path. From 0ec43f83c633f9242a6aae66a5159e901095a45c Mon Sep 17 00:00:00 2001 From: Adam Wood <1017872+adamwoodnz@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:56:17 +1200 Subject: [PATCH 5/6] fix(premium-analytics): keep leaderboard overlay bar tint opaque MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In overlay-label mode the bar should read as a faint tint of the primary color so the label stays legible on top of it. We were passing `hexToRgba( primary, 0.08 )` to the chart's `primaryColor` prop, but that prop resolves through getElementStyles, which strips the alpha channel — so the bar rendered at full primary color and the intended fade never appeared. Pre-blend the primary with white instead, producing the opaque equivalent of an 8% alpha fill via charts' own `lightenHexColor` (`normalizeColorToHex` first so non-hex theme colors are handled). The result is opaque, so nothing gets stripped, and it still tracks the themed primary. Top Posts and any other consumer of this shared component inherit the fix automatically. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01MtarPnJhd516aUvykDVubW --- .../chart-leaderboard/leaderboard-chart.tsx | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.tsx index 099a44db2d0d..dfee85fbaef4 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.tsx +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-leaderboard/leaderboard-chart.tsx @@ -5,7 +5,8 @@ import { LeaderboardChartUnresponsive as BaseLeaderboardChart, useGlobalChartsContext, Legend, - hexToRgba, + lightenHexColor, + normalizeColorToHex, } from '@automattic/charts'; import { formatMetricValue } from '@jetpack-premium-analytics/formatters'; import { Icon, Stack } from '@wordpress/ui'; @@ -136,16 +137,21 @@ export function LeaderboardChart( { ); /** - * Get chart colors for legend + * Bar color for overlay-label mode. + * + * The label sits on top of the bar, so the bar needs to read as a faint + * tint of the primary color. We can't pass a translucent color through the + * chart's `primaryColor` prop — it resolves the value via getElementStyles, + * which strips the alpha channel. Instead we pre-blend the primary with + * white to produce the opaque equivalent of an 8% alpha fill. */ - const chartColors = useMemo( () => { - const { color: primaryColor } = getElementStyles( { index: 0 } ); - if ( ! withComparison ) { - return { primaryColor }; + const barColor = useMemo( () => { + if ( ! withOverlayLabel ) { + return undefined; } - const { color: secondaryColor } = getElementStyles( { index: 1 } ); - return { primaryColor, secondaryColor }; - }, [ withComparison, getElementStyles ] ); + const { color: primaryColor } = getElementStyles( { index: 0 } ); + return lightenHexColor( normalizeColorToHex( primaryColor ), 0.92 ); + }, [ withOverlayLabel, getElementStyles ] ); /** * Merge theme bar border radius with style prop. @@ -183,7 +189,7 @@ export function LeaderboardChart( { withComparison={ withComparison } valueFormatter={ valueFormatter } legendLabels={ legendLabels } - primaryColor={ withOverlayLabel ? hexToRgba( chartColors.primaryColor, 0.08 ) : undefined } + primaryColor={ barColor } withOverlayLabel={ withOverlayLabel } showLegend={ false } style={ chartStyle } From 9d5a93284c72b168274ccfef63087df234e66ae4 Mon Sep 17 00:00:00 2001 From: Lourens Schep Date: Fri, 19 Jun 2026 11:05:05 +0200 Subject: [PATCH 6/6] Premium Analytics: fix Top Posts loading state and changelog entry Address review feedback on the Top posts & pages widget: - The Loading story rendered the empty state instead of loading. The toolkit LeaderboardChart short-circuits empty data to ChartEmptyState before the loading prop reaches the base chart, so loading+empty never showed a spinner. Render WidgetLoadingOverlay directly for the initial loading-with-no-data case (matching the sibling leaderboard widgets), and let the chart own the empty state via emptyStateText. - Export WidgetLoadingOverlay from the widgets-toolkit public entry so consumers outside the toolkit can reuse it. - Use a plain "&" in the changelog entry instead of "&". Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01X6csUCvWX89ymfNSHGMXYv --- .../changelog/add-top-posts-widget-storybook | 2 +- .../packages/widgets-toolkit/src/components/index.ts | 1 + .../packages/widgets-toolkit/src/index.ts | 1 + .../widgets/top-posts/top-posts-widget.tsx | 8 +++++--- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/projects/packages/premium-analytics/changelog/add-top-posts-widget-storybook b/projects/packages/premium-analytics/changelog/add-top-posts-widget-storybook index 36a43081b6fc..fb3d577fbe6d 100644 --- a/projects/packages/premium-analytics/changelog/add-top-posts-widget-storybook +++ b/projects/packages/premium-analytics/changelog/add-top-posts-widget-storybook @@ -1,4 +1,4 @@ Significance: patch Type: added -Storybook: Add a prop-driven Top posts & pages widget story under Packages/Premium Analytics/Widgets. +Storybook: Add a prop-driven Top posts & pages widget story under Packages/Premium Analytics/Widgets. diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/index.ts index 121c13e7227b..40aa6c4178e4 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/index.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/index.ts @@ -18,3 +18,4 @@ export { } from './chart-leaderboard'; export { BarChart, type BarChartProps, type BarChartData, type BarChartStyle } from './chart-bar'; export { ChartEmptyState, type ChartEmptyStateProps } from './chart-empty-state'; +export { WidgetLoadingOverlay } from './widget-loading-overlay'; diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/index.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/index.ts index 9b561dd55f5b..87259b94517c 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/index.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/index.ts @@ -20,6 +20,7 @@ export { type BarChartProps, type BarChartData, type BarChartStyle, + WidgetLoadingOverlay, } from './components'; /** diff --git a/projects/packages/premium-analytics/widgets/top-posts/top-posts-widget.tsx b/projects/packages/premium-analytics/widgets/top-posts/top-posts-widget.tsx index 7bbe01f81eef..60b71a283c8c 100644 --- a/projects/packages/premium-analytics/widgets/top-posts/top-posts-widget.tsx +++ b/projects/packages/premium-analytics/widgets/top-posts/top-posts-widget.tsx @@ -5,6 +5,7 @@ import { __ } from '@wordpress/i18n'; import { Text } from '@wordpress/ui'; import { LeaderboardChart, + WidgetLoadingOverlay, calculateDelta, type LeaderboardChartData, type LegendLabels, @@ -22,7 +23,7 @@ export type TopPostsWidgetProps = { */ rows?: TopPostRow[]; /** - * When `true`, the chart renders its loading overlay instead of data. + * When `true`, a loading overlay is rendered instead of data. */ isLoading?: boolean; /** @@ -127,8 +128,8 @@ export const TopPostsWidget = ( { return { __( 'Unable to load top posts.', 'jetpack-premium-analytics' ) }; } - if ( ! isLoading && rows.length === 0 ) { - return { __( 'No views in this period.', 'jetpack-premium-analytics' ) }; + if ( isLoading && ( ! rows || rows.length === 0 ) ) { + return ; } return ( @@ -139,6 +140,7 @@ export const TopPostsWidget = ( { withOverlayLabel showLegend={ showLegend } legendLabels={ legendLabels } + emptyStateText={ __( 'No views in this period.', 'jetpack-premium-analytics' ) } dataFormat={ { type: 'number', options: { useMultipliers: true, decimals: 0 } } } /> );