diff --git a/.codex/hooks.json b/.codex/hooks.json new file mode 100644 index 000000000..2c1e4983f --- /dev/null +++ b/.codex/hooks.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "cat ../.agents/skills/seldon/SKILL.md ../.agents/skills/vercel-composition-patterns/SKILL.md" + } + ] + } + ] + } +} diff --git a/output/playwright/designer-screenshots-2026-06-23/01-desktop-filter-row.png b/output/playwright/designer-screenshots-2026-06-23/01-desktop-filter-row.png new file mode 100644 index 000000000..7e6dd7b8f Binary files /dev/null and b/output/playwright/designer-screenshots-2026-06-23/01-desktop-filter-row.png differ diff --git a/output/playwright/designer-screenshots-2026-06-23/02-desktop-departments-dropdown-selected.png b/output/playwright/designer-screenshots-2026-06-23/02-desktop-departments-dropdown-selected.png new file mode 100644 index 000000000..4be90dd28 Binary files /dev/null and b/output/playwright/designer-screenshots-2026-06-23/02-desktop-departments-dropdown-selected.png differ diff --git a/output/playwright/designer-screenshots-2026-06-23/03-mobile-filter-row-horizontal.png b/output/playwright/designer-screenshots-2026-06-23/03-mobile-filter-row-horizontal.png new file mode 100644 index 000000000..c2beebde0 Binary files /dev/null and b/output/playwright/designer-screenshots-2026-06-23/03-mobile-filter-row-horizontal.png differ diff --git a/output/playwright/designer-screenshots-2026-06-23/04-mobile-departments-drawer-selected.png b/output/playwright/designer-screenshots-2026-06-23/04-mobile-departments-drawer-selected.png new file mode 100644 index 000000000..7bf6fd64a Binary files /dev/null and b/output/playwright/designer-screenshots-2026-06-23/04-mobile-departments-drawer-selected.png differ diff --git a/output/playwright/designer-screenshots-2026-06-23/05-mobile-main-filters-drawer-selected.png b/output/playwright/designer-screenshots-2026-06-23/05-mobile-main-filters-drawer-selected.png new file mode 100644 index 000000000..e72751623 Binary files /dev/null and b/output/playwright/designer-screenshots-2026-06-23/05-mobile-main-filters-drawer-selected.png differ diff --git a/output/playwright/designer-screenshots-2026-06-23/06-desktop-main-filters-slide-menu-selected.png b/output/playwright/designer-screenshots-2026-06-23/06-desktop-main-filters-slide-menu-selected.png new file mode 100644 index 000000000..50148c751 Binary files /dev/null and b/output/playwright/designer-screenshots-2026-06-23/06-desktop-main-filters-slide-menu-selected.png differ diff --git a/output/playwright/designer-screenshots-2026-06-23/07-mobile-main-filters-slide-menu-selected.png b/output/playwright/designer-screenshots-2026-06-23/07-mobile-main-filters-slide-menu-selected.png new file mode 100644 index 000000000..e72751623 Binary files /dev/null and b/output/playwright/designer-screenshots-2026-06-23/07-mobile-main-filters-slide-menu-selected.png differ diff --git a/phillips-seldon-1.272.0.tgz b/phillips-seldon-1.272.0.tgz new file mode 100644 index 000000000..8c62e083a Binary files /dev/null and b/phillips-seldon-1.272.0.tgz differ diff --git a/src/patterns/FiltersInline/FilterDropdownMenuDesktop.tsx b/src/patterns/FiltersInline/FilterDropdownMenuDesktop.tsx index 7e87b7c08..1cb9ef0cd 100644 --- a/src/patterns/FiltersInline/FilterDropdownMenuDesktop.tsx +++ b/src/patterns/FiltersInline/FilterDropdownMenuDesktop.tsx @@ -1,7 +1,7 @@ import classnames from 'classnames'; import React from 'react'; import Button from '../../components/Button/Button'; -import { ButtonSizes, ButtonVariants } from '../../components/Button/types'; +import { ButtonVariants } from '../../components/Button/types'; import FilterInput from '../../components/Filter/FilterInput'; import { px } from '../../utils'; import { FilterDimension, FilterDropdownMenuProps } from './types'; @@ -45,38 +45,27 @@ export const FilterDropdownMenuDesktop = React.forwardRef handleInputChangeUtil(e, buttonType ?? '', onSelectFilter)} + onChange={(e) => handleInputChangeUtil(e, buttonType ?? '', onSelectFilter, 'dropdown')} /> ))} -
- {isSortButton ? ( + {/* Sort applies on selection, so the sort dropdown renders no action buttons. */} + {!isSortButton && ( +
- ) : ( - <> - - - - )} -
+ +
+ )} ); }, diff --git a/src/patterns/FiltersInline/FilterDropdownMenuMobile.tsx b/src/patterns/FiltersInline/FilterDropdownMenuMobile.tsx index 895ed7bd4..a9189efcb 100644 --- a/src/patterns/FiltersInline/FilterDropdownMenuMobile.tsx +++ b/src/patterns/FiltersInline/FilterDropdownMenuMobile.tsx @@ -1,10 +1,8 @@ import classnames from 'classnames'; import React from 'react'; import Button from '../../components/Button/Button'; -import { ButtonSizes, ButtonVariants } from '../../components/Button/types'; +import { ButtonVariants } from '../../components/Button/types'; import FilterInput from '../../components/Filter/FilterInput'; -import Text from '../../components/Text/Text'; -import { TextVariants } from '../../components/Text/types'; import { px } from '../../utils'; import { FilterDimension, FilterDropdownMenuProps } from './types'; import { getFilterDimensions, handleInputChange as handleInputChangeUtil, hasActiveDimensions } from './utils'; @@ -47,45 +45,31 @@ export const FilterDropdownMenuMobile = React.forwardRef handleInputChangeUtil(e, buttonType ?? '', onSelectFilter)} + onChange={(e) => handleInputChangeUtil(e, buttonType ?? '', onSelectFilter, 'dropdown')} /> ))} -
- {isSortButton ? ( + {/* Sort applies on selection, so the sort dropdown renders no action + buttons — just a spacer so the safe-area inset still keeps the last + option clear of the iOS home indicator. */} + {isSortButton ? ( + + )}
); }, diff --git a/src/patterns/FiltersInline/FiltersInline.test.tsx b/src/patterns/FiltersInline/FiltersInline.test.tsx index e756e0e9a..7ce0352a7 100644 --- a/src/patterns/FiltersInline/FiltersInline.test.tsx +++ b/src/patterns/FiltersInline/FiltersInline.test.tsx @@ -135,7 +135,7 @@ describe('FilterDropdown', () => { clearFilterUpdate.mockClear(); }); - it('renders mobile variant', () => { + it('renders mobile variant without action buttons for sort', () => { render( { />, ); expect(screen.getByTestId('filter-dropdown-mobile')).toBeInTheDocument(); - expect(screen.getByText('Confirm')).toBeInTheDocument(); + expect(screen.getByLabelText('Ascending')).toBeInTheDocument(); + expect(screen.queryByText('Confirm')).not.toBeInTheDocument(); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + }); + + it('renders desktop sort variant without action buttons and applies selection via onSelectFilter', () => { + const onSelectFilter = vi.fn(); + render(); + expect(screen.queryByText('Confirm')).not.toBeInTheDocument(); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + + fireEvent.click(screen.getByLabelText('Ascending')); + expect(onSelectFilter).toHaveBeenCalledWith(expect.anything(), 'Sort', 'dropdown'); }); }); @@ -260,6 +272,27 @@ describe('MainFilterDropdown', () => { fireEvent.click(screen.getByTestId('main-filter-filter-button')); expect(handleClick).toHaveBeenCalled(); }); + + it('marks drawer input changes with the drawer source', () => { + render( + , + ); + fireEvent.click(screen.getByLabelText('Foo')); + expect(handleFilterSelection).toHaveBeenCalledWith(expect.anything(), FilterButtonType.Filter, 'drawer'); + }); }); describe('SubFilterDropdown', () => { @@ -523,7 +556,16 @@ describe('handleInputChange', () => { handleInputChange(mockEvent, 'Filter', mockHandler); - expect(mockHandler).toHaveBeenCalledWith(mockEvent, 'Filter'); + expect(mockHandler).toHaveBeenCalledWith(mockEvent, 'Filter', undefined); + }); + + it('passes the change source through to handleFilterSelection', () => { + const mockEvent = { target: { value: 'foo' } } as React.ChangeEvent; + const mockHandler = vi.fn(); + + handleInputChange(mockEvent, 'Sort', mockHandler, 'dropdown'); + + expect(mockHandler).toHaveBeenCalledWith(mockEvent, 'Sort', 'dropdown'); }); it('does nothing if handleFilterSelection is not provided', () => { diff --git a/src/patterns/FiltersInline/FiltersInline.tsx b/src/patterns/FiltersInline/FiltersInline.tsx index 2abdd1208..93aa89e85 100644 --- a/src/patterns/FiltersInline/FiltersInline.tsx +++ b/src/patterns/FiltersInline/FiltersInline.tsx @@ -69,25 +69,36 @@ const FiltersInline = React.forwardRef( resultsCount={resultsCount} dropdownMenuTranslation={dropdownMenuTranslation} /> - {filters?.map((filter, index) => ( - - ))} + {filters + ?.map((filter, index) => ({ filter, filterId: index + 1 })) + // Render the Sort pill last in the DOM so keyboard/reading order + // matches the visual order. Sort is listed first for the drawer, but + // sits last (right-aligned) in the row; a stable sort keeps the other + // pills in their original order. + .sort( + (a, b) => + (a.filter.buttonType === FilterButtonType.Sort ? 1 : 0) - + (b.filter.buttonType === FilterButtonType.Sort ? 1 : 0), + ) + .map(({ filter, filterId }) => ( + + ))} ); }, diff --git a/src/patterns/FiltersInline/MainFilterDropdown.tsx b/src/patterns/FiltersInline/MainFilterDropdown.tsx index 2048e9cf6..645e8ba00 100644 --- a/src/patterns/FiltersInline/MainFilterDropdown.tsx +++ b/src/patterns/FiltersInline/MainFilterDropdown.tsx @@ -1,7 +1,7 @@ import classnames from 'classnames'; import React from 'react'; import Button from '../../components/Button/Button'; -import { ButtonSizes, ButtonVariants } from '../../components/Button/types'; +import { ButtonVariants } from '../../components/Button/types'; import Drawer from '../../components/Drawer/Drawer'; import Filter from '../../components/Filter/Filter'; import FilterHeader from '../../components/Filter/FilterHeader'; @@ -64,7 +64,7 @@ export const MainFilterDropdown = React.forwardRef onSelectFilter?.(e, filter.buttonType)} + onChange={(e) => onSelectFilter?.(e, filter.buttonType, 'drawer')} type={filter.type as 'checkbox' | 'radio'} disabled={value?.disabled} name={value.label} @@ -82,13 +82,12 @@ export const MainFilterDropdown = React.forwardRef - diff --git a/src/patterns/FiltersInline/_filterDropdownMenu.scss b/src/patterns/FiltersInline/_filterDropdownMenu.scss index 48d2b5663..45758b1de 100644 --- a/src/patterns/FiltersInline/_filterDropdownMenu.scss +++ b/src/patterns/FiltersInline/_filterDropdownMenu.scss @@ -26,11 +26,6 @@ $dropdown-width: 320px; } } - &__button-wrap { - display: flex; - justify-content: flex-end; - } - &__buttons-wrap { border-top: 1px solid $bg-border; display: flex; @@ -65,6 +60,12 @@ $dropdown-width: 320px; padding-bottom: calc(#{$spacing-sm} + env(safe-area-inset-bottom)); } + // Sort dropdown has no footer buttons; this spacer preserves the safe-area + // inset so the last option stays clear of the iOS home indicator. + &__mobile-safe-area { + padding-bottom: calc(#{$spacing-sm} + env(safe-area-inset-bottom)); + } + &__header { margin-bottom: $spacing-sm; margin-top: 0; @@ -74,21 +75,6 @@ $dropdown-width: 320px; } } - &__button { - align-self: flex-end; - padding: $spacing-sm $spacing-md; - - &--text { - text-wrap: nowrap; - } - - &--mobile { - display: block; - margin: $spacing-sm auto $spacing-md auto; - width: 80%; - } - } - &__button-text { color: $white-100; } @@ -119,6 +105,9 @@ $dropdown-width: 320px; align-self: center; margin-left: auto; } + .#{$px}-input__input[type='checkbox']:disabled { + border-color: $keyline-gray; + } .#{$px}-input__input[type='checkbox'] { align-self: center; appearance: none; @@ -159,6 +148,14 @@ $dropdown-width: 320px; max-height: 2rem; } +// Filter option labels use the labels/label-small-bold type token. +.#{$px}-filter-dropdown-menu, +.#{$px}-filter-drawer-menu { + .#{$px}-input__label.#{$px}-text { + @include label-styles($font-size: $font-size-label-small, $font-weight: 600); + } +} + .#{$px}-filter-drawer-mobile { height: min(70dvh, 640px); padding: 0; @@ -186,6 +183,10 @@ $dropdown-width: 320px; flex: 0 0 auto; margin-top: 0; } + + .#{$px}-filter-dropdown-menu__mobile-safe-area { + flex: 0 0 auto; + } } .#{$px}-filter-drawer { diff --git a/src/patterns/FiltersInline/_filtersInline.scss b/src/patterns/FiltersInline/_filtersInline.scss index 2ddc71b2e..81425bc51 100644 --- a/src/patterns/FiltersInline/_filtersInline.scss +++ b/src/patterns/FiltersInline/_filtersInline.scss @@ -5,7 +5,9 @@ flex-wrap: wrap; gap: $spacing-sm; - // Desktop: align the Sort filter (and anything following it) to the right of the filter row. + // Desktop: align the Sort filter to the right of the filter row. The Sort + // pill is already rendered last in the DOM (see FiltersInline.tsx) so visual + // and tab order match; this just pushes it to the right edge. @include media($breakpoint-md) { > .#{$px}-filters-inline__sort { margin-left: auto; diff --git a/src/patterns/FiltersInline/types.ts b/src/patterns/FiltersInline/types.ts index a998f63fc..9483f5b10 100644 --- a/src/patterns/FiltersInline/types.ts +++ b/src/patterns/FiltersInline/types.ts @@ -73,12 +73,19 @@ export type FilterType = { filterButtonLabelTranslated?: string; }; +/** + * Where a filter input change originated: the main filter drawer or an + * individual filter dropdown (desktop popover or mobile bottom sheet). + */ +export type FilterChangeSource = 'drawer' | 'dropdown'; + /** * Handler for when a filter input changes (checkbox, radio, or select). */ export type FilterChangeHandler = ( e: React.ChangeEvent, filterId: string, + source?: FilterChangeSource, ) => void; /** @@ -151,11 +158,11 @@ export interface FilterDropdownProps extends BaseFilterProps { /** * Object containing translated strings for dropdown menu actions. - * - confirm: Label for the confirm/apply button. * - clearAll: Label for the "Clear all" button. * - showAuctions: Label for the button showing the number of auctions/results. */ export type DropdownMenuTranslation = { + /** @deprecated The sort dropdown no longer renders a confirm button; sort applies on selection. */ confirm?: string | null; clearAll?: string | null; showAuctions?: string | null; @@ -174,7 +181,7 @@ export interface FilterDropdownMenuProps extends React.HTMLAttributes, filterType: string) => void; + onSelectFilter?: FilterChangeHandler; /** Handler for when the filter is confirmed or updated */ onApplyFilter?: (returnCountOnly?: boolean) => void; /** Handler for clearing the filter (by type) */ diff --git a/src/patterns/FiltersInline/utils.tsx b/src/patterns/FiltersInline/utils.tsx index a0ec60091..3939a97b3 100644 --- a/src/patterns/FiltersInline/utils.tsx +++ b/src/patterns/FiltersInline/utils.tsx @@ -1,4 +1,11 @@ -import { FilterButtonIconType, FilterButtonType, FilterDimension, FilterType } from './types'; +import { + FilterButtonIconType, + FilterButtonType, + FilterChangeHandler, + FilterChangeSource, + FilterDimension, + FilterType, +} from './types'; export const FilterButtons: FilterButtonType[] = [ FilterButtonType.Filter, @@ -171,10 +178,11 @@ export const getFilterButtonClickHandler = ( export function handleInputChange( e: React.ChangeEvent, buttonType: string, - handleFilterSelection?: (e: React.ChangeEvent, filterType: string) => void, + handleFilterSelection?: FilterChangeHandler, + source?: FilterChangeSource, ) { if (handleFilterSelection) { - handleFilterSelection(e, buttonType); + handleFilterSelection(e, buttonType, source); } }