diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 39be62eeb6a7..8f2900069e6e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -3829,12 +3829,21 @@ importers:
'@automattic/number-formatters':
specifier: workspace:*
version: link:../../js-packages/number-formatters
+ '@automattic/ui':
+ specifier: 1.0.2
+ version: 1.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@date-fns/tz':
specifier: 1.4.1
version: 1.4.1
'@wordpress/boot':
specifier: 0.13.0
version: 0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@wordpress/components':
+ specifier: 33.1.0
+ version: 33.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@wordpress/compose':
+ specifier: 7.46.0
+ version: 7.46.0(react@18.3.1)
'@wordpress/data':
specifier: 10.46.0
version: 10.46.0(react@18.3.1)
@@ -3844,9 +3853,18 @@ importers:
'@wordpress/icons':
specifier: ^13.0.0
version: 13.1.0(react@18.3.1)
+ '@wordpress/private-apis':
+ specifier: 1.46.0
+ version: 1.46.0
'@wordpress/route':
specifier: 0.12.0
version: 0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@wordpress/ui':
+ specifier: 0.13.0
+ version: 0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ clsx:
+ specifier: 2.1.1
+ version: 2.1.1
date-fns:
specifier: 4.1.0
version: 4.1.0
@@ -3866,9 +3884,12 @@ importers:
'@typescript/native-preview':
specifier: 7.0.0-dev.20260225.1
version: 7.0.0-dev.20260225.1
+ '@wordpress/base-styles':
+ specifier: 8.0.0
+ version: 8.0.0
'@wordpress/build':
specifier: 0.14.0
- version: 0.14.0(@babel/core@7.29.0)(@wordpress/boot@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)
+ version: 0.14.0(@babel/core@7.29.0)(@wordpress/boot@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/private-apis@1.46.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)
browserslist:
specifier: 4.28.2
version: 4.28.2
@@ -23985,7 +24006,7 @@ snapshots:
- browserslist
- supports-color
- '@wordpress/build@0.14.0(@babel/core@7.29.0)(@wordpress/boot@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)':
+ '@wordpress/build@0.14.0(@babel/core@7.29.0)(@wordpress/boot@0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@wordpress/private-apis@1.46.0)(@wordpress/route@0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(browserslist@4.28.2)':
dependencies:
'@emotion/babel-plugin': 11.13.5
'@wordpress/style-runtime': 0.2.0
@@ -24005,6 +24026,7 @@ snapshots:
sass-embedded: 1.97.3
optionalDependencies:
'@wordpress/boot': 0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@wordpress/private-apis': 1.46.0
'@wordpress/route': 0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
transitivePeerDependencies:
- '@babel/core'
diff --git a/projects/packages/premium-analytics/changelog/wooa7s-1318-integrate-components-package-into-analytics b/projects/packages/premium-analytics/changelog/wooa7s-1318-integrate-components-package-into-analytics
new file mode 100644
index 000000000000..777a39c9cea4
--- /dev/null
+++ b/projects/packages/premium-analytics/changelog/wooa7s-1318-integrate-components-package-into-analytics
@@ -0,0 +1,4 @@
+Significance: patch
+Type: added
+
+Port the components package (date range/comparison filter UI components and SCSS) from next-woocommerce-analytics as the internal `ui` package.
diff --git a/projects/packages/premium-analytics/eslint.config.mjs b/projects/packages/premium-analytics/eslint.config.mjs
index 6b5bdb16906e..cf5c8fdf69ba 100644
--- a/projects/packages/premium-analytics/eslint.config.mjs
+++ b/projects/packages/premium-analytics/eslint.config.mjs
@@ -27,5 +27,21 @@ export default defineConfig(
'jsdoc/require-returns': 'off',
'jsdoc/check-indentation': 'off',
},
+ },
+ {
+ // First UI package in the port: also soften JSDoc rules for the ui
+ // package and allow the upstream inline-handler JSX style. Temporary —
+ // tighten these up in a follow-up alongside datetime/formatters.
+ files: [ 'packages/ui/**' ],
+ rules: {
+ 'jsdoc/require-jsdoc': 'off',
+ 'jsdoc/require-description': 'off',
+ 'jsdoc/require-param': 'off',
+ 'jsdoc/require-param-description': 'off',
+ 'jsdoc/require-returns': 'off',
+ 'jsdoc/check-indentation': 'off',
+ 'jsdoc/escape-inline-tags': 'off',
+ 'react/jsx-no-bind': 'off',
+ },
}
);
diff --git a/projects/packages/premium-analytics/package.json b/projects/packages/premium-analytics/package.json
index e50feb5cb962..4e1ba3ad66c7 100644
--- a/projects/packages/premium-analytics/package.json
+++ b/projects/packages/premium-analytics/package.json
@@ -30,12 +30,18 @@
},
"dependencies": {
"@automattic/number-formatters": "workspace:*",
+ "@automattic/ui": "1.0.2",
"@date-fns/tz": "1.4.1",
"@wordpress/boot": "0.13.0",
+ "@wordpress/components": "33.1.0",
+ "@wordpress/compose": "7.46.0",
"@wordpress/data": "10.46.0",
"@wordpress/i18n": "^6.9.0",
"@wordpress/icons": "^13.0.0",
+ "@wordpress/private-apis": "1.46.0",
"@wordpress/route": "0.12.0",
+ "@wordpress/ui": "0.13.0",
+ "clsx": "2.1.1",
"date-fns": "4.1.0",
"react": "18.3.1",
"react-dom": "18.3.1"
@@ -44,6 +50,7 @@
"@babel/core": "7.29.0",
"@types/jest": "30.0.0",
"@typescript/native-preview": "7.0.0-dev.20260225.1",
+ "@wordpress/base-styles": "8.0.0",
"@wordpress/build": "0.14.0",
"browserslist": "4.28.2"
}
diff --git a/projects/packages/premium-analytics/packages/ui/package.json b/projects/packages/premium-analytics/packages/ui/package.json
new file mode 100644
index 000000000000..1aae2e2cda00
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/ui/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@automattic/jetpack-premium-analytics-ui",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "sideEffects": [
+ "*.scss"
+ ],
+ "dependencies": {
+ "@automattic/ui": "1.0.2",
+ "@jetpack-premium-analytics/datetime": "workspace:*",
+ "@jetpack-premium-analytics/formatters": "workspace:*",
+ "@wordpress/components": "33.1.0",
+ "@wordpress/compose": "7.46.0",
+ "@wordpress/i18n": "^6.9.0",
+ "@wordpress/icons": "^13.0.0",
+ "@wordpress/private-apis": "1.46.0",
+ "@wordpress/ui": "0.13.0",
+ "clsx": "2.1.1",
+ "react": "18.3.1"
+ }
+}
diff --git a/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.scss b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.scss
new file mode 100644
index 000000000000..29fea4bf18a3
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.scss
@@ -0,0 +1,36 @@
+.date-comparison-dropdown {
+
+ &__button {
+ background-color: var(--wpds-color-bg-surface-neutral-strong);
+ }
+}
+
+.date-filters-panel-button {
+ background-color: var(--wpds-color-bg-surface-neutral-strong);
+}
+
+.date-comparison-dropdown__popover {
+ width: 235px;
+}
+
+/* disable animation for the date range popover */
+/* stylelint-disable property-no-unknown, selector-pseudo-element-no-unknown */
+@media not ( prefers-reduced-motion: reduce ) {
+
+ .date-comparison-dropdown__popover {
+ view-transition-name: next-admin--date-comparison-dropdown;
+ transition: none !important;
+ }
+}
+
+/* ensure it's above the canvas/stage during the transition */
+::view-transition-group(next-admin--date-comparison-dropdown) {
+ z-index: 3000;
+}
+
+/* no animation for the snapshot (avoid "flashing") */
+::view-transition-new(next-admin--date-comparison-dropdown),
+::view-transition-old(next-admin--date-comparison-dropdown) {
+ animation: none;
+}
+/* stylelint-enable property-no-unknown, selector-pseudo-element-no-unknown */
diff --git a/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx
new file mode 100644
index 000000000000..34f39700b781
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx
@@ -0,0 +1,168 @@
+/**
+ * External dependencies
+ */
+import { formatDateRange } from '@jetpack-premium-analytics/formatters';
+import { privateApis as componentsPrivateApis } from '@wordpress/components';
+import { sprintf, __ } from '@wordpress/i18n';
+import { Button } from '@wordpress/ui';
+import { useMemo } from 'react';
+/**
+ * Internal dependencies
+ */
+import { DateRangePresets } from '../date-range-presets';
+import { unlock } from '../lock/unlock';
+import type { ComparisonDateRangePreset } from '../use-comparison-date-presets';
+import type {
+ ComparisonPresetId,
+ DateRangePreset,
+ PrimaryPresetId,
+} from '@jetpack-premium-analytics/datetime';
+import './date-comparison-dropdown.scss';
+
+const { Menu } = unlock( componentsPrivateApis );
+
+type DateComparisonDropdownProps = {
+ /**
+ * Available comparison presets (e.g., previous-period, previous-month)
+ */
+ presets: ComparisonDateRangePreset[];
+ /**
+ * Whether comparison is enabled
+ */
+ enabled: boolean;
+ /**
+ * Currently selected comparison preset ID
+ */
+ presetId?: ComparisonPresetId;
+ /**
+ * Whether to remove "Compare to:" prefix from button label
+ */
+ removeCompareToPrefix?: boolean;
+ /**
+ * Callback when comparison is enabled
+ */
+ onEnable: () => void;
+ /**
+ * Callback when a comparison preset is selected
+ */
+ onPresetChange: ( id: ComparisonPresetId ) => void;
+ /**
+ * Callback when comparison is cleared
+ */
+ onClear: () => void;
+};
+
+export function DateComparisonDropdown( {
+ presets,
+ enabled,
+ presetId,
+ removeCompareToPrefix = false,
+ onEnable,
+ onPresetChange,
+ onClear,
+}: DateComparisonDropdownProps ) {
+ const selectedPreset = useMemo(
+ () => ( presetId ? presets.find( p => p.id === presetId ) : undefined ),
+ [ presets, presetId ]
+ );
+
+ const comparisonRange = selectedPreset?.range;
+ const hasValidPreset = !! comparisonRange;
+ const hasPresets = presets.length > 0;
+
+ if ( ! enabled ) {
+ return (
+
+ );
+ }
+
+ let label: string = __( 'Select comparison', 'jetpack-premium-analytics' );
+ if ( hasValidPreset ) {
+ if ( removeCompareToPrefix ) {
+ label = formatDateRange( comparisonRange );
+ } else {
+ label = sprintf(
+ // translators: %s is the comparison range label
+ __( 'Compare to: %s', 'jetpack-premium-analytics' ),
+ formatDateRange( comparisonRange )
+ );
+ }
+ }
+
+ return (
+
+ );
+}
diff --git a/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/index.ts b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/index.ts
new file mode 100644
index 000000000000..89ca07f36f91
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/index.ts
@@ -0,0 +1 @@
+export { DateComparisonDropdown } from './date-comparison-dropdown';
diff --git a/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/date-filters-panel.tsx b/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/date-filters-panel.tsx
new file mode 100644
index 000000000000..dd9f6a3fc189
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/date-filters-panel.tsx
@@ -0,0 +1,229 @@
+/**
+ * External dependencies
+ */
+import {
+ isComparisonPresetId,
+ isPrimaryPreset,
+ type ComparisonPresetId,
+ type PrimaryPresetId,
+} from '@jetpack-premium-analytics/datetime';
+import { BaseControl } from '@wordpress/components';
+import { Stack } from '@wordpress/ui';
+import { useMemo, useCallback } from 'react';
+/**
+ * Internal dependencies
+ */
+import { DateComparisonDropdown } from '../date-comparison-dropdown';
+import { DateRangePopover } from '../date-range-popover';
+import { useComparisonDatePresets } from '../use-comparison-date-presets';
+
+type DateRangePopoverProps = Parameters< typeof DateRangePopover >[ 0 ];
+
+export type DateRange = DateRangePopoverProps[ 'range' ];
+
+export type DateFiltersPanelProps = {
+ /**
+ * The current date range preset ID (e.g., 'last-7-days', 'last-30-days').
+ */
+ presetId?: PrimaryPresetId;
+
+ /**
+ * The current primary date range.
+ */
+ range: DateRange;
+
+ /**
+ * The current comparison preset ID (e.g., 'previous-period', 'previous-month').
+ */
+ comparisonPresetId?: ComparisonPresetId;
+
+ /**
+ * Callback when the primary date range changes.
+ */
+ onChange: DateRangePopoverProps[ 'onChange' ];
+
+ /**
+ * Callback when the comparison date range changes.
+ * Receives the calculated comparison range and the preset ID used.
+ */
+ onComparisonChange: ( range: DateRange | undefined, presetId?: ComparisonPresetId ) => void;
+
+ /**
+ * Props for the date range popover.
+ */
+ rangeControlProps?: Omit< Parameters< typeof BaseControl >[ 0 ], 'children' >;
+
+ /**
+ * Props for the date comparison dropdown.
+ */
+ comparisonControlProps?: Omit< Parameters< typeof BaseControl >[ 0 ], 'children' >;
+
+ /**
+ * Callback when the primary date range is applied.
+ */
+ onApply: DateRangePopoverProps[ 'onApply' ];
+
+ /**
+ * Callback when the primary date range is canceled.
+ */
+ onCancel: DateRangePopoverProps[ 'onCancel' ];
+
+ /**
+ * Whether the primary date range can be applied.
+ */
+ canApply?: boolean;
+
+ /**
+ * IANA timezone string (e.g., 'America/New_York', 'Europe/London').
+ * Required for proper date/time handling.
+ */
+ timeZone: string;
+
+ /**
+ * Optional external container element for responsive calculations.
+ * When provided, the DateRangePopover will measure this container's width
+ * instead of its own wrapper to determine mobile/wide layouts.
+ */
+ containerElement?: HTMLElement | null;
+};
+
+/**
+ * DateFiltersPanel - Manages date range selection and comparison controls
+ *
+ * This component serves as the container for date filtering functionality,
+ * managing both the primary date range selection and the comparison date range.
+ * It owns the comparison state and delegates to child components for UI.
+ */
+export function DateFiltersPanel( {
+ presetId,
+ range,
+ comparisonPresetId,
+ onChange,
+ onComparisonChange,
+ rangeControlProps = {
+ label: null,
+ help: null,
+ },
+ comparisonControlProps = {
+ label: null,
+ help: null,
+ },
+ onApply,
+ onCancel,
+ canApply = true,
+ timeZone,
+ containerElement,
+}: DateFiltersPanelProps ) {
+ /**
+ * Validate and normalize the primary preset ID.
+ * Only accepts built-in preset IDs (including 'custom').
+ * Invalid/unknown values are treated as undefined, which allows
+ * DateRangePopover to handle them gracefully (falls back to custom).
+ */
+ const validatedPresetId = useMemo( () => {
+ if ( ! presetId ) {
+ return undefined;
+ }
+ // Only accept known built-in presets
+ // Unknown/garbage values from URL are rejected to prevent UI inconsistency
+ return isPrimaryPreset( presetId ) ? presetId : undefined;
+ }, [ presetId ] );
+
+ // Validate and normalize the comparison preset ID
+ const validatedComparisonPresetId = useMemo( () => {
+ return isComparisonPresetId( comparisonPresetId ) ? comparisonPresetId : undefined;
+ }, [ comparisonPresetId ] );
+
+ // Derive comparison enabled state directly from validated prop
+ const comparisonEnabled = !! validatedComparisonPresetId;
+
+ // Get available presets for the current range
+ const presets = useComparisonDatePresets( range );
+
+ /**
+ * Determines the default preset ID to use when comparison is enabled.
+ * Priority order:
+ * 1. 'previous-period'
+ * 2. 'previous-month'
+ * 3. First available preset
+ */
+ const defaultPresetId = useMemo( () => {
+ return (
+ presets.find( p => p.id === 'previous-period' )?.id ??
+ presets.find( p => p.id === 'previous-month' )?.id ??
+ presets[ 0 ]?.id
+ );
+ }, [ presets ] );
+
+ /**
+ * Currently selected comparison preset,
+ * based on the validated stored preset ID, or the default preset.
+ * Returns undefined if no preset is selected
+ * or if the ID doesn't match any available preset.
+ */
+ const preset = useMemo( () => {
+ const id = validatedComparisonPresetId ?? defaultPresetId;
+ return id ? presets.find( p => p.id === id ) : undefined;
+ }, [ presets, validatedComparisonPresetId, defaultPresetId ] );
+
+ const presetChange = useCallback(
+ ( id: ComparisonPresetId ) => {
+ const nextPreset = presets.find( p => p.id === id );
+ onComparisonChange( nextPreset?.range, id );
+ },
+ [ onComparisonChange, presets ]
+ );
+
+ /**
+ * Handles clearing the comparison completely.
+ * Clears the selected preset and notifies parent.
+ */
+ const clearComparison = useCallback( () => {
+ onComparisonChange( undefined, undefined );
+ }, [ onComparisonChange ] );
+
+ const handleEnable = useCallback( () => {
+ // Use validated ID with fallback to default
+ const presetIdToUse = validatedComparisonPresetId ?? defaultPresetId;
+ if ( preset?.range && presetIdToUse ) {
+ onComparisonChange( preset.range, presetIdToUse );
+ }
+ }, [ onComparisonChange, preset, validatedComparisonPresetId, defaultPresetId ] );
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/index.ts b/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/index.ts
new file mode 100644
index 000000000000..9a1d3322329e
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/index.ts
@@ -0,0 +1 @@
+export { DateFiltersPanel } from './date-filters-panel';
diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.scss b/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.scss
new file mode 100644
index 000000000000..f0ebd4c84d75
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.scss
@@ -0,0 +1,38 @@
+.input-date-control {
+ flex: 1;
+ font-size: var(--wpds-typography-font-size-sm);
+
+ @supports selector( &::-webkit-calendar-picker-indicator ) {
+
+ input[type="date"] {
+ // Removes extra spaces for the calendar icon.
+ width: fit-content;
+ padding-right: 0;
+
+ appearance: none;
+ -webkit-appearance: none;
+ background: none;
+
+ font-size: var(--wpds-typography-font-size-md);
+
+ // Removes the calendar icon.
+ &::-webkit-calendar-picker-indicator {
+ display: none;
+ }
+ }
+ }
+
+ @supports not selector( &::-webkit-calendar-picker-indicator ) {
+
+ input[type="date"] {
+ padding: 0; // We'll control input's inner spacing manually
+
+ min-width: fit-content; // Prevent extra space on smaller screens
+
+ // Use flex to center the input's content horizontally
+ display: flex;
+ width: calc(100% - 20px); // Take almost all the space
+ margin-inline: auto; // Keep things centered
+ }
+ }
+}
diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.tsx
new file mode 100644
index 000000000000..562fe3f07f98
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.tsx
@@ -0,0 +1,103 @@
+/**
+ * External dependencies
+ */
+import { createTZDateFromParts } from '@jetpack-premium-analytics/datetime';
+import { formatDate } from '@jetpack-premium-analytics/formatters';
+import { __ } from '@wordpress/i18n';
+import { Field, Input, Stack } from '@wordpress/ui';
+import { useCallback, useEffect, useState } from 'react';
+/**
+ * Internal dependencies
+ */
+import { DateRangePopover } from '../date-range-popover/date-range-filter';
+import './date-range-input.scss';
+
+type DateRangeInputProps = Pick<
+ Parameters< typeof DateRangePopover >[ 0 ],
+ 'range' | 'onChange'
+> & {
+ timeZone: string;
+};
+
+type DateInputProps = Pick< DateRangeInputProps, 'timeZone' > & {
+ label: string;
+ date?: Date;
+ onChange: ( date?: Date ) => void;
+};
+
+const formatToString = ( date?: Date ) => ( date ? formatDate( date, 'iso' ) : '' );
+
+function parseFromString( dateString: string, timeZone: string ) {
+ const [ year, month, day ] = dateString.split( '-' ).map( x => Number( x ) );
+
+ const parsedDate = createTZDateFromParts( [ year, month - 1, day ], timeZone );
+
+ return ! isNaN( parsedDate.getTime() ) ? parsedDate : undefined;
+}
+
+function DateInput( { label, date, onChange, timeZone }: DateInputProps ) {
+ const [ value, setValue ] = useState( formatToString( date ) );
+
+ useEffect( () => {
+ setValue( formatToString( date ) );
+ }, [ date ] );
+
+ const onInputChange = useCallback(
+ ( event: React.ChangeEvent< HTMLInputElement > ) => {
+ const newValue = event.target.value;
+ setValue( newValue );
+
+ const newDate = parseFromString( newValue, timeZone );
+
+ // Call onChange only when the date is complete and reasonable, to avoid unwanted updates.
+ // Also avoids parseFromString auto-filling partial input (e.g. "20" → "1920").
+ if ( newDate && newDate.getFullYear() > 2000 ) {
+ onChange( newDate );
+ }
+ },
+ [ onChange, timeZone ]
+ );
+
+ const onClick = useCallback( ( e: React.MouseEvent ) => {
+ // Prevents the date input from opening the browser date picker,
+ // as we want to use a custom date picker elsewhere.
+ e.preventDefault();
+ }, [] );
+
+ return (
+
+ { label }
+
+
+ );
+}
+
+export function DateRangeInput( { range, onChange, timeZone }: DateRangeInputProps ) {
+ const { from, to } = range;
+
+ return (
+
+ {
+ if ( nextFrom && to && nextFrom <= to ) {
+ onChange( { from: nextFrom, to } );
+ }
+ } }
+ />
+
+ {
+ if ( nextTo && from && from <= nextTo ) {
+ onChange( { from, to: nextTo } );
+ }
+ } }
+ />
+
+ );
+}
diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-input/index.ts b/projects/packages/premium-analytics/packages/ui/src/date-range-input/index.ts
new file mode 100644
index 000000000000..d42bdcd634fe
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/ui/src/date-range-input/index.ts
@@ -0,0 +1 @@
+export { DateRangeInput } from './date-range-input';
diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.scss b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.scss
new file mode 100644
index 000000000000..aa17958cb048
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.scss
@@ -0,0 +1,106 @@
+@use "@wordpress/base-styles/variables" as vars;
+@use "@wordpress/base-styles/colors" as colors;
+
+.date-range-popover-content {
+ --wca-popover-padding: var(--wpds-dimension-padding-lg);
+ --wca-popover-border-color: #{colors.$gray-300};
+ --wca-popover-border-width: #{vars.$border-width};
+
+ display: grid;
+ grid-template-columns: auto 1fr;
+ grid-template-rows: 1fr auto;
+
+ // Grid lines: background = line color, gap = line width
+ background-color: var(--wca-popover-border-color);
+ column-gap: var(--wca-popover-border-width);
+ row-gap: var(--wca-popover-border-width);
+
+ .date-range-calendar {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ // Mobile layout: override grid to use flex column
+ &--mobile {
+ display: flex;
+ flex-direction: column;
+ gap: var(--wpds-dimension-gap-lg);
+ padding: var(--wca-popover-padding);
+ background-color: #fff;
+
+ .date-range-calendar {
+ --a8c-calendar-button-width: 32px;
+ --a8c-calendar-button-height: 32px;
+ }
+
+ .date-range-popover-actions {
+ padding: 0;
+ padding-block-start: var(--wpds-dimension-gap-sm);
+ }
+ }
+}
+
+.date-range-presets-wrapper {
+ grid-row: 1;
+ display: grid;
+ grid-template-columns: minmax(0, max-content) 1fr;
+ background-color: #fff;
+ padding-bottom: var(--wpds-dimension-gap-sm);
+ width: calc(60 * var(--wpds-dimension-base));
+ padding-right: var(--wpds-dimension-padding-sm);
+}
+
+.date-filters-panel-button {
+ background-color: #fff; // ToDo: handle this upstream.
+}
+
+.date-range-calendar-wrapper {
+ --wca-calendar-button-width: 32px;
+ --wca-calendar-button-height: 32px;
+ --wca-calendar-button-gap: 1rem; // consistent with automattic/ui style
+ --wca-calendar-padding: var(--wca-popover-padding);
+ --wca-calendar-width: calc(var(--wca-calendar-button-width) * 7 + var(--wca-calendar-padding) * 2);
+ --wca-calendar-width-wide: calc(var(--wca-calendar-button-width) * 14 + var(--wca-calendar-padding) * 2 + var(--wca-calendar-button-gap));
+
+ grid-row: 1;
+ padding: var(--wca-calendar-padding);
+ width: var(--wca-calendar-width);
+ background-color: #fff;
+
+ .date-range-calendar {
+ --a8c-calendar-button-width: var(--wca-calendar-button-width);
+ --a8c-calendar-button-height: var(--wca-calendar-button-height);
+ }
+
+ &__wide {
+ width: var(--wca-calendar-width-wide);
+ }
+}
+
+.date-range-popover-actions {
+ grid-column: 1 / -1;
+ padding: calc(var(--wca-popover-padding) / 2) var(--wca-popover-padding);
+ background-color: #fff;
+}
+
+/* disable animation for the date range popover */
+/* stylelint-disable property-no-unknown, selector-pseudo-element-no-unknown */
+@media not ( prefers-reduced-motion: reduce ) {
+
+ .date-filters-panel__popover {
+ view-transition-name: next-admin--date-range-popover;
+ }
+}
+
+/* ensure it's above the canvas/stage during the transition */
+::view-transition-group(next-admin--date-range-popover) {
+ z-index: 3000;
+}
+
+/* no animation for the snapshot (avoid "flashing") */
+::view-transition-new(next-admin--date-range-popover),
+::view-transition-old(next-admin--date-range-popover) {
+ animation: none;
+}
+/* stylelint-enable property-no-unknown, selector-pseudo-element-no-unknown */
diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.tsx
new file mode 100644
index 000000000000..926074092897
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.tsx
@@ -0,0 +1,373 @@
+/**
+ * External dependencies
+ */
+import { DateRangeCalendar } from '@automattic/ui';
+import {
+ getPresetLabel,
+ getDefaultDateRangePresets,
+ PRESET_CUSTOM,
+ type PrimaryPresetId,
+ type DateRangePreset,
+} from '@jetpack-premium-analytics/datetime';
+import { formatDateRange } from '@jetpack-premium-analytics/formatters';
+import {
+ Dropdown,
+ SelectControl,
+ privateApis as componentsPrivateApis,
+} from '@wordpress/components';
+import { useResizeObserver } from '@wordpress/compose';
+import { __ } from '@wordpress/i18n';
+import { calendar } from '@wordpress/icons';
+import { Badge, Button, Stack } from '@wordpress/ui';
+import clsx from 'clsx';
+import { useState, useCallback, useMemo, useEffect } from 'react';
+import '@automattic/ui/style.css';
+/**
+ * Internal dependencies
+ */
+import { DateRangeInput } from '../date-range-input';
+import { DateRangePresets } from '../date-range-presets';
+import { unlock } from '../lock/unlock';
+import './date-range-filter.scss';
+
+const { Menu } = unlock( componentsPrivateApis );
+
+/**
+ * Threshold width (in pixels) below which we consider the layout "mobile".
+ * This is based on the container width, not the viewport.
+ */
+const MOBILE_CONTAINER_WIDTH_THRESHOLD = 480;
+
+/**
+ * Date range type from @automattic/ui.
+ * Represents a range with `from` and `to` Date objects.
+ */
+export type DateRange = NonNullable< Parameters< typeof DateRangeCalendar >[ 0 ][ 'selected' ] >;
+
+/**
+ * Props for DateRangePopoverContent component.
+ */
+type DateRangePopoverContentProps = {
+ /**
+ * Currently selected preset identifier
+ */
+ presetId?: PrimaryPresetId;
+
+ /**
+ * The selected date range
+ */
+ range: DateRange;
+
+ /**
+ * Callback when range or preset changes
+ */
+ onChange: ( range?: DateRange, preset?: PrimaryPresetId ) => void;
+
+ /**
+ * Callback when user applies the selection
+ */
+ onApply: () => void;
+
+ /**
+ * Callback when user cancels the selection
+ */
+ onCancel: () => void;
+
+ /**
+ * Whether the Apply button should be enabled
+ */
+ canApply: boolean;
+
+ /**
+ * Whether to show wide screen layout (2 months)
+ */
+ isWideScreen?: boolean;
+
+ /**
+ * Whether to show mobile layout (dropdown presets instead of sidebar)
+ */
+ isMobile?: boolean;
+
+ /**
+ * IANA timezone string (e.g., 'America/New_York', 'Europe/London').
+ * Required for proper date/time handling.
+ */
+ timeZone: string;
+};
+
+/**
+ * Props for DateRangePresetsDropdown component.
+ */
+type DateRangePresetsDropdownProps = {
+ value: PrimaryPresetId | null;
+ onRangeChange: ( range: DateRange, id: PrimaryPresetId ) => void;
+ presets?: DateRangePreset[];
+ timeZone: string;
+};
+
+function getDisplayedMonth( range: DateRange ): Date {
+ return range?.from ?? new Date();
+}
+
+/**
+ * Action buttons for the date range popover (Cancel/Apply).
+ */
+function DateRangePopoverActions( {
+ onCancel,
+ onApply,
+ canApply,
+}: Pick< DateRangePopoverContentProps, 'onCancel' | 'onApply' | 'canApply' > ) {
+ return (
+
+
+
+
+ );
+}
+
+/**
+ * Dropdown version of DateRangePresets for mobile layout.
+ * Displays presets as a SelectControl instead of a menu list.
+ */
+function DateRangePresetsDropdown( {
+ value,
+ onRangeChange,
+ presets: presetsProp,
+ timeZone,
+}: DateRangePresetsDropdownProps ) {
+ const defaultPresets = useMemo(
+ () => ( presetsProp ? [] : getDefaultDateRangePresets( timeZone ) ),
+ [ presetsProp, timeZone ]
+ );
+ const presets = presetsProp || defaultPresets;
+
+ const options = useMemo(
+ () => [
+ ...presets.map( ( { id, label } ) => ( {
+ value: id,
+ label,
+ } ) ),
+ {
+ value: PRESET_CUSTOM,
+ label: __( 'Custom range', 'jetpack-premium-analytics' ),
+ },
+ ],
+ [ presets ]
+ );
+
+ const handleChange = useCallback(
+ ( selectedValue: string ) => {
+ const preset = presets.find( p => p.id === selectedValue );
+ if ( preset ) {
+ onRangeChange( preset.range, preset.id );
+ }
+ },
+ [ presets, onRangeChange ]
+ );
+
+ return (
+
+ );
+}
+
+/**
+ * Content of the DateRangePopover, extracted for Storybook visualization.
+ * This component is exported for internal use only (stories, testing).
+ */
+export function DateRangePopoverContent( {
+ presetId,
+ range,
+ onChange,
+ onApply,
+ onCancel,
+ canApply,
+ isWideScreen = false,
+ isMobile = false,
+ timeZone,
+}: DateRangePopoverContentProps ) {
+ const [ displayedMonth, setDisplayedMonth ] = useState( getDisplayedMonth( range ) );
+
+ const handleChange = ( nextRange?: DateRange, nextPrimaryPresetId?: PrimaryPresetId ) => {
+ if ( nextRange ) {
+ setDisplayedMonth( getDisplayedMonth( nextRange ) );
+ }
+
+ // If nextPrimaryPresetId is undefined, the user manually changed the dates
+ // (via calendar or input fields), so we switch to PRESET_CUSTOM
+ const effectivePrimaryPresetId = nextPrimaryPresetId ?? PRESET_CUSTOM;
+
+ onChange( nextRange, effectivePrimaryPresetId );
+ };
+
+ // Mobile layout: single column with dropdown presets
+ if ( isMobile ) {
+ return (
+
+
+
+
+
+ handleChange( nextRange ) }
+ numberOfMonths={ 1 }
+ month={ displayedMonth }
+ onMonthChange={ setDisplayedMonth }
+ timeZone={ timeZone }
+ />
+
+
+
+ );
+ }
+
+ // Desktop layout: grid with sidebar presets
+ return (
+
+
+
+
+
+
+
+
+ handleChange( nextRange ) }
+ numberOfMonths={ isWideScreen ? 2 : 1 }
+ month={ displayedMonth }
+ onMonthChange={ setDisplayedMonth }
+ timeZone={ timeZone }
+ />
+
+
+
+
+ );
+}
+
+type DateRangePopoverProps = Omit< DateRangePopoverContentProps, 'isWideScreen' | 'isMobile' > & {
+ /**
+ * Optional external container element for responsive calculations.
+ * When provided, the component will measure this container's width
+ * instead of its own wrapper to determine mobile/wide layouts.
+ */
+ containerElement?: HTMLElement | null;
+};
+
+/**
+ * Threshold width (in pixels) for showing 2 months in calendar.
+ * Based on CSS: --wca-calendar-width-wide (~500px for 2 months + presets sidebar)
+ */
+const WIDE_CONTAINER_THRESHOLD = 780;
+
+export function DateRangePopover( {
+ presetId,
+ range,
+ onChange,
+ onApply,
+ onCancel,
+ canApply,
+ timeZone,
+ containerElement,
+}: DateRangePopoverProps ) {
+ const [ containerWidth, setContainerWidth ] = useState< number | null >( null );
+
+ // Callback to update container width
+ const handleResize = useCallback( ( entries: ResizeObserverEntry[] ) => {
+ const entry = entries[ 0 ];
+ if ( entry ) {
+ setContainerWidth( entry.contentRect.width );
+ }
+ }, [] );
+
+ // ResizeObserver for the reference container
+ const setObserverRef = useResizeObserver< HTMLElement >( handleResize );
+
+ // Attach observer to containerElement if provided, otherwise use document.body
+ useEffect( () => {
+ const element = containerElement ?? document.body;
+ setObserverRef( element );
+ }, [ containerElement, setObserverRef ] );
+
+ // Determine layout based on container width
+ const isMobile = containerWidth !== null && containerWidth < MOBILE_CONTAINER_WIDTH_THRESHOLD;
+
+ const isWideScreen = containerWidth !== null && containerWidth >= WIDE_CONTAINER_THRESHOLD;
+
+ const presetLabel = getPresetLabel( presetId );
+
+ return (
+ (
+
+ ) }
+ renderContent={ ( { onClose } ) => (
+ {
+ onApply();
+ onClose();
+ } }
+ onCancel={ () => {
+ onCancel();
+ onClose();
+ } }
+ canApply={ canApply }
+ isWideScreen={ isWideScreen }
+ isMobile={ isMobile }
+ timeZone={ timeZone }
+ />
+ ) }
+ />
+ );
+}
diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-popover/index.ts b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/index.ts
new file mode 100644
index 000000000000..9d4c1f786baf
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/index.ts
@@ -0,0 +1,2 @@
+export { DateRangePopover, DateRangePopoverContent } from './date-range-filter';
+export type { DateRange } from './date-range-filter';
diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.scss b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.scss
new file mode 100644
index 000000000000..446ea4a41439
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.scss
@@ -0,0 +1,29 @@
+@use "@wordpress/base-styles/variables" as vars;
+@use "@wordpress/base-styles/colors" as colors;
+
+.date-range-presets {
+ max-width: 240px;
+}
+
+.date-range-presets,
+.date-range-presets__custom-group {
+
+ .date-range-presets__item {
+ min-height: var(--wpds-typography-line-height-2xl);
+ }
+}
+
+.date-range-presets__custom-group {
+ // Custom button acts as a label, not an interactive element.
+ // Override disabled styles to show selection state visually.
+ .date-range-presets__custom {
+
+ &[aria-disabled="true"] {
+ color: var(--wpds-color-fg-content-neutral-weak);
+ }
+
+ &[aria-checked="true"] {
+ color: var(--wpds-color-fg-content-neutral);
+ }
+ }
+}
diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.tsx
new file mode 100644
index 000000000000..f27ac51d264c
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.tsx
@@ -0,0 +1,139 @@
+/**
+ * External dependencies
+ */
+import {
+ PRESET_CUSTOM,
+ getDefaultDateRangePresets,
+ type PrimaryPresetId,
+ type DateRangePreset,
+} from '@jetpack-premium-analytics/datetime';
+import { privateApis as componentsPrivateApis } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { useMemo } from 'react';
+/**
+ * Internal dependencies
+ */
+import { DateRangePopover } from '../date-range-popover/date-range-filter';
+import { unlock } from '../lock/unlock';
+import './date-range-presets.scss';
+
+const { Menu } = unlock( componentsPrivateApis );
+
+type DateRange = Parameters< typeof DateRangePopover >[ 0 ][ 'range' ];
+
+/**
+ * Props for the DateRangePresets component.
+ */
+type DateRangePresetsProps = {
+ /**
+ * Callback fired when a preset is selected
+ */
+ onRangeChange: ( range: DateRange, id: PrimaryPresetId ) => void;
+
+ /**
+ * Currently selected preset ID, or null if none
+ */
+ value: PrimaryPresetId | null;
+
+ /**
+ * IANA timezone string (e.g., 'America/New_York').
+ * Required when using default presets. Optional if explicit presets are provided.
+ */
+ timeZone?: string;
+
+ /**
+ * Custom presets to display instead of defaults
+ */
+ presets?: DateRangePreset[];
+
+ /**
+ * Whether to show the custom date option
+ */
+ supportCustom?: boolean;
+
+ /**
+ * Optional callback to clear/remove comparison.
+ * When provided, shows a "No comparison" option.
+ */
+ onClear?: () => void;
+
+ /**
+ * Whether clicking a preset item should close the parent popover.
+ * Defaults to undefined (Ariakit default: checkbox items stay open).
+ */
+ hideOnClick?: boolean;
+};
+
+export function DateRangePresets( {
+ onRangeChange,
+ value,
+ timeZone,
+ presets: presetsProp,
+ onClear,
+ hideOnClick,
+}: DateRangePresetsProps ) {
+ const defaultPresets = useMemo( () => {
+ if ( presetsProp ) {
+ return [];
+ }
+
+ if ( ! timeZone ) {
+ throw new Error(
+ 'DateRangePresets: `timeZone` is required when `presets` are not provided.'
+ );
+ }
+
+ return getDefaultDateRangePresets( timeZone );
+ }, [ presetsProp, timeZone ] );
+
+ const presets = useMemo( () => presetsProp || defaultPresets, [ presetsProp, defaultPresets ] );
+
+ return (
+ <>
+
+ { presets.map( ( { id, label, range: presetRange } ) => (
+ onRangeChange( presetRange, id ) }
+ hideOnClick={ hideOnClick }
+ >
+ { label }
+
+ ) ) }
+
+
+
+
+
+
+ { __( 'Custom', 'jetpack-premium-analytics' ) }
+
+
+ { onClear && (
+
+ { __( 'No comparison', 'jetpack-premium-analytics' ) }
+
+ ) }
+
+ >
+ );
+}
diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/index.ts b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/index.ts
new file mode 100644
index 000000000000..ed2d27e2e31b
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/index.ts
@@ -0,0 +1,29 @@
+export { DateRangePresets } from './date-range-presets';
+
+/**
+ * Re-export types, constants, and guards from datetime
+ * so existing consumers of this barrel continue to work.
+ */
+export {
+ getDefaultDateRangePresets,
+ getPresetLabel,
+ isSelectablePreset,
+ isPrimaryPreset,
+ // Preset constants
+ PRESET_TODAY,
+ PRESET_YESTERDAY,
+ PRESET_LAST_7_DAYS,
+ PRESET_LAST_30_DAYS,
+ PRESET_LAST_90_DAYS,
+ PRESET_LAST_365_DAYS,
+ PRESET_LAST_MONTH,
+ PRESET_LAST_12_MONTHS,
+ PRESET_LAST_YEAR,
+ PRESET_CUSTOM,
+} from '@jetpack-premium-analytics/datetime';
+
+export type {
+ PrimaryPresetId,
+ SelectablePresetId,
+ DateRangePreset,
+} from '@jetpack-premium-analytics/datetime';
diff --git a/projects/packages/premium-analytics/packages/ui/src/index.ts b/projects/packages/premium-analytics/packages/ui/src/index.ts
new file mode 100644
index 000000000000..9a1d3322329e
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/ui/src/index.ts
@@ -0,0 +1 @@
+export { DateFiltersPanel } from './date-filters-panel';
diff --git a/projects/packages/premium-analytics/packages/ui/src/lock/unlock.ts b/projects/packages/premium-analytics/packages/ui/src/lock/unlock.ts
new file mode 100644
index 000000000000..ca799e25b34c
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/ui/src/lock/unlock.ts
@@ -0,0 +1,22 @@
+/**
+ * Local unlock helper for reaching the private `Menu` component that
+ * `@wordpress/components` exposes through `@wordpress/private-apis`.
+ *
+ * Upstream reached `Menu` via `@automattic/admin-toolkit`'s `unlock`, which is
+ * not available in this monorepo. This mirrors existing Jetpack precedent that
+ * opts in to the private APIs directly:
+ *
+ * - `projects/packages/jetpack-mu-wpcom/src/common/utils.ts` (`getUnlock()`)
+ * - `projects/js-packages/charts/src/stories/unlock.ts`
+ *
+ * The opt-in module name only needs to be an allow-listed core module; the
+ * returned `unlock` reads private data bound to any object, so it resolves the
+ * private APIs locked onto `@wordpress/components`' `privateApis`.
+ */
+
+import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis';
+
+export const { unlock } = __dangerousOptInToUnstableAPIsOnlyForCoreModules(
+ 'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.',
+ '@wordpress/components'
+);
diff --git a/projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/index.ts b/projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/index.ts
new file mode 100644
index 000000000000..945c2ce54278
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/index.ts
@@ -0,0 +1,2 @@
+export { useComparisonDatePresets } from './use-comparison-date-presets';
+export type { ComparisonDateRangePreset } from './use-comparison-date-presets';
diff --git a/projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/use-comparison-date-presets.ts b/projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/use-comparison-date-presets.ts
new file mode 100644
index 000000000000..1a018083db1c
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/use-comparison-date-presets.ts
@@ -0,0 +1,45 @@
+/**
+ * External dependencies
+ */
+import {
+ getComparisonRangeFromPreset,
+ getComparisonPresetConfigs,
+ type ComparisonPresetId,
+} from '@jetpack-premium-analytics/datetime';
+import { useMemo } from 'react';
+/**
+ * Internal dependencies
+ */
+import type { DateRange } from '../date-range-popover/date-range-filter';
+
+/**
+ * A comparison-specific date range preset.
+ * Similar to DateRangePreset but with a strongly-typed ComparisonPresetId.
+ */
+export type ComparisonDateRangePreset = {
+ id: ComparisonPresetId;
+ label: string;
+ range: DateRange;
+};
+
+/**
+ * Custom hook that generates comparison date presets
+ * based on a reference date range.
+ *
+ * @param referenceRange - The primary date range to compare against
+ * @return Array of comparison presets with strongly-typed IDs
+ */
+export function useComparisonDatePresets( referenceRange: DateRange ): ComparisonDateRangePreset[] {
+ return useMemo( () => {
+ if ( ! referenceRange.from || ! referenceRange.to ) {
+ return [];
+ }
+
+ return getComparisonPresetConfigs()
+ .map( ( { id, label } ) => {
+ const range = getComparisonRangeFromPreset( referenceRange, id );
+ return range ? { id, label, range } : null;
+ } )
+ .filter( ( preset ): preset is ComparisonDateRangePreset => preset !== null );
+ }, [ referenceRange ] );
+}