diff --git a/.cursor/rules/code-style.mdc b/.cursor/rules/code-style.mdc
index eca5855c6..c144f4c9a 100644
--- a/.cursor/rules/code-style.mdc
+++ b/.cursor/rules/code-style.mdc
@@ -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
diff --git a/.ladle/adapters/PlainComponentAdapter.tsx b/.ladle/adapters/PlainComponentAdapter.tsx
index db277366d..b815202bf 100644
--- a/.ladle/adapters/PlainComponentAdapter.tsx
+++ b/.ladle/adapters/PlainComponentAdapter.tsx
@@ -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) => {
@@ -1359,4 +1360,26 @@ export const PlainComponentAdapter: ComponentsContextType = {
)
},
+ DescriptionList: ({ items, className }: DescriptionListProps) => {
+ const renderTerms = (term: React.ReactNode | React.ReactNode[]) => {
+ const terms = Array.isArray(term) ? term : [term]
+ return terms.map((t, i) =>
{t})
+ }
+
+ const renderDescriptions = (description: React.ReactNode | React.ReactNode[]) => {
+ const descriptions = Array.isArray(description) ? description : [description]
+ return descriptions.map((d, i) => {d})
+ }
+
+ return (
+
+ {items.map((item, index) => (
+
+ {renderTerms(item.term)}
+ {renderDescriptions(item.description)}
+
+ ))}
+
+ )
+ },
}
diff --git a/src/components/Common/UI/DescriptionList/DescriptionList.module.scss b/src/components/Common/UI/DescriptionList/DescriptionList.module.scss
new file mode 100644
index 000000000..df8f59dac
--- /dev/null
+++ b/src/components/Common/UI/DescriptionList/DescriptionList.module.scss
@@ -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);
+ }
+ }
+}
diff --git a/src/components/Common/UI/DescriptionList/DescriptionList.stories.tsx b/src/components/Common/UI/DescriptionList/DescriptionList.stories.tsx
new file mode 100644
index 000000000..2e87a86b6
--- /dev/null
+++ b/src/components/Common/UI/DescriptionList/DescriptionList.stories.tsx
@@ -0,0 +1,160 @@
+import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext'
+
+export default {
+ title: 'UI/Components/DescriptionList',
+}
+
+export const Basic = () => {
+ const Components = useComponentContext()
+ return (
+ First Term,
+ description: First description with some content,
+ },
+ {
+ term: Second Term,
+ description: Second description with more content,
+ },
+ {
+ term: Third Term,
+ description: Third description with even more content,
+ },
+ ]}
+ />
+ )
+}
+
+export const BankAccountExample = () => {
+ const Components = useComponentContext()
+ return (
+ Routing Number,
+ description: 123456789,
+ },
+ {
+ term: Account Number,
+ description: ****1234,
+ },
+ ]}
+ />
+ )
+}
+
+export const MultipleTermsOneDescription = () => {
+ const Components = useComponentContext()
+ return (
+ Firefox,
+ Mozilla Firefox,
+ Fx,
+ ],
+ description: (
+
+ A free, open-source, cross-platform web browser developed by the Mozilla Corporation
+ and volunteers.
+
+ ),
+ },
+ {
+ term: [
+ Chrome,
+ Google Chrome,
+ ],
+ description: (
+
+ A cross-platform web browser developed by Google, based on the Chromium open-source
+ project.
+
+ ),
+ },
+ ]}
+ />
+ )
+}
+
+export const OneTermMultipleDescriptions = () => {
+ const Components = useComponentContext()
+ return (
+ Firefox,
+ description: [
+
+ A free, open-source, cross-platform web browser developed by the Mozilla Corporation
+ and volunteers.
+ ,
+
+ The Red Panda, also known as the Lesser Panda, is a mostly herbivorous mammal,
+ slightly larger than a domestic cat.
+ ,
+ ],
+ },
+ ]}
+ />
+ )
+}
+
+export const MixedPatterns = () => {
+ const Components = useComponentContext()
+ return (
+ Single term, single description,
+ description: A simple key-value pair,
+ },
+ {
+ term: [
+ Multiple,
+ Terms,
+ ],
+ description: One description for multiple terms,
+ },
+ {
+ term: One term,
+ description: [
+ First description,
+ Second description,
+ ],
+ },
+ ]}
+ />
+ )
+}
+
+export const WithCustomClassName = () => {
+ const Components = useComponentContext()
+ return (
+ Custom Styled Term,
+ description: Custom styled description,
+ },
+ ]}
+ />
+ )
+}
+
+export const SingleItem = () => {
+ const Components = useComponentContext()
+ return (
+ Single Term,
+ description: Single description,
+ },
+ ]}
+ />
+ )
+}
diff --git a/src/components/Common/UI/DescriptionList/DescriptionList.test.tsx b/src/components/Common/UI/DescriptionList/DescriptionList.test.tsx
new file mode 100644
index 000000000..8d86c7b3a
--- /dev/null
+++ b/src/components/Common/UI/DescriptionList/DescriptionList.test.tsx
@@ -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(
+ ,
+ )
+
+ 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(
+ ,
+ )
+
+ const dl = container.querySelector('dl')
+ expect(dl).toHaveClass('custom-class')
+ })
+
+ it('renders with ReactNode items', () => {
+ render(
+ Bold Term,
+ description: Italic Description,
+ },
+ ]}
+ />,
+ )
+
+ expect(screen.getByText('Bold Term')).toBeInTheDocument()
+ expect(screen.getByText('Italic Description')).toBeInTheDocument()
+ })
+
+ it('renders empty list when items array is empty', () => {
+ const { container } = render()
+
+ const dl = container.querySelector('dl')
+ expect(dl).toBeInTheDocument()
+ expect(dl?.children.length).toBe(0)
+ })
+
+ it('renders multiple terms for one description', () => {
+ const { container } = render(
+ ,
+ )
+
+ 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(
+ ,
+ )
+
+ 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(
+ ,
+ )
+
+ const dts = container.querySelectorAll('dt')
+ expect(dts).toHaveLength(4)
+
+ const dds = container.querySelectorAll('dd')
+ expect(dds).toHaveLength(4)
+ })
+})
diff --git a/src/components/Common/UI/DescriptionList/DescriptionList.tsx b/src/components/Common/UI/DescriptionList/DescriptionList.tsx
new file mode 100644
index 000000000..620c6b8c7
--- /dev/null
+++ b/src/components/Common/UI/DescriptionList/DescriptionList.tsx
@@ -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) => {t})
+ }
+
+ const renderDescriptions = (description: ReactNode | ReactNode[]) => {
+ const descriptions = Array.isArray(description) ? description : [description]
+ return descriptions.map((d, i) => {d})
+ }
+
+ return (
+
+ {items.map((item, index) => (
+
+ {renderTerms(item.term)}
+ {renderDescriptions(item.description)}
+
+ ))}
+
+ )
+}
diff --git a/src/components/Common/UI/DescriptionList/DescriptionListTypes.ts b/src/components/Common/UI/DescriptionList/DescriptionListTypes.ts
new file mode 100644
index 000000000..35e27b1e0
--- /dev/null
+++ b/src/components/Common/UI/DescriptionList/DescriptionListTypes.ts
@@ -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
diff --git a/src/components/Common/UI/DescriptionList/index.ts b/src/components/Common/UI/DescriptionList/index.ts
new file mode 100644
index 000000000..5d6cbfd00
--- /dev/null
+++ b/src/components/Common/UI/DescriptionList/index.ts
@@ -0,0 +1,2 @@
+export { DescriptionList } from './DescriptionList'
+export type { DescriptionListProps, DescriptionListItem } from './DescriptionListTypes'
diff --git a/src/components/Company/BankAccount/BankAccountList/AccountView.tsx b/src/components/Company/BankAccount/BankAccountList/AccountView.tsx
index f061e505f..23446ef94 100644
--- a/src/components/Company/BankAccount/BankAccountList/AccountView.tsx
+++ b/src/components/Company/BankAccount/BankAccountList/AccountView.tsx
@@ -8,23 +8,17 @@ export function AccountView() {
const Components = useComponentContext()
return (
-
-
-
-
- {t('routingNumberLabel')}
-
- -
- {bankAccount?.routingNumber}
-
-
-
-
-
- {t('accountNumberLabel')}
-
- -
- {bankAccount?.hiddenAccountNumber}
-
-
-
+ {t('routingNumberLabel')},
+ description: {bankAccount?.routingNumber},
+ },
+ {
+ term: {t('accountNumberLabel')},
+ description: {bankAccount?.hiddenAccountNumber},
+ },
+ ]}
+ />
)
}
diff --git a/src/contexts/ComponentAdapter/adapters/defaultComponentAdapter.tsx b/src/contexts/ComponentAdapter/adapters/defaultComponentAdapter.tsx
index eb409011c..32c45bc0c 100644
--- a/src/contexts/ComponentAdapter/adapters/defaultComponentAdapter.tsx
+++ b/src/contexts/ComponentAdapter/adapters/defaultComponentAdapter.tsx
@@ -56,6 +56,8 @@ import type { DialogProps } from '@/components/Common/UI/Dialog/DialogTypes'
import { Dialog } from '@/components/Common/UI/Dialog'
import type { LoadingSpinnerProps } from '@/components/Common/UI/LoadingSpinner/LoadingSpinnerTypes'
import { LoadingSpinner } from '@/components/Common/UI/LoadingSpinner'
+import type { DescriptionListProps } from '@/components/Common/UI/DescriptionList/DescriptionListTypes'
+import { DescriptionList } from '@/components/Common/UI/DescriptionList'
export const defaultComponents: ComponentsContextType = {
Alert: (props: AlertProps) => ,
@@ -86,4 +88,5 @@ export const defaultComponents: ComponentsContextType = {
Tabs: (props: TabsProps) => ,
Dialog: (props: DialogProps) => ,
LoadingSpinner: (props: LoadingSpinnerProps) => ,
+ DescriptionList: (props: DescriptionListProps) => ,
}
diff --git a/src/contexts/ComponentAdapter/componentAdapterTypes.ts b/src/contexts/ComponentAdapter/componentAdapterTypes.ts
index 2dc86d992..18d880af6 100644
--- a/src/contexts/ComponentAdapter/componentAdapterTypes.ts
+++ b/src/contexts/ComponentAdapter/componentAdapterTypes.ts
@@ -29,3 +29,4 @@ export type { PaginationControlProps } from '@/components/Common/PaginationContr
export type { TextProps } from '@/components/Common/UI/Text/TextTypes'
export type { CalendarPreviewProps } from '@/components/Common/UI/CalendarPreview/CalendarPreviewTypes'
export type { DialogProps } from '@/components/Common/UI/Dialog/DialogTypes'
+export type { DescriptionListProps } from '@/components/Common/UI/DescriptionList/DescriptionListTypes'
diff --git a/src/contexts/ComponentAdapter/useComponentContext.ts b/src/contexts/ComponentAdapter/useComponentContext.ts
index bb65ca000..2a1cbff10 100644
--- a/src/contexts/ComponentAdapter/useComponentContext.ts
+++ b/src/contexts/ComponentAdapter/useComponentContext.ts
@@ -27,6 +27,7 @@ import type { BreadcrumbsProps } from '@/components/Common/UI/Breadcrumbs/Breadc
import type { TabsProps } from '@/components/Common/UI/Tabs/TabsTypes'
import type { DialogProps } from '@/components/Common/UI/Dialog/DialogTypes'
import type { LoadingSpinnerProps } from '@/components/Common/UI/LoadingSpinner/LoadingSpinnerTypes'
+import type { DescriptionListProps } from '@/components/Common/UI/DescriptionList/DescriptionListTypes'
export interface ComponentsContextType {
Alert: (props: AlertProps) => JSX.Element | null
@@ -58,6 +59,7 @@ export interface ComponentsContextType {
Tabs: (props: TabsProps) => JSX.Element | null
Dialog: (props: DialogProps) => JSX.Element | null
LoadingSpinner: (props: LoadingSpinnerProps) => JSX.Element | null
+ DescriptionList: (props: DescriptionListProps) => JSX.Element | null
}
export const ComponentsContext = createContext(null)
diff --git a/src/styles/_Base.scss b/src/styles/_Base.scss
index 3b0993c25..7a0aeb164 100644
--- a/src/styles/_Base.scss
+++ b/src/styles/_Base.scss
@@ -14,14 +14,6 @@
box-sizing: border-box;
}
- dl {
- width: 100%;
- div:not(:last-child) {
- padding-bottom: toRem(20);
- margin-bottom: toRem(20);
- border-bottom: 1px solid var(--g-colorBorder);
- }
- }
small {
font-size: var(--g-fontSizeSmall);
}