Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Significance: patch
Type: changed
Comment: Register the premium-analytics widgets directory as a story discovery path.
1 change: 1 addition & 0 deletions projects/js-packages/storybook/storybook/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: added

Storybook: Add a prop-driven Top posts & pages widget story under Packages/Premium Analytics/Widgets.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export {
type BarChartProps,
type BarChartData,
type BarChartStyle,
WidgetLoadingOverlay,
} from './components';

/**
Expand Down
2 changes: 1 addition & 1 deletion projects/packages/premium-analytics/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
"@jetpack-premium-analytics/*": [ "./packages/*/src" ]
}
},
"include": [ "routes/**/*", "packages/**/*", "types/**/*" ]
"include": [ "routes/**/*", "packages/**/*", "types/**/*", "widgets/**/*" ]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { TopPostsWidget, type TopPostsWidgetProps } from './top-posts-widget';
export type { TopPostRow } from './types';
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/**
* Internal dependencies
*/
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';

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 => (
<div
style={ {
width,
height,
border: '1px dashed #ccc',
borderRadius: '8px',
padding: '16px',
background: '#fafafa',
containerType: 'inline-size',
containerName: 'widget',
} }
>
<Story />
</div>
);
};

/**
* 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' ) ],
};
Original file line number Diff line number Diff line change
@@ -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;
}
Loading
Loading