From aaab684db23485c2eba257a10c7896fc7bcf14b1 Mon Sep 17 00:00:00 2001 From: Jeff Johnson Date: Thu, 23 Oct 2025 14:21:03 -0700 Subject: [PATCH 1/3] feat: introduce
component to our component adapter --- .../DescriptionList.module.scss | 13 ++ .../DescriptionList.stories.tsx | 160 ++++++++++++++++++ .../DescriptionList/DescriptionList.test.tsx | 117 +++++++++++++ .../UI/DescriptionList/DescriptionList.tsx | 31 ++++ .../DescriptionList/DescriptionListTypes.ts | 13 ++ .../Common/UI/DescriptionList/index.ts | 2 + .../BankAccountList/AccountView.tsx | 30 ++-- .../adapters/defaultComponentAdapter.tsx | 3 + .../ComponentAdapter/componentAdapterTypes.ts | 1 + .../ComponentAdapter/useComponentContext.ts | 2 + src/styles/_Base.scss | 8 - 11 files changed, 354 insertions(+), 26 deletions(-) create mode 100644 src/components/Common/UI/DescriptionList/DescriptionList.module.scss create mode 100644 src/components/Common/UI/DescriptionList/DescriptionList.stories.tsx create mode 100644 src/components/Common/UI/DescriptionList/DescriptionList.test.tsx create mode 100644 src/components/Common/UI/DescriptionList/DescriptionList.tsx create mode 100644 src/components/Common/UI/DescriptionList/DescriptionListTypes.ts create mode 100644 src/components/Common/UI/DescriptionList/index.ts 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..c360150ef --- /dev/null +++ b/src/components/Common/UI/DescriptionList/DescriptionList.module.scss @@ -0,0 +1,13 @@ +@use '@/styles/Helpers' as *; + +.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); } From 2cdb513ae1b292383095eed2e272da6970adfb5e Mon Sep 17 00:00:00 2001 From: Jeff Johnson Date: Thu, 23 Oct 2025 14:26:51 -0700 Subject: [PATCH 2/3] feat: add DescriptionList component with flexible term/description support - Create DescriptionList component in Common/UI directory - Support single or multiple terms per description - Support single or multiple descriptions per term - Add component to adapter system (default and plain adapters) - Replace dl usage in AccountView with DescriptionList component - Remove global dl styles from Base.scss - Add comprehensive tests and Ladle stories --- .ladle/adapters/PlainComponentAdapter.tsx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) 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)} +
+ ))} +
+ ) + }, } From 036eb3891d8cfd506f81dcd5b216c666984a315a Mon Sep 17 00:00:00 2001 From: Jeff Johnson Date: Fri, 24 Oct 2025 08:43:59 -0700 Subject: [PATCH 3/3] chore: pr feedback --- .cursor/rules/code-style.mdc | 13 +++++++++++++ .../UI/DescriptionList/DescriptionList.module.scss | 2 -- 2 files changed, 13 insertions(+), 2 deletions(-) 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/src/components/Common/UI/DescriptionList/DescriptionList.module.scss b/src/components/Common/UI/DescriptionList/DescriptionList.module.scss index c360150ef..df8f59dac 100644 --- a/src/components/Common/UI/DescriptionList/DescriptionList.module.scss +++ b/src/components/Common/UI/DescriptionList/DescriptionList.module.scss @@ -1,5 +1,3 @@ -@use '@/styles/Helpers' as *; - .root { width: 100%;