diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-10-18 19:25:02 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-11-11 18:14:41 +0100 |
| commit | 94448fa278ab352a741ff13f22d6104869571144 (patch) | |
| tree | 2185e77f2866d11a0144d4ac5a01c71a76807341 /src | |
| parent | c153f93dc8691a71dc76aad3dd618298da9d238a (diff) | |
feat(components): add a generic Grid component
* merge Columns, Gallery and CardsList into Grid component
* add more options to control the grid
Diffstat (limited to 'src')
28 files changed, 430 insertions, 709 deletions
diff --git a/src/components/atoms/lists/list/list.module.scss b/src/components/atoms/lists/list/list.module.scss index 5a38b44..fc23ce5 100644 --- a/src/components/atoms/lists/list/list.module.scss +++ b/src/components/atoms/lists/list/list.module.scss @@ -15,14 +15,12 @@ } } - &--stack#{&} { - &--ordered, - &--unordered { - @extend %regular-list; - } - } - &--no-marker { list-style-type: none; } } + +.list--ordered:where(.list--stack), +.list--unordered:where(.list--stack) { + @extend %regular-list; +} diff --git a/src/components/molecules/grid/grid.module.scss b/src/components/molecules/grid/grid.module.scss new file mode 100644 index 0000000..e253a89 --- /dev/null +++ b/src/components/molecules/grid/grid.module.scss @@ -0,0 +1,41 @@ +.wrapper { + display: grid; + gap: var(--gap); + + &--is-centered { + place-content: center; + } + + &--has-fixed-size { + grid-template-columns: repeat( + var(--col, auto-fit), + min(100vw - (var(--spacing-md) * 2), var(--size)) + ); + } + + &--has-min-size { + grid-template-columns: repeat( + var(--col, auto-fit), + minmax( + min(100vw - (var(--spacing-md) * 2), var(--size-min)), + var(--size-max, 1fr) + ) + ); + } + + &:not(#{&}--has-fixed-size):not(#{&}--has-min-size) { + grid-template-columns: repeat( + var(--col, auto-fit), + minmax(0, var(--size-max, 1fr)) + ); + } +} + +.item { + display: flex; + flex-flow: row wrap; + + > * { + flex: 1; + } +} diff --git a/src/components/molecules/grid/grid.stories.tsx b/src/components/molecules/grid/grid.stories.tsx new file mode 100644 index 0000000..ce3ee2b --- /dev/null +++ b/src/components/molecules/grid/grid.stories.tsx @@ -0,0 +1,140 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import type { FC, ReactNode } from 'react'; +import { Grid } from './grid'; + +export default { + title: 'Molecules/Grid', + component: Grid, + argTypes: { + items: { + description: 'The grid items.', + type: { + name: 'object', + required: true, + value: {}, + }, + }, + }, +} as ComponentMeta<typeof Grid>; + +const Template: ComponentStory<typeof Grid> = (args) => <Grid {...args} />; + +type ItemProps = { + children: ReactNode; +}; + +const Item: FC<ItemProps> = ({ children }) => ( + <div style={{ border: '1px solid #000', padding: '1rem' }}>{children}</div> +); + +export const Default = Template.bind({}); +Default.args = { + items: [ + { id: 'item-1', item: <Item>Item 1</Item> }, + { id: 'item-2', item: <Item>Item 2</Item> }, + { id: 'item-3', item: <Item>Item 3</Item> }, + { id: 'item-4', item: <Item>Item 4</Item> }, + { id: 'item-5', item: <Item>Item 5</Item> }, + ], +}; + +export const OneColumn = Template.bind({}); +OneColumn.args = { + items: [ + { id: 'item-1', item: <Item>Item 1</Item> }, + { id: 'item-2', item: <Item>Item 2</Item> }, + { id: 'item-3', item: <Item>Item 3</Item> }, + ], + col: 1, + gap: 'sm', +}; + +export const TwoColumns = Template.bind({}); +TwoColumns.args = { + items: [ + { id: 'item-1', item: <Item>Item 1</Item> }, + { id: 'item-2', item: <Item>Item 2</Item> }, + { id: 'item-3', item: <Item>Item 3</Item> }, + ], + col: 2, + gap: 'sm', +}; + +export const ThreeColumns = Template.bind({}); +ThreeColumns.args = { + items: [ + { id: 'item-1', item: <Item>Item 1</Item> }, + { id: 'item-2', item: <Item>Item 2</Item> }, + { id: 'item-3', item: <Item>Item 3</Item> }, + { id: 'item-4', item: <Item>Item 4</Item> }, + ], + col: 3, + gap: 'sm', +}; + +export const FixedSize = Template.bind({}); +FixedSize.args = { + items: [ + { id: 'item-1', item: <Item>Item 1</Item> }, + { id: 'item-2', item: <Item>Item 2</Item> }, + { id: 'item-3', item: <Item>Item 3</Item> }, + { id: 'item-4', item: <Item>Item 4</Item> }, + { id: 'item-5', item: <Item>Item 5</Item> }, + ], + size: '300px', + gap: 'sm', +}; + +export const MaxSize = Template.bind({}); +MaxSize.args = { + items: [ + { id: 'item-1', item: <Item>Item 1</Item> }, + { id: 'item-2', item: <Item>Item 2</Item> }, + { id: 'item-3', item: <Item>Item 3</Item> }, + { id: 'item-4', item: <Item>Item 4</Item> }, + { id: 'item-5', item: <Item>Item 5</Item> }, + ], + sizeMax: '300px', + gap: 'sm', +}; + +export const MinSize = Template.bind({}); +MinSize.args = { + items: [ + { id: 'item-1', item: <Item>Item 1</Item> }, + { id: 'item-2', item: <Item>Item 2</Item> }, + { id: 'item-3', item: <Item>Item 3</Item> }, + { id: 'item-4', item: <Item>Item 4</Item> }, + { id: 'item-5', item: <Item>Item 5</Item> }, + ], + sizeMin: '100px', + gap: 'sm', +}; + +export const MinAndMaxSize = Template.bind({}); +MinAndMaxSize.args = { + items: [ + { id: 'item-1', item: <Item>Item 1</Item> }, + { id: 'item-2', item: <Item>Item 2</Item> }, + { id: 'item-3', item: <Item>Item 3</Item> }, + { id: 'item-4', item: <Item>Item 4</Item> }, + { id: 'item-5', item: <Item>Item 5</Item> }, + ], + sizeMax: '300px', + sizeMin: '100px', + gap: 'sm', +}; + +export const Fill = Template.bind({}); +Fill.args = { + items: [ + { id: 'item-1', item: <Item>Item 1</Item> }, + { id: 'item-2', item: <Item>Item 2</Item> }, + { id: 'item-3', item: <Item>Item 3</Item> }, + { id: 'item-4', item: <Item>Item 4</Item> }, + { id: 'item-5', item: <Item>Item 5</Item> }, + ], + col: 'auto-fill', + sizeMin: '100px', + gap: 'sm', +}; diff --git a/src/components/molecules/grid/grid.test.tsx b/src/components/molecules/grid/grid.test.tsx new file mode 100644 index 0000000..212bdc4 --- /dev/null +++ b/src/components/molecules/grid/grid.test.tsx @@ -0,0 +1,69 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import { Grid, type GridItem } from './grid'; + +const items: GridItem[] = [ + { id: 'item-1', item: 'Item 1' }, + { id: 'item-2', item: 'Item 2' }, + { id: 'item-3', item: 'Item 3' }, + { id: 'item-4', item: 'Item 4' }, + { id: 'item-5', item: 'Item 5' }, +]; + +describe('Grid', () => { + it('render a list of items as grid', () => { + render(<Grid items={items} />); + + expect(rtlScreen.getAllByRole('listitem')).toHaveLength(items.length); + }); + + it('can render a list of items with fixed size', () => { + const size = '200px'; + + render(<Grid items={items} size={size} />); + + expect(rtlScreen.getByRole('list')).toHaveClass('wrapper--has-fixed-size'); + expect(rtlScreen.getByRole('list')).toHaveStyle({ '--size': size }); + }); + + it('can render a list of items with min size', () => { + const size = '200px'; + + render(<Grid items={items} sizeMin={size} />); + + expect(rtlScreen.getByRole('list')).toHaveClass('wrapper--has-min-size'); + expect(rtlScreen.getByRole('list')).toHaveStyle({ '--size-min': size }); + }); + + it('can render a list of items with max size', () => { + const size = '200px'; + + render(<Grid items={items} sizeMax={size} />); + + expect(rtlScreen.getByRole('list')).toHaveStyle({ '--size-max': size }); + }); + + it('can render a list of items with a custom gap', () => { + const gap = 'md'; + + render(<Grid items={items} gap={gap} />); + + expect(rtlScreen.getByRole('list')).toHaveStyle({ + '--gap': `var(--spacing-${gap})`, + }); + }); + + it('can render a list of items with an explicit number of columns', () => { + const col = 4; + + render(<Grid col={col} items={items} />); + + expect(rtlScreen.getByRole('list')).toHaveStyle(`--col: ${col}`); + }); + + it('can render a centered list of items', () => { + render(<Grid isCentered items={items} />); + + expect(rtlScreen.getByRole('list')).toHaveClass('wrapper--is-centered'); + }); +}); diff --git a/src/components/molecules/grid/grid.tsx b/src/components/molecules/grid/grid.tsx new file mode 100644 index 0000000..ca920f8 --- /dev/null +++ b/src/components/molecules/grid/grid.tsx @@ -0,0 +1,116 @@ +import { + type ForwardedRef, + type ReactNode, + forwardRef, + type CSSProperties, +} from 'react'; +import type { Spacing } from '../../../types'; +import { List, ListItem, type ListProps } from '../../atoms'; +import styles from './grid.module.scss'; + +export type GridItem = { + id: string; + item: ReactNode; +}; + +export type GridProps<T extends boolean> = Omit< + ListProps<T, false>, + 'children' | 'hideMarker' | 'isHierarchical' | 'isInline' | 'spacing' +> & { + /** + * Control the number of column. + * + * @default 'auto-fit' + */ + col?: number | 'auto-fill' | 'auto-fit'; + /** + * The gap between the items. + * + * @default null + */ + gap?: Spacing | null; + /** + * Should the grid be centered? + * + * @default false + */ + isCentered?: boolean; + /** + * The grid items. + */ + items: GridItem[]; + /** + * Define a fixed size for each item. + * + * You should either use `size` or `sizeMax`/`sizeMin` not both. + * + * @default undefined + */ + size?: string; + /** + * Define the maximal size of each item. + * + * You should either use `size` or `sizeMax`/`sizeMin` not both. + * + * @default '1fr' + */ + sizeMax?: string; + /** + * Define the maximal size of each item. + * + * You should either use `size` or `sizeMax`/`sizeMin` not both. + * + * @default 0 + */ + sizeMin?: 0 | string; +}; + +const GridWithRef = <T extends boolean>( + { + className = '', + col = 'auto-fit', + gap, + isCentered = false, + items, + size, + sizeMax, + sizeMin, + style, + ...props + }: GridProps<T>, + ref?: ForwardedRef<T extends true ? HTMLOListElement : HTMLUListElement> +) => { + const gridClass = [ + styles.wrapper, + styles[isCentered ? 'wrapper--is-centered' : ''], + styles[size ? 'wrapper--has-fixed-size' : ''], + styles[sizeMin ? 'wrapper--has-min-size' : ''], + className, + ].join(' '); + const gridStyles = { + ...style, + '--col': col, + ...(size ? { '--size': size } : {}), + ...(sizeMax ? { '--size-max': sizeMax } : {}), + ...(sizeMin ? { '--size-min': sizeMin } : {}), + ...(gap ? { '--gap': `var(--spacing-${gap})` } : {}), + } as CSSProperties; + + return ( + <List + {...props} + className={gridClass} + hideMarker + ref={ref} + style={gridStyles} + > + {items.map(({ id, item }) => ( + <ListItem className={styles.item} key={id}> + {item} + </ListItem> + ))} + </List> + ); +}; + +export const Grid = forwardRef(GridWithRef); diff --git a/src/components/molecules/grid/index.ts b/src/components/molecules/grid/index.ts new file mode 100644 index 0000000..d24d1bd --- /dev/null +++ b/src/components/molecules/grid/index.ts @@ -0,0 +1 @@ +export * from './grid'; diff --git a/src/components/molecules/index.ts b/src/components/molecules/index.ts index b042a96..7f48e45 100644 --- a/src/components/molecules/index.ts +++ b/src/components/molecules/index.ts @@ -4,6 +4,7 @@ export * from './card'; export * from './code'; export * from './collapsible'; export * from './forms'; +export * from './grid'; export * from './images'; export * from './layout'; export * from './meta-list'; diff --git a/src/components/molecules/layout/columns.module.scss b/src/components/molecules/layout/columns.module.scss deleted file mode 100644 index 0d383e7..0000000 --- a/src/components/molecules/layout/columns.module.scss +++ /dev/null @@ -1,30 +0,0 @@ -@use "../../../styles/abstracts/mixins" as mix; - -.wrapper { - display: grid; - gap: var(--spacing-md); - - &--responsive#{&} { - @for $i from 2 through 4 { - &--#{$i}-columns { - @include mix.media("screen") { - @include mix.dimensions("sm") { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - @include mix.dimensions("md") { - grid-template-columns: repeat($i, minmax(0, 1fr)); - } - } - } - } - } - - &--no-responsive#{&} { - @for $i from 2 through 4 { - &--#{$i}-columns { - grid-template-columns: repeat($i, minmax(0, 1fr)); - } - } - } -} diff --git a/src/components/molecules/layout/columns.stories.tsx b/src/components/molecules/layout/columns.stories.tsx deleted file mode 100644 index 43d0629..0000000 --- a/src/components/molecules/layout/columns.stories.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import { Column, Columns } from './columns'; - -export default { - title: 'Molecules/Layout/Columns', - args: { - responsive: true, - }, - component: Columns, - argTypes: { - children: { - description: 'The columns.', - type: { - name: 'function', - required: true, - }, - }, - className: { - control: { - type: 'text', - }, - description: 'Set additional classnames to the columns wrapper.', - table: { - category: 'Styles', - }, - type: { - name: 'string', - required: false, - }, - }, - count: { - control: { - type: 'number', - min: 2, - max: 4, - }, - description: 'The number of columns.', - type: { - name: 'number', - required: true, - }, - }, - responsive: { - control: { - type: 'boolean', - }, - description: 'Should the columns be stacked on small devices?', - table: { - category: 'Options', - defaultValue: { summary: true }, - }, - type: { - name: 'boolean', - required: false, - }, - }, - }, -} as ComponentMeta<typeof Columns>; - -const Template: ComponentStory<typeof Columns> = (args) => ( - <Columns {...args} /> -); - -const column1 = - 'Non praesentium voluptas quisquam ex est. Distinctio accusamus facilis libero in aut. Et veritatis quo impedit fugit amet sit accusantium. Ut est rerum asperiores sint libero eveniet. Molestias placeat recusandae suscipit eligendi sunt hic.'; - -const column2 = - 'Quaerat eum dignissimos tempore ab enim. Iusto inventore nemo. Veritatis voluptas quod maxime earum soluta illo atque vel. Nam et corrupti. Dolorem qui cum dolorem. Aut ut nobis. Mollitia qui voluptas rerum et quibusdam.'; - -const column3 = - 'Libero aut ab neque voluptatem commodi. Quam quia voluptatem iusto dolorum. Enim ipsa totam corrupti qui cum quidem ea. Eos sed aliquam porro consequatur officia sed.'; - -const column4 = - 'Ratione placeat ea ea. Explicabo rem eaque voluptatibus. Nihil nulla culpa et dolor numquam omnis est. Quis quas excepturi est dignissimos ducimus et ad quis quis. Eos enim et nam delectus.'; - -export const TwoColumns = Template.bind({}); -TwoColumns.args = { - children: [ - <Column key="column-1">{column1}</Column>, - <Column key="column-2">{column2}</Column>, - <Column key="column-3">{column3}</Column>, - <Column key="column-4">{column4}</Column>, - ], - count: 2, -}; - -export const ThreeColumns = Template.bind({}); -ThreeColumns.args = { - children: [ - <Column key="column-1">{column1}</Column>, - <Column key="column-2">{column2}</Column>, - <Column key="column-3">{column3}</Column>, - <Column key="column-4">{column4}</Column>, - ], - count: 3, -}; - -export const FourColumns = Template.bind({}); -FourColumns.args = { - children: [ - <Column key="column-1">{column1}</Column>, - <Column key="column-2">{column2}</Column>, - <Column key="column-3">{column3}</Column>, - <Column key="column-4">{column4}</Column>, - ], - count: 4, -}; diff --git a/src/components/molecules/layout/columns.test.tsx b/src/components/molecules/layout/columns.test.tsx deleted file mode 100644 index 2b016e7..0000000 --- a/src/components/molecules/layout/columns.test.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { render, screen as rtlScreen } from '../../../../tests/utils'; -import { Column, Columns } from './columns'; - -const column1 = - 'Non praesentium voluptas quisquam ex est. Distinctio accusamus facilis libero in aut. Et veritatis quo impedit fugit amet sit accusantium. Ut est rerum asperiores sint libero eveniet. Molestias placeat recusandae suscipit eligendi sunt hic.'; - -const column2 = - 'Quo perspiciatis mollitia non et. Modi voluptatem molestias. Facere ut molestiae exercitationem non nesciunt unde adipisci. Non cupiditate provident repudiandae. Natus quia necessitatibus libero enim earum quam et.'; - -const column3 = - 'Libero aut ab neque voluptatem commodi. Quam quia voluptatem iusto dolorum. Enim ipsa totam corrupti qui cum quidem ea. Eos sed aliquam porro consequatur officia sed.'; - -const column4 = - 'Ratione placeat ea ea. Explicabo rem eaque voluptatibus. Nihil nulla culpa et dolor numquam omnis est. Quis quas excepturi est dignissimos ducimus et ad quis quis. Eos enim et nam delectus.'; - -describe('Columns', () => { - it('renders all the children', () => { - render( - <Columns count={2}> - <Column key="column-1">{column1}</Column> - <Column key="column-2">{column2}</Column> - <Column key="column-3">{column3}</Column> - <Column key="column-4">{column4}</Column> - </Columns> - ); - - expect(rtlScreen.getByText(column1)).toBeInTheDocument(); - expect(rtlScreen.getByText(column2)).toBeInTheDocument(); - expect(rtlScreen.getByText(column3)).toBeInTheDocument(); - expect(rtlScreen.getByText(column4)).toBeInTheDocument(); - }); - - it('renders the right number of columns', () => { - render( - <Columns count={3}> - <Column key="column-1">{column1}</Column> - <Column key="column-2">{column2}</Column> - <Column key="column-3">{column3}</Column> - <Column key="column-4">{column4}</Column> - </Columns> - ); - - const container = rtlScreen.getByText(column1).parentElement; - - expect(container).toHaveClass('wrapper--3-columns'); - }); -}); diff --git a/src/components/molecules/layout/columns.tsx b/src/components/molecules/layout/columns.tsx deleted file mode 100644 index 56cd1a1..0000000 --- a/src/components/molecules/layout/columns.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import type { - FC, - HTMLAttributes, - ReactComponentElement, - ReactNode, -} from 'react'; -import styles from './columns.module.scss'; - -export type ColumnProps = HTMLAttributes<HTMLDivElement> & { - children: ReactNode; -}; - -/** - * Column component. - * - * Render the body as a column. - */ -export const Column: FC<ColumnProps> = ({ children, ...props }) => ( - <div {...props}>{children}</div> -); - -// eslint-disable-next-line @typescript-eslint/no-magic-numbers -type ColumnsNumber = 2 | 3 | 4; - -export type ColumnsProps = { - /** - * The columns. - */ - children: ReactComponentElement<typeof Column>[]; - /** - * Set additional classnames to the columns wrapper. - */ - className?: string; - /** - * The number of columns. - */ - count: ColumnsNumber; - /** - * Should the columns be stacked on small devices? Default: true. - */ - responsive?: boolean; -}; - -/** - * Columns component. - * - * Render some Column components as columns. - */ -export const Columns: FC<ColumnsProps> = ({ - children, - className = '', - count, - responsive = true, -}) => { - const countClass = `wrapper--${count}-columns`; - const responsiveClass = responsive - ? `wrapper--responsive` - : 'wrapper--no-responsive'; - const wrapperClass = `${styles.wrapper} ${styles[countClass]} ${styles[responsiveClass]} ${className}`; - - return <div className={wrapperClass}>{children}</div>; -}; diff --git a/src/components/molecules/layout/index.ts b/src/components/molecules/layout/index.ts index 75cbe28..f204f56 100644 --- a/src/components/molecules/layout/index.ts +++ b/src/components/molecules/layout/index.ts @@ -1,3 +1,2 @@ -export * from './columns'; export * from './page-footer'; export * from './page-header'; diff --git a/src/components/organisms/images/gallery.module.scss b/src/components/organisms/images/gallery.module.scss deleted file mode 100644 index 31960a4..0000000 --- a/src/components/organisms/images/gallery.module.scss +++ /dev/null @@ -1,24 +0,0 @@ -@use "../../../styles/abstracts/mixins" as mix; -@use "../../../styles/abstracts/placeholders"; - -.wrapper { - display: grid; - grid-template-columns: minmax(0, 1fr); - gap: var(--spacing-sm); - max-width: 100%; - margin: var(--spacing-sm) 0; - - @for $i from 0 to 6 { - &--#{$i}-columns { - @include mix.media("screen") { - @include mix.dimensions("xs") { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - @include mix.dimensions("sm") { - grid-template-columns: repeat(#{$i}, minmax(0, 1fr)); - } - } - } - } -} diff --git a/src/components/organisms/images/gallery.stories.tsx b/src/components/organisms/images/gallery.stories.tsx deleted file mode 100644 index 016b18e..0000000 --- a/src/components/organisms/images/gallery.stories.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import NextImage from 'next/image'; -import { Figure } from '../../atoms'; -import { Gallery } from './gallery'; - -/** - * Gallery - Storybook Meta - */ -export default { - title: 'Organisms/Images/Gallery', - component: Gallery, - argTypes: { - children: { - control: { - type: null, - }, - description: 'Two or more images.', - type: { - name: 'function', - required: true, - }, - }, - columns: { - control: { - type: 'number', - min: 2, - max: 4, - }, - description: 'The columns count.', - type: { - name: 'number', - required: true, - }, - }, - }, -} as ComponentMeta<typeof Gallery>; - -const image = { - alt: 'Modi provident omnis', - height: 480, - src: 'https://picsum.photos/640/480', - width: 640, -}; - -const Template: ComponentStory<typeof Gallery> = (args) => ( - <Gallery {...args}> - <Figure> - <NextImage {...image} /> - </Figure> - <Figure> - <NextImage {...image} /> - </Figure> - <Figure> - <NextImage {...image} /> - </Figure> - <Figure> - <NextImage {...image} /> - </Figure> - </Gallery> -); - -/** - * Gallery Stories - Two columns - */ -export const TwoColumns = Template.bind({}); -TwoColumns.args = { - columns: 2, -}; - -/** - * Gallery Stories - Three columns - */ -export const ThreeColumns = Template.bind({}); -ThreeColumns.args = { - columns: 3, -}; - -/** - * Gallery Stories - Four columns - */ -export const FourColumns = Template.bind({}); -FourColumns.args = { - columns: 4, -}; diff --git a/src/components/organisms/images/gallery.test.tsx b/src/components/organisms/images/gallery.test.tsx deleted file mode 100644 index bffc3b2..0000000 --- a/src/components/organisms/images/gallery.test.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { render, screen as rtlScreen } from '@testing-library/react'; -import NextImage from 'next/image'; -import { Gallery } from './gallery'; - -const columns = 3; - -const image = { - alt: 'Modi provident omnis', - height: 480, - src: 'http://picsum.photos/640/480', - width: 640, -}; - -describe('Gallery', () => { - it('renders the correct number of items', () => { - render( - <Gallery columns={columns}> - <NextImage {...image} /> - <NextImage {...image} /> - <NextImage {...image} /> - <NextImage {...image} /> - </Gallery> - ); - - // eslint-disable-next-line @typescript-eslint/no-magic-numbers - expect(rtlScreen.getAllByRole('listitem')).toHaveLength(4); - }); - - it('renders the right number of columns', () => { - render( - <Gallery columns={columns}> - <NextImage {...image} /> - <NextImage {...image} /> - <NextImage {...image} /> - <NextImage {...image} /> - </Gallery> - ); - expect(rtlScreen.getByRole('list')).toHaveClass( - `wrapper--${columns}-columns` - ); - }); -}); diff --git a/src/components/organisms/images/gallery.tsx b/src/components/organisms/images/gallery.tsx deleted file mode 100644 index 2f17130..0000000 --- a/src/components/organisms/images/gallery.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Children, type FC, type ReactElement } from 'react'; -import { List, ListItem } from '../../atoms'; -import styles from './gallery.module.scss'; - -// eslint-disable-next-line @typescript-eslint/no-magic-numbers -export type GalleryColumn = 2 | 3 | 4; - -export type GalleryProps = { - /** - * The images. - */ - children: ReactElement[]; - /** - * The columns count. - */ - columns: GalleryColumn; -}; - -/** - * Gallery component - * - * Render a gallery of images. - */ -export const Gallery: FC<GalleryProps> = ({ children, columns }) => { - const columnsClass = `wrapper--${columns}-columns`; - - return ( - <List className={`${styles.wrapper} ${styles[columnsClass]}`} hideMarker> - {Children.map(children, (child) => ( - <ListItem className={styles.item}>{child}</ListItem> - ))} - </List> - ); -}; diff --git a/src/components/organisms/images/index.ts b/src/components/organisms/images/index.ts deleted file mode 100644 index de0da26..0000000 --- a/src/components/organisms/images/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './gallery'; diff --git a/src/components/organisms/index.ts b/src/components/organisms/index.ts index 8dfc61b..386eebf 100644 --- a/src/components/organisms/index.ts +++ b/src/components/organisms/index.ts @@ -1,5 +1,4 @@ export * from './forms'; -export * from './images'; export * from './layout'; export * from './modals'; export * from './toolbar'; diff --git a/src/components/organisms/layout/cards-list.module.scss b/src/components/organisms/layout/cards-list.module.scss deleted file mode 100644 index 1665829..0000000 --- a/src/components/organisms/layout/cards-list.module.scss +++ /dev/null @@ -1,24 +0,0 @@ -@use "../../../styles/abstracts/mixins" as mix; -@use "../../../styles/abstracts/placeholders"; - -.wrapper { - display: grid; - grid-template-columns: repeat( - auto-fit, - min(calc(100vw - (var(--spacing-md) * 2)), var(--card-width, 30ch)) - ); - gap: var(--spacing-sm); - place-content: center; - align-items: stretch; - justify-items: stretch; - - @include mix.media("screen") { - @include mix.dimensions(null, "sm") { - gap: var(--spacing-lg); - } - } -} - -.item > * { - height: 100%; -} diff --git a/src/components/organisms/layout/cards-list.stories.tsx b/src/components/organisms/layout/cards-list.stories.tsx deleted file mode 100644 index 3f8e72a..0000000 --- a/src/components/organisms/layout/cards-list.stories.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import { Card, CardBody, CardHeader, CardTitle } from '../../molecules'; -import { - CardsList as CardsListComponent, - type CardsListItem, -} from './cards-list'; - -/** - * CardsList - Storybook Meta - */ -export default { - title: 'Organisms/Layout', - component: CardsListComponent, - argTypes: { - items: { - description: 'The cards data.', - type: { - name: 'object', - required: true, - value: {}, - }, - }, - isOrdered: { - control: { - type: 'boolean', - }, - description: 'Should the list be ordered?', - table: { - category: 'Options', - defaultValue: { summary: false }, - }, - type: { - name: 'boolean', - required: false, - }, - }, - }, -} as ComponentMeta<typeof CardsListComponent>; - -const Template: ComponentStory<typeof CardsListComponent> = (args) => ( - <CardsListComponent {...args} /> -); - -const items: CardsListItem[] = [ - { - id: 'card-1', - card: ( - <Card> - <CardHeader> - <CardTitle>Et alias omnis</CardTitle> - </CardHeader> - <CardBody> - Rerum voluptatem sint sint sit dignissimos. Labore totam possimus - tempore atque veniam. Doloremque tenetur quidem beatae veritatis quo. - Quaerat voluptatem deleniti voluptas quia. Qui voluptatem iure iste - expedita et sed beatae. - </CardBody> - </Card> - ), - }, - { - id: 'card-2', - card: ( - <Card> - <CardHeader> - <CardTitle>Fugiat magnam nesciunt</CardTitle> - </CardHeader> - <CardBody> - Sit corporis animi ea. Earum asperiores error et. Aliquid quia et - consequatur. Magnam sit ut facere explicabo vel dolorem earum - assumenda. Aspernatur inventore quod libero est. - </CardBody> - </Card> - ), - }, - { - id: 'card-3', - card: ( - <Card> - <CardHeader> - <CardTitle>Asperiores eum quas</CardTitle> - </CardHeader> - <CardBody> - Doloremque ut cupiditate distinctio aperiam. Neque tempora unde - perferendis asperiores. Doloremque velit vel quam. Temporibus itaque - non non exercitationem. - </CardBody> - </Card> - ), - }, -]; - -/** - * Layout Stories - Cards list - */ -export const CardsList = Template.bind({}); -CardsList.args = { - items, -}; diff --git a/src/components/organisms/layout/cards-list.test.tsx b/src/components/organisms/layout/cards-list.test.tsx deleted file mode 100644 index b04e109..0000000 --- a/src/components/organisms/layout/cards-list.test.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { render, screen as rtlScreen } from '@testing-library/react'; -import { Card, CardBody, CardHeader, CardTitle } from '../../molecules'; -import { CardsList, type CardsListItem } from './cards-list'; - -const items: CardsListItem[] = [ - { - id: 'card-1', - card: ( - <Card> - <CardHeader> - <CardTitle>Et alias omnis</CardTitle> - </CardHeader> - <CardBody> - Rerum voluptatem sint sint sit dignissimos. Labore totam possimus - tempore atque veniam. Doloremque tenetur quidem beatae veritatis quo. - Quaerat voluptatem deleniti voluptas quia. Qui voluptatem iure iste - expedita et sed beatae. - </CardBody> - </Card> - ), - }, - { - id: 'card-2', - card: ( - <Card> - <CardHeader> - <CardTitle>Fugiat magnam nesciunt</CardTitle> - </CardHeader> - <CardBody> - Sit corporis animi ea. Earum asperiores error et. Aliquid quia et - consequatur. Magnam sit ut facere explicabo vel dolorem earum - assumenda. Aspernatur inventore quod libero est. - </CardBody> - </Card> - ), - }, - { - id: 'card-3', - card: ( - <Card> - <CardHeader> - <CardTitle>Asperiores eum quas</CardTitle> - </CardHeader> - <CardBody> - Doloremque ut cupiditate distinctio aperiam. Neque tempora unde - perferendis asperiores. Doloremque velit vel quam. Temporibus itaque - non non exercitationem. - </CardBody> - </Card> - ), - }, -]; - -describe('CardsList', () => { - it('renders a list of cards', () => { - render(<CardsList items={items} />); - expect(rtlScreen.getAllByRole('heading', { level: 2 })).toHaveLength( - items.length - ); - }); -}); diff --git a/src/components/organisms/layout/cards-list.tsx b/src/components/organisms/layout/cards-list.tsx deleted file mode 100644 index 4f920e8..0000000 --- a/src/components/organisms/layout/cards-list.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import type { FC, ReactElement } from 'react'; -import { List, ListItem } from '../../atoms'; -import type { CardProps } from '../../molecules'; -import styles from './cards-list.module.scss'; - -export type CardsListItem = { - /** - * The card. - */ - card: ReactElement<CardProps<string> | CardProps<undefined>>; - /** - * The card id. - */ - id: string; -}; - -export type CardsListProps = { - /** - * Set additional classnames to the list wrapper. - */ - className?: string; - /** - * Should the cards list be ordered? - * - * @default false - */ - isOrdered?: boolean; - /** - * The cards data. - */ - items: CardsListItem[]; -}; - -/** - * CardsList component - * - * Return a list of Card components. - */ -export const CardsList: FC<CardsListProps> = ({ - className = '', - isOrdered = false, - items, -}) => { - const kindModifier = `wrapper--${isOrdered ? 'ordered' : 'unordered'}`; - - return ( - <List - className={`${styles.wrapper} ${styles[kindModifier]} ${className}`} - hideMarker - isInline - isOrdered={isOrdered} - > - {items.map(({ id, card }) => ( - <ListItem className={styles.item} key={id}> - {card} - </ListItem> - ))} - </List> - ); -}; diff --git a/src/components/organisms/layout/index.ts b/src/components/organisms/layout/index.ts index 1351537..4593ccc 100644 --- a/src/components/organisms/layout/index.ts +++ b/src/components/organisms/layout/index.ts @@ -1,4 +1,3 @@ -export * from './cards-list'; export * from './comment'; export * from './comments-list'; export * from './no-results'; diff --git a/src/content b/src/content -Subproject c6be8a1c511e5848a0317f825a29d07d09c4731 +Subproject 5099889918cd056394e5bae8d5864b7c25ecaef diff --git a/src/pages/index.tsx b/src/pages/index.tsx index e482fb4..7ff1379 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -4,7 +4,7 @@ import type { GetStaticProps } from 'next'; import Head from 'next/head'; import NextImage, { type ImageProps as NextImageProps } from 'next/image'; import Script from 'next/script'; -import type { FC, HTMLAttributes } from 'react'; +import type { FC, HTMLAttributes, ReactNode } from 'react'; import { useIntl } from 'react-intl'; import { ButtonLink, @@ -14,13 +14,10 @@ import { CardHeader, CardMeta, CardTitle, - CardsList, - type CardsListItem, - Column, - Columns, - type ColumnsProps, Figure, getLayout, + Grid, + type GridItem, Heading, Icon, List, @@ -38,6 +35,15 @@ import { getSchemaJson, getWebPageSchema } from '../utils/helpers'; import { loadTranslation, type Messages } from '../utils/helpers/server'; import { useBreadcrumb, useSettings } from '../utils/hooks'; +/** + * Column component. + * + * Render the body as a column. + */ +const Column = ({ children, ...props }: HTMLAttributes<HTMLDivElement>) => ( + <div {...props}>{children}</div> +); + const H1 = ({ children = '', ...props @@ -256,8 +262,15 @@ const MoreLinks: FC = () => { ); }; -const StyledColumns = (props: ColumnsProps) => ( - <Columns className={styles.columns} {...props} /> +const StyledGrid = ({ children }: { children: ReactNode[] }) => ( + <Grid + className={styles.columns} + gap="sm" + items={children.map((child, index) => { + return { id: `${index}`, item: child }; + })} + sizeMin="250px" + /> ); /** @@ -303,9 +316,9 @@ const HomePage: NextPageWithLayout<HomeProps> = ({ recentPosts }) => { * @returns {JSX.Element} - The cards list. */ const getRecentPosts = (): JSX.Element => { - const posts: CardsListItem[] = recentPosts.map((post) => { + const posts: GridItem[] = recentPosts.map((post) => { return { - card: ( + item: ( <Card cover={ post.cover ? ( @@ -345,14 +358,22 @@ const HomePage: NextPageWithLayout<HomeProps> = ({ recentPosts }) => { }); const listClass = `${styles.list} ${styles['list--cards']}`; - return <CardsList className={listClass} items={posts} />; + return ( + <Grid + className={listClass} + gap="sm" + isCentered + items={posts} + sizeMax="25ch" + /> + ); }; const components: MDXComponents = { CodingLinks, ColdarkRepos, Column, - Columns: StyledColumns, + Grid: StyledGrid, h1: H1, h2: H2, h3: H3, diff --git a/src/pages/projets/[slug].tsx b/src/pages/projets/[slug].tsx index 3d3c57e..17be961 100644 --- a/src/pages/projets/[slug].tsx +++ b/src/pages/projets/[slug].tsx @@ -6,11 +6,10 @@ import Head from 'next/head'; import NextImage, { type ImageProps as NextImageProps } from 'next/image'; import { useRouter } from 'next/router'; import Script from 'next/script'; -import type { ComponentType, HTMLAttributes } from 'react'; +import type { ComponentType, HTMLAttributes, ReactNode } from 'react'; import { useIntl } from 'react-intl'; import { Code, - Gallery, getLayout, Link, Overview, @@ -25,6 +24,7 @@ import { type MetaItemData, type MetaValues, Time, + Grid, } from '../../components'; import styles from '../../styles/pages/project.module.scss'; import type { NextPageWithLayout, ProjectPreview, Repos } from '../../types'; @@ -120,6 +120,16 @@ const UnorderedList = ({ </List> ); +const Gallery = ({ children }: { children: ReactNode[] }) => ( + <Grid + gap="sm" + items={children.map((child, index) => { + return { id: `${index}`, item: child }; + })} + sizeMin="250px" + /> +); + const components: MDXComponents = { Code, Gallery, diff --git a/src/pages/projets/index.tsx b/src/pages/projets/index.tsx index abb6da0..6ae476e 100644 --- a/src/pages/projets/index.tsx +++ b/src/pages/projets/index.tsx @@ -13,9 +13,9 @@ import { CardFooter, CardHeader, CardTitle, - CardsList, - type CardsListItem, getLayout, + Grid, + type GridItem, Link, MetaList, PageLayout, @@ -61,7 +61,7 @@ const ProjectsPage: NextPageWithLayout<ProjectsPageProps> = ({ projects }) => { id: 'ADQmDF', }); - const items: CardsListItem[] = projects.map( + const items: GridItem[] = projects.map( ({ id, meta: projectMeta, slug, title: projectTitle }) => { const { cover, tagline, technologies } = projectMeta; const figureLabel = intl.formatMessage( @@ -74,7 +74,7 @@ const ProjectsPage: NextPageWithLayout<ProjectsPageProps> = ({ projects }) => { ); return { - card: ( + item: ( <Card cover={ cover ? ( @@ -165,7 +165,13 @@ const ProjectsPage: NextPageWithLayout<ProjectsPageProps> = ({ projects }) => { breadcrumb={breadcrumbItems} breadcrumbSchema={breadcrumbSchema} > - <CardsList className={styles.list} items={items} /> + <Grid + className={styles.list} + gap="sm" + isCentered + items={items} + sizeMax="30ch" + /> </PageLayout> </> ); diff --git a/src/styles/abstracts/placeholders/_lists.scss b/src/styles/abstracts/placeholders/_lists.scss index 2200336..cbf9b08 100644 --- a/src/styles/abstracts/placeholders/_lists.scss +++ b/src/styles/abstracts/placeholders/_lists.scss @@ -12,8 +12,7 @@ list-style-position: inside; - ul, - ol { + :where(%regular-list) { margin-block-start: var(--itemSpacing); padding-inline-start: var(--spacing-sm); } |
