Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .cursor/rules/code-style.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@ interface Props {
- **Legal requirements**: Copyright notices, license headers
- **Performance workarounds**: When you've tried to make it readable but performance requires the complex version

## SCSS Imports

Do not include `@use` imports in `.module.scss` files for modules that are already globally available via Vite's preprocessor configuration. The `@/styles/Helpers` module is globally available and should never be re-imported in individual component stylesheets.

```scss
// Bad: Remove this line - @/styles/Helpers is globally available
@use '@/styles/Helpers' as *;

.root {
padding: toRem(20);
}
```

## Refactoring Tips

1. **Extract complex logic** into well-named functions
Expand Down
23 changes: 23 additions & 0 deletions .ladle/adapters/PlainComponentAdapter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import type { CalendarPreviewProps } from '@/components/Common/UI/CalendarPrevie
import type { DialogProps } from '@/components/Common/UI/Dialog/DialogTypes'
import type { LoadingSpinnerProps } from '@/components/Common/UI/LoadingSpinner/LoadingSpinnerTypes'
import type { PaginationItemsPerPage } from '@/components/Common/PaginationControl/PaginationControlTypes'
import type { DescriptionListProps } from '@/components/Common/UI/DescriptionList/DescriptionListTypes'

export const PlainComponentAdapter: ComponentsContextType = {
Alert: ({ label, children, status = 'info', icon }: AlertProps) => {
Expand Down Expand Up @@ -1359,4 +1360,26 @@ export const PlainComponentAdapter: ComponentsContextType = {
</div>
)
},
DescriptionList: ({ items, className }: DescriptionListProps) => {
const renderTerms = (term: React.ReactNode | React.ReactNode[]) => {
const terms = Array.isArray(term) ? term : [term]
return terms.map((t, i) => <dt key={i}>{t}</dt>)
}

const renderDescriptions = (description: React.ReactNode | React.ReactNode[]) => {
const descriptions = Array.isArray(description) ? description : [description]
return descriptions.map((d, i) => <dd key={i}>{d}</dd>)
}

return (
<dl className={className}>
{items.map((item, index) => (
<div key={index}>
{renderTerms(item.term)}
{renderDescriptions(item.description)}
</div>
))}
</dl>
)
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.root {
width: 100%;

.item {
&:not(:last-child) {
padding-bottom: toRem(20);
margin-bottom: toRem(20);
border-bottom: 1px solid var(--g-colorBorder);
}
}
}
160 changes: 160 additions & 0 deletions src/components/Common/UI/DescriptionList/DescriptionList.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext'

export default {
title: 'UI/Components/DescriptionList',
}

export const Basic = () => {
const Components = useComponentContext()
return (
<Components.DescriptionList
items={[
{
term: <Components.Text>First Term</Components.Text>,
description: <Components.Text>First description with some content</Components.Text>,
},
{
term: <Components.Text>Second Term</Components.Text>,
description: <Components.Text>Second description with more content</Components.Text>,
},
{
term: <Components.Text>Third Term</Components.Text>,
description: <Components.Text>Third description with even more content</Components.Text>,
},
]}
/>
)
}

export const BankAccountExample = () => {
const Components = useComponentContext()
return (
<Components.DescriptionList
items={[
{
term: <Components.Text>Routing Number</Components.Text>,
description: <Components.Text>123456789</Components.Text>,
},
{
term: <Components.Text>Account Number</Components.Text>,
description: <Components.Text>****1234</Components.Text>,
},
]}
/>
)
}

export const MultipleTermsOneDescription = () => {
const Components = useComponentContext()
return (
<Components.DescriptionList
items={[
{
term: [
<Components.Text key="1">Firefox</Components.Text>,
<Components.Text key="2">Mozilla Firefox</Components.Text>,
<Components.Text key="3">Fx</Components.Text>,
],
description: (
<Components.Text>
A free, open-source, cross-platform web browser developed by the Mozilla Corporation
and volunteers.
</Components.Text>
),
},
{
term: [
<Components.Text key="1">Chrome</Components.Text>,
<Components.Text key="2">Google Chrome</Components.Text>,
],
description: (
<Components.Text>
A cross-platform web browser developed by Google, based on the Chromium open-source
project.
</Components.Text>
),
},
]}
/>
)
}

export const OneTermMultipleDescriptions = () => {
const Components = useComponentContext()
return (
<Components.DescriptionList
items={[
{
term: <Components.Text>Firefox</Components.Text>,
description: [
<Components.Text key="1">
A free, open-source, cross-platform web browser developed by the Mozilla Corporation
and volunteers.
</Components.Text>,
<Components.Text key="2">
The Red Panda, also known as the Lesser Panda, is a mostly herbivorous mammal,
slightly larger than a domestic cat.
</Components.Text>,
],
},
]}
/>
)
}

export const MixedPatterns = () => {
const Components = useComponentContext()
return (
<Components.DescriptionList
items={[
{
term: <Components.Text>Single term, single description</Components.Text>,
description: <Components.Text>A simple key-value pair</Components.Text>,
},
{
term: [
<Components.Text key="1">Multiple</Components.Text>,
<Components.Text key="2">Terms</Components.Text>,
],
description: <Components.Text>One description for multiple terms</Components.Text>,
},
{
term: <Components.Text>One term</Components.Text>,
description: [
<Components.Text key="1">First description</Components.Text>,
<Components.Text key="2">Second description</Components.Text>,
],
},
]}
/>
)
}

export const WithCustomClassName = () => {
const Components = useComponentContext()
return (
<Components.DescriptionList
className="custom-class"
items={[
{
term: <Components.Text>Custom Styled Term</Components.Text>,
description: <Components.Text>Custom styled description</Components.Text>,
},
]}
/>
)
}

export const SingleItem = () => {
const Components = useComponentContext()
return (
<Components.DescriptionList
items={[
{
term: <Components.Text>Single Term</Components.Text>,
description: <Components.Text>Single description</Components.Text>,
},
]}
/>
)
}
117 changes: 117 additions & 0 deletions src/components/Common/UI/DescriptionList/DescriptionList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { DescriptionList } from './DescriptionList'

describe('DescriptionList', () => {
it('renders a description list with items', () => {
render(
<DescriptionList
items={[
{ term: 'Term 1', description: 'Description 1' },
{ term: 'Term 2', description: 'Description 2' },
]}
/>,
)

expect(screen.getByText('Term 1')).toBeInTheDocument()
expect(screen.getByText('Description 1')).toBeInTheDocument()
expect(screen.getByText('Term 2')).toBeInTheDocument()
expect(screen.getByText('Description 2')).toBeInTheDocument()
})

it('applies custom className', () => {
const { container } = render(
<DescriptionList className="custom-class" items={[{ term: 'Term', description: 'Desc' }]} />,
)

const dl = container.querySelector('dl')
expect(dl).toHaveClass('custom-class')
})

it('renders with ReactNode items', () => {
render(
<DescriptionList
items={[
{
term: <strong>Bold Term</strong>,
description: <em>Italic Description</em>,
},
]}
/>,
)

expect(screen.getByText('Bold Term')).toBeInTheDocument()
expect(screen.getByText('Italic Description')).toBeInTheDocument()
})

it('renders empty list when items array is empty', () => {
const { container } = render(<DescriptionList items={[]} />)

const dl = container.querySelector('dl')
expect(dl).toBeInTheDocument()
expect(dl?.children.length).toBe(0)
})

it('renders multiple terms for one description', () => {
const { container } = render(
<DescriptionList
items={[
{
term: ['Firefox', 'Mozilla Firefox', 'Fx'],
description: 'A web browser',
},
]}
/>,
)

const dts = container.querySelectorAll('dt')
expect(dts).toHaveLength(3)
expect(dts[0]).toHaveTextContent('Firefox')
expect(dts[1]).toHaveTextContent('Mozilla Firefox')
expect(dts[2]).toHaveTextContent('Fx')

const dds = container.querySelectorAll('dd')
expect(dds).toHaveLength(1)
expect(dds[0]).toHaveTextContent('A web browser')
})

it('renders one term with multiple descriptions', () => {
const { container } = render(
<DescriptionList
items={[
{
term: 'Firefox',
description: ['A web browser', 'The Red Panda'],
},
]}
/>,
)

const dts = container.querySelectorAll('dt')
expect(dts).toHaveLength(1)
expect(dts[0]).toHaveTextContent('Firefox')

const dds = container.querySelectorAll('dd')
expect(dds).toHaveLength(2)
expect(dds[0]).toHaveTextContent('A web browser')
expect(dds[1]).toHaveTextContent('The Red Panda')
})

it('renders mixed patterns of terms and descriptions', () => {
const { container } = render(
<DescriptionList
items={[
{ term: 'Single', description: 'Single' },
{ term: ['Multiple', 'Terms'], description: 'One desc' },
{ term: 'One term', description: ['Desc 1', 'Desc 2'] },
]}
/>,
)

const dts = container.querySelectorAll('dt')
expect(dts).toHaveLength(4)

const dds = container.querySelectorAll('dd')
expect(dds).toHaveLength(4)
})
})
31 changes: 31 additions & 0 deletions src/components/Common/UI/DescriptionList/DescriptionList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import classNames from 'classnames'
import type { ReactNode } from 'react'
import { type DescriptionListProps, DescriptionListDefaults } from './DescriptionListTypes'
import styles from './DescriptionList.module.scss'
import { applyMissingDefaults } from '@/helpers/applyMissingDefaults'

export function DescriptionList(rawProps: DescriptionListProps) {
const resolvedProps = applyMissingDefaults(rawProps, DescriptionListDefaults)
const { items, className } = resolvedProps

const renderTerms = (term: ReactNode | ReactNode[]) => {
const terms = Array.isArray(term) ? term : [term]
return terms.map((t, i) => <dt key={i}>{t}</dt>)
}

const renderDescriptions = (description: ReactNode | ReactNode[]) => {
const descriptions = Array.isArray(description) ? description : [description]
return descriptions.map((d, i) => <dd key={i}>{d}</dd>)
}

return (
<dl className={classNames(styles.root, className)}>
{items.map((item, index) => (
<div key={index} className={styles.item}>
{renderTerms(item.term)}
{renderDescriptions(item.description)}
</div>
))}
</dl>
)
}
13 changes: 13 additions & 0 deletions src/components/Common/UI/DescriptionList/DescriptionListTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { ReactNode } from 'react'

export interface DescriptionListItem {
term: ReactNode | ReactNode[]
description: ReactNode | ReactNode[]
}

export interface DescriptionListProps {
items: DescriptionListItem[]
className?: string
}

export const DescriptionListDefaults = {} as const satisfies Partial<DescriptionListProps>
2 changes: 2 additions & 0 deletions src/components/Common/UI/DescriptionList/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { DescriptionList } from './DescriptionList'
export type { DescriptionListProps, DescriptionListItem } from './DescriptionListTypes'
Loading