From 2fde63ee42c40a88e5b547ffdda513ead1831a36 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Wed, 28 Jan 2026 17:39:31 +0300 Subject: [PATCH 01/13] Initial implementation --- spec/components/Chip/Chip.test.tsx | 180 +++++++++ .../FilterOptionListRow.test.tsx | 270 ++++++++++++++ .../VisualFilterOptionListRow.test.tsx | 353 ++++++++++++++++++ src/components/chip.tsx | 117 ++++++ src/components/filter-option-list-row.tsx | 131 +++++++ .../visual-filter-option-list-row.tsx | 142 +++++++ src/index.ts | 18 + src/stories/components/Chip/Chip.stories.tsx | 143 +++++++ .../FilterOptionListRow.stories.tsx | 151 ++++++++ .../VisualFilterOptionListRow.stories.tsx | 252 +++++++++++++ src/styles.css | 80 ++++ 11 files changed, 1837 insertions(+) create mode 100644 spec/components/Chip/Chip.test.tsx create mode 100644 spec/components/FilterOptionListRow/FilterOptionListRow.test.tsx create mode 100644 spec/components/VisualFilterOptionListRow/VisualFilterOptionListRow.test.tsx create mode 100644 src/components/chip.tsx create mode 100644 src/components/filter-option-list-row.tsx create mode 100644 src/components/visual-filter-option-list-row.tsx create mode 100644 src/stories/components/Chip/Chip.stories.tsx create mode 100644 src/stories/components/FilterOptionListRow/FilterOptionListRow.stories.tsx create mode 100644 src/stories/components/VisualFilterOptionListRow/VisualFilterOptionListRow.stories.tsx 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..e15f7f9 --- /dev/null +++ b/spec/components/FilterOptionListRow/FilterOptionListRow.test.tsx @@ -0,0 +1,270 @@ +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'); + }); + }); + + describe('showCheckbox prop', () => { + test('shows checkbox indicator by default', () => { + render( + , + ); + const checkboxIndicator = document.querySelector('.cio-checkbox'); + expect(checkboxIndicator).toBeInTheDocument(); + }); + + test('hides checkbox indicator when showCheckbox is false', () => { + render( + , + ); + const checkboxIndicator = document.querySelector('.cio-checkbox'); + expect(checkboxIndicator).not.toBeInTheDocument(); + }); + }); + + describe('startContent prop', () => { + test('renders startContent before display value', () => { + render( + ★} + />, + ); + expect(screen.getByTestId('start-content')).toBeInTheDocument(); + }); + + test('startContent appears in correct position', () => { + render( + ★} + />, + ); + 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, + }} + />, + ); + expect(screen.getByTestId('custom-override')).toBeInTheDocument(); + expect(screen.getByText('Custom Row')).toBeInTheDocument(); + }); + + test('does not render default content when override provided', () => { + render( + Override, + }} + />, + ); + 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(); + }); + }); + + 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..c0e5ac2 --- /dev/null +++ b/spec/components/VisualFilterOptionListRow/VisualFilterOptionListRow.test.tsx @@ -0,0 +1,353 @@ +import React from 'react'; +import { render, screen, cleanup, fireEvent } from '@testing-library/react'; +import { describe, test, expect, afterEach, vi } from 'vitest'; +import VisualFilterOptionListRow from '@/components/visual-filter-option-list-row'; + +describe('VisualFilterOptionListRow 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('685')).toBeInTheDocument(); + }); + + test('renders as list item', () => { + render( + , + ); + const listItem = screen.getByRole('listitem'); + expect(listItem).toBeInTheDocument(); + }); + }); + + 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('visual swatch size', () => { + test('renders sm size swatch', () => { + render( + , + ); + const swatch = document.querySelector('.cio-filter-visual-swatch'); + expect(swatch?.classList.contains('w-4')).toBeTruthy(); + }); + + test('renders md size swatch by default', () => { + render( + , + ); + const swatch = document.querySelector('.cio-filter-visual-swatch'); + expect(swatch?.classList.contains('w-6')).toBeTruthy(); + }); + + test('renders lg size swatch', () => { + render( + , + ); + const swatch = document.querySelector('.cio-filter-visual-swatch'); + expect(swatch?.classList.contains('w-8')).toBeTruthy(); + }); + }); + + 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('purple'); + }); + }); + + describe('showCheckbox prop', () => { + test('shows checkbox indicator by default', () => { + render( + , + ); + const checkboxIndicator = document.querySelector('.cio-checkbox'); + expect(checkboxIndicator).toBeInTheDocument(); + }); + + test('hides checkbox indicator when showCheckbox is false', () => { + render( + , + ); + const checkboxIndicator = document.querySelector('.cio-checkbox'); + expect(checkboxIndicator).not.toBeInTheDocument(); + }); + }); + + describe('componentOverrides', () => { + test('renders componentOverride.reactNode when passed', () => { + render( + Custom Visual Row, + }} + />, + ); + 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('has cio-filter-multiple-option class', () => { + render( + , + ); + const listItem = screen.getByRole('listitem'); + expect(listItem.classList.contains('cio-filter-multiple-option')).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', () => { + render( + , + ); + const listItem = screen.getByRole('listitem'); + expect(listItem).toHaveAttribute('data-slot', 'visual-filter-option-list-row'); + }); + + test('spreads data-* attributes correctly', () => { + render( + , + ); + const listItem = screen.getByTestId('visual-filter-row'); + expect(listItem.dataset.facet).toBe('color'); + }); + }); + + 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(); + }); + }); +}); diff --git a/src/components/chip.tsx b/src/components/chip.tsx new file mode 100644 index 0000000..e165fe7 --- /dev/null +++ b/src/components/chip.tsx @@ -0,0 +1,117 @@ +import React, { ReactNode } 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; + /** Optional children to render inside the chip */ + children?: ReactNode; +} + +export type ChipOverrides = ComponentOverrideProps; + +export default function Chip({ + className, + size, + type, + value, + name, + componentOverrides, + children, + ...props +}: ChipProps) { + const renderProps = React.useMemo( + () => ({ type, value, name, size, className, ...props }), + [type, value, name, size, className, props], + ); + + // Determine what to render based on type and value + const renderContent = () => { + // Empty value fallback - white circle + if (!value || value.trim() === '') { + return ( +
+ ); + } + + if (type === 'color') { + return ( +
+ ); + } + + if (type === 'image') { + return ( +
+ {name} +
+ ); + } + + // Fallback + return ( +
+ ); + }; + + 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..548ae73 --- /dev/null +++ b/src/components/filter-option-list-row.tsx @@ -0,0 +1,131 @@ +import React, { ReactNode } from 'react'; +import { cn, RenderPropsWrapper } from '@/utils'; +import { ComponentOverrideProps, IncludeComponentOverrides } from '@/types'; +import { cva, VariantProps } from 'class-variance-authority'; + +const filterOptionListRowVariants = cva( + 'cio-components cio-filter-option-list-row cio-filter-multiple-option', + { + variants: { + size: { + sm: 'text-sm', + md: 'text-base', + lg: 'text-lg', + }, + }, + defaultVariants: { + size: 'md', + }, + }, +); + +export type FilterOptionListRowVariants = VariantProps; + +export interface FilterOptionListRowProps + extends Omit, 'onChange' | 'children'>, + FilterOptionListRowVariants, + 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; + /** Whether to show the checkbox indicator */ + showCheckbox?: boolean; + /** 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, + size, + id, + optionValue, + displayValue, + displayCountValue, + isChecked = false, + onChange, + showCheckbox = true, + startContent, + componentOverrides, + children, + ...props +}: FilterOptionListRowProps) { + const renderProps = React.useMemo( + () => ({ + id, + optionValue, + displayValue, + displayCountValue, + isChecked, + onChange, + showCheckbox, + startContent, + size, + className, + ...props, + }), + [id, optionValue, displayValue, displayCountValue, isChecked, onChange, showCheckbox, startContent, size, className, props], + ); + + const handleChange = () => { + onChange?.(optionValue); + }; + + return ( + +
  • + + {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..6354239 --- /dev/null +++ b/src/components/visual-filter-option-list-row.tsx @@ -0,0 +1,142 @@ +import React from 'react'; +import { cn, RenderPropsWrapper } from '@/utils'; +import { ComponentOverrideProps, IncludeComponentOverrides } from '@/types'; +import { cva, VariantProps } from 'class-variance-authority'; +import Chip from './chip'; + +const visualFilterOptionListRowVariants = cva( + 'cio-components cio-visual-filter-option-list-row cio-filter-multiple-option', + { + variants: { + size: { + sm: 'text-sm', + md: 'text-base', + lg: 'text-lg', + }, + }, + defaultVariants: { + size: 'md', + }, + }, +); + +export type VisualFilterOptionListRowVariants = VariantProps; + +export interface VisualFilterOptionListRowProps + extends Omit, 'onChange' | 'children'>, + VisualFilterOptionListRowVariants, + 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; + /** Whether to show the checkbox indicator */ + showCheckbox?: boolean; + /** 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; + /** Size of the visual swatch */ + visualSize?: 'sm' | 'md' | 'lg'; +} + +export type VisualFilterOptionListRowOverrides = ComponentOverrideProps; + +export default function VisualFilterOptionListRow({ + className, + size, + id, + optionValue, + displayValue, + displayCountValue, + isChecked = false, + onChange, + showCheckbox = true, + visualType, + visualValue, + visualSize = 'md', + componentOverrides, + ...props +}: VisualFilterOptionListRowProps) { + const renderProps = React.useMemo( + () => ({ + id, + optionValue, + displayValue, + displayCountValue, + isChecked, + onChange, + showCheckbox, + visualType, + visualValue, + visualSize, + size, + className, + ...props, + }), + [id, optionValue, displayValue, displayCountValue, isChecked, onChange, showCheckbox, visualType, visualValue, visualSize, size, className, props], + ); + + const handleChange = () => { + onChange?.(optionValue); + }; + + return ( + +
  • + +
  • +
    + ); +} diff --git a/src/index.ts b/src/index.ts index b310ab0..99c1e1f 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,19 @@ 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 { + FilterOptionListRowVariants, + FilterOptionListRowOverrides, + FilterOptionListRowProps, +} from '@/components/filter-option-list-row'; +export type { + VisualFilterOptionListRowVariants, + 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..355fb27 --- /dev/null +++ b/src/stories/components/Chip/Chip.stories.tsx @@ -0,0 +1,143 @@ +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', + }, + name: 'Color', +}; + +// --- Image Variant --- + +export const Image: Story = { + args: { + type: 'image', + value: 'https://constructor.com/hubfs/constructor-favicon-2024-1.svg', + name: 'Constructor', + }, + name: 'Image', +}; + +// --- 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', + }, + name: 'Empty Fallback', +}; + +// --- 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
    +
    +
    + ), + name: 'Size Comparison', + 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..ec0e513 --- /dev/null +++ b/src/stories/components/FilterOptionListRow/FilterOptionListRow.stories.tsx @@ -0,0 +1,151 @@ +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', + }, + name: 'Without Count', +}; + +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: , + }, + name: 'With Start Content', +}; + +// Multiple options list +export const FilterList: Story = { + render: () => ( +
      + + + + + + + +
    + ), + name: 'Filter List', + 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..4334345 --- /dev/null +++ b/src/stories/components/VisualFilterOptionListRow/VisualFilterOptionListRow.stories.tsx @@ -0,0 +1,252 @@ +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'], + }, + visualSize: { + control: 'radio', + options: ['sm', 'md', 'lg'], + }, + }, +} 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', + }, + name: 'Color', +}; + +// --- 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', + }, + name: 'Image', +}; + +// --- Checked State --- + +export const CheckedState: Story = { + args: { + id: 'color-purple', + optionValue: 'purple', + displayValue: 'Purple', + displayCountValue: '291', + visualType: 'color', + visualValue: '#A855F7', + isChecked: true, + }, + name: 'Checked State', +}; + +// --- Visual Size Variants --- + +export const SmallSwatch: Story = { + args: { + id: 'small-swatch', + optionValue: 'green', + displayValue: 'Green', + displayCountValue: '195', + visualType: 'color', + visualValue: '#22C55E', + visualSize: 'sm', + }, + name: 'Visual Size - sm', +}; + +export const LargeSwatch: Story = { + args: { + id: 'large-swatch', + optionValue: 'orange', + displayValue: 'Orange', + displayCountValue: '224', + visualType: 'color', + visualValue: '#F97316', + visualSize: 'lg', + }, + name: 'Visual Size - lg', +}; + +// --- Complete Color Filter List (matching the reference image) --- + +export const ColorFilterList: Story = { + render: () => ( +
      + + + + + + + + + +
    + ), + name: 'Color Filter List', + parameters: { + controls: { disable: true }, + }, +}; + +// --- Mixed Visual Types --- + +export const MixedVisualTypes: Story = { + render: () => ( +
      + + + +
    + ), + name: 'Mixed Visual Types', + 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'], +}; diff --git a/src/styles.css b/src/styles.css index 500b44a..10f14c3 100644 --- a/src/styles.css +++ b/src/styles.css @@ -19,6 +19,86 @@ } } +/* Filter Option List Row Styles */ +.cio-filter-multiple-option { + cursor: pointer; + display: flex; + list-style: none; +} + +.cio-filter-multiple-option label, +.cio-filter-option-label { + font-size: 14px; + display: flex; + flex-direction: row; + align-items: center; + cursor: pointer; + flex-grow: 1; + padding: 4px; +} + +.cio-filter-multiple-option input, +.cio-filter-option-input { + display: none; +} + +.cio-checkbox { + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + margin-right: 8px; + background-color: #fff; + width: 20px; + height: 20px; + min-width: 20px; + min-height: 20px; + border-radius: 4px; + transition: 0.25s ease; + border: 1px solid #00000033; +} + +.cio-check { + opacity: 0; + transition: opacity 0.25s ease; +} + +.cio-filter-multiple-option:has(input:checked) .cio-checkbox { + box-shadow: inset 0 0 0 32px #000000; +} + +.cio-filter-multiple-option:has(input:checked) .cio-check { + opacity: 1; +} + +.cio-filter-multiple-option:hover { + background-color: #f2f2f2; + border-radius: 4px; +} + +.cio-filter-multiple-option-display { + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; + align-items: center; +} + +.cio-filter-option-name { + flex-grow: 1; + word-break: break-word; +} + +.cio-filter-option-count { + color: #999; + margin-left: 8px; +} + +.cio-filter-visual-swatch { + margin-right: 8px; + flex-shrink: 0; +} + @theme inline { --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); From 98749192754eda08368d32b24f785f8e53616e40 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Wed, 28 Jan 2026 17:56:23 +0300 Subject: [PATCH 02/13] Improvements and lint --- .../FilterOptionListRow.test.tsx | 14 ++++++++ .../VisualFilterOptionListRow.test.tsx | 16 ++++++++++ src/components/chip.tsx | 21 ++++++++---- src/components/filter-option-list-row.tsx | 21 +++++++----- .../visual-filter-option-list-row.tsx | 32 ++++++++++++------- src/index.ts | 6 +--- .../FilterOptionListRow.stories.tsx | 7 +--- 7 files changed, 80 insertions(+), 37 deletions(-) diff --git a/spec/components/FilterOptionListRow/FilterOptionListRow.test.tsx b/spec/components/FilterOptionListRow/FilterOptionListRow.test.tsx index e15f7f9..f07fa54 100644 --- a/spec/components/FilterOptionListRow/FilterOptionListRow.test.tsx +++ b/spec/components/FilterOptionListRow/FilterOptionListRow.test.tsx @@ -114,6 +114,20 @@ describe('FilterOptionListRow component', () => { 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', () => { diff --git a/spec/components/VisualFilterOptionListRow/VisualFilterOptionListRow.test.tsx b/spec/components/VisualFilterOptionListRow/VisualFilterOptionListRow.test.tsx index c0e5ac2..4a18d9a 100644 --- a/spec/components/VisualFilterOptionListRow/VisualFilterOptionListRow.test.tsx +++ b/spec/components/VisualFilterOptionListRow/VisualFilterOptionListRow.test.tsx @@ -205,6 +205,22 @@ describe('VisualFilterOptionListRow component', () => { fireEvent.click(checkbox); expect(handleChange).toHaveBeenCalledWith('purple'); }); + + test('calls onChange when label is clicked', () => { + const handleChange = vi.fn(); + render( + , + ); + fireEvent.click(screen.getByText('Blue')); + expect(handleChange).toHaveBeenCalledWith('blue'); + }); }); describe('showCheckbox prop', () => { diff --git a/src/components/chip.tsx b/src/components/chip.tsx index e165fe7..8cc8fe2 100644 --- a/src/components/chip.tsx +++ b/src/components/chip.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react'; +import React from 'react'; import { cn, RenderPropsWrapper } from '@/utils'; import { ComponentOverrideProps, IncludeComponentOverrides } from '@/types'; import { cva, VariantProps } from 'class-variance-authority'; @@ -31,8 +31,6 @@ export interface ChipProps value: string; /** Name for accessibility (used in aria-label and alt text) */ name: string; - /** Optional children to render inside the chip */ - children?: ReactNode; } export type ChipOverrides = ComponentOverrideProps; @@ -44,18 +42,20 @@ export default function Chip({ value, name, componentOverrides, - children, ...props }: ChipProps) { const renderProps = React.useMemo( - () => ({ type, value, name, size, className, ...props }), - [type, value, name, size, className, props], + () => ({ type, value, name, size, className }), + [type, value, name, size, className], ); // Determine what to render based on type and value const renderContent = () => { // Empty value fallback - white circle if (!value || value.trim() === '') { + if (process.env.NODE_ENV === 'development') { + console.warn(`Chip: Empty value provided for "${name}". Rendering fallback.`); + } return (
    { + e.currentTarget.style.display = 'none'; + e.currentTarget.parentElement?.classList.add('bg-gray-200'); + }} />
    ); } - // Fallback + // Fallback - should be unreachable with correct TypeScript usage + if (process.env.NODE_ENV === 'development') { + console.error(`Chip: Invalid type "${type}" provided. Expected 'color' or 'image'.`); + } return (
    { @@ -107,12 +117,7 @@ export default function FilterOptionListRow({ fill='none' xmlns='http://www.w3.org/2000/svg' className='cio-check'> - +
    )} diff --git a/src/components/visual-filter-option-list-row.tsx b/src/components/visual-filter-option-list-row.tsx index 6354239..88185a3 100644 --- a/src/components/visual-filter-option-list-row.tsx +++ b/src/components/visual-filter-option-list-row.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { cn, RenderPropsWrapper } from '@/utils'; import { ComponentOverrideProps, IncludeComponentOverrides } from '@/types'; import { cva, VariantProps } from 'class-variance-authority'; -import Chip from './chip'; +import Chip from '@/components/chip'; const visualFilterOptionListRowVariants = cva( 'cio-components cio-visual-filter-option-list-row cio-filter-multiple-option', @@ -20,7 +20,9 @@ const visualFilterOptionListRowVariants = cva( }, ); -export type VisualFilterOptionListRowVariants = VariantProps; +export type VisualFilterOptionListRowVariants = VariantProps< + typeof visualFilterOptionListRowVariants +>; export interface VisualFilterOptionListRowProps extends Omit, 'onChange' | 'children'>, @@ -48,7 +50,8 @@ export interface VisualFilterOptionListRowProps visualSize?: 'sm' | 'md' | 'lg'; } -export type VisualFilterOptionListRowOverrides = ComponentOverrideProps; +export type VisualFilterOptionListRowOverrides = + ComponentOverrideProps; export default function VisualFilterOptionListRow({ className, @@ -80,9 +83,21 @@ export default function VisualFilterOptionListRow({ visualSize, size, className, - ...props, }), - [id, optionValue, displayValue, displayCountValue, isChecked, onChange, showCheckbox, visualType, visualValue, visualSize, size, className, props], + [ + id, + optionValue, + displayValue, + displayCountValue, + isChecked, + onChange, + showCheckbox, + visualType, + visualValue, + visualSize, + size, + className, + ], ); const handleChange = () => { @@ -113,12 +128,7 @@ export default function VisualFilterOptionListRow({ fill='none' xmlns='http://www.w3.org/2000/svg' className='cio-check'> - +
    )} diff --git a/src/index.ts b/src/index.ts index 99c1e1f..167851b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,11 +13,7 @@ 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 { ChipVariants, ChipOverrides, ChipProps } from '@/components/chip'; export type { FilterOptionListRowVariants, FilterOptionListRowOverrides, diff --git a/src/stories/components/FilterOptionListRow/FilterOptionListRow.stories.tsx b/src/stories/components/FilterOptionListRow/FilterOptionListRow.stories.tsx index ec0e513..430e373 100644 --- a/src/stories/components/FilterOptionListRow/FilterOptionListRow.stories.tsx +++ b/src/stories/components/FilterOptionListRow/FilterOptionListRow.stories.tsx @@ -87,12 +87,7 @@ export const FilterList: Story = { displayValue='Black' displayCountValue='685' /> - + Date: Wed, 28 Jan 2026 19:23:12 +0300 Subject: [PATCH 03/13] Remove unneeded features --- .../FilterOptionListRow.test.tsx | 12 ++++ .../VisualFilterOptionListRow.test.tsx | 60 +++++-------------- src/components/filter-option-list-row.tsx | 19 +----- .../visual-filter-option-list-row.tsx | 25 +------- .../VisualFilterOptionListRow.stories.tsx | 32 ---------- 5 files changed, 30 insertions(+), 118 deletions(-) diff --git a/spec/components/FilterOptionListRow/FilterOptionListRow.test.tsx b/spec/components/FilterOptionListRow/FilterOptionListRow.test.tsx index f07fa54..79d350f 100644 --- a/spec/components/FilterOptionListRow/FilterOptionListRow.test.tsx +++ b/spec/components/FilterOptionListRow/FilterOptionListRow.test.tsx @@ -252,6 +252,18 @@ describe('FilterOptionListRow component', () => { 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', () => { diff --git a/spec/components/VisualFilterOptionListRow/VisualFilterOptionListRow.test.tsx b/spec/components/VisualFilterOptionListRow/VisualFilterOptionListRow.test.tsx index 4a18d9a..5b88f80 100644 --- a/spec/components/VisualFilterOptionListRow/VisualFilterOptionListRow.test.tsx +++ b/spec/components/VisualFilterOptionListRow/VisualFilterOptionListRow.test.tsx @@ -113,52 +113,6 @@ describe('VisualFilterOptionListRow component', () => { }); }); - describe('visual swatch size', () => { - test('renders sm size swatch', () => { - render( - , - ); - const swatch = document.querySelector('.cio-filter-visual-swatch'); - expect(swatch?.classList.contains('w-4')).toBeTruthy(); - }); - - test('renders md size swatch by default', () => { - render( - , - ); - const swatch = document.querySelector('.cio-filter-visual-swatch'); - expect(swatch?.classList.contains('w-6')).toBeTruthy(); - }); - - test('renders lg size swatch', () => { - render( - , - ); - const swatch = document.querySelector('.cio-filter-visual-swatch'); - expect(swatch?.classList.contains('w-8')).toBeTruthy(); - }); - }); - describe('checkbox behavior', () => { test('checkbox is unchecked by default', () => { render( @@ -315,6 +269,20 @@ describe('VisualFilterOptionListRow component', () => { const swatch = document.querySelector('.cio-filter-visual-swatch'); expect(swatch).toBeInTheDocument(); }); + + test('has text-base class by default', () => { + render( + , + ); + const listItem = screen.getByRole('listitem'); + expect(listItem.classList.contains('text-base')).toBeTruthy(); + }); }); describe('data attributes', () => { diff --git a/src/components/filter-option-list-row.tsx b/src/components/filter-option-list-row.tsx index 9d6ba0a..54de4ac 100644 --- a/src/components/filter-option-list-row.tsx +++ b/src/components/filter-option-list-row.tsx @@ -4,19 +4,7 @@ import { ComponentOverrideProps, IncludeComponentOverrides } from '@/types'; import { cva, VariantProps } from 'class-variance-authority'; const filterOptionListRowVariants = cva( - 'cio-components cio-filter-option-list-row cio-filter-multiple-option', - { - variants: { - size: { - sm: 'text-sm', - md: 'text-base', - lg: 'text-lg', - }, - }, - defaultVariants: { - size: 'md', - }, - }, + 'cio-components cio-filter-option-list-row cio-filter-multiple-option text-base', ); export type FilterOptionListRowVariants = VariantProps; @@ -49,7 +37,6 @@ export type FilterOptionListRowOverrides = ComponentOverrideProps
  • {children}
  • diff --git a/src/components/visual-filter-option-list-row.tsx b/src/components/visual-filter-option-list-row.tsx index 9858a10..05c304e 100644 --- a/src/components/visual-filter-option-list-row.tsx +++ b/src/components/visual-filter-option-list-row.tsx @@ -28,8 +28,8 @@ export interface VisualFilterOptionListRowProps isChecked?: boolean; /** Callback when option selection changes */ onChange?: (value: string) => void; - /** Whether to show the checkbox indicator */ - showCheckbox?: boolean; + /** Position of the checkbox. Can be 'left', 'right', or 'none'. Defaults to 'left' */ + checkboxPosition?: 'left' | 'right' | 'none'; /** Type of visual - 'color' for hex colors, 'image' for image URLs */ visualType: 'color' | 'image'; /** The visual value - hex color code or image URL */ @@ -47,7 +47,7 @@ export default function VisualFilterOptionListRow({ displayCountValue, isChecked = false, onChange, - showCheckbox = true, + checkboxPosition = 'right', visualType, visualValue, componentOverrides, @@ -61,7 +61,7 @@ export default function VisualFilterOptionListRow({ displayCountValue, isChecked, onChange, - showCheckbox, + checkboxPosition, visualType, visualValue, className, @@ -73,7 +73,7 @@ export default function VisualFilterOptionListRow({ displayCountValue, isChecked, onChange, - showCheckbox, + checkboxPosition, visualType, visualValue, className, @@ -84,6 +84,21 @@ export default function VisualFilterOptionListRow({ onChange?.(optionValue); }; + const checkboxVisible = checkboxPosition !== 'none'; + const checkboxEl = checkboxVisible && ( +
    + + + +
    + ); + return (
  • diff --git a/src/styles.css b/src/styles.css index 10f14c3..fd123ac 100644 --- a/src/styles.css +++ b/src/styles.css @@ -48,6 +48,7 @@ align-items: center; cursor: pointer; margin-right: 8px; + margin-left: 8px; background-color: #fff; width: 20px; height: 20px; From ca05d400be76766dfd8f0b8c83ea7b670eeee966 Mon Sep 17 00:00:00 2001 From: Enes Kutay SEZEN Date: Wed, 28 Jan 2026 19:46:52 +0300 Subject: [PATCH 05/13] Move styles --- src/components/filter-option-list-row.tsx | 20 +++-- .../visual-filter-option-list-row.tsx | 22 ++--- src/styles.css | 81 ------------------- 3 files changed, 25 insertions(+), 98 deletions(-) diff --git a/src/components/filter-option-list-row.tsx b/src/components/filter-option-list-row.tsx index f8ba2e5..0c69c05 100644 --- a/src/components/filter-option-list-row.tsx +++ b/src/components/filter-option-list-row.tsx @@ -4,7 +4,7 @@ import { ComponentOverrideProps, IncludeComponentOverrides } from '@/types'; import { cva, VariantProps } from 'class-variance-authority'; const filterOptionListRowVariants = cva( - 'cio-components cio-filter-option-list-row cio-filter-multiple-option text-base', + '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 type FilterOptionListRowVariants = VariantProps; @@ -80,14 +80,14 @@ export default function FilterOptionListRow({ const checkboxVisible = checkboxPosition !== 'none'; const checkboxEl = checkboxVisible && ( -
    +
    + className='cio-check opacity-0 transition-opacity duration-250 group-has-[input:checked]:opacity-100'>
    @@ -99,7 +99,9 @@ export default function FilterOptionListRow({ data-slot='filter-option-list-row' className={cn(filterOptionListRowVariants({ className }))} {...props}> -