diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 31d2c4026d9a..856c328da897 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -3999,6 +3999,9 @@ importers:
'@jetpack-premium-analytics/formatters':
specifier: link:packages/formatters
version: link:packages/formatters
+ '@jetpack-premium-analytics/icons':
+ specifier: link:packages/icons
+ version: link:packages/icons
'@jetpack-premium-analytics/routing':
specifier: link:packages/routing
version: link:packages/routing
@@ -4008,6 +4011,9 @@ importers:
'@jetpack-premium-analytics/ui':
specifier: link:packages/ui
version: link:packages/ui
+ '@jetpack-premium-analytics/widgets-toolkit':
+ specifier: link:packages/widgets-toolkit
+ version: link:packages/widgets-toolkit
'@tanstack/react-query':
specifier: 5.90.8
version: 5.90.8(react@18.3.1)
diff --git a/projects/js-packages/storybook/changelog/add-average-items-order-widget-story b/projects/js-packages/storybook/changelog/add-average-items-order-widget-story
new file mode 100644
index 000000000000..27410992d609
--- /dev/null
+++ b/projects/js-packages/storybook/changelog/add-average-items-order-widget-story
@@ -0,0 +1,4 @@
+Significance: patch
+Type: added
+
+Add a Premium Analytics Average items per order widget story.
diff --git a/projects/packages/premium-analytics/changelog/add-average-items-per-order-widget b/projects/packages/premium-analytics/changelog/add-average-items-per-order-widget
new file mode 100644
index 000000000000..bc2072729512
--- /dev/null
+++ b/projects/packages/premium-analytics/changelog/add-average-items-per-order-widget
@@ -0,0 +1,4 @@
+Significance: patch
+Type: added
+
+Port the Average items per order dashboard widget from next-woocommerce-analytics, composed from the widgets-toolkit and data packages.
diff --git a/projects/packages/premium-analytics/eslint.config.mjs b/projects/packages/premium-analytics/eslint.config.mjs
index 0e3f236bbb13..26499b983533 100644
--- a/projects/packages/premium-analytics/eslint.config.mjs
+++ b/projects/packages/premium-analytics/eslint.config.mjs
@@ -105,5 +105,22 @@ export default defineConfig(
'@wordpress/no-unsafe-wp-apis': 'off',
'@wordpress/no-unknown-ds-tokens': 'off',
},
+ },
+ {
+ // Ported widget code keeps the upstream next-woocommerce-analytics
+ // JSDoc style, and imports internal `@jetpack-premium-analytics/*`
+ // link: packages whose deps are declared on the parent manifest.
+ files: [ 'widgets/**' ],
+ rules: {
+ 'import/order': 'off',
+ 'jsdoc/require-description': 'off',
+ 'jsdoc/require-param-description': 'off',
+ 'jsdoc/require-returns': 'off',
+ 'jsdoc/check-indentation': 'off',
+ 'jsdoc/require-param': 'off',
+ 'jsdoc/require-jsdoc': 'off',
+ 'jsdoc/escape-inline-tags': 'off',
+ 'import/no-extraneous-dependencies': 'off',
+ },
}
);
diff --git a/projects/packages/premium-analytics/package.json b/projects/packages/premium-analytics/package.json
index 64e79e53425b..024318130e0e 100644
--- a/projects/packages/premium-analytics/package.json
+++ b/projects/packages/premium-analytics/package.json
@@ -45,9 +45,11 @@
"@jetpack-premium-analytics/datetime": "link:packages/datetime",
"@jetpack-premium-analytics/fields": "link:packages/fields",
"@jetpack-premium-analytics/formatters": "link:packages/formatters",
+ "@jetpack-premium-analytics/icons": "link:packages/icons",
"@jetpack-premium-analytics/routing": "link:packages/routing",
"@jetpack-premium-analytics/site-sync": "link:packages/site-sync",
"@jetpack-premium-analytics/ui": "link:packages/ui",
+ "@jetpack-premium-analytics/widgets-toolkit": "link:packages/widgets-toolkit",
"@tanstack/react-query": "5.90.8",
"@wordpress/admin-ui": "2.3.1",
"@wordpress/api-fetch": "7.48.1",
diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.module.scss
index 2f4c511d9c7b..da1949600279 100644
--- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.module.scss
+++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.module.scss
@@ -1,23 +1,40 @@
.chart {
height: 100%;
+ min-height: 0;
+ flex: 1 1 auto;
+
+ .chartContent {
+ flex: 1 1 auto;
+ height: 100%;
+ min-height: 0;
+ }
.legend {
flex: 0 0 auto;
- height: var(--wpds-typography-line-height-lg);
+ width: 100%;
+ height: auto;
min-height: var(--wpds-typography-line-height-lg);
- flex-wrap: nowrap;
+ align-content: center;
+ align-items: center;
+ justify-content: center;
+ flex-wrap: wrap;
+ row-gap: var(--wpds-dimension-gap-xs) !important;
}
.legendItem {
min-width: 0;
+ max-width: 100%;
+ flex: 0 1 auto;
gap: var(--wpds-dimension-gap-sm);
+ justify-content: flex-start;
padding: var(--wpds-dimension-padding-xs) var(--wpds-dimension-padding-sm); // 4px 6px->8px
}
.legendLabel {
// font-size and color come from chartTheme.legendLabelStyles
min-width: 0;
- flex: 1 1 0 !important; // Override the 0 0 auto default value
+ flex: 0 1 auto !important; // Override the 0 0 auto default value
+ text-align: start;
span {
display: block;
diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.tsx
index b65f1f95780c..0218024be9f8 100644
--- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.tsx
+++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/chart-comparative-line/comparative-line-chart.tsx
@@ -3,6 +3,7 @@
*/
import { LineChart } from '@automattic/charts';
import { formatDate, formatMetricValue } from '@jetpack-premium-analytics/formatters';
+import { Stack } from '@wordpress/ui';
import clsx from 'clsx';
import { useCallback, useMemo } from 'react';
import { type ComponentProps } from 'react';
@@ -317,35 +318,37 @@ export function ComparativeLineChart( {
] );
return (
-
-
-
+
+
+
+
+
);
}
diff --git a/projects/packages/premium-analytics/widgets/average-items-per-order/package.json b/projects/packages/premium-analytics/widgets/average-items-per-order/package.json
new file mode 100644
index 000000000000..eab2c17bec43
--- /dev/null
+++ b/projects/packages/premium-analytics/widgets/average-items-per-order/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "@automattic/jetpack-premium-analytics-widget-average-items-per-order",
+ "version": "0.1.0-alpha",
+ "private": true,
+ "type": "module",
+ "dependencies": {
+ "@jetpack-premium-analytics/widgets-toolkit": "link:../../packages/widgets-toolkit",
+ "@wordpress/i18n": "^6.9.0",
+ "@wordpress/icons": "^13.0.0",
+ "@wordpress/ui": "0.13.0",
+ "react": "18.3.1"
+ }
+}
diff --git a/projects/packages/premium-analytics/widgets/average-items-per-order/render.tsx b/projects/packages/premium-analytics/widgets/average-items-per-order/render.tsx
new file mode 100644
index 000000000000..b56bce693a78
--- /dev/null
+++ b/projects/packages/premium-analytics/widgets/average-items-per-order/render.tsx
@@ -0,0 +1,27 @@
+import {
+ OrderMetricWidget,
+ WidgetRoot,
+ type ReportParamsFieldAttributes,
+} from '@jetpack-premium-analytics/widgets-toolkit';
+
+type AverageItemsPerOrderRenderProps = {
+ attributes?: Partial< ReportParamsFieldAttributes >;
+};
+
+/**
+ * Average items per order widget.
+ *
+ * Thin composition over the widgets-toolkit: WidgetRoot provides the query
+ * client, chart theme, and resolved report params; OrderMetricWidget fetches
+ * the orders report and renders the avg_items metric with a comparison delta
+ * and sparkline.
+ */
+export default function AverageItemsPerOrderRender( {
+ attributes,
+}: AverageItemsPerOrderRenderProps ) {
+ return (
+
+
+
+ );
+}
diff --git a/projects/packages/premium-analytics/widgets/average-items-per-order/stories/average-items-per-order-widget.stories.tsx b/projects/packages/premium-analytics/widgets/average-items-per-order/stories/average-items-per-order-widget.stories.tsx
new file mode 100644
index 000000000000..b1ea6d7901cb
--- /dev/null
+++ b/projects/packages/premium-analytics/widgets/average-items-per-order/stories/average-items-per-order-widget.stories.tsx
@@ -0,0 +1,220 @@
+import { getDefaultQueryParams } from '@jetpack-premium-analytics/data';
+import { SELECTABLE_PRESETS, type SelectablePresetId } from '@jetpack-premium-analytics/datetime';
+import {
+ DEFAULT_WIDGET_DASHBOARD_STORY_ARGS,
+ WidgetDashboardWithWidget as WidgetDashboardWithWidgetStory,
+ widgetDashboardWithWidgetArgTypes,
+ type WidgetDashboardWithWidgetControls,
+} from '../../stories/widget-dashboard-with-widget';
+import { registerReportMocks } from '../../../packages/widgets-toolkit/src/stories/mocks/register-report-mocks';
+import AverageItemsPerOrderRender from '../render';
+import widgetDefinition from '../widget';
+import type { Decorator, Meta, StoryObj } from '@storybook/react';
+import type { WidgetRenderProps } from '@wordpress/widget-primitives';
+import type { ComponentProps, ComponentType } from 'react';
+
+registerReportMocks();
+
+const AVERAGE_ITEMS_RENDER_MODULE = 'storybook/average-items-per-order';
+const DEFAULT_PRESET = 'last-30-days' satisfies SelectablePresetId;
+const PRESET_OPTIONS = SELECTABLE_PRESETS;
+
+type AverageItemsPerOrderRenderProps = ComponentProps< typeof AverageItemsPerOrderRender >;
+
+interface AverageItemsPerOrderStoryControls {
+ withComparison: boolean;
+ preset: SelectablePresetId;
+}
+
+type AverageItemsPerOrderStoryProps = AverageItemsPerOrderRenderProps &
+ AverageItemsPerOrderStoryControls;
+
+interface AverageItemsPerOrderDashboardStoryProps
+ extends WidgetDashboardWithWidgetControls,
+ AverageItemsPerOrderStoryControls {}
+
+const withWidgetCanvas: Decorator = Story => (
+
+
+
+);
+
+function getAverageItemsPerOrderAttributes(
+ withComparison = false,
+ preset: SelectablePresetId = DEFAULT_PRESET
+): AverageItemsPerOrderRenderProps[ 'attributes' ] {
+ return {
+ reportParams: getDefaultQueryParams( withComparison, preset ),
+ };
+}
+
+function getDefaultQueryParamsSource( {
+ withComparison,
+ preset,
+}: Partial< AverageItemsPerOrderStoryControls > ) {
+ const hasComparison = Boolean( withComparison );
+ const storyPreset = preset ?? DEFAULT_PRESET;
+
+ if ( ! hasComparison && storyPreset === DEFAULT_PRESET ) {
+ return 'getDefaultQueryParams()';
+ }
+
+ if ( hasComparison && storyPreset === DEFAULT_PRESET ) {
+ return 'getDefaultQueryParams( true )';
+ }
+
+ return `getDefaultQueryParams( ${ hasComparison ? 'true' : 'false' }, '${ storyPreset }' )`;
+}
+
+function getAverageItemsPerOrderSource( args: Partial< AverageItemsPerOrderStoryControls > ) {
+ return `import { getDefaultQueryParams } from '@jetpack-premium-analytics/data';
+
+`;
+}
+
+function renderAverageItemsPerOrder( {
+ withComparison,
+ preset,
+}: AverageItemsPerOrderStoryControls ) {
+ return (
+
+ );
+}
+
+function AverageItemsPerOrderDashboardStory( {
+ withComparison,
+ preset,
+ ...dashboardStoryArgs
+}: AverageItemsPerOrderDashboardStoryProps ) {
+ return (
+ >
+ }
+ attributes={ getAverageItemsPerOrderAttributes( withComparison, preset ) }
+ />
+ );
+}
+
+const meta = {
+ title: 'Packages/Premium Analytics/Widgets/AverageItemsPerOrder',
+ component: AverageItemsPerOrderRender,
+ tags: [ 'autodocs' ],
+ argTypes: {
+ preset: {
+ control: 'select',
+ options: PRESET_OPTIONS,
+ description: 'Date-range preset used to generate the widget report params.',
+ },
+ withComparison: {
+ control: 'boolean',
+ description: 'Include previous-period comparison report params.',
+ },
+ },
+ parameters: {
+ docs: {
+ description: {
+ component:
+ 'The "Average items per order" widget. Fetches the orders report and displays the average number of items per order with optional period-over-period comparison and a sparkline.',
+ },
+ },
+ },
+} satisfies Meta< AverageItemsPerOrderStoryProps >;
+
+export default meta;
+
+type Story = StoryObj< typeof meta >;
+type DashboardStory = StoryObj< AverageItemsPerOrderDashboardStoryProps >;
+
+/**
+ * Default state for the current report period.
+ */
+export const Default: Story = {
+ render: renderAverageItemsPerOrder,
+ args: {
+ preset: DEFAULT_PRESET,
+ withComparison: false,
+ },
+ decorators: [ withWidgetCanvas ],
+ parameters: {
+ docs: {
+ source: {
+ transform: (
+ _source: string,
+ storyContext: { args: Partial< AverageItemsPerOrderStoryControls > }
+ ) => getAverageItemsPerOrderSource( storyContext.args ),
+ },
+ },
+ },
+};
+
+/**
+ * Comparison period enabled, showing period-over-period change and sparkline data.
+ */
+export const WithComparison: Story = {
+ render: renderAverageItemsPerOrder,
+ args: {
+ preset: DEFAULT_PRESET,
+ withComparison: true,
+ },
+ decorators: [ withWidgetCanvas ],
+ parameters: {
+ docs: {
+ source: {
+ transform: (
+ _source: string,
+ storyContext: { args: Partial< AverageItemsPerOrderStoryControls > }
+ ) => getAverageItemsPerOrderSource( storyContext.args ),
+ },
+ },
+ },
+};
+
+/**
+ * Renders the widget through the shared dashboard harness.
+ */
+export const WidgetDashboardWithWidget: DashboardStory = {
+ render: args => ,
+ args: {
+ ...DEFAULT_WIDGET_DASHBOARD_STORY_ARGS,
+ preset: DEFAULT_PRESET,
+ withComparison: true,
+ },
+ argTypes: {
+ ...widgetDashboardWithWidgetArgTypes,
+ preset: {
+ control: 'select',
+ options: PRESET_OPTIONS,
+ description: 'Date-range preset used to generate the widget report params.',
+ },
+ withComparison: {
+ control: 'boolean',
+ description: 'Include previous-period comparison report params.',
+ },
+ },
+ parameters: {
+ docs: {
+ source: {
+ code: `import { getDefaultQueryParams } from '@jetpack-premium-analytics/data';
+
+`,
+ },
+ },
+ },
+};
diff --git a/projects/packages/premium-analytics/widgets/average-items-per-order/widget.json b/projects/packages/premium-analytics/widgets/average-items-per-order/widget.json
new file mode 100644
index 000000000000..5547536c1e47
--- /dev/null
+++ b/projects/packages/premium-analytics/widgets/average-items-per-order/widget.json
@@ -0,0 +1,6 @@
+{
+ "name": "jpa/average-items-per-order",
+ "title": "Average items per order",
+ "description": "Show the average number of products per order over a set period of time.",
+ "category": "orders"
+}
diff --git a/projects/packages/premium-analytics/widgets/average-items-per-order/widget.ts b/projects/packages/premium-analytics/widgets/average-items-per-order/widget.ts
new file mode 100644
index 000000000000..c62915e58d5c
--- /dev/null
+++ b/projects/packages/premium-analytics/widgets/average-items-per-order/widget.ts
@@ -0,0 +1,26 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { chartBar } from '@wordpress/icons';
+
+/**
+ * Widget type definition.
+ *
+ * Ported from `woocommerce-analytics/average-items-per-order` in
+ * woocommerce/woocommerce-analytics (next-woocommerce-analytics).
+ *
+ * Report params intentionally come from the analytics dashboard's global
+ * date-range state for now. Adding widget-level overrides needs a host-level
+ * control registry so analytics dashboards can hide the field while other
+ * dashboards can opt in.
+ */
+export default {
+ name: 'jpa/average-items-per-order',
+ title: __( 'Average items per order', 'jetpack-premium-analytics' ),
+ description: __(
+ 'Show the average number of products per order over a set period of time.',
+ 'jetpack-premium-analytics'
+ ),
+ icon: chartBar,
+};
diff --git a/projects/packages/premium-analytics/widgets/stories/widget-dashboard-with-widget.tsx b/projects/packages/premium-analytics/widgets/stories/widget-dashboard-with-widget.tsx
new file mode 100644
index 000000000000..e60f5f02e3c2
--- /dev/null
+++ b/projects/packages/premium-analytics/widgets/stories/widget-dashboard-with-widget.tsx
@@ -0,0 +1,190 @@
+import { Page } from '@wordpress/admin-ui';
+import { WidgetDashboard, type DashboardWidget } from '@wordpress/widget-dashboard';
+import { useEffect, useMemo, useState, type ComponentType, type ReactNode } from 'react';
+import type {
+ ResolveWidgetModule,
+ WidgetRenderProps,
+ WidgetType,
+} from '@wordpress/widget-primitives';
+
+const DASHBOARD_ROW_HEIGHT = 300;
+const DASHBOARD_GRID_GAP = 24;
+const DASHBOARD_ONE_COLUMN_WIDTH = 381;
+const DASHBOARD_PAGE_INLINE_PADDING = 48;
+
+export const WIDGET_DASHBOARD_STORY_WIDTHS = {
+ desktop: `${
+ DASHBOARD_ONE_COLUMN_WIDTH * 4 + DASHBOARD_GRID_GAP * 3 + DASHBOARD_PAGE_INLINE_PADDING
+ }px`,
+ narrow: '640px',
+ mobile: '370px',
+} as const;
+
+export type WidgetDashboardStoryHostEnvironment = 'wordpress-admin' | 'storybook';
+
+const HOST_ROOT_FONT_SIZE: Record< WidgetDashboardStoryHostEnvironment, string | undefined > = {
+ 'wordpress-admin': '13px',
+ storybook: undefined,
+};
+
+export interface WidgetDashboardWithWidgetControls {
+ dashboardWidth: string;
+ widgetWidth: number;
+ widgetHeight: number;
+ rowHeight: number;
+ editMode: boolean;
+ hostEnvironment: WidgetDashboardStoryHostEnvironment;
+}
+
+export const DEFAULT_WIDGET_DASHBOARD_STORY_ARGS: WidgetDashboardWithWidgetControls = {
+ dashboardWidth: WIDGET_DASHBOARD_STORY_WIDTHS.desktop,
+ widgetWidth: 1,
+ widgetHeight: 2,
+ rowHeight: DASHBOARD_ROW_HEIGHT,
+ editMode: false,
+ hostEnvironment: 'wordpress-admin',
+};
+
+export const widgetDashboardWithWidgetArgTypes = {
+ dashboardWidth: {
+ control: 'select',
+ options: Object.values( WIDGET_DASHBOARD_STORY_WIDTHS ),
+ },
+ widgetWidth: {
+ control: { type: 'number', min: 1, max: 4, step: 1 },
+ },
+ widgetHeight: {
+ control: { type: 'number', min: 1, max: 4, step: 1 },
+ },
+ rowHeight: {
+ control: { type: 'number', min: 200, max: 400, step: 100 },
+ },
+ editMode: {
+ control: 'boolean',
+ },
+ hostEnvironment: {
+ control: 'select',
+ options: Object.keys( HOST_ROOT_FONT_SIZE ),
+ },
+};
+
+type StoryWidgetMetadata = {
+ name: string;
+ title: WidgetType[ 'title' ];
+} & Partial< Omit< WidgetType, 'apiVersion' | 'name' | 'renderModule' | 'title' > >;
+
+interface WidgetDashboardWithWidgetProps extends WidgetDashboardWithWidgetControls {
+ widgetType: StoryWidgetMetadata;
+ renderModule: string;
+ renderComponent: ComponentType< WidgetRenderProps< unknown > >;
+ attributes?: DashboardWidget[ 'attributes' ];
+ pageTitle?: string;
+ widgetUuid?: string;
+}
+
+function HostRootFontSize( {
+ children,
+ hostEnvironment,
+}: {
+ children: ReactNode;
+ hostEnvironment: WidgetDashboardStoryHostEnvironment;
+} ) {
+ useEffect( () => {
+ const { fontSize } = document.documentElement.style;
+ const rootFontSize = HOST_ROOT_FONT_SIZE[ hostEnvironment ];
+
+ if ( rootFontSize ) {
+ document.documentElement.style.fontSize = rootFontSize;
+ }
+
+ return () => {
+ document.documentElement.style.fontSize = fontSize;
+ };
+ }, [ hostEnvironment ] );
+
+ return children;
+}
+
+export function WidgetDashboardWithWidget( {
+ widgetType,
+ renderModule,
+ renderComponent,
+ attributes,
+ dashboardWidth,
+ widgetWidth,
+ widgetHeight,
+ rowHeight,
+ editMode,
+ hostEnvironment,
+ pageTitle = 'Analytics',
+ widgetUuid = `${ widgetType.name }-story`,
+}: WidgetDashboardWithWidgetProps ) {
+ const storyWidgetType = useMemo< WidgetType >(
+ () => ( {
+ ...widgetType,
+ apiVersion: 1,
+ name: widgetType.name as WidgetType[ 'name' ],
+ renderModule,
+ } ),
+ [ renderModule, widgetType ]
+ );
+
+ const resolveWidgetModule = useMemo< ResolveWidgetModule >(
+ () => async moduleId => {
+ if ( moduleId !== renderModule ) {
+ throw new Error( `Unknown widget render module: ${ moduleId }` );
+ }
+
+ return {
+ default: renderComponent,
+ };
+ },
+ [ renderComponent, renderModule ]
+ );
+
+ const widget = useMemo< DashboardWidget >(
+ () => ( {
+ uuid: widgetUuid,
+ type: storyWidgetType.name,
+ attributes,
+ placement: {
+ width: widgetWidth,
+ height: widgetHeight,
+ order: 0,
+ },
+ } ),
+ [ attributes, storyWidgetType.name, widgetHeight, widgetUuid, widgetWidth ]
+ );
+
+ const [ layout, setLayout ] = useState< DashboardWidget[] >( () => [ widget ] );
+ const [ currentEditMode, setCurrentEditMode ] = useState( editMode );
+
+ useEffect( () => {
+ setLayout( [ widget ] );
+ }, [ widget ] );
+
+ useEffect( () => {
+ setCurrentEditMode( editMode );
+ }, [ editMode ] );
+
+ return (
+
+
+
+ } hasPadding>
+
+
+
+
+
+
+ );
+}