diff --git a/spec/components/Chip/Chip.test.tsx b/spec/components/Chip/Chip.test.tsx new file mode 100644 index 0000000..c012ba1 --- /dev/null +++ b/spec/components/Chip/Chip.test.tsx @@ -0,0 +1,180 @@ +import React from 'react'; +import { render, screen, cleanup } from '@testing-library/react'; +import { describe, test, expect, afterEach } from 'vitest'; +import Chip from '@/components/chip'; + +describe('Chip component', () => { + afterEach(() => { + cleanup(); + }); + + describe('color type', () => { + test('renders div with backgroundColor for color type', () => { + render(); + const element = screen.getByRole('img', { name: 'Red' }); + expect(element).toBeInTheDocument(); + expect(element.style.backgroundColor).toBe('rgb(255, 0, 0)'); + }); + + test('renders with hex color value', () => { + render(); + const element = screen.getByRole('img', { name: 'Blue' }); + expect(element.style.backgroundColor).toBe('rgb(59, 130, 246)'); + }); + }); + + describe('image type', () => { + test('renders img element with src for image type', () => { + render(); + const img = screen.getByAltText('Pattern'); + expect(img).toBeInTheDocument(); + expect(img).toHaveAttribute('src', 'https://example.com/image.jpg'); + expect(img.tagName).toBe('IMG'); + }); + + test('renders img with correct alt text', () => { + render(); + const img = screen.getByAltText('Floral Pattern'); + expect(img).toBeInTheDocument(); + }); + + test('wraps img in container with data-slot', () => { + render(); + const container = screen.getByTestId('chip'); + expect(container).toHaveAttribute('data-slot', 'chip'); + expect(container.querySelector('img')).toBeInTheDocument(); + }); + }); + + describe('empty value fallback', () => { + test('renders white fallback when value is empty string', () => { + render(); + const element = screen.getByRole('img', { name: 'Empty' }); + expect(element).toBeInTheDocument(); + expect(element.classList.contains('bg-white')).toBeTruthy(); + }); + + test('renders white fallback when value is whitespace', () => { + render(); + const element = screen.getByRole('img', { name: 'Whitespace' }); + expect(element.classList.contains('bg-white')).toBeTruthy(); + }); + + test('renders white fallback for image type with empty value', () => { + render(); + const element = screen.getByRole('img', { name: 'Empty Image' }); + expect(element.classList.contains('bg-white')).toBeTruthy(); + }); + }); + + describe('size variants', () => { + test('applies sm size class', () => { + render(); + const element = screen.getByRole('img', { name: 'Small' }); + expect(element.classList.contains('w-4')).toBeTruthy(); + expect(element.classList.contains('h-4')).toBeTruthy(); + }); + + test('applies md size class (default)', () => { + render(); + const element = screen.getByRole('img', { name: 'Medium' }); + expect(element.classList.contains('w-6')).toBeTruthy(); + expect(element.classList.contains('h-6')).toBeTruthy(); + }); + + test('applies lg size class', () => { + render(); + const element = screen.getByRole('img', { name: 'Large' }); + expect(element.classList.contains('w-8')).toBeTruthy(); + expect(element.classList.contains('h-8')).toBeTruthy(); + }); + }); + + describe('componentOverrides', () => { + test('renders componentOverride.reactNode when passed', () => { + render( + Custom Chip, + }} + />, + ); + expect(screen.getByTestId('custom-override')).toBeInTheDocument(); + expect(screen.getByText('Custom Chip')).toBeInTheDocument(); + }); + + test('does not render default content when override provided', () => { + render( + Override, + }} + />, + ); + expect(screen.queryByRole('img', { name: 'Overridden' })).not.toBeInTheDocument(); + }); + }); + + describe('accessibility', () => { + test('uses name prop for aria-label', () => { + render(); + const element = screen.getByLabelText('Crimson Red'); + expect(element).toBeInTheDocument(); + }); + + test('has role="img"', () => { + render(); + const element = screen.getByRole('img', { name: 'Black' }); + expect(element).toBeInTheDocument(); + }); + }); + + describe('data attributes', () => { + test('spreads data-* attributes correctly', () => { + render( + , + ); + const element = screen.getByTestId('chip-test'); + expect(element).toBeInTheDocument(); + expect(element.dataset.cnstrcFilter).toBe('color'); + }); + + test('has data-slot attribute', () => { + render(); + const element = screen.getByRole('img', { name: 'Slot Test' }); + expect(element).toHaveAttribute('data-slot', 'chip'); + }); + }); + + describe('CSS classes', () => { + test('has cio-chip class', () => { + render(); + const element = screen.getByRole('img', { name: 'Class Test' }); + expect(element.classList.contains('cio-chip')).toBeTruthy(); + }); + + test('has cio-components class', () => { + render(); + const element = screen.getByRole('img', { name: 'Components Class' }); + expect(element.classList.contains('cio-components')).toBeTruthy(); + }); + + test('merges custom className', () => { + render(); + const element = screen.getByRole('img', { name: 'Custom Class' }); + expect(element.classList.contains('my-custom-class')).toBeTruthy(); + }); + }); +}); diff --git a/spec/components/FilterOptionListRow/FilterOptionListRow.test.tsx b/spec/components/FilterOptionListRow/FilterOptionListRow.test.tsx new file mode 100644 index 0000000..1479457 --- /dev/null +++ b/spec/components/FilterOptionListRow/FilterOptionListRow.test.tsx @@ -0,0 +1,314 @@ +import React from 'react'; +import { render, screen, cleanup, fireEvent } from '@testing-library/react'; +import { describe, test, expect, afterEach, vi } from 'vitest'; +import FilterOptionListRow from '@/components/filter-option-list-row'; + +describe('FilterOptionListRow component', () => { + afterEach(() => { + cleanup(); + }); + + describe('basic rendering', () => { + test('renders with display value', () => { + render( + {}} + />, + ); + expect(screen.getByText('Red')).toBeInTheDocument(); + }); + + test('renders with display count', () => { + render( + {}} + />, + ); + expect(screen.getByText('1572')).toBeInTheDocument(); + }); + + test('renders as list item', () => { + render( + {}} + />, + ); + const listItem = screen.getByRole('listitem'); + expect(listItem).toBeInTheDocument(); + }); + + test('renders checkbox input', () => { + render( + {}} + />, + ); + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).toBeInTheDocument(); + expect(checkbox).toHaveAttribute('id', 'test-checkbox'); + expect(checkbox).toHaveAttribute('value', 'blue'); + }); + }); + + describe('checkbox behavior', () => { + test('checkbox is unchecked by default', () => { + render( + {}} + />, + ); + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).not.toBeChecked(); + }); + + test('checkbox is checked when isChecked is true', () => { + render( + {}} + />, + ); + const checkbox = screen.getByRole('checkbox'); + expect(checkbox).toBeChecked(); + }); + + test('calls onChange when checkbox is clicked', () => { + const handleChange = vi.fn(); + render( + , + ); + const checkbox = screen.getByRole('checkbox'); + fireEvent.click(checkbox); + expect(handleChange).toHaveBeenCalledWith('red'); + }); + + test('calls onChange with correct value', () => { + const handleChange = vi.fn(); + render( + , + ); + const checkbox = screen.getByRole('checkbox'); + fireEvent.click(checkbox); + expect(handleChange).toHaveBeenCalledWith('my-custom-value'); + }); + + test('calls onChange when label is clicked', () => { + const handleChange = vi.fn(); + render( + , + ); + fireEvent.click(screen.getByText('Blue')); + expect(handleChange).toHaveBeenCalledWith('blue'); + }); + }); + + describe('showCheckbox prop', () => { + test('shows checkbox indicator by default', () => { + render( + {}} + />, + ); + const checkboxIndicator = document.querySelector('.cio-checkbox'); + expect(checkboxIndicator).toBeInTheDocument(); + }); + + test('hides checkbox indicator when checkboxPosition is none', () => { + render( + {}} + />, + ); + const checkboxIndicator = document.querySelector('.cio-checkbox'); + expect(checkboxIndicator).not.toBeInTheDocument(); + }); + }); + + describe('startContent prop', () => { + test('renders startContent before display value', () => { + render( + ★} + onChange={() => {}} + />, + ); + expect(screen.getByTestId('start-content')).toBeInTheDocument(); + }); + + test('startContent appears in correct position', () => { + render( + ★} + onChange={() => {}} + />, + ); + const display = document.querySelector('.cio-filter-multiple-option-display'); + expect(display?.firstElementChild).toHaveAttribute('data-testid', 'start-content'); + }); + }); + + describe('componentOverrides', () => { + test('renders componentOverride.reactNode when passed', () => { + render( + Custom Row, + }} + onChange={() => {}} + />, + ); + expect(screen.getByTestId('custom-override')).toBeInTheDocument(); + expect(screen.getByText('Custom Row')).toBeInTheDocument(); + }); + + test('does not render default content when override provided', () => { + render( + Override, + }} + onChange={() => {}} + />, + ); + expect(screen.queryByText('Red')).not.toBeInTheDocument(); + }); + }); + + describe('CSS classes', () => { + test('has cio-filter-option-list-row class', () => { + render( + {}} + />, + ); + const listItem = screen.getByRole('listitem'); + expect(listItem.classList.contains('cio-filter-option-list-row')).toBeTruthy(); + }); + + test('has cio-filter-multiple-option class', () => { + render( + {}} + />, + ); + const listItem = screen.getByRole('listitem'); + expect(listItem.classList.contains('cio-filter-multiple-option')).toBeTruthy(); + }); + + test('merges custom className', () => { + render( + {}} + />, + ); + const listItem = screen.getByRole('listitem'); + expect(listItem.classList.contains('my-custom-class')).toBeTruthy(); + }); + + test('has text-base class by default', () => { + render( + {}} + />, + ); + const listItem = screen.getByRole('listitem'); + expect(listItem.classList.contains('text-base')).toBeTruthy(); + }); + }); + + describe('data attributes', () => { + test('has data-slot attribute', () => { + render( + {}} + />, + ); + const listItem = screen.getByRole('listitem'); + expect(listItem).toHaveAttribute('data-slot', 'filter-option-list-row'); + }); + + test('spreads data-* attributes correctly', () => { + render( + {}} + />, + ); + const listItem = screen.getByTestId('filter-row'); + expect(listItem.dataset.facet).toBe('color'); + }); + }); +}); diff --git a/spec/components/VisualFilterOptionListRow/VisualFilterOptionListRow.test.tsx b/spec/components/VisualFilterOptionListRow/VisualFilterOptionListRow.test.tsx new file mode 100644 index 0000000..03667e0 --- /dev/null +++ b/spec/components/VisualFilterOptionListRow/VisualFilterOptionListRow.test.tsx @@ -0,0 +1,189 @@ +import React from 'react'; +import { render, screen, cleanup } from '@testing-library/react'; +import { describe, test, expect, afterEach } from 'vitest'; +import VisualFilterOptionListRow from '@/components/visual-filter-option-list-row'; + +/** + * Tests unique to VisualFilterOptionListRow. + * Common functionality (checkbox behavior, basic rendering, onChange, etc.) + * is tested in FilterOptionListRow.test.tsx since this component wraps it. + */ +describe('VisualFilterOptionListRow component', () => { + afterEach(() => { + cleanup(); + }); + + describe('visual swatch - color type', () => { + test('renders color swatch with hex value', () => { + render( + {}} + />, + ); + const swatch = document.querySelector('.cio-filter-visual-swatch'); + expect(swatch).toBeInTheDocument(); + expect(swatch).toHaveStyle({ backgroundColor: 'rgb(255, 0, 0)' }); + }); + + test('renders black color swatch', () => { + render( + {}} + />, + ); + const swatch = document.querySelector('.cio-filter-visual-swatch'); + expect(swatch).toHaveStyle({ backgroundColor: 'rgb(0, 0, 0)' }); + }); + + test('renders white color swatch', () => { + render( + {}} + />, + ); + const swatch = document.querySelector('.cio-filter-visual-swatch'); + expect(swatch).toHaveStyle({ backgroundColor: 'rgb(255, 255, 255)' }); + }); + }); + + describe('visual swatch - image type', () => { + test('renders image swatch with src', () => { + render( + {}} + />, + ); + const img = screen.getByAltText('Floral'); + expect(img).toBeInTheDocument(); + expect(img).toHaveAttribute('src', 'https://example.com/pattern.jpg'); + }); + }); + + describe('componentOverrides', () => { + test('renders componentOverride.reactNode when passed', () => { + render( + Custom Visual Row, + }} + onChange={() => {}} + />, + ); + expect(screen.getByTestId('custom-override')).toBeInTheDocument(); + expect(screen.getByText('Custom Visual Row')).toBeInTheDocument(); + }); + }); + + describe('CSS classes', () => { + test('has cio-visual-filter-option-list-row class', () => { + render( + {}} + />, + ); + const listItem = screen.getByRole('listitem'); + expect(listItem.classList.contains('cio-visual-filter-option-list-row')).toBeTruthy(); + }); + + test('swatch has cio-filter-visual-swatch class', () => { + render( + {}} + />, + ); + const swatch = document.querySelector('.cio-filter-visual-swatch'); + expect(swatch).toBeInTheDocument(); + }); + }); + + describe('data attributes', () => { + test('has data-slot attribute with visual-filter value', () => { + render( + {}} + />, + ); + const listItem = screen.getByRole('listitem'); + expect(listItem).toHaveAttribute('data-slot', 'visual-filter-option-list-row'); + }); + }); + + describe('layout structure', () => { + test('swatch appears before option name in DOM order', () => { + render( + {}} + />, + ); + const display = document.querySelector('.cio-filter-multiple-option-display'); + const children = display?.children; + expect(children?.[0]?.classList.contains('cio-filter-visual-swatch')).toBeTruthy(); + expect(children?.[1]?.classList.contains('cio-filter-option-name')).toBeTruthy(); + }); + }); + + describe('checkbox default position', () => { + test('checkbox defaults to right position', () => { + render( + {}} + />, + ); + const label = document.querySelector('.cio-filter-option-label'); + const checkbox = label?.querySelector('.cio-checkbox'); + // Checkbox should be after the display div (right position) + const displayDiv = label?.querySelector('.cio-filter-multiple-option-display'); + expect(displayDiv?.nextElementSibling).toBe(checkbox); + }); + }); +}); diff --git a/src/components/chip.tsx b/src/components/chip.tsx new file mode 100644 index 0000000..c41aee9 --- /dev/null +++ b/src/components/chip.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import { cn, RenderPropsWrapper } from '@/utils'; +import { ComponentOverrideProps, IncludeComponentOverrides } from '@/types'; +import { cva, VariantProps } from 'class-variance-authority'; + +const chipVariants = cva( + 'cio-components cio-chip inline-flex items-center justify-center rounded-full overflow-hidden border border-gray-200 flex-shrink-0', + { + variants: { + size: { + sm: 'w-4 h-4', + md: 'w-6 h-6', + lg: 'w-8 h-8', + }, + }, + defaultVariants: { + size: 'md', + }, + }, +); + +export type ChipVariants = VariantProps; + +export interface ChipProps + extends Omit, 'children'>, + ChipVariants, + IncludeComponentOverrides { + /** Type of chip - 'color' for hex colors, 'image' for image URLs */ + type: 'color' | 'image'; + /** The value - hex color code for color type, URL for image type */ + value: string; + /** Name for accessibility (used in aria-label and alt text) */ + name: string; +} + +export type ChipOverrides = ComponentOverrideProps; + +export default function Chip({ + className, + size, + type, + value, + name, + componentOverrides, + ...props +}: ChipProps) { + const renderProps = React.useMemo( + () => ({ type, value, name, size, className }), + [type, value, name, size, className], + ); + + const renderContent = () => { + // Fallback + if (!value || value.trim() === '' || !['color', 'image'].includes(type)) { + return ( + + ); + } + + if (type === 'color') { + return ( + + ); + } + + if (type === 'image') { + return ( + + { + e.currentTarget.style.display = 'none'; + e.currentTarget.parentElement?.classList.add('bg-gray-200'); + }} + /> + + ); + } + }; + + return ( + + {renderContent()} + + ); +} diff --git a/src/components/filter-option-list-row.tsx b/src/components/filter-option-list-row.tsx new file mode 100644 index 0000000..fc31990 --- /dev/null +++ b/src/components/filter-option-list-row.tsx @@ -0,0 +1,105 @@ +import React, { ReactNode } from 'react'; +import { cn, RenderPropsWrapper } from '@/utils'; +import { ComponentOverrideProps, IncludeComponentOverrides } from '@/types'; +const baseClasses = + 'cio-components cio-filter-option-list-row cio-filter-multiple-option group cursor-pointer flex list-none text-base hover:bg-neutral-100 hover:rounded'; + +export interface FilterOptionListRowProps + extends Omit, 'onChange' | 'children'>, + IncludeComponentOverrides { + /** Unique identifier for the filter option */ + id: string; + /** Value to be used when the option is selected */ + optionValue: string; + /** Display text for the option */ + displayValue: string; + /** Count to display (e.g., "1572") */ + displayCountValue?: string; + /** Whether the option is currently selected */ + isChecked?: boolean; + /** Callback when option selection changes */ + onChange: (value: string) => void; + /** Position of the checkbox. Can be 'left', 'right', or 'none'. Defaults to 'left' */ + checkboxPosition?: 'left' | 'right' | 'none'; + /** Optional content to render before the display value (e.g., color swatch) */ + startContent?: ReactNode; + /** Optional children to render inside the component */ + children?: ReactNode; +} + +export type FilterOptionListRowOverrides = ComponentOverrideProps; + +export default function FilterOptionListRow({ + className, + id, + optionValue, + displayValue, + displayCountValue, + isChecked = false, + onChange, + checkboxPosition = 'left', + startContent, + componentOverrides, + children, + ...props +}: FilterOptionListRowProps) { + const checkboxVisible = checkboxPosition !== 'none'; + const checkboxEl = checkboxVisible && ( + + + + + + ); + + return ( + + + + {checkboxVisible && ( + onChange(optionValue)} + className='cio-filter-option-input hidden' + /> + )} + {checkboxPosition === 'left' && checkboxEl} + + {startContent} + {displayValue} + {displayCountValue && ( + + {displayCountValue} + + )} + + {checkboxPosition === 'right' && checkboxEl} + + {children} + + + ); +} diff --git a/src/components/visual-filter-option-list-row.tsx b/src/components/visual-filter-option-list-row.tsx new file mode 100644 index 0000000..972a5dc --- /dev/null +++ b/src/components/visual-filter-option-list-row.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { cn, RenderPropsWrapper } from '@/utils'; +import { ComponentOverrideProps, IncludeComponentOverrides } from '@/types'; +import Chip from '@/components/chip'; +import FilterOptionListRow, { FilterOptionListRowProps } from '@/components/filter-option-list-row'; + +export interface VisualFilterOptionListRowProps + extends Omit, + IncludeComponentOverrides { + /** Type of visual - 'color' for hex colors, 'image' for image URLs */ + visualType: 'color' | 'image'; + /** The visual value - hex color code or image URL */ + visualValue: string; +} + +export type VisualFilterOptionListRowOverrides = + ComponentOverrideProps; + +export default function VisualFilterOptionListRow({ + className, + checkboxPosition = 'right', + visualType, + visualValue, + displayValue, + componentOverrides, + ...props +}: VisualFilterOptionListRowProps) { + return ( + + + } + {...props} + /> + + ); +} diff --git a/src/index.ts b/src/index.ts index b310ab0..e406679 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,9 @@ export { default as Button } from '@/components/button'; export { default as Badge } from '@/components/badge'; export { default as ProductCard } from '@/components/product-card'; export { default as Carousel } from '@/components/carousel'; +export { default as Chip } from '@/components/chip'; +export { default as FilterOptionListRow } from '@/components/filter-option-list-row'; +export { default as VisualFilterOptionListRow } from '@/components/visual-filter-option-list-row'; export { RenderPropsWrapper } from '@/utils'; // Hooks @@ -10,4 +13,13 @@ export { RenderPropsWrapper } from '@/utils'; // Types export type { ButtonVariants, ButtonOverrides, ButtonProps } from '@/components/button'; export type { BadgeVariants, BadgeOverrides, BadgeProps } from '@/components/badge'; +export type { ChipVariants, ChipOverrides, ChipProps } from '@/components/chip'; +export type { + FilterOptionListRowOverrides, + FilterOptionListRowProps, +} from '@/components/filter-option-list-row'; +export type { + VisualFilterOptionListRowOverrides, + VisualFilterOptionListRowProps, +} from '@/components/visual-filter-option-list-row'; export * from '@/types'; diff --git a/src/stories/components/Chip/Chip.stories.tsx b/src/stories/components/Chip/Chip.stories.tsx new file mode 100644 index 0000000..0786cdf --- /dev/null +++ b/src/stories/components/Chip/Chip.stories.tsx @@ -0,0 +1,139 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import Chip from '../../../components/chip'; + +const meta = { + title: 'Components/Chip', + component: Chip, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + argTypes: { + type: { + control: 'radio', + options: ['color', 'image'], + }, + size: { + control: 'radio', + options: ['sm', 'md', 'lg'], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// --- Color Variant --- + +export const Color: Story = { + args: { + type: 'color', + value: '#3B82F6', + name: 'Blue', + }, +}; + +// --- Image Variant --- + +export const Image: Story = { + args: { + type: 'image', + value: 'https://constructor.com/hubfs/constructor-favicon-2024-1.svg', + name: 'Constructor', + }, +}; + +// --- Size Variants --- + +export const SizeSmall: Story = { + args: { + type: 'color', + value: '#3B82F6', + name: 'Small Blue', + size: 'sm', + }, + name: 'Size - sm', +}; + +export const SizeMedium: Story = { + args: { + type: 'color', + value: '#3B82F6', + name: 'Medium Blue', + size: 'md', + }, + name: 'Size - md (default)', +}; + +export const SizeLarge: Story = { + args: { + type: 'color', + value: '#3B82F6', + name: 'Large Blue', + size: 'lg', + }, + name: 'Size - lg', +}; + +// --- Empty/Fallback --- + +export const EmptyFallback: Story = { + args: { + type: 'color', + value: '', + name: 'No color specified', + }, +}; + +// --- componentOverrides --- + +const componentOverrides = { + chip: { + reactNode: ( + + ), + }, +}; + +export const ComponentOverrideExample: Story = { + args: { + type: 'color', + value: '#FF0000', + name: 'Overridden', + // @ts-expect-error: Composed types + componentOverrides: componentOverrides.chip, + }, + name: 'componentOverride Example', + tags: ['!autodocs', '!dev'], +}; + +// --- Size Comparison --- + +export const SizeComparison: Story = { + render: () => ( + + + + sm + + + + md + + + + lg + + + ), + parameters: { + controls: { disable: true }, + }, +}; diff --git a/src/stories/components/FilterOptionListRow/FilterOptionListRow.stories.tsx b/src/stories/components/FilterOptionListRow/FilterOptionListRow.stories.tsx new file mode 100644 index 0000000..a938d55 --- /dev/null +++ b/src/stories/components/FilterOptionListRow/FilterOptionListRow.stories.tsx @@ -0,0 +1,143 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import FilterOptionListRow from '../../../components/filter-option-list-row'; + +const meta = { + title: 'Components/FilterOptionListRow', + component: FilterOptionListRow, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + decorators: [ + (Story) => ( + + + + ), + ], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + id: 'filter-1', + optionValue: 'red', + displayValue: 'Red', + displayCountValue: '1572', + }, +}; + +export const Checked: Story = { + args: { + id: 'filter-2', + optionValue: 'blue', + displayValue: 'Blue', + displayCountValue: '394', + isChecked: true, + }, + name: 'Checked State', +}; + +export const WithoutCount: Story = { + args: { + id: 'filter-3', + optionValue: 'green', + displayValue: 'Green', + }, +}; + +export const WithoutCheckbox: Story = { + args: { + id: 'filter-4', + optionValue: 'purple', + displayValue: 'Purple', + displayCountValue: '291', + showCheckbox: false, + }, + name: 'Without Checkbox Indicator', +}; + +export const WithStartContent: Story = { + args: { + id: 'filter-5', + optionValue: 'featured', + displayValue: 'Featured', + displayCountValue: '42', + startContent: ⭐, + }, +}; + +// Multiple options list +export const FilterList: Story = { + render: () => ( + + + + + + + + + + ), + parameters: { + controls: { disable: true }, + }, +}; + +// componentOverrides example +const componentOverrides = { + filterOptionListRow: { + reactNode: ( + + Custom rendered row + + ), + }, +}; + +export const ComponentOverrideExample: Story = { + args: { + id: 'override-1', + optionValue: 'custom', + displayValue: 'This will be overridden', + // @ts-expect-error: Composed types + componentOverrides: componentOverrides.filterOptionListRow, + }, + name: 'componentOverride Example', + tags: ['!autodocs', '!dev'], +}; diff --git a/src/stories/components/VisualFilterOptionListRow/VisualFilterOptionListRow.stories.tsx b/src/stories/components/VisualFilterOptionListRow/VisualFilterOptionListRow.stories.tsx new file mode 100644 index 0000000..2d61bc5 --- /dev/null +++ b/src/stories/components/VisualFilterOptionListRow/VisualFilterOptionListRow.stories.tsx @@ -0,0 +1,215 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import VisualFilterOptionListRow from '../../../components/visual-filter-option-list-row'; + +const meta = { + title: 'Components/VisualFilterOptionListRow', + component: VisualFilterOptionListRow, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], + decorators: [ + (Story) => ( + + + + ), + ], + argTypes: { + visualType: { + control: 'radio', + options: ['color', 'image'], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// --- Color Example --- + +export const Color: Story = { + args: { + id: 'color-red', + optionValue: 'red', + displayValue: 'Red', + displayCountValue: '646', + visualType: 'color', + visualValue: '#EF4444', + }, +}; + +// --- Image Example --- + +export const Image: Story = { + args: { + id: 'pattern-constructor', + optionValue: 'constructor', + displayValue: 'Constructor', + displayCountValue: '128', + visualType: 'image', + visualValue: 'https://constructor.com/hubfs/constructor-favicon-2024-1.svg', + }, +}; + +// --- Checked State --- + +export const CheckedState: Story = { + args: { + id: 'color-purple', + optionValue: 'purple', + displayValue: 'Purple', + displayCountValue: '291', + visualType: 'color', + visualValue: '#A855F7', + isChecked: true, + }, +}; + +// --- Complete Color Filter List (matching the reference image) --- + +export const ColorFilterList: Story = { + render: () => ( + + + + + + + + + + + + ), + parameters: { + controls: { disable: true }, + }, +}; + +// --- Mixed Visual Types --- + +export const MixedVisualTypes: Story = { + render: () => ( + + + + + + ), + parameters: { + controls: { disable: true }, + }, +}; + +// --- componentOverrides --- + +const componentOverrides = { + visualFilterOptionListRow: { + reactNode: ( + + Custom rendered visual row + + ), + }, +}; + +export const ComponentOverrideExample: Story = { + args: { + id: 'override-1', + optionValue: 'custom', + displayValue: 'This will be overridden', + visualType: 'color', + visualValue: '#FF0000', + // @ts-expect-error: Composed types + componentOverrides: componentOverrides.visualFilterOptionListRow, + }, + name: 'componentOverride Example', + tags: ['!autodocs', '!dev'], +};