diff --git a/.storybook/adapters/PlainComponentAdapter.tsx b/.storybook/adapters/PlainComponentAdapter.tsx index 94f09094e..354669df6 100644 --- a/.storybook/adapters/PlainComponentAdapter.tsx +++ b/.storybook/adapters/PlainComponentAdapter.tsx @@ -136,14 +136,15 @@ export const PlainComponentAdapter: ComponentsContextType = { ) }, - Box: ({ children, footer, className }: BoxProps) => { - return ( -
-
{children}
- {footer &&
{footer}
} + Box: ({ children, header, footer, withPadding = true, className }: BoxProps) => ( +
+ {header &&
{header}
} +
+ {children}
- ) - }, + {footer &&
{footer}
} +
+ ), TextInput: ({ label, @@ -1117,10 +1118,15 @@ export const PlainComponentAdapter: ComponentsContextType = { className, 'aria-label': ariaLabel, emptyState, + variant, ...props }: TableProps) => { + const embeddedStyles = + variant === 'embedded' + ? { border: 'none', borderRadius: 0, boxShadow: 'none', background: 'transparent' } + : undefined return ( - +
{headers.map((header: TableData) => ( diff --git a/docs/component-adapter/component-inventory.md b/docs/component-adapter/component-inventory.md index 60452bada..02886c52d 100644 --- a/docs/component-adapter/component-inventory.md +++ b/docs/component-adapter/component-inventory.md @@ -5,6 +5,7 @@ - [BannerProps](#bannerprops) - [BaseListProps](#baselistprops) - [BoxProps](#boxprops) +- [BoxSectionProps](#boxsectionprops) - [BreadcrumbsProps](#breadcrumbsprops) - [Breadcrumb](#breadcrumb) - [ButtonIconProps](#buttoniconprops) @@ -97,11 +98,21 @@ ## BoxProps -| Prop | Type | Required | Description | -| ------------- | ----------------- | -------- | ------------------------------------------------------------------------- | -| **children** | `React.ReactNode` | Yes | Content to be displayed inside the box | -| **footer** | `React.ReactNode` | No | Content rendered at the bottom of the box with an edge-to-edge top border | -| **className** | `string` | No | CSS className to be applied | +| Prop | Type | Required | Description | +| --------------- | ----------------- | -------- | ----------- | +| **children** | `React.ReactNode` | Yes | - | +| **header** | `React.ReactNode` | No | - | +| **footer** | `React.ReactNode` | No | - | +| **withPadding** | `boolean` | No | - | +| **className** | `string` | No | - | + +## BoxSectionProps + +| Prop | Type | Required | Description | +| ------------- | ---------------------- | -------- | ----------- | +| **children** | `React.ReactNode` | Yes | - | +| **className** | `string` | No | - | +| **variant** | `"default" \| "flush"` | No | - | ## BreadcrumbsProps @@ -575,7 +586,7 @@ type PaginationItemsPerPage = 5 | 10 | 50 | **rows** | [TableRow](#tablerow)[] | Yes | Array of rows to be displayed in the table | | **footer** | [TableData](#tabledata)[] | No | Array of footer cells for the table | | **emptyState** | `React.ReactNode` | No | Content to display when the table has no rows | -| **variant** | `"default" \| "minimal"` | No | Visual style variant of the table | +| **variant** | `"default" \| "minimal" \| "embedded"` | No | Visual style variant of the table | | **hasCheckboxColumn** | `boolean` | No | Whether the first column contains checkboxes (affects which column gets leading variant) | | **className** | `string` | No | - | | **id** | `string` | No | - | diff --git a/src/components/Common/UI/Box/Box.module.scss b/src/components/Common/UI/Box/Box.module.scss index d79954f4b..28d07fc8c 100644 --- a/src/components/Common/UI/Box/Box.module.scss +++ b/src/components/Common/UI/Box/Box.module.scss @@ -1,4 +1,4 @@ -.boxContainer { +.root { width: 100%; background-color: var(--g-colorBody); box-shadow: var(--g-shadowResting); @@ -6,11 +6,20 @@ border: 1px solid var(--g-colorBorderSecondary); } -.boxBody { +.header { padding: toRem(20); + border-bottom: 1px solid var(--g-colorBorderSecondary); } -.boxFooter { +.content { + padding: toRem(20); +} + +.contentFlush { + padding: 0; +} + +.footer { padding: toRem(20); border-top: 1px solid var(--g-colorBorderSecondary); } diff --git a/src/components/Common/UI/Box/Box.stories.tsx b/src/components/Common/UI/Box/Box.stories.tsx index f74d03fa0..519fd77ee 100644 --- a/src/components/Common/UI/Box/Box.stories.tsx +++ b/src/components/Common/UI/Box/Box.stories.tsx @@ -1,11 +1,12 @@ import type { StoryObj } from '@storybook/react-vite' -import type { BoxProps } from './BoxTypes' import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentContext' +import { Flex } from '@/components/Common/Flex' +import PlusCircleIcon from '@/assets/icons/plus-circle.svg?react' -const BoxWrapper = (args: Omit) => { +const BoxWrapper = () => { const Components = useComponentContext() return ( - + This is content inside a box. ) @@ -18,8 +19,17 @@ export default { type Story = StoryObj -export const Default: Story = { - args: {}, +export const Default: Story = {} + +export const WithHeader: Story = { + render: () => { + const Components = useComponentContext() + return ( + Box Header}> + This is the main content area with padding. + + ) + }, } export const WithFooter: Story = { @@ -39,6 +49,38 @@ export const WithFooter: Story = { }, } +export const WithAllSections: Story = { + render: () => { + const Components = useComponentContext() + return ( + + + Box Header + + This is a super cool description of the box header. + + + {}}> + + Do a thing + + + } + footer={ + {}}> + Add another something + + } + > + + There is so much we can do here! + + ) + }, +} + export const WithCustomClassName: Story = { decorators: [ Story => ( @@ -48,7 +90,71 @@ export const WithCustomClassName: Story = { ), ], - args: { - className: 'custom-box', + render: () => { + const Components = useComponentContext() + return ( + + This box has a custom className applied. + + ) + }, +} + +export const FlushContent: Story = { + render: () => { + const Components = useComponentContext() + return ( + + This content has no padding (flush variant). + + ) + }, +} + +export const WithEmbeddedTable: Story = { + render: () => { + const Components = useComponentContext() + return ( + Team Members} + withPadding={false} + > + + + ) }, } diff --git a/src/components/Common/UI/Box/Box.test.tsx b/src/components/Common/UI/Box/Box.test.tsx index 62d77da1c..81a9f9852 100644 --- a/src/components/Common/UI/Box/Box.test.tsx +++ b/src/components/Common/UI/Box/Box.test.tsx @@ -4,22 +4,51 @@ import { Box } from './Box' import { renderWithProviders } from '@/test-utils/renderWithProviders' describe('Box Component', () => { - test('renders children correctly', () => { + test('renders content correctly', () => { renderWithProviders(Test Content) expect(screen.getByText('Test Content')).toBeInTheDocument() }) + test('renders header when provided', () => { + renderWithProviders(Content) + + expect(screen.getByText('Header Text')).toBeInTheDocument() + expect(screen.getByText('Content')).toBeInTheDocument() + }) + test('renders footer when provided', () => { - renderWithProviders(Save}>Content) + renderWithProviders(Save}>Content) expect(screen.getByText('Save')).toBeInTheDocument() }) - test('does not render footer when omitted', () => { - renderWithProviders(Content) + test('renders all sections together', () => { + renderWithProviders( + + Content + , + ) - expect(screen.queryByRole('button')).not.toBeInTheDocument() + expect(screen.getByText('Header')).toBeInTheDocument() + expect(screen.getByText('Content')).toBeInTheDocument() + expect(screen.getByText('Footer')).toBeInTheDocument() + }) + + test('renders flush content variant', () => { + renderWithProviders(Flush Content) + + expect(screen.getByText('Flush Content')).toBeInTheDocument() + }) + + test('renders default and flush content with different classes', () => { + const { rerender } = renderWithProviders(Default Content) + const defaultWrapper = screen.getByText('Default Content').closest('div')! + const defaultClassName = defaultWrapper.className + + rerender(Flush Content) + const flushWrapper = screen.getByText('Flush Content').closest('div')! + expect(defaultClassName).not.toBe(flushWrapper.className) }) test('applies custom className', () => { @@ -28,41 +57,53 @@ describe('Box Component', () => { expect(screen.getByTestId('data-box')).toHaveClass('custom-style') }) + test('does not render header section when header is not provided', () => { + renderWithProviders(Content) + + const box = screen.getByTestId('data-box') + expect(box.children).toHaveLength(1) + }) + + test('does not render footer section when footer is not provided', () => { + renderWithProviders(Content) + + const box = screen.getByTestId('data-box') + expect(box.children).toHaveLength(1) + }) + describe('Accessibility', () => { const testCases = [ { - name: 'basic box', - props: { children: 'Basic box content' }, + name: 'box with content', + element: Basic box content, }, { name: 'box with custom className', - props: { className: 'custom-style', children: 'Styled box' }, + element: Styled box, }, { name: 'box with complex content', - props: { - children: ( -
-

Box Title

-

Box description with multiple elements.

- -
- ), - }, + element: ( + Box Title}> +

Box description with multiple elements.

+ +
+ ), }, { - name: 'box with footer', - props: { - children: 'Main content', - footer: , - }, + name: 'box with all sections', + element: ( + Save}> + Main content + + ), }, ] it.each(testCases)( 'should not have any accessibility violations - $name', - async ({ props }) => { - const { container } = renderWithProviders() + async ({ element }) => { + const { container } = renderWithProviders(element) await expectNoAxeViolations(container) }, ) diff --git a/src/components/Common/UI/Box/Box.tsx b/src/components/Common/UI/Box/Box.tsx index 9fe625473..266218ecd 100644 --- a/src/components/Common/UI/Box/Box.tsx +++ b/src/components/Common/UI/Box/Box.tsx @@ -1,12 +1,13 @@ import cn from 'classnames' import styles from './Box.module.scss' -import { type BoxProps } from '@/components/Common/UI/Box/BoxTypes' +import type { BoxProps } from '@/components/Common/UI/Box/BoxTypes' -export function Box({ children, footer, className }: BoxProps) { +export function Box({ children, header, footer, withPadding = true, className }: BoxProps) { return ( -
-
{children}
- {footer &&
{footer}
} +
+ {header &&
{header}
} +
{children}
+ {footer &&
{footer}
}
) } diff --git a/src/components/Common/UI/Box/BoxTypes.ts b/src/components/Common/UI/Box/BoxTypes.ts index 13e3a17f6..4d94238a1 100644 --- a/src/components/Common/UI/Box/BoxTypes.ts +++ b/src/components/Common/UI/Box/BoxTypes.ts @@ -1,16 +1,16 @@ import type { ReactNode } from 'react' export interface BoxProps { - /** - * Content to be displayed inside the box - */ children: ReactNode - /** - * Content rendered at the bottom of the box with an edge-to-edge top border - */ + header?: ReactNode footer?: ReactNode - /** - * CSS className to be applied - */ + withPadding?: boolean className?: string } + +/** @deprecated Use BoxProps with header/footer/children instead of compound subcomponents */ +export interface BoxSectionProps { + children: ReactNode + className?: string + variant?: 'default' | 'flush' +} diff --git a/src/components/Common/UI/Button/Button.module.scss b/src/components/Common/UI/Button/Button.module.scss index 1eed02fe6..c6d05e125 100644 --- a/src/components/Common/UI/Button/Button.module.scss +++ b/src/components/Common/UI/Button/Button.module.scss @@ -3,9 +3,10 @@ position: relative; border: none; display: inline-flex; + flex-shrink: 0; align-items: center; justify-content: center; - gap: toRem(4); + gap: toRem(8); color: var(--g-colorPrimaryContent); background: var(--g-colorPrimary); border-radius: var(--g-buttonRadius); diff --git a/src/components/Common/UI/Heading/Heading.module.scss b/src/components/Common/UI/Heading/Heading.module.scss index 8fd5d13a4..cfb619999 100644 --- a/src/components/Common/UI/Heading/Heading.module.scss +++ b/src/components/Common/UI/Heading/Heading.module.scss @@ -2,7 +2,7 @@ .root { text-wrap: balance; margin: 0; - font-weight: var(--g-fontWeightMedium); + font-weight: var(--g-fontWeightSemibold); line-height: 1.2; } diff --git a/src/components/Common/UI/Table/Table.module.scss b/src/components/Common/UI/Table/Table.module.scss index a821748f3..7a255719f 100644 --- a/src/components/Common/UI/Table/Table.module.scss +++ b/src/components/Common/UI/Table/Table.module.scss @@ -229,4 +229,35 @@ } } } + + &[data-variant='embedded'] { + :global(.react-aria-Table) { + border: none; + border-radius: 0; + box-shadow: none; + background: transparent; + + :global(.react-aria-TableHeader) { + th { + &:first-child { + border-top-left-radius: 0; + } + + &:last-child { + border-top-right-radius: 0; + } + } + } + + :global(.react-aria-Row[data-footer='true'] .react-aria-Cell) { + &:first-child { + border-bottom-left-radius: 0; + } + + &:last-child { + border-bottom-right-radius: 0; + } + } + } + } } diff --git a/src/components/Common/UI/Table/Table.stories.tsx b/src/components/Common/UI/Table/Table.stories.tsx index 0ab093e7c..b9a3d311b 100644 --- a/src/components/Common/UI/Table/Table.stories.tsx +++ b/src/components/Common/UI/Table/Table.stories.tsx @@ -410,6 +410,47 @@ export const MinimalWithComplexContent = () => { ) } +export const EmbeddedVariant = () => { + const { Table } = useComponentContext() + + const headers: TableData[] = [ + { key: 'name-header', content: 'Name' }, + { key: 'email-header', content: 'Email' }, + { key: 'role-header', content: 'Role' }, + ] + + const rows: TableRow[] = [ + { + key: 'row-1', + data: [ + { key: 'name-1', content: 'John Doe' }, + { key: 'email-1', content: 'john@example.com' }, + { key: 'role-1', content: 'Admin' }, + ], + }, + { + key: 'row-2', + data: [ + { key: 'name-2', content: 'Jane Smith' }, + { key: 'email-2', content: 'jane@example.com' }, + { key: 'role-2', content: 'User' }, + ], + }, + { + key: 'row-3', + data: [ + { key: 'name-3', content: 'Bob Johnson' }, + { key: 'email-3', content: 'bob@example.com' }, + { key: 'role-3', content: 'Editor' }, + ], + }, + ] + + return ( +
+ ) +} + export const VariantComparison = () => { const { Table } = useComponentContext() @@ -452,6 +493,17 @@ export const VariantComparison = () => {
+
+

+ Embedded Variant +

+
+ ) } diff --git a/src/components/Common/UI/Table/TableTypes.ts b/src/components/Common/UI/Table/TableTypes.ts index a2f579eb9..b0fd42c98 100644 --- a/src/components/Common/UI/Table/TableTypes.ts +++ b/src/components/Common/UI/Table/TableTypes.ts @@ -45,7 +45,7 @@ export interface TableProps extends Pick< /** * Visual style variant of the table */ - variant?: 'default' | 'minimal' + variant?: 'default' | 'minimal' | 'embedded' /** * Whether the first column contains checkboxes (affects which column gets leading variant) */ diff --git a/src/contexts/ComponentAdapter/componentAdapterTypes.ts b/src/contexts/ComponentAdapter/componentAdapterTypes.ts index 75c71d9c8..d931d8fc1 100644 --- a/src/contexts/ComponentAdapter/componentAdapterTypes.ts +++ b/src/contexts/ComponentAdapter/componentAdapterTypes.ts @@ -2,7 +2,7 @@ export type { ProgressBarProps } from '@/components/Common/UI/ProgressBar/Progre export type { BreadcrumbsProps } from '@/components/Common/UI/Breadcrumbs/BreadcrumbsTypes' export type { ButtonProps, ButtonIconProps } from '@/components/Common/UI/Button/ButtonTypes' export type { CardProps } from '@/components/Common/UI/Card/CardTypes' -export type { BoxProps } from '@/components/Common/UI/Box/BoxTypes' +export type { BoxProps, BoxSectionProps } from '@/components/Common/UI/Box/BoxTypes' export type { CheckboxProps } from '@/components/Common/UI/Checkbox/CheckboxTypes' export type { CheckboxGroupProps,