From 604a8c13347382c006c0c5ba3b36edddfe1d201a Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 3 Jun 2026 14:52:42 +0800 Subject: [PATCH 01/10] feat(premium-analytics): add ui package from next-woocommerce-analytics components Verbatim copy of packages/components from next-woocommerce-analytics (source, SCSS, stories, manifest, tsconfig), with only the directory renamed components -> ui: @wordpress/build hard-codes an Emotion Babel pass for any package directory literally named "components", which breaks TS transpilation; these components use SCSS, not Emotion. No content changes - later commits adapt the package to the monorepo. --- .../packages/ui/package.json | 25 ++ .../date-comparison-dropdown.scss | 34 ++ .../date-comparison-dropdown.tsx | 170 ++++++++ .../ui/src/date-comparison-dropdown/index.ts | 1 + .../date-comparison-dropdown.stories.tsx | 127 ++++++ .../date-filters-panel/date-filters-panel.tsx | 246 +++++++++++ .../ui/src/date-filters-panel/index.ts | 1 + .../date-range-input/date-range-input.scss | 36 ++ .../src/date-range-input/date-range-input.tsx | 119 +++++ .../packages/ui/src/date-range-input/index.ts | 1 + .../date-range-popover/date-range-filter.scss | 105 +++++ .../date-range-popover/date-range-filter.tsx | 411 ++++++++++++++++++ .../ui/src/date-range-popover/index.ts | 2 + .../stories/date-range-popover.stories.tsx | 189 ++++++++ .../date-range-presets.scss | 27 ++ .../date-range-presets/date-range-presets.tsx | 148 +++++++ .../ui/src/date-range-presets/index.ts | 29 ++ .../stories/date-range-presets.stories.tsx | 118 +++++ .../packages/ui/src/index.ts | 1 + .../src/use-comparison-date-presets/index.ts | 2 + .../use-comparison-date-presets.ts | 54 +++ .../packages/ui/tsconfig.json | 9 + 22 files changed, 1855 insertions(+) create mode 100644 projects/packages/premium-analytics/packages/ui/package.json create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.scss create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/index.ts create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/stories/date-comparison-dropdown.stories.tsx create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-filters-panel/date-filters-panel.tsx create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-filters-panel/index.ts create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.scss create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.tsx create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-range-input/index.ts create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.scss create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.tsx create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-range-popover/index.ts create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-range-popover/stories/date-range-popover.stories.tsx create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.scss create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.tsx create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-range-presets/index.ts create mode 100644 projects/packages/premium-analytics/packages/ui/src/date-range-presets/stories/date-range-presets.stories.tsx create mode 100644 projects/packages/premium-analytics/packages/ui/src/index.ts create mode 100644 projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/index.ts create mode 100644 projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/use-comparison-date-presets.ts create mode 100644 projects/packages/premium-analytics/packages/ui/tsconfig.json 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..746a1f67760b --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/package.json @@ -0,0 +1,25 @@ +{ + "name": "@next-woo-analytics/components", + "description": "WooCommerce Analytics components", + "version": "1.0.0", + "type": "module", + "wpModule": true, + "main": "src/index.ts", + "exports": { + ".": "./build/src/index.js" + }, + "dependencies": { + "@automattic/ui": "*", + "date-fns": "*", + "@next-woo-analytics/data": "workspace:*", + "@next-woo-analytics/datetime": "workspace:*", + "@wc-analytics/formatters": "workspace:*", + "@wordpress/components": "*", + "@automattic/admin-toolkit": "*", + "@automattic/design-system": "*", + "@wordpress/icons": "*", + "@wordpress/compose": "*", + "@wordpress/ui": "*", + "clsx": "*" + } +} 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..93626fe01f0b --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.scss @@ -0,0 +1,34 @@ +.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..5334eabbca17 --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx @@ -0,0 +1,170 @@ +/** + * External dependencies + */ +import { privateApis as componentsPrivateApis } from '@wordpress/components'; +import { unlock } from '@automattic/admin-toolkit'; +import { Button } from '@wordpress/ui'; +import { formatDateRange } from '@wc-analytics/formatters'; +import { sprintf, __ } from '@wordpress/i18n'; +import { useMemo } from 'react'; +import type { ComparisonPresetId } from '@next-woo-analytics/datetime'; + +const { Menu } = unlock( componentsPrivateApis ); + +/** + * Internal dependencies + */ +import { DateRangePresets } from '../date-range-presets'; +import type { ComparisonDateRangePreset } from '../use-comparison-date-presets'; +import './date-comparison-dropdown.scss'; + +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 ( + + + { __( 'No comparison', 'woocommerce-analytics' ) } + + } + /> + + + + + { __( + 'No comparison', + 'woocommerce-analytics' + ) } + + + + + + { __( + 'Comparison to past', + 'woocommerce-analytics' + ) } + + + + + + ); + } + + let label: string = __( 'Select comparison', 'woocommerce-analytics' ); + if ( hasValidPreset ) { + if ( removeCompareToPrefix ) { + label = formatDateRange( comparisonRange ); + } else { + label = sprintf( + // translators: %s is the comparison range label + __( 'Compare to: %s', 'woocommerce-analytics' ), + formatDateRange( comparisonRange ) + ); + } + } + + return ( + + + { label } + + } + /> + + { hasPresets && ( + { + /* + * Type assertion is safe here because: + * 1. presets is ComparisonDateRangePreset[] (strongly typed) + * 2. DateRangePresets picks id from our presets array + * 3. Therefore id must be ComparisonPresetId + */ + onPresetChange( id as ComparisonPresetId ); + } } + onClear={ onClear } + /> + ) } + + + ); +} 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-comparison-dropdown/stories/date-comparison-dropdown.stories.tsx b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/stories/date-comparison-dropdown.stories.tsx new file mode 100644 index 000000000000..8d0ba6bc3d14 --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/stories/date-comparison-dropdown.stories.tsx @@ -0,0 +1,127 @@ +/** + * External dependencies + */ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { useState } from 'react'; +import { subDays, startOfDay, endOfDay } from 'date-fns'; +import type { ComparisonPresetId } from '@next-woo-analytics/datetime'; + +/** + * Internal dependencies + */ +import { DateComparisonDropdown } from '../date-comparison-dropdown'; +import { useComparisonDatePresets } from '../../use-comparison-date-presets'; +import type { DateRange } from '../../date-range-popover'; + +const meta: Meta< typeof DateComparisonDropdown > = { + title: 'Components/DateComparisonDropdown', + component: DateComparisonDropdown, + tags: [ 'autodocs' ], + parameters: { + docs: { + description: { + component: + 'A dropdown component for selecting date comparison ranges. ' + + 'Supports enabling/disabling comparison and selecting from preset comparison periods.', + }, + }, + }, +}; + +export default meta; + +type Story = StoryObj< typeof DateComparisonDropdown >; + +const today = new Date(); +const defaultRange: DateRange = { + from: startOfDay( subDays( today, 30 ) ), + to: endOfDay( subDays( today, 1 ) ), +}; + +function DateComparisonDropdownWithState( { + initialEnabled = true, + initialPresetId = 'previous-period', +}: { + initialEnabled?: boolean; + initialPresetId?: ComparisonPresetId; +} ) { + const [ enabled, setEnabled ] = useState( initialEnabled ); + const [ presetId, setPresetId ] = useState< + ComparisonPresetId | undefined + >( initialEnabled ? initialPresetId : undefined ); + + const presets = useComparisonDatePresets( defaultRange ); + + return ( + { + setEnabled( true ); + setPresetId( 'previous-period' ); + } } + onPresetChange={ setPresetId } + onClear={ () => { + setEnabled( false ); + setPresetId( undefined ); + } } + /> + ); +} + +/** + * Default state with comparison enabled and "Previous period" selected. + */ +export const Default: Story = { + render: () => , +}; + +/** + * Comparison disabled - shows "No comparison" button. + * Clicking opens a menu to enable comparison. + */ +export const Disabled: Story = { + render: () => , +}; + +/** + * With "Previous month" preset selected. + */ +export const PreviousMonthSelected: Story = { + render: () => ( + + ), +}; + +/** + * Without the "Compare:" prefix - just shows the date range. + */ +export const WithoutPrefix: Story = { + render: () => { + const [ enabled, setEnabled ] = useState( true ); + const [ presetId, setPresetId ] = useState< + ComparisonPresetId | undefined + >( 'previous-period' ); + + const presets = useComparisonDatePresets( defaultRange ); + + return ( + { + setEnabled( true ); + setPresetId( 'previous-period' ); + } } + onPresetChange={ setPresetId } + onClear={ () => { + setEnabled( false ); + setPresetId( undefined ); + } } + /> + ); + }, +}; 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..ff4b7498adfd --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-filters-panel/date-filters-panel.tsx @@ -0,0 +1,246 @@ +/** + * External dependencies + */ +import { Stack } from '@wordpress/ui'; +import { BaseControl } from '@wordpress/components'; +import { useMemo, useCallback } from 'react'; +import { + isComparisonPresetId, + isPrimaryPreset, + type ComparisonPresetId, + type PrimaryPresetId, +} from '@next-woo-analytics/datetime'; + +/** + * Internal dependencies + */ +import { DateRangePopover } from '../date-range-popover'; +import { DateComparisonDropdown } from '../date-comparison-dropdown'; +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..754e7105acaf --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.scss @@ -0,0 +1,36 @@ +.input-date-control { + flex: 1; + font-size: var( --wpds-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-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..740951cfd3a4 --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-input/date-range-input.tsx @@ -0,0 +1,119 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Field, Input, Stack } from '@wordpress/ui'; +import { useCallback, useEffect, useState } from 'react'; +import { createTZDateFromParts } from '@next-woo-analytics/datetime'; +import { formatDate } from '@wc-analytics/formatters'; + +/** + * 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..b0bc48158ac7 --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.scss @@ -0,0 +1,105 @@ +@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..d09f8b634c51 --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.tsx @@ -0,0 +1,411 @@ +/** + * External dependencies + */ +import { DateRangeCalendar } from '@automattic/ui'; +import { + Dropdown, + SelectControl, + privateApis as componentsPrivateApis, +} from '@wordpress/components'; +import { unlock } from '@automattic/admin-toolkit'; +import { calendar } from '@wordpress/icons'; +import { Badge, Button, Stack } from '@wordpress/ui'; +import { useState, useCallback, useMemo, useEffect } from 'react'; +import { useResizeObserver } from '@wordpress/compose'; +import { formatDateRange } from '@wc-analytics/formatters'; +import { __ } from '@wordpress/i18n'; +import clsx from 'clsx'; +import '@automattic/ui/style.css'; +import { + getPresetLabel, + getDefaultDateRangePresets, + PRESET_CUSTOM, + type PrimaryPresetId, + type DateRangePreset, +} from '@next-woo-analytics/datetime'; + +/** + * Internal dependencies + */ +import { DateRangePresets } from '../date-range-presets'; +import { DateRangeInput } from '../date-range-input'; +import './date-range-filter.scss'; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment +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', 'woocommerce-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-popover/stories/date-range-popover.stories.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/stories/date-range-popover.stories.tsx new file mode 100644 index 000000000000..407299009089 --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/stories/date-range-popover.stories.tsx @@ -0,0 +1,189 @@ +/** + * External dependencies + */ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { useState } from 'react'; +import { subDays, startOfDay, endOfDay } from 'date-fns'; +import type { PrimaryPresetId } from '@next-woo-analytics/datetime'; + +/** + * Internal dependencies + */ +import { + DateRangePopover, + DateRangePopoverContent, +} from '../date-range-filter'; +import type { DateRange } from '../date-range-filter'; + +const meta: Meta< typeof DateRangePopover > = { + title: 'Components/DateRangePopover', + component: DateRangePopover, + tags: [ 'autodocs' ], + decorators: [ + ( Story ) => ( +
+ +
+ ), + ], +}; +export default meta; + +type Story = StoryObj< typeof DateRangePopover >; + +const today = new Date(); +const defaultRange: DateRange = { + from: startOfDay( subDays( today, 7 ) ), + to: endOfDay( subDays( today, 1 ) ), +}; + +// Default timezone for Storybook - avoids dependency on WordPress stores +const STORYBOOK_TIMEZONE = 'America/New_York'; + +function DateRangePopoverWithState() { + const [ range, setRange ] = useState< DateRange >( defaultRange ); + const [ presetId, setPrimaryPresetId ] = + useState< PrimaryPresetId >( 'last-7-days' ); + const [ pendingRange, setPendingRange ] = + useState< DateRange >( defaultRange ); + const [ pendingPrimaryPresetId, setPendingPrimaryPresetId ] = + useState< PrimaryPresetId >( 'last-7-days' ); + + const handleChange = ( + nextRange?: DateRange, + nextPrimaryPresetId?: PrimaryPresetId + ) => { + if ( nextRange ) { + setPendingRange( nextRange ); + } + if ( nextPrimaryPresetId ) { + setPendingPrimaryPresetId( nextPrimaryPresetId ); + } + }; + + const handleApply = () => { + setRange( pendingRange ); + setPrimaryPresetId( pendingPrimaryPresetId ); + }; + + const handleCancel = () => { + setPendingRange( range ); + setPendingPrimaryPresetId( presetId ); + }; + + const canApply = + pendingRange.from !== range.from || pendingRange.to !== range.to; + + return ( + + ); +} + +export const Default: Story = { + render: () => , +}; + +/** + * `Custom` preset selected. + */ +export const CustomPreset: Story = { + render: () => { + const [ range, setRange ] = useState< DateRange >( { + from: startOfDay( subDays( today, 14 ) ), + to: endOfDay( subDays( today, 3 ) ), + } ); + + return ( + nextRange && setRange( nextRange ) } + onApply={ () => {} } + onCancel={ () => {} } + canApply={ true } + timeZone={ STORYBOOK_TIMEZONE } + /> + ); + }, +}; + +/** + * `Today` preset selected. + */ +export const TodayPreset: Story = { + render: () => { + const todayRange: DateRange = { + from: startOfDay( today ), + to: endOfDay( today ), + }; + const [ range, setRange ] = useState< DateRange >( todayRange ); + + return ( + nextRange && setRange( nextRange ) } + onApply={ () => {} } + onCancel={ () => {} } + canApply={ false } + timeZone={ STORYBOOK_TIMEZONE } + /> + ); + }, +}; + +/** + * Interactive DateRangePopoverContent with state management. + */ +function PopoverContentWithState( { isWideScreen = false } ) { + const [ range, setRange ] = useState< DateRange >( defaultRange ); + const [ presetId, setPrimaryPresetId ] = + useState< PrimaryPresetId >( 'last-7-days' ); + + const handleChange = ( + nextRange?: DateRange, + nextPrimaryPresetId?: PrimaryPresetId + ) => { + if ( nextRange ) { + setRange( nextRange ); + } + if ( nextPrimaryPresetId ) { + setPrimaryPresetId( nextPrimaryPresetId ); + } + }; + + return ( + {} } + onCancel={ () => {} } + canApply={ true } + isWideScreen={ isWideScreen } + timeZone={ STORYBOOK_TIMEZONE } + /> + ); +} + +/** + * Interactive DateRangePopoverContent with state management. + */ +export const PopoverContent: Story = { + render: () => , +}; + +/** + * Interactive DateRangePopoverContent with state management. + */ +export const PopoverContentWide: Story = { + render: () => , +}; 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..1c1f1306fc48 --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.scss @@ -0,0 +1,27 @@ +@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-font-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..e60fd29057b5 --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/date-range-presets.tsx @@ -0,0 +1,148 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; +import { unlock } from '@automattic/admin-toolkit'; +import { useMemo } from 'react'; +import { + PRESET_CUSTOM, + getDefaultDateRangePresets, + type PrimaryPresetId, + type DateRangePreset, +} from '@next-woo-analytics/datetime'; + +/** + * Internal dependencies + */ +import { DateRangePopover } from '../date-range-popover/date-range-filter'; +import './date-range-presets.scss'; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment +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', 'woocommerce-analytics' ) } + + + + { onClear && ( + + + { __( 'No comparison', 'woocommerce-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..2d34bfc5454d --- /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 '@next-woo-analytics/datetime'; + +export type { + PrimaryPresetId, + SelectablePresetId, + DateRangePreset, +} from '@next-woo-analytics/datetime'; diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/stories/date-range-presets.stories.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/stories/date-range-presets.stories.tsx new file mode 100644 index 000000000000..c0c7d1771614 --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/stories/date-range-presets.stories.tsx @@ -0,0 +1,118 @@ +/** + * External dependencies + */ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { useState } from 'react'; +import { subDays, startOfDay, endOfDay } from 'date-fns'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; +import { unlock } from '@automattic/admin-toolkit'; +import type { PrimaryPresetId } from '@next-woo-analytics/datetime'; + +/** + * Internal dependencies + */ +import { DateRangePresets } from '../date-range-presets'; +import type { DateRange } from '../../date-range-popover'; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment +const { Menu } = unlock( componentsPrivateApis ); + +/** + * Timezone used in stories for consistent date calculations. + */ +const STORY_TIMEZONE = 'America/New_York'; + +const meta: Meta< typeof DateRangePresets > = { + title: 'Components/DateRangePresets', + component: DateRangePresets, + tags: [ 'autodocs' ], + decorators: [ + ( Story ) => ( + // Menu.Group must be wrapped in a Menu to work correctly in Storybook. + + + + ), + ], +}; + +export default meta; + +type Story = StoryObj< typeof DateRangePresets >; + +const today = new Date(); + +function DateRangePresetsWithState( { + initialPrimaryPresetId = 'last-7-days', +}: { + initialPrimaryPresetId?: PrimaryPresetId; +} ) { + const [ presetId, setPrimaryPresetId ] = useState< PrimaryPresetId >( + initialPrimaryPresetId + ); + const [ , setRange ] = useState< DateRange >( { + from: startOfDay( subDays( today, 7 ) ), + to: endOfDay( subDays( today, 1 ) ), + } ); + + const handleChange = ( + nextRange: DateRange, + nextPrimaryPresetId: PrimaryPresetId + ) => { + setRange( nextRange ); + setPrimaryPresetId( nextPrimaryPresetId ); + }; + + return ( + + ); +} + +export const Default: Story = { + render: () => , +}; + +/** + * `Today` preset selected. + */ +export const TodaySelected: Story = { + render: () => , +}; + +/** + * `Custom` preset selected. + */ +export const CustomSelected: Story = { + render: () => , +}; + +/** + * No preset selected. + */ +export const NoSelection: Story = { + render: () => { + const [ presetId, setPrimaryPresetId ] = + useState< PrimaryPresetId | null >( null ); + const [ , setRange ] = useState< DateRange | null >( null ); + + const handleChange = ( + nextRange: DateRange, + nextPrimaryPresetId: PrimaryPresetId + ) => { + setRange( nextRange ); + setPrimaryPresetId( nextPrimaryPresetId ); + }; + + return ( + + ); + }, +}; 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/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..a7f9f48e731f --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/src/use-comparison-date-presets/use-comparison-date-presets.ts @@ -0,0 +1,54 @@ +/** + * External dependencies + */ +import { useMemo } from 'react'; +import { + getComparisonRangeFromPreset, + getComparisonPresetConfigs, + type ComparisonPresetId, +} from '@next-woo-analytics/datetime'; + +/** + * 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 ] ); +} diff --git a/projects/packages/premium-analytics/packages/ui/tsconfig.json b/projects/packages/premium-analytics/packages/ui/tsconfig.json new file mode 100644 index 000000000000..18b7c843caa5 --- /dev/null +++ b/projects/packages/premium-analytics/packages/ui/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "build", + "skipLibCheck": true + }, + "include": [ "src/**/*" ], + "exclude": [ "build", "node_modules" ] +} \ No newline at end of file From 4becf55eb310f9532f527137a1dc826d619e1675 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 5 Jun 2026 16:58:09 +0800 Subject: [PATCH 02/10] refactor(premium-analytics): adapt ui package imports and manifest for monorepo Mechanical port adaptations, no behavior changes: - Import specifiers: @next-woo-analytics/datetime -> @jetpack-premium-analytics/datetime, @wc-analytics/formatters -> @jetpack-premium-analytics/formatters - Text domain: woocommerce-analytics -> jetpack-premium-analytics - Manifest: internal-packages name/private/types conventions, pinned versions matching siblings, drop @next-woo-analytics/data and @automattic/design-system (declared upstream, imported nowhere), declare @wordpress/i18n and react (imported but undeclared upstream), move date-fns to devDependencies (story-only import), add @storybook/react for the stories - Drop upstream wpModule/exports build fields: main/types point at the TS source like the sibling internal packages; the script-module build is deferred until the leaves are Node-resolvable - Stories: @storybook/react-vite types -> @storybook/react, titles follow the monorepo convention (Packages/Premium Analytics/UI/) - Delete leaf tsconfig.json: the parent tsconfig already includes packages/**/* and supplies the @jetpack-premium-analytics/* path alias (mirrors packages/datetime and packages/formatters) --- .../packages/ui/package.json | 39 ++++++++++--------- .../date-comparison-dropdown.tsx | 14 +++---- .../date-comparison-dropdown.stories.tsx | 6 +-- .../date-filters-panel/date-filters-panel.tsx | 2 +- .../src/date-range-input/date-range-input.tsx | 8 ++-- .../date-range-popover/date-range-filter.tsx | 10 ++--- .../stories/date-range-popover.stories.tsx | 6 +-- .../date-range-presets/date-range-presets.tsx | 6 +-- .../ui/src/date-range-presets/index.ts | 4 +- .../stories/date-range-presets.stories.tsx | 6 +-- .../use-comparison-date-presets.ts | 2 +- .../packages/ui/tsconfig.json | 9 ----- 12 files changed, 53 insertions(+), 59 deletions(-) delete mode 100644 projects/packages/premium-analytics/packages/ui/tsconfig.json diff --git a/projects/packages/premium-analytics/packages/ui/package.json b/projects/packages/premium-analytics/packages/ui/package.json index 746a1f67760b..da07b3ddd9e1 100644 --- a/projects/packages/premium-analytics/packages/ui/package.json +++ b/projects/packages/premium-analytics/packages/ui/package.json @@ -1,25 +1,28 @@ { - "name": "@next-woo-analytics/components", - "description": "WooCommerce Analytics components", - "version": "1.0.0", + "name": "@automattic/jetpack-premium-analytics-ui", + "version": "0.1.0", + "private": true, "type": "module", - "wpModule": true, "main": "src/index.ts", - "exports": { - ".": "./build/src/index.js" - }, + "types": "src/index.ts", + "sideEffects": [ + "*.scss" + ], "dependencies": { - "@automattic/ui": "*", - "date-fns": "*", - "@next-woo-analytics/data": "workspace:*", - "@next-woo-analytics/datetime": "workspace:*", - "@wc-analytics/formatters": "workspace:*", - "@wordpress/components": "*", "@automattic/admin-toolkit": "*", - "@automattic/design-system": "*", - "@wordpress/icons": "*", - "@wordpress/compose": "*", - "@wordpress/ui": "*", - "clsx": "*" + "@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/ui": "0.13.0", + "clsx": "2.1.1", + "react": "18.3.1" + }, + "devDependencies": { + "@storybook/react": "10.3.6", + "date-fns": "4.1.0" } } 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 index 5334eabbca17..3088dd941c6c 100644 --- 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 @@ -4,10 +4,10 @@ import { privateApis as componentsPrivateApis } from '@wordpress/components'; import { unlock } from '@automattic/admin-toolkit'; import { Button } from '@wordpress/ui'; -import { formatDateRange } from '@wc-analytics/formatters'; +import { formatDateRange } from '@jetpack-premium-analytics/formatters'; import { sprintf, __ } from '@wordpress/i18n'; import { useMemo } from 'react'; -import type { ComparisonPresetId } from '@next-woo-analytics/datetime'; +import type { ComparisonPresetId } from '@jetpack-premium-analytics/datetime'; const { Menu } = unlock( componentsPrivateApis ); @@ -80,7 +80,7 @@ export function DateComparisonDropdown( { size="compact" id="date-comparison-dropdown-button" > - { __( 'No comparison', 'woocommerce-analytics' ) } + { __( 'No comparison', 'jetpack-premium-analytics' ) } } /> @@ -94,7 +94,7 @@ export function DateComparisonDropdown( { { __( 'No comparison', - 'woocommerce-analytics' + 'jetpack-premium-analytics' ) } @@ -109,7 +109,7 @@ export function DateComparisonDropdown( { { __( 'Comparison to past', - 'woocommerce-analytics' + 'jetpack-premium-analytics' ) } @@ -119,14 +119,14 @@ export function DateComparisonDropdown( { ); } - let label: string = __( 'Select comparison', 'woocommerce-analytics' ); + 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', 'woocommerce-analytics' ), + __( 'Compare to: %s', 'jetpack-premium-analytics' ), formatDateRange( comparisonRange ) ); } diff --git a/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/stories/date-comparison-dropdown.stories.tsx b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/stories/date-comparison-dropdown.stories.tsx index 8d0ba6bc3d14..2cc688d1f311 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/stories/date-comparison-dropdown.stories.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/stories/date-comparison-dropdown.stories.tsx @@ -1,10 +1,10 @@ /** * External dependencies */ -import type { Meta, StoryObj } from '@storybook/react-vite'; +import type { Meta, StoryObj } from '@storybook/react'; import { useState } from 'react'; import { subDays, startOfDay, endOfDay } from 'date-fns'; -import type { ComparisonPresetId } from '@next-woo-analytics/datetime'; +import type { ComparisonPresetId } from '@jetpack-premium-analytics/datetime'; /** * Internal dependencies @@ -14,7 +14,7 @@ import { useComparisonDatePresets } from '../../use-comparison-date-presets'; import type { DateRange } from '../../date-range-popover'; const meta: Meta< typeof DateComparisonDropdown > = { - title: 'Components/DateComparisonDropdown', + title: 'Packages/Premium Analytics/UI/DateComparisonDropdown', component: DateComparisonDropdown, tags: [ 'autodocs' ], parameters: { 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 index ff4b7498adfd..d2a3202800ad 100644 --- 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 @@ -9,7 +9,7 @@ import { isPrimaryPreset, type ComparisonPresetId, type PrimaryPresetId, -} from '@next-woo-analytics/datetime'; +} from '@jetpack-premium-analytics/datetime'; /** * Internal dependencies 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 index 740951cfd3a4..18e5d3d3e955 100644 --- 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 @@ -4,8 +4,8 @@ import { __ } from '@wordpress/i18n'; import { Field, Input, Stack } from '@wordpress/ui'; import { useCallback, useEffect, useState } from 'react'; -import { createTZDateFromParts } from '@next-woo-analytics/datetime'; -import { formatDate } from '@wc-analytics/formatters'; +import { createTZDateFromParts } from '@jetpack-premium-analytics/datetime'; +import { formatDate } from '@jetpack-premium-analytics/formatters'; /** * Internal dependencies @@ -94,7 +94,7 @@ export function DateRangeInput( { return ( { @@ -105,7 +105,7 @@ export function DateRangeInput( { /> { 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 index d09f8b634c51..33caecc2f0ec 100644 --- 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 @@ -12,7 +12,7 @@ import { calendar } from '@wordpress/icons'; import { Badge, Button, Stack } from '@wordpress/ui'; import { useState, useCallback, useMemo, useEffect } from 'react'; import { useResizeObserver } from '@wordpress/compose'; -import { formatDateRange } from '@wc-analytics/formatters'; +import { formatDateRange } from '@jetpack-premium-analytics/formatters'; import { __ } from '@wordpress/i18n'; import clsx from 'clsx'; import '@automattic/ui/style.css'; @@ -22,7 +22,7 @@ import { PRESET_CUSTOM, type PrimaryPresetId, type DateRangePreset, -} from '@next-woo-analytics/datetime'; +} from '@jetpack-premium-analytics/datetime'; /** * Internal dependencies @@ -130,7 +130,7 @@ function DateRangePopoverActions( { className="date-range-popover-actions" > ); @@ -168,7 +168,7 @@ function DateRangePresetsDropdown( { } ) ), { value: PRESET_CUSTOM, - label: __( 'Custom range', 'woocommerce-analytics' ), + label: __( 'Custom range', 'jetpack-premium-analytics' ), }, ], [ presets ] diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-popover/stories/date-range-popover.stories.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/stories/date-range-popover.stories.tsx index 407299009089..3e529122e160 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-range-popover/stories/date-range-popover.stories.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/stories/date-range-popover.stories.tsx @@ -1,10 +1,10 @@ /** * External dependencies */ -import type { Meta, StoryObj } from '@storybook/react-vite'; +import type { Meta, StoryObj } from '@storybook/react'; import { useState } from 'react'; import { subDays, startOfDay, endOfDay } from 'date-fns'; -import type { PrimaryPresetId } from '@next-woo-analytics/datetime'; +import type { PrimaryPresetId } from '@jetpack-premium-analytics/datetime'; /** * Internal dependencies @@ -16,7 +16,7 @@ import { import type { DateRange } from '../date-range-filter'; const meta: Meta< typeof DateRangePopover > = { - title: 'Components/DateRangePopover', + title: 'Packages/Premium Analytics/UI/DateRangePopover', component: DateRangePopover, tags: [ 'autodocs' ], decorators: [ 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 index e60fd29057b5..7048c34be05f 100644 --- 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 @@ -10,7 +10,7 @@ import { getDefaultDateRangePresets, type PrimaryPresetId, type DateRangePreset, -} from '@next-woo-analytics/datetime'; +} from '@jetpack-premium-analytics/datetime'; /** * Internal dependencies @@ -123,7 +123,7 @@ export function DateRangePresets( { disabled > - { __( 'Custom', 'woocommerce-analytics' ) } + { __( 'Custom', 'jetpack-premium-analytics' ) } @@ -138,7 +138,7 @@ export function DateRangePresets( { hideOnClick > - { __( 'No comparison', 'woocommerce-analytics' ) } + { __( '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 index 2d34bfc5454d..ed2d27e2e31b 100644 --- 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 @@ -20,10 +20,10 @@ export { PRESET_LAST_12_MONTHS, PRESET_LAST_YEAR, PRESET_CUSTOM, -} from '@next-woo-analytics/datetime'; +} from '@jetpack-premium-analytics/datetime'; export type { PrimaryPresetId, SelectablePresetId, DateRangePreset, -} from '@next-woo-analytics/datetime'; +} from '@jetpack-premium-analytics/datetime'; diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/stories/date-range-presets.stories.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/stories/date-range-presets.stories.tsx index c0c7d1771614..4c57b4c893ac 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/stories/date-range-presets.stories.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/stories/date-range-presets.stories.tsx @@ -1,12 +1,12 @@ /** * External dependencies */ -import type { Meta, StoryObj } from '@storybook/react-vite'; +import type { Meta, StoryObj } from '@storybook/react'; import { useState } from 'react'; import { subDays, startOfDay, endOfDay } from 'date-fns'; import { privateApis as componentsPrivateApis } from '@wordpress/components'; import { unlock } from '@automattic/admin-toolkit'; -import type { PrimaryPresetId } from '@next-woo-analytics/datetime'; +import type { PrimaryPresetId } from '@jetpack-premium-analytics/datetime'; /** * Internal dependencies @@ -23,7 +23,7 @@ const { Menu } = unlock( componentsPrivateApis ); const STORY_TIMEZONE = 'America/New_York'; const meta: Meta< typeof DateRangePresets > = { - title: 'Components/DateRangePresets', + title: 'Packages/Premium Analytics/UI/DateRangePresets', component: DateRangePresets, tags: [ 'autodocs' ], decorators: [ 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 index a7f9f48e731f..021b5702696e 100644 --- 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 @@ -6,7 +6,7 @@ import { getComparisonRangeFromPreset, getComparisonPresetConfigs, type ComparisonPresetId, -} from '@next-woo-analytics/datetime'; +} from '@jetpack-premium-analytics/datetime'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/ui/tsconfig.json b/projects/packages/premium-analytics/packages/ui/tsconfig.json deleted file mode 100644 index 18b7c843caa5..000000000000 --- a/projects/packages/premium-analytics/packages/ui/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "build", - "skipLibCheck": true - }, - "include": [ "src/**/*" ], - "exclude": [ "build", "node_modules" ] -} \ No newline at end of file From e14415029a10161435c11f0fb46a5e957a2542c4 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 5 Jun 2026 16:58:59 +0800 Subject: [PATCH 03/10] refactor(premium-analytics): decouple ui package from admin-toolkit unlock @automattic/admin-toolkit is not available in this monorepo. Replace its unlock with a local src/lock/unlock.ts that opts in to @wordpress/private-apis directly, mirroring existing Jetpack precedent (jetpack-mu-wpcom getUnlock(), js-packages/charts stories). Swap the manifest dependency accordingly. The typed local helper also makes upstream's @typescript-eslint/no-unsafe-assignment disables unused (the repo lints unused disable directives), so drop them at the three unlock call sites. --- .../packages/ui/package.json | 2 +- .../date-comparison-dropdown.tsx | 2 +- .../date-range-popover/date-range-filter.tsx | 3 +-- .../date-range-presets/date-range-presets.tsx | 3 +-- .../stories/date-range-presets.stories.tsx | 3 +-- .../packages/ui/src/lock/unlock.ts | 22 +++++++++++++++++++ 6 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 projects/packages/premium-analytics/packages/ui/src/lock/unlock.ts diff --git a/projects/packages/premium-analytics/packages/ui/package.json b/projects/packages/premium-analytics/packages/ui/package.json index da07b3ddd9e1..1a0ef18366fe 100644 --- a/projects/packages/premium-analytics/packages/ui/package.json +++ b/projects/packages/premium-analytics/packages/ui/package.json @@ -9,7 +9,6 @@ "*.scss" ], "dependencies": { - "@automattic/admin-toolkit": "*", "@automattic/ui": "1.0.2", "@jetpack-premium-analytics/datetime": "workspace:*", "@jetpack-premium-analytics/formatters": "workspace:*", @@ -17,6 +16,7 @@ "@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.tsx b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/date-comparison-dropdown.tsx index 3088dd941c6c..19cfe7f37f4e 100644 --- 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 @@ -2,7 +2,7 @@ * External dependencies */ import { privateApis as componentsPrivateApis } from '@wordpress/components'; -import { unlock } from '@automattic/admin-toolkit'; +import { unlock } from '../lock/unlock'; import { Button } from '@wordpress/ui'; import { formatDateRange } from '@jetpack-premium-analytics/formatters'; import { sprintf, __ } from '@wordpress/i18n'; 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 index 33caecc2f0ec..e7038f7b2f53 100644 --- 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 @@ -7,7 +7,7 @@ import { SelectControl, privateApis as componentsPrivateApis, } from '@wordpress/components'; -import { unlock } from '@automattic/admin-toolkit'; +import { unlock } from '../lock/unlock'; import { calendar } from '@wordpress/icons'; import { Badge, Button, Stack } from '@wordpress/ui'; import { useState, useCallback, useMemo, useEffect } from 'react'; @@ -31,7 +31,6 @@ import { DateRangePresets } from '../date-range-presets'; import { DateRangeInput } from '../date-range-input'; import './date-range-filter.scss'; -// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { Menu } = unlock( componentsPrivateApis ); /** 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 index 7048c34be05f..2be0faa40188 100644 --- 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 @@ -3,7 +3,7 @@ */ import { __ } from '@wordpress/i18n'; import { privateApis as componentsPrivateApis } from '@wordpress/components'; -import { unlock } from '@automattic/admin-toolkit'; +import { unlock } from '../lock/unlock'; import { useMemo } from 'react'; import { PRESET_CUSTOM, @@ -18,7 +18,6 @@ import { import { DateRangePopover } from '../date-range-popover/date-range-filter'; import './date-range-presets.scss'; -// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { Menu } = unlock( componentsPrivateApis ); type DateRange = Parameters< typeof DateRangePopover >[ 0 ][ 'range' ]; diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/stories/date-range-presets.stories.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/stories/date-range-presets.stories.tsx index 4c57b4c893ac..992ce62e9647 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/stories/date-range-presets.stories.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/stories/date-range-presets.stories.tsx @@ -5,7 +5,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { useState } from 'react'; import { subDays, startOfDay, endOfDay } from 'date-fns'; import { privateApis as componentsPrivateApis } from '@wordpress/components'; -import { unlock } from '@automattic/admin-toolkit'; +import { unlock } from '../../lock/unlock'; import type { PrimaryPresetId } from '@jetpack-premium-analytics/datetime'; /** @@ -14,7 +14,6 @@ import type { PrimaryPresetId } from '@jetpack-premium-analytics/datetime'; import { DateRangePresets } from '../date-range-presets'; import type { DateRange } from '../../date-range-popover'; -// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const { Menu } = unlock( componentsPrivateApis ); /** 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' +); From 78eb626d7f533dc0e9e343b0216830d3d5bb4947 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 5 Jun 2026 17:00:00 +0800 Subject: [PATCH 04/10] refactor(premium-analytics): cast comparison presets passed to DateRangePresets DateComparisonDropdown reuses DateRangePresets (typed for primary presets) to render comparison presets. Upstream's looser tsc allowed the mismatch; the repo's tsgo does not. Cast the two props at the one call site - the component only reads id/label/range to render, so the runtime shape matches. --- .../date-comparison-dropdown.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) 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 index 19cfe7f37f4e..f31de5d2c522 100644 --- 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 @@ -7,7 +7,11 @@ import { Button } from '@wordpress/ui'; import { formatDateRange } from '@jetpack-premium-analytics/formatters'; import { sprintf, __ } from '@wordpress/i18n'; import { useMemo } from 'react'; -import type { ComparisonPresetId } from '@jetpack-premium-analytics/datetime'; +import type { + ComparisonPresetId, + DateRangePreset, + PrimaryPresetId, +} from '@jetpack-premium-analytics/datetime'; const { Menu } = unlock( componentsPrivateApis ); @@ -149,8 +153,14 @@ export function DateComparisonDropdown( { { hasPresets && ( { /* From f4114732d476bd13341b04da15b075e80066a64e Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 5 Jun 2026 17:00:12 +0800 Subject: [PATCH 05/10] refactor(premium-analytics): use current wpds typography token names The design-system version shipped with the repo's @wordpress/ui renamed the font tokens: --wpds-font-size-* -> --wpds-typography-font-size-* and --wpds-font-line-height-* -> --wpds-typography-line-height-*. Upstream's names resolve to nothing here, so the inputs and preset rows would lose their sizing. --- .../packages/ui/src/date-range-input/date-range-input.scss | 4 ++-- .../ui/src/date-range-presets/date-range-presets.scss | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 index 754e7105acaf..9e3daf43d42e 100644 --- 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 @@ -1,6 +1,6 @@ .input-date-control { flex: 1; - font-size: var( --wpds-font-size-sm ); + font-size: var( --wpds-typography-font-size-sm ); @supports selector(&::-webkit-calendar-picker-indicator) { input[type="date"] { @@ -12,7 +12,7 @@ -webkit-appearance: none; background: none; - font-size: var( --wpds-font-size-md ); + font-size: var( --wpds-typography-font-size-md ); // Removes the calendar icon. &::-webkit-calendar-picker-indicator { 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 index 1c1f1306fc48..735391620607 100644 --- 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 @@ -8,7 +8,7 @@ .date-range-presets, .date-range-presets__custom-group { .date-range-presets__item { - min-height: var( --wpds-font-line-height-2xl ); + min-height: var( --wpds-typography-line-height-2xl ); } } From 2c0fc0696842dc3c3c96548c15ecf80ff01488a2 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 5 Jun 2026 17:02:01 +0800 Subject: [PATCH 06/10] refactor(premium-analytics): extract story render hooks into named components Upstream stories call useState inside story render callbacks, which the repo's react-hooks/rules-of-hooks rejects (a render callback is not a component). Extract each stateful render body into a named *WithState component; WithoutPrefix reuses DateComparisonDropdownWithState via a new removeCompareToPrefix prop instead of duplicating its body. --- .../date-comparison-dropdown.stories.tsx | 30 +------ .../stories/date-range-popover.stories.tsx | 78 ++++++++++--------- .../stories/date-range-presets.stories.tsx | 40 +++++----- 3 files changed, 64 insertions(+), 84 deletions(-) diff --git a/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/stories/date-comparison-dropdown.stories.tsx b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/stories/date-comparison-dropdown.stories.tsx index 2cc688d1f311..55b63ef7ab94 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/stories/date-comparison-dropdown.stories.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/stories/date-comparison-dropdown.stories.tsx @@ -41,9 +41,11 @@ const defaultRange: DateRange = { function DateComparisonDropdownWithState( { initialEnabled = true, initialPresetId = 'previous-period', + removeCompareToPrefix = false, }: { initialEnabled?: boolean; initialPresetId?: ComparisonPresetId; + removeCompareToPrefix?: boolean; } ) { const [ enabled, setEnabled ] = useState( initialEnabled ); const [ presetId, setPresetId ] = useState< @@ -57,6 +59,7 @@ function DateComparisonDropdownWithState( { presets={ presets } enabled={ enabled } presetId={ presetId } + removeCompareToPrefix={ removeCompareToPrefix } onEnable={ () => { setEnabled( true ); setPresetId( 'previous-period' ); @@ -98,30 +101,5 @@ export const PreviousMonthSelected: Story = { * Without the "Compare:" prefix - just shows the date range. */ export const WithoutPrefix: Story = { - render: () => { - const [ enabled, setEnabled ] = useState( true ); - const [ presetId, setPresetId ] = useState< - ComparisonPresetId | undefined - >( 'previous-period' ); - - const presets = useComparisonDatePresets( defaultRange ); - - return ( - { - setEnabled( true ); - setPresetId( 'previous-period' ); - } } - onPresetChange={ setPresetId } - onClear={ () => { - setEnabled( false ); - setPresetId( undefined ); - } } - /> - ); - }, + render: () => , }; diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-popover/stories/date-range-popover.stories.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/stories/date-range-popover.stories.tsx index 3e529122e160..a33a73f6afe5 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-range-popover/stories/date-range-popover.stories.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/stories/date-range-popover.stories.tsx @@ -91,53 +91,57 @@ export const Default: Story = { render: () => , }; +function DateRangePopoverCustomPreset() { + const [ range, setRange ] = useState< DateRange >( { + from: startOfDay( subDays( today, 14 ) ), + to: endOfDay( subDays( today, 3 ) ), + } ); + + return ( + nextRange && setRange( nextRange ) } + onApply={ () => {} } + onCancel={ () => {} } + canApply={ true } + timeZone={ STORYBOOK_TIMEZONE } + /> + ); +} + /** * `Custom` preset selected. */ export const CustomPreset: Story = { - render: () => { - const [ range, setRange ] = useState< DateRange >( { - from: startOfDay( subDays( today, 14 ) ), - to: endOfDay( subDays( today, 3 ) ), - } ); - - return ( - nextRange && setRange( nextRange ) } - onApply={ () => {} } - onCancel={ () => {} } - canApply={ true } - timeZone={ STORYBOOK_TIMEZONE } - /> - ); - }, + render: () => , }; +function DateRangePopoverTodayPreset() { + const todayRange: DateRange = { + from: startOfDay( today ), + to: endOfDay( today ), + }; + const [ range, setRange ] = useState< DateRange >( todayRange ); + + return ( + nextRange && setRange( nextRange ) } + onApply={ () => {} } + onCancel={ () => {} } + canApply={ false } + timeZone={ STORYBOOK_TIMEZONE } + /> + ); +} + /** * `Today` preset selected. */ export const TodayPreset: Story = { - render: () => { - const todayRange: DateRange = { - from: startOfDay( today ), - to: endOfDay( today ), - }; - const [ range, setRange ] = useState< DateRange >( todayRange ); - - return ( - nextRange && setRange( nextRange ) } - onApply={ () => {} } - onCancel={ () => {} } - canApply={ false } - timeZone={ STORYBOOK_TIMEZONE } - /> - ); - }, + render: () => , }; /** diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/stories/date-range-presets.stories.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/stories/date-range-presets.stories.tsx index 992ce62e9647..38a8173e2007 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/stories/date-range-presets.stories.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/stories/date-range-presets.stories.tsx @@ -89,29 +89,27 @@ export const CustomSelected: Story = { render: () => , }; +function DateRangePresetsNoSelection() { + const [ presetId, setPrimaryPresetId ] = useState< PrimaryPresetId | null >( null ); + const [ , setRange ] = useState< DateRange | null >( null ); + + const handleChange = ( nextRange: DateRange, nextPrimaryPresetId: PrimaryPresetId ) => { + setRange( nextRange ); + setPrimaryPresetId( nextPrimaryPresetId ); + }; + + return ( + + ); +} + /** * No preset selected. */ export const NoSelection: Story = { - render: () => { - const [ presetId, setPrimaryPresetId ] = - useState< PrimaryPresetId | null >( null ); - const [ , setRange ] = useState< DateRange | null >( null ); - - const handleChange = ( - nextRange: DateRange, - nextPrimaryPresetId: PrimaryPresetId - ) => { - setRange( nextRange ); - setPrimaryPresetId( nextPrimaryPresetId ); - }; - - return ( - - ); - }, + render: () => , }; From cd23fb79ab1a9e90a0b8d6a753e01d012414f5db Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 5 Jun 2026 17:03:06 +0800 Subject: [PATCH 07/10] style(premium-analytics): align ported ui package with jetpack formatting Pure formatting, no content changes: prettier (line wrapping, arrow parens, paren spacing, scss quote style) plus eslint/stylelint autofix output (import order, import/first, scss blank-line and paren-spacing rules). The upstream external/internal dependency banner comments are dropped in the stories where the repo's import order interleaves the two groups. --- .../date-comparison-dropdown.scss | 8 +- .../date-comparison-dropdown.tsx | 38 +++----- .../date-comparison-dropdown.stories.tsx | 25 ++--- .../date-filters-panel/date-filters-panel.tsx | 43 +++------ .../date-range-input/date-range-input.scss | 10 +- .../src/date-range-input/date-range-input.tsx | 34 ++----- .../date-range-popover/date-range-filter.scss | 41 ++++---- .../date-range-popover/date-range-filter.tsx | 93 ++++++------------- .../stories/date-range-popover.stories.tsx | 42 +++------ .../date-range-presets.scss | 10 +- .../date-range-presets/date-range-presets.tsx | 22 ++--- .../stories/date-range-presets.stories.tsx | 26 ++---- .../use-comparison-date-presets.ts | 17 +--- 13 files changed, 138 insertions(+), 271 deletions(-) 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 index 93626fe01f0b..29fea4bf18a3 100644 --- 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 @@ -1,11 +1,12 @@ .date-comparison-dropdown { + &__button { - background-color: var( --wpds-color-bg-surface-neutral-strong ); + background-color: var(--wpds-color-bg-surface-neutral-strong); } } .date-filters-panel-button { - background-color: var( --wpds-color-bg-surface-neutral-strong ); + background-color: var(--wpds-color-bg-surface-neutral-strong); } .date-comparison-dropdown__popover { @@ -14,7 +15,8 @@ /* disable animation for the date range popover */ /* stylelint-disable property-no-unknown, selector-pseudo-element-no-unknown */ -@media not (prefers-reduced-motion: reduce) { +@media not ( prefers-reduced-motion: reduce ) { + .date-comparison-dropdown__popover { view-transition-name: next-admin--date-comparison-dropdown; transition: none !important; 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 index f31de5d2c522..34f39700b781 100644 --- 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 @@ -1,27 +1,26 @@ /** * External dependencies */ -import { privateApis as componentsPrivateApis } from '@wordpress/components'; -import { unlock } from '../lock/unlock'; -import { Button } from '@wordpress/ui'; 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 ); -/** - * Internal dependencies - */ -import { DateRangePresets } from '../date-range-presets'; -import type { ComparisonDateRangePreset } from '../use-comparison-date-presets'; -import './date-comparison-dropdown.scss'; - type DateComparisonDropdownProps = { /** * Available comparison presets (e.g., previous-period, previous-month) @@ -63,8 +62,7 @@ export function DateComparisonDropdown( { onClear, }: DateComparisonDropdownProps ) { const selectedPreset = useMemo( - () => - presetId ? presets.find( ( p ) => p.id === presetId ) : undefined, + () => ( presetId ? presets.find( p => p.id === presetId ) : undefined ), [ presets, presetId ] ); @@ -90,16 +88,9 @@ export function DateComparisonDropdown( { /> - + - { __( - 'No comparison', - 'jetpack-premium-analytics' - ) } + { __( 'No comparison', 'jetpack-premium-analytics' ) } @@ -111,10 +102,7 @@ export function DateComparisonDropdown( { hideOnClick > - { __( - 'Comparison to past', - 'jetpack-premium-analytics' - ) } + { __( 'Comparison to past', 'jetpack-premium-analytics' ) } diff --git a/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/stories/date-comparison-dropdown.stories.tsx b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/stories/date-comparison-dropdown.stories.tsx index 55b63ef7ab94..ba8bc33340e3 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/stories/date-comparison-dropdown.stories.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-comparison-dropdown/stories/date-comparison-dropdown.stories.tsx @@ -1,17 +1,10 @@ -/** - * External dependencies - */ -import type { Meta, StoryObj } from '@storybook/react'; -import { useState } from 'react'; import { subDays, startOfDay, endOfDay } from 'date-fns'; -import type { ComparisonPresetId } from '@jetpack-premium-analytics/datetime'; - -/** - * Internal dependencies - */ -import { DateComparisonDropdown } from '../date-comparison-dropdown'; +import { useState } from 'react'; import { useComparisonDatePresets } from '../../use-comparison-date-presets'; +import { DateComparisonDropdown } from '../date-comparison-dropdown'; import type { DateRange } from '../../date-range-popover'; +import type { ComparisonPresetId } from '@jetpack-premium-analytics/datetime'; +import type { Meta, StoryObj } from '@storybook/react'; const meta: Meta< typeof DateComparisonDropdown > = { title: 'Packages/Premium Analytics/UI/DateComparisonDropdown', @@ -48,9 +41,9 @@ function DateComparisonDropdownWithState( { removeCompareToPrefix?: boolean; } ) { const [ enabled, setEnabled ] = useState( initialEnabled ); - const [ presetId, setPresetId ] = useState< - ComparisonPresetId | undefined - >( initialEnabled ? initialPresetId : undefined ); + const [ presetId, setPresetId ] = useState< ComparisonPresetId | undefined >( + initialEnabled ? initialPresetId : undefined + ); const presets = useComparisonDatePresets( defaultRange ); @@ -92,9 +85,7 @@ export const Disabled: Story = { * With "Previous month" preset selected. */ export const PreviousMonthSelected: Story = { - render: () => ( - - ), + render: () => , }; /** 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 index d2a3202800ad..dd9f6a3fc189 100644 --- 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 @@ -1,21 +1,20 @@ /** * External dependencies */ -import { Stack } from '@wordpress/ui'; -import { BaseControl } from '@wordpress/components'; -import { useMemo, useCallback } from 'react'; 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 { DateRangePopover } from '../date-range-popover'; 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 ]; @@ -47,26 +46,17 @@ export type DateFiltersPanelProps = { * Callback when the comparison date range changes. * Receives the calculated comparison range and the preset ID used. */ - onComparisonChange: ( - range: DateRange | undefined, - presetId?: ComparisonPresetId - ) => void; + onComparisonChange: ( range: DateRange | undefined, presetId?: ComparisonPresetId ) => void; /** * Props for the date range popover. */ - rangeControlProps?: Omit< - Parameters< typeof BaseControl >[ 0 ], - 'children' - >; + rangeControlProps?: Omit< Parameters< typeof BaseControl >[ 0 ], 'children' >; /** * Props for the date comparison dropdown. */ - comparisonControlProps?: Omit< - Parameters< typeof BaseControl >[ 0 ], - 'children' - >; + comparisonControlProps?: Omit< Parameters< typeof BaseControl >[ 0 ], 'children' >; /** * Callback when the primary date range is applied. @@ -141,9 +131,7 @@ export function DateFiltersPanel( { // Validate and normalize the comparison preset ID const validatedComparisonPresetId = useMemo( () => { - return isComparisonPresetId( comparisonPresetId ) - ? comparisonPresetId - : undefined; + return isComparisonPresetId( comparisonPresetId ) ? comparisonPresetId : undefined; }, [ comparisonPresetId ] ); // Derive comparison enabled state directly from validated prop @@ -161,8 +149,8 @@ export function DateFiltersPanel( { */ const defaultPresetId = useMemo( () => { return ( - presets.find( ( p ) => p.id === 'previous-period' )?.id ?? - presets.find( ( p ) => p.id === 'previous-month' )?.id ?? + presets.find( p => p.id === 'previous-period' )?.id ?? + presets.find( p => p.id === 'previous-month' )?.id ?? presets[ 0 ]?.id ); }, [ presets ] ); @@ -175,12 +163,12 @@ export function DateFiltersPanel( { */ const preset = useMemo( () => { const id = validatedComparisonPresetId ?? defaultPresetId; - return id ? presets.find( ( p ) => p.id === id ) : undefined; + 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 ); + const nextPreset = presets.find( p => p.id === id ); onComparisonChange( nextPreset?.range, id ); }, [ onComparisonChange, presets ] @@ -200,12 +188,7 @@ export function DateFiltersPanel( { if ( preset?.range && presetIdToUse ) { onComparisonChange( preset.range, presetIdToUse ); } - }, [ - onComparisonChange, - preset, - validatedComparisonPresetId, - defaultPresetId, - ] ); + }, [ onComparisonChange, preset, validatedComparisonPresetId, defaultPresetId ] ); return ( 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 index 9e3daf43d42e..f0ebd4c84d75 100644 --- 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 @@ -1,8 +1,9 @@ .input-date-control { flex: 1; - font-size: var( --wpds-typography-font-size-sm ); + font-size: var(--wpds-typography-font-size-sm); + + @supports selector( &::-webkit-calendar-picker-indicator ) { - @supports selector(&::-webkit-calendar-picker-indicator) { input[type="date"] { // Removes extra spaces for the calendar icon. width: fit-content; @@ -12,7 +13,7 @@ -webkit-appearance: none; background: none; - font-size: var( --wpds-typography-font-size-md ); + font-size: var(--wpds-typography-font-size-md); // Removes the calendar icon. &::-webkit-calendar-picker-indicator { @@ -21,7 +22,8 @@ } } - @supports not selector(&::-webkit-calendar-picker-indicator) { + @supports not selector( &::-webkit-calendar-picker-indicator ) { + input[type="date"] { padding: 0; // We'll control input's inner spacing manually 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 index 18e5d3d3e955..562fe3f07f98 100644 --- 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 @@ -1,12 +1,11 @@ /** * 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'; -import { createTZDateFromParts } from '@jetpack-premium-analytics/datetime'; -import { formatDate } from '@jetpack-premium-analytics/formatters'; - /** * Internal dependencies */ @@ -26,18 +25,12 @@ type DateInputProps = Pick< DateRangeInputProps, 'timeZone' > & { onChange: ( date?: Date ) => void; }; -const formatToString = ( date?: Date ) => - date ? formatDate( date, 'iso' ) : ''; +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 [ year, month, day ] = dateString.split( '-' ).map( x => Number( x ) ); - const parsedDate = createTZDateFromParts( - [ year, month - 1, day ], - timeZone - ); + const parsedDate = createTZDateFromParts( [ year, month - 1, day ], timeZone ); return ! isNaN( parsedDate.getTime() ) ? parsedDate : undefined; } @@ -74,21 +67,12 @@ function DateInput( { label, date, onChange, timeZone }: DateInputProps ) { return ( { label } - + ); } -export function DateRangeInput( { - range, - onChange, - timeZone, -}: DateRangeInputProps ) { +export function DateRangeInput( { range, onChange, timeZone }: DateRangeInputProps ) { const { from, to } = range; return ( @@ -97,7 +81,7 @@ export function DateRangeInput( { label={ __( 'From', 'jetpack-premium-analytics' ) } date={ from } timeZone={ timeZone } - onChange={ ( nextFrom ) => { + onChange={ nextFrom => { if ( nextFrom && to && nextFrom <= to ) { onChange( { from: nextFrom, to } ); } @@ -108,7 +92,7 @@ export function DateRangeInput( { label={ __( 'To', 'jetpack-premium-analytics' ) } date={ to } timeZone={ timeZone } - onChange={ ( nextTo ) => { + onChange={ nextTo => { if ( nextTo && from && from <= nextTo ) { onChange( { from, to: nextTo } ); } 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 index b0bc48158ac7..aa17958cb048 100644 --- 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 @@ -2,7 +2,7 @@ @use "@wordpress/base-styles/colors" as colors; .date-range-popover-content { - --wca-popover-padding: var( --wpds-dimension-padding-lg ); + --wca-popover-padding: var(--wpds-dimension-padding-lg); --wca-popover-border-color: #{colors.$gray-300}; --wca-popover-border-width: #{vars.$border-width}; @@ -11,9 +11,9 @@ 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 ); + 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; @@ -25,8 +25,8 @@ &--mobile { display: flex; flex-direction: column; - gap: var( --wpds-dimension-gap-lg ); - padding: var( --wca-popover-padding ); + gap: var(--wpds-dimension-gap-lg); + padding: var(--wca-popover-padding); background-color: #fff; .date-range-calendar { @@ -36,7 +36,7 @@ .date-range-popover-actions { padding: 0; - padding-block-start: var( --wpds-dimension-gap-sm ); + padding-block-start: var(--wpds-dimension-gap-sm); } } } @@ -46,9 +46,9 @@ 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 ); + 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 { @@ -59,34 +59,35 @@ --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 ) ); + --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 ); + 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 ); + --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 ); + 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 ); + 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) { +@media not ( prefers-reduced-motion: reduce ) { + .date-filters-panel__popover { view-transition-name: next-admin--date-range-popover; } 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 index e7038f7b2f53..926074092897 100644 --- 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 @@ -2,33 +2,32 @@ * 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 { unlock } from '../lock/unlock'; -import { calendar } from '@wordpress/icons'; -import { Badge, Button, Stack } from '@wordpress/ui'; -import { useState, useCallback, useMemo, useEffect } from 'react'; import { useResizeObserver } from '@wordpress/compose'; -import { formatDateRange } from '@jetpack-premium-analytics/formatters'; 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'; -import { - getPresetLabel, - getDefaultDateRangePresets, - PRESET_CUSTOM, - type PrimaryPresetId, - type DateRangePreset, -} from '@jetpack-premium-analytics/datetime'; - /** * Internal dependencies */ -import { DateRangePresets } from '../date-range-presets'; 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 ); @@ -43,9 +42,7 @@ 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' ] ->; +export type DateRange = NonNullable< Parameters< typeof DateRangeCalendar >[ 0 ][ 'selected' ] >; /** * Props for DateRangePopoverContent component. @@ -131,12 +128,7 @@ function DateRangePopoverActions( { - @@ -175,7 +167,7 @@ function DateRangePresetsDropdown( { const handleChange = useCallback( ( selectedValue: string ) => { - const preset = presets.find( ( p ) => p.id === selectedValue ); + const preset = presets.find( p => p.id === selectedValue ); if ( preset ) { onRangeChange( preset.range, preset.id ); } @@ -208,14 +200,9 @@ export function DateRangePopoverContent( { isMobile = false, timeZone, }: DateRangePopoverContentProps ) { - const [ displayedMonth, setDisplayedMonth ] = useState( - getDisplayedMonth( range ) - ); + const [ displayedMonth, setDisplayedMonth ] = useState( getDisplayedMonth( range ) ); - const handleChange = ( - nextRange?: DateRange, - nextPrimaryPresetId?: PrimaryPresetId - ) => { + const handleChange = ( nextRange?: DateRange, nextPrimaryPresetId?: PrimaryPresetId ) => { if ( nextRange ) { setDisplayedMonth( getDisplayedMonth( nextRange ) ); } @@ -237,27 +224,19 @@ export function DateRangePopoverContent( { timeZone={ timeZone } /> - + handleChange( nextRange ) } + onSelect={ nextRange => handleChange( nextRange ) } numberOfMonths={ 1 } month={ displayedMonth } onMonthChange={ setDisplayedMonth } timeZone={ timeZone } /> - + ); } @@ -282,16 +261,12 @@ export function DateRangePopoverContent( { gap="lg" direction="column" > - + handleChange( nextRange ) } + onSelect={ nextRange => handleChange( nextRange ) } numberOfMonths={ isWideScreen ? 2 : 1 } month={ displayedMonth } onMonthChange={ setDisplayedMonth } @@ -299,19 +274,12 @@ export function DateRangePopoverContent( { /> - + ); } -type DateRangePopoverProps = Omit< - DateRangePopoverContentProps, - 'isWideScreen' | 'isMobile' -> & { +type DateRangePopoverProps = Omit< DateRangePopoverContentProps, 'isWideScreen' | 'isMobile' > & { /** * Optional external container element for responsive calculations. * When provided, the component will measure this container's width @@ -336,9 +304,7 @@ export function DateRangePopover( { timeZone, containerElement, }: DateRangePopoverProps ) { - const [ containerWidth, setContainerWidth ] = useState< number | null >( - null - ); + const [ containerWidth, setContainerWidth ] = useState< number | null >( null ); // Callback to update container width const handleResize = useCallback( ( entries: ResizeObserverEntry[] ) => { @@ -358,12 +324,9 @@ export function DateRangePopover( { }, [ containerElement, setObserverRef ] ); // Determine layout based on container width - const isMobile = - containerWidth !== null && - containerWidth < MOBILE_CONTAINER_WIDTH_THRESHOLD; + const isMobile = containerWidth !== null && containerWidth < MOBILE_CONTAINER_WIDTH_THRESHOLD; - const isWideScreen = - containerWidth !== null && containerWidth >= WIDE_CONTAINER_THRESHOLD; + const isWideScreen = containerWidth !== null && containerWidth >= WIDE_CONTAINER_THRESHOLD; const presetLabel = getPresetLabel( presetId ); diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-popover/stories/date-range-popover.stories.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/stories/date-range-popover.stories.tsx index a33a73f6afe5..8c6ef8640763 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-range-popover/stories/date-range-popover.stories.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/stories/date-range-popover.stories.tsx @@ -1,26 +1,16 @@ -/** - * External dependencies - */ -import type { Meta, StoryObj } from '@storybook/react'; -import { useState } from 'react'; import { subDays, startOfDay, endOfDay } from 'date-fns'; -import type { PrimaryPresetId } from '@jetpack-premium-analytics/datetime'; - -/** - * Internal dependencies - */ -import { - DateRangePopover, - DateRangePopoverContent, -} from '../date-range-filter'; +import { useState } from 'react'; +import { DateRangePopover, DateRangePopoverContent } from '../date-range-filter'; import type { DateRange } from '../date-range-filter'; +import type { PrimaryPresetId } from '@jetpack-premium-analytics/datetime'; +import type { Meta, StoryObj } from '@storybook/react'; const meta: Meta< typeof DateRangePopover > = { title: 'Packages/Premium Analytics/UI/DateRangePopover', component: DateRangePopover, tags: [ 'autodocs' ], decorators: [ - ( Story ) => ( + Story => (
@@ -42,17 +32,12 @@ const STORYBOOK_TIMEZONE = 'America/New_York'; function DateRangePopoverWithState() { const [ range, setRange ] = useState< DateRange >( defaultRange ); - const [ presetId, setPrimaryPresetId ] = - useState< PrimaryPresetId >( 'last-7-days' ); - const [ pendingRange, setPendingRange ] = - useState< DateRange >( defaultRange ); + const [ presetId, setPrimaryPresetId ] = useState< PrimaryPresetId >( 'last-7-days' ); + const [ pendingRange, setPendingRange ] = useState< DateRange >( defaultRange ); const [ pendingPrimaryPresetId, setPendingPrimaryPresetId ] = useState< PrimaryPresetId >( 'last-7-days' ); - const handleChange = ( - nextRange?: DateRange, - nextPrimaryPresetId?: PrimaryPresetId - ) => { + const handleChange = ( nextRange?: DateRange, nextPrimaryPresetId?: PrimaryPresetId ) => { if ( nextRange ) { setPendingRange( nextRange ); } @@ -71,8 +56,7 @@ function DateRangePopoverWithState() { setPendingPrimaryPresetId( presetId ); }; - const canApply = - pendingRange.from !== range.from || pendingRange.to !== range.to; + const canApply = pendingRange.from !== range.from || pendingRange.to !== range.to; return ( ( defaultRange ); - const [ presetId, setPrimaryPresetId ] = - useState< PrimaryPresetId >( 'last-7-days' ); + const [ presetId, setPrimaryPresetId ] = useState< PrimaryPresetId >( 'last-7-days' ); - const handleChange = ( - nextRange?: DateRange, - nextPrimaryPresetId?: PrimaryPresetId - ) => { + const handleChange = ( nextRange?: DateRange, nextPrimaryPresetId?: PrimaryPresetId ) => { if ( nextRange ) { setRange( nextRange ); } 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 index 735391620607..446ea4a41439 100644 --- 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 @@ -1,14 +1,15 @@ @use "@wordpress/base-styles/variables" as vars; @use "@wordpress/base-styles/colors" as colors; -.date-range-presets{ +.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 ); + min-height: var(--wpds-typography-line-height-2xl); } } @@ -16,12 +17,13 @@ // 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 ); + color: var(--wpds-color-fg-content-neutral-weak); } &[aria-checked="true"] { - color: var( --wpds-color-fg-content-neutral ); + 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 index 2be0faa40188..f27ac51d264c 100644 --- 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 @@ -1,21 +1,20 @@ /** * External dependencies */ -import { __ } from '@wordpress/i18n'; -import { privateApis as componentsPrivateApis } from '@wordpress/components'; -import { unlock } from '../lock/unlock'; -import { useMemo } from 'react'; 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 ); @@ -87,10 +86,7 @@ export function DateRangePresets( { return getDefaultDateRangePresets( timeZone ); }, [ presetsProp, timeZone ] ); - const presets = useMemo( - () => presetsProp || defaultPresets, - [ presetsProp, defaultPresets ] - ); + const presets = useMemo( () => presetsProp || defaultPresets, [ presetsProp, defaultPresets ] ); return ( <> @@ -121,9 +117,7 @@ export function DateRangePresets( { checked={ value === PRESET_CUSTOM } disabled > - - { __( 'Custom', 'jetpack-premium-analytics' ) } - + { __( 'Custom', 'jetpack-premium-analytics' ) } { onClear && ( @@ -136,9 +130,7 @@ export function DateRangePresets( { onChange={ onClear } hideOnClick > - - { __( 'No comparison', 'jetpack-premium-analytics' ) } - + { __( 'No comparison', 'jetpack-premium-analytics' ) } ) } diff --git a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/stories/date-range-presets.stories.tsx b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/stories/date-range-presets.stories.tsx index 38a8173e2007..70d1725174b1 100644 --- a/projects/packages/premium-analytics/packages/ui/src/date-range-presets/stories/date-range-presets.stories.tsx +++ b/projects/packages/premium-analytics/packages/ui/src/date-range-presets/stories/date-range-presets.stories.tsx @@ -1,18 +1,11 @@ -/** - * External dependencies - */ -import type { Meta, StoryObj } from '@storybook/react'; -import { useState } from 'react'; -import { subDays, startOfDay, endOfDay } from 'date-fns'; import { privateApis as componentsPrivateApis } from '@wordpress/components'; +import { subDays, startOfDay, endOfDay } from 'date-fns'; +import { useState } from 'react'; import { unlock } from '../../lock/unlock'; -import type { PrimaryPresetId } from '@jetpack-premium-analytics/datetime'; - -/** - * Internal dependencies - */ import { DateRangePresets } from '../date-range-presets'; import type { DateRange } from '../../date-range-popover'; +import type { PrimaryPresetId } from '@jetpack-premium-analytics/datetime'; +import type { Meta, StoryObj } from '@storybook/react'; const { Menu } = unlock( componentsPrivateApis ); @@ -26,7 +19,7 @@ const meta: Meta< typeof DateRangePresets > = { component: DateRangePresets, tags: [ 'autodocs' ], decorators: [ - ( Story ) => ( + Story => ( // Menu.Group must be wrapped in a Menu to work correctly in Storybook. @@ -46,18 +39,13 @@ function DateRangePresetsWithState( { }: { initialPrimaryPresetId?: PrimaryPresetId; } ) { - const [ presetId, setPrimaryPresetId ] = useState< PrimaryPresetId >( - initialPrimaryPresetId - ); + const [ presetId, setPrimaryPresetId ] = useState< PrimaryPresetId >( initialPrimaryPresetId ); const [ , setRange ] = useState< DateRange >( { from: startOfDay( subDays( today, 7 ) ), to: endOfDay( subDays( today, 1 ) ), } ); - const handleChange = ( - nextRange: DateRange, - nextPrimaryPresetId: PrimaryPresetId - ) => { + const handleChange = ( nextRange: DateRange, nextPrimaryPresetId: PrimaryPresetId ) => { setRange( nextRange ); setPrimaryPresetId( nextPrimaryPresetId ); }; 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 index 021b5702696e..1a018083db1c 100644 --- 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 @@ -1,13 +1,12 @@ /** * External dependencies */ -import { useMemo } from 'react'; import { getComparisonRangeFromPreset, getComparisonPresetConfigs, type ComparisonPresetId, } from '@jetpack-premium-analytics/datetime'; - +import { useMemo } from 'react'; /** * Internal dependencies */ @@ -30,9 +29,7 @@ export type ComparisonDateRangePreset = { * @param referenceRange - The primary date range to compare against * @return Array of comparison presets with strongly-typed IDs */ -export function useComparisonDatePresets( - referenceRange: DateRange -): ComparisonDateRangePreset[] { +export function useComparisonDatePresets( referenceRange: DateRange ): ComparisonDateRangePreset[] { return useMemo( () => { if ( ! referenceRange.from || ! referenceRange.to ) { return []; @@ -40,15 +37,9 @@ export function useComparisonDatePresets( return getComparisonPresetConfigs() .map( ( { id, label } ) => { - const range = getComparisonRangeFromPreset( - referenceRange, - id - ); + const range = getComparisonRangeFromPreset( referenceRange, id ); return range ? { id, label, range } : null; } ) - .filter( - ( preset ): preset is ComparisonDateRangePreset => - preset !== null - ); + .filter( ( preset ): preset is ComparisonDateRangePreset => preset !== null ); }, [ referenceRange ] ); } From a414d088bc91d2c7402fbffc5f129c0dab5f020d Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 5 Jun 2026 17:03:21 +0800 Subject: [PATCH 08/10] chore(premium-analytics): wire ui package deps and relax lint for the port Parent-level wiring for the ui leaf (leaves are not pnpm workspace members, so the parent's dependencies are the load-bearing ones): - package.json: add the runtime deps the components import (@automattic/ui, @wordpress/components, @wordpress/compose, @wordpress/private-apis, clsx; move @wordpress/ui to dependencies) plus @wordpress/base-styles (dev) so the SCSS @use resolves at build - eslint.config.mjs: temporary packages/ui/** block softening JSDoc rules and react/jsx-no-bind so the upstream style lands as-is; tracked for follow-up alongside datetime/formatters - changelog entry --- pnpm-lock.yaml | 29 +++++++++++++++---- ...ntegrate-components-package-into-analytics | 4 +++ .../premium-analytics/eslint.config.mjs | 16 ++++++++++ .../packages/premium-analytics/package.json | 8 ++++- 4 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 projects/packages/premium-analytics/changelog/wooa7s-1318-integrate-components-package-into-analytics diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 54d70fd092e0..8839b4df2df7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3869,12 +3869,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) @@ -3887,9 +3896,18 @@ importers: '@wordpress/primitives': specifier: 4.46.0 version: 4.46.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 @@ -3915,12 +3933,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) - '@wordpress/ui': - specifier: 0.13.0 - version: 0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + 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 @@ -24109,7 +24127,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 @@ -24129,6 +24147,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 38a1a71417d7..5dd7217b8f47 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 fd3c20aab0a9..8d0b97a8552f 100644 --- a/projects/packages/premium-analytics/package.json +++ b/projects/packages/premium-analytics/package.json @@ -31,13 +31,19 @@ }, "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/primitives": "4.46.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" @@ -48,8 +54,8 @@ "@testing-library/dom": "10.4.1", "@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", - "@wordpress/ui": "0.13.0", "browserslist": "4.28.2", "storybook": "10.3.6", "typescript": "5.9.3" From ce8c5165918d88e4402efd6ef78b151a0e2e6cd1 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 5 Jun 2026 16:43:31 +0800 Subject: [PATCH 09/10] feat(storybook): resolve @jetpack-premium-analytics imports in vite --- .../add-premium-analytics-internal-package-alias | 4 ++++ projects/js-packages/storybook/storybook/main.js | 8 ++++++++ 2 files changed, 12 insertions(+) create mode 100644 projects/js-packages/storybook/changelog/add-premium-analytics-internal-package-alias diff --git a/projects/js-packages/storybook/changelog/add-premium-analytics-internal-package-alias b/projects/js-packages/storybook/changelog/add-premium-analytics-internal-package-alias new file mode 100644 index 000000000000..3dcc264e505d --- /dev/null +++ b/projects/js-packages/storybook/changelog/add-premium-analytics-internal-package-alias @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Add a Vite alias resolving `@jetpack-premium-analytics/*` internal package imports so Premium Analytics ui package stories build. diff --git a/projects/js-packages/storybook/storybook/main.js b/projects/js-packages/storybook/storybook/main.js index 874e107e53a4..07a5219a9b75 100644 --- a/projects/js-packages/storybook/storybook/main.js +++ b/projects/js-packages/storybook/storybook/main.js @@ -143,6 +143,14 @@ const sbconfig = { alias: { ...config.resolve?.alias, + // Premium Analytics internal packages. At build time wp-build maps + // `@jetpack-premium-analytics/` to `packages/`; mirror that here + // (each package's `main` points at its TS source). + '@jetpack-premium-analytics': path.join( + __dirname, + '../../../packages/premium-analytics/packages' + ), + // Boost-specific aliases $lib: path.join( __dirname, '../../../plugins/boost/app/assets/src/js/lib' ), $features: path.join( __dirname, '../../../plugins/boost/app/assets/src/js/features' ), From bf3248b7dcd34d203eceeb43f7c8ac797fd8e491 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 10 Jun 2026 16:35:33 +0900 Subject: [PATCH 10/10] refactor(premium-analytics): rename view-transition ids from next-admin prefix --- .../date-comparison-dropdown.scss | 8 ++++---- .../ui/src/date-range-popover/date-range-filter.scss | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) 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 index 29fea4bf18a3..5ad8a7aa8ec5 100644 --- 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 @@ -18,19 +18,19 @@ @media not ( prefers-reduced-motion: reduce ) { .date-comparison-dropdown__popover { - view-transition-name: next-admin--date-comparison-dropdown; + view-transition-name: jp-premium-analytics--date-comparison-dropdown; transition: none !important; } } /* ensure it's above the canvas/stage during the transition */ -::view-transition-group(next-admin--date-comparison-dropdown) { +::view-transition-group(jp-premium-analytics--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) { +::view-transition-new(jp-premium-analytics--date-comparison-dropdown), +::view-transition-old(jp-premium-analytics--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-range-popover/date-range-filter.scss b/projects/packages/premium-analytics/packages/ui/src/date-range-popover/date-range-filter.scss index aa17958cb048..7dfaaac4eed1 100644 --- 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 @@ -89,18 +89,18 @@ @media not ( prefers-reduced-motion: reduce ) { .date-filters-panel__popover { - view-transition-name: next-admin--date-range-popover; + view-transition-name: jp-premium-analytics--date-range-popover; } } /* ensure it's above the canvas/stage during the transition */ -::view-transition-group(next-admin--date-range-popover) { +::view-transition-group(jp-premium-analytics--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) { +::view-transition-new(jp-premium-analytics--date-range-popover), +::view-transition-old(jp-premium-analytics--date-range-popover) { animation: none; } /* stylelint-enable property-no-unknown, selector-pseudo-element-no-unknown */