diff options
Diffstat (limited to 'src/components/atoms/layout')
26 files changed, 926 insertions, 0 deletions
diff --git a/src/components/atoms/layout/column.stories.tsx b/src/components/atoms/layout/column.stories.tsx new file mode 100644 index 0000000..a03c462 --- /dev/null +++ b/src/components/atoms/layout/column.stories.tsx @@ -0,0 +1,29 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import ColumnComponent from './column'; + +export default { + title: 'Atoms/Layout/Column', + component: ColumnComponent, + argTypes: { + children: { + description: 'The column body.', + type: { + name: 'array', + required: true, + value: {}, + }, + }, + }, +} as ComponentMeta<typeof ColumnComponent>; + +const Template: ComponentStory<typeof ColumnComponent> = (args) => ( + <ColumnComponent {...args} /> +); + +const body = + '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.'; + +export const Column = Template.bind({}); +Column.args = { + children: body, +}; diff --git a/src/components/atoms/layout/column.test.tsx b/src/components/atoms/layout/column.test.tsx new file mode 100644 index 0000000..c5c6554 --- /dev/null +++ b/src/components/atoms/layout/column.test.tsx @@ -0,0 +1,12 @@ +import { render, screen } from '@test-utils'; +import Column from './column'; + +const body = + '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.'; + +describe('Column', () => { + it('renders the column body', () => { + render(<Column>{body}</Column>); + expect(screen.getByText(body)).toBeInTheDocument(); + }); +}); diff --git a/src/components/atoms/layout/column.tsx b/src/components/atoms/layout/column.tsx new file mode 100644 index 0000000..ec6440d --- /dev/null +++ b/src/components/atoms/layout/column.tsx @@ -0,0 +1,16 @@ +import { FC, ReactNode } from 'react'; + +export type ColumnProps = { + children: ReactNode | ReactNode[]; +}; + +/** + * Column component. + * + * Render the body as a column. + */ +const Column: FC<ColumnProps> = ({ children }) => { + return <div>{children}</div>; +}; + +export default Column; diff --git a/src/components/atoms/layout/copyright.module.scss b/src/components/atoms/layout/copyright.module.scss new file mode 100644 index 0000000..a0e5347 --- /dev/null +++ b/src/components/atoms/layout/copyright.module.scss @@ -0,0 +1,32 @@ +@use "@styles/abstracts/functions" as fun; +@use "@styles/abstracts/mixins" as mix; + +.wrapper { + --icon-size: #{fun.convert-px(70)}; + + display: flex; + flex-flow: row wrap; + align-items: center; + place-content: center; + gap: var(--spacing-2xs); + margin: 0; + font-family: var(--font-family-secondary); + font-size: var(--font-size-md); + text-align: center; + + @include mix.media("screen") { + @include mix.dimensions("sm") { + text-align: left; + } + } +} + +.owner { + flex: 1 0 100%; + + @include mix.media("screen") { + @include mix.dimensions("sm") { + flex: initial; + } + } +} diff --git a/src/components/atoms/layout/copyright.stories.tsx b/src/components/atoms/layout/copyright.stories.tsx new file mode 100644 index 0000000..612b114 --- /dev/null +++ b/src/components/atoms/layout/copyright.stories.tsx @@ -0,0 +1,58 @@ +import CCBySA from '@components/atoms/icons/cc-by-sa'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import CopyrightComponent from './copyright'; + +/** + * Copyright - Storybook Meta + */ +export default { + title: 'Atoms/Layout', + component: CopyrightComponent, + argTypes: { + dates: { + description: 'The copyright dates.', + type: { + name: 'object', + required: true, + value: {}, + }, + }, + icon: { + control: { + type: null, + }, + description: 'The copyright icon.', + type: { + name: 'string', + required: true, + }, + }, + owner: { + control: { + type: 'text', + }, + description: 'The copyright owner', + type: { + name: 'string', + required: true, + }, + }, + }, +} as ComponentMeta<typeof CopyrightComponent>; + +const Template: ComponentStory<typeof CopyrightComponent> = (args) => ( + <CopyrightComponent {...args} /> +); + +/** + * Layout Stories - Copyright + */ +export const Copyright = Template.bind({}); +Copyright.args = { + dates: { + start: '2012', + end: '2022', + }, + icon: <CCBySA />, + owner: 'Your name', +}; diff --git a/src/components/atoms/layout/copyright.test.tsx b/src/components/atoms/layout/copyright.test.tsx new file mode 100644 index 0000000..6bfe612 --- /dev/null +++ b/src/components/atoms/layout/copyright.test.tsx @@ -0,0 +1,32 @@ +import CCBySA from '@components/atoms/icons/cc-by-sa'; +import { render, screen } from '@test-utils'; +import Copyright from './copyright'; + +const dates = { + start: '2012', + end: '2022', +}; +const icon = <CCBySA />; +const owner = 'Your name'; + +describe('Copyright', () => { + it('renders the copyright owner', () => { + render(<Copyright dates={dates} icon={icon} owner={owner} />); + expect(screen.getByText(owner)).toBeInTheDocument(); + }); + + it('renders the copyright start date', () => { + render(<Copyright dates={dates} icon={icon} owner={owner} />); + expect(screen.getByText(dates.start)).toBeInTheDocument(); + }); + + it('renders the copyright end date', () => { + render(<Copyright dates={dates} icon={icon} owner={owner} />); + expect(screen.getByText(dates.end)).toBeInTheDocument(); + }); + + it('renders the copyright icon', () => { + render(<Copyright dates={dates} icon={icon} owner={owner} />); + expect(screen.getByTitle('CC BY SA')).toBeInTheDocument(); + }); +}); diff --git a/src/components/atoms/layout/copyright.tsx b/src/components/atoms/layout/copyright.tsx new file mode 100644 index 0000000..f70695d --- /dev/null +++ b/src/components/atoms/layout/copyright.tsx @@ -0,0 +1,59 @@ +import { FC, ReactNode } from 'react'; +import styles from './copyright.module.scss'; + +export type CopyrightDates = { + /** + * The copyright start year. + */ + start: string; + /** + * The copyright end year. + */ + end?: string; +}; + +export type CopyrightProps = { + /** + * The copyright owner. + */ + owner: string; + /** + * The copyright dates. + */ + dates: CopyrightDates; + /** + * The copyright icon. + */ + icon: ReactNode; +}; + +/** + * Copyright component + * + * Renders a copyright information (owner, dates, license icon). + */ +const Copyright: FC<CopyrightProps> = ({ owner, dates, icon }) => { + const getFormattedDate = (date: string) => { + const datetime = new Date(date).toISOString(); + + return <time dateTime={datetime}>{date}</time>; + }; + + return ( + <div className={styles.wrapper}> + <span className={styles.owner}>{owner}</span> + {icon} + {getFormattedDate(dates.start)} + {dates.end ? ( + <> + <span>-</span> + {getFormattedDate(dates.end)} + </> + ) : ( + '' + )} + </div> + ); +}; + +export default Copyright; diff --git a/src/components/atoms/layout/main.stories.tsx b/src/components/atoms/layout/main.stories.tsx new file mode 100644 index 0000000..5bde475 --- /dev/null +++ b/src/components/atoms/layout/main.stories.tsx @@ -0,0 +1,58 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import MainComponent from './main'; + +/** + * Main - Storybook Meta + */ +export default { + title: 'Atoms/Layout', + component: MainComponent, + argTypes: { + children: { + control: { + type: 'text', + }, + description: 'The content.', + type: { + name: 'string', + required: true, + }, + }, + className: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the main element.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + id: { + control: { + type: 'text', + }, + description: 'The main element id.', + type: { + name: 'string', + required: true, + }, + }, + }, +} as ComponentMeta<typeof MainComponent>; + +const Template: ComponentStory<typeof MainComponent> = (args) => ( + <MainComponent {...args} /> +); + +/** + * Layout Stories - Main + */ +export const Main = Template.bind({}); +Main.args = { + children: 'The main content.', + id: '#main', +}; diff --git a/src/components/atoms/layout/main.test.tsx b/src/components/atoms/layout/main.test.tsx new file mode 100644 index 0000000..f91846f --- /dev/null +++ b/src/components/atoms/layout/main.test.tsx @@ -0,0 +1,12 @@ +import { render, screen } from '@test-utils'; +import Main from './main'; + +const id = 'main'; +const children = 'The main content.'; + +describe('Main', () => { + it('renders the content of main element', () => { + render(<Main id={id}>{children}</Main>); + expect(screen.getByRole('main')).toHaveTextContent(children); + }); +}); diff --git a/src/components/atoms/layout/main.tsx b/src/components/atoms/layout/main.tsx new file mode 100644 index 0000000..d92a5c7 --- /dev/null +++ b/src/components/atoms/layout/main.tsx @@ -0,0 +1,27 @@ +import { FC, ReactNode } from 'react'; + +export type MainProps = { + /** + * The main body. + */ + children: ReactNode; + /** + * Set additional classnames to the main element. + */ + className?: string; + /** + * The main wrapper id. + */ + id: string; +}; + +/** + * Main component + * + * Render a main element. + */ +const Main: FC<MainProps> = ({ children, ...props }) => { + return <main {...props}>{children}</main>; +}; + +export default Main; diff --git a/src/components/atoms/layout/no-script.module.scss b/src/components/atoms/layout/no-script.module.scss new file mode 100644 index 0000000..d8712af --- /dev/null +++ b/src/components/atoms/layout/no-script.module.scss @@ -0,0 +1,19 @@ +@use "@styles/abstracts/functions" as fun; + +.noscript { + color: var(--color-primary-darker); + + &--top { + padding: var(--spacing-xs) var(--spacing-sm); + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 10; + background: var(--color-bg); + border-bottom: fun.convert-px(3) solid var(--color-border); + font-size: var(--font-size-sm); + font-weight: 600; + text-align: center; + } +} diff --git a/src/components/atoms/layout/no-script.stories.tsx b/src/components/atoms/layout/no-script.stories.tsx new file mode 100644 index 0000000..22d2fea --- /dev/null +++ b/src/components/atoms/layout/no-script.stories.tsx @@ -0,0 +1,62 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import NoScript from './no-script'; + +/** + * NoScript - Storybook Meta + */ +export default { + title: 'Atoms/Layout/NoScript', + component: NoScript, + args: { + position: 'initial', + }, + argTypes: { + message: { + control: { + type: 'text', + }, + description: 'A message to display when Javascript is disabled.', + type: { + name: 'string', + required: true, + }, + }, + position: { + control: { + type: 'select', + }, + description: 'The message position.', + options: ['initial', 'top'], + table: { + category: 'Options', + defaultValue: 'initial', + }, + type: { + name: 'string', + required: false, + }, + }, + }, +} as ComponentMeta<typeof NoScript>; + +const Template: ComponentStory<typeof NoScript> = (args) => ( + <NoScript {...args} /> +); + +/** + * NoScript Stories - Default + */ +export const Default = Template.bind({}); +Default.args = { + message: 'A noscript only message.', + position: 'initial', +}; + +/** + * NoScript Stories - Top + */ +export const Top = Template.bind({}); +Top.args = { + message: 'A noscript only message.', + position: 'top', +}; diff --git a/src/components/atoms/layout/no-script.test.tsx b/src/components/atoms/layout/no-script.test.tsx new file mode 100644 index 0000000..9ed9c4c --- /dev/null +++ b/src/components/atoms/layout/no-script.test.tsx @@ -0,0 +1,11 @@ +import { render, screen } from '@test-utils'; +import NoScript from './no-script'; + +const message = 'A noscript message.'; + +describe('NoScript', () => { + it('renders a message', () => { + render(<NoScript message={message} />); + expect(screen.getByText(message)).toBeInTheDocument(); + }); +}); diff --git a/src/components/atoms/layout/no-script.tsx b/src/components/atoms/layout/no-script.tsx new file mode 100644 index 0000000..a503e0c --- /dev/null +++ b/src/components/atoms/layout/no-script.tsx @@ -0,0 +1,21 @@ +import { FC } from 'react'; +import styles from './no-script.module.scss'; + +export type NoScriptProps = { + /** + * The noscript message. + */ + message: string; + /** + * The message position. Default: initial. + */ + position?: 'initial' | 'top'; +}; + +const NoScript: FC<NoScriptProps> = ({ message, position = 'initial' }) => { + const positionClass = styles[`noscript--${position}`]; + + return <div className={`${styles.noscript} ${positionClass}`}>{message}</div>; +}; + +export default NoScript; diff --git a/src/components/atoms/layout/notice.module.scss b/src/components/atoms/layout/notice.module.scss new file mode 100644 index 0000000..7fd972c --- /dev/null +++ b/src/components/atoms/layout/notice.module.scss @@ -0,0 +1,27 @@ +@use "@styles/abstracts/functions" as fun; + +.wrapper { + padding: var(--spacing-2xs) var(--spacing-xs); + border: fun.convert-px(2) solid; + font-weight: bold; + + &--error { + border-color: var(--color-token-red); + color: var(--color-token-red); + } + + &--info { + border-color: var(--color-token-blue); + color: var(--color-token-blue); + } + + &--success { + border-color: var(--color-token-green); + color: var(--color-token-green); + } + + &--warning { + border-color: var(--color-token-orange); + color: var(--color-token-orange); + } +} diff --git a/src/components/atoms/layout/notice.stories.tsx b/src/components/atoms/layout/notice.stories.tsx new file mode 100644 index 0000000..dedf834 --- /dev/null +++ b/src/components/atoms/layout/notice.stories.tsx @@ -0,0 +1,86 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import NoticeComponent from './notice'; + +/** + * Notice - Storybook Meta + */ +export default { + title: 'Atoms/Layout/Notice', + component: NoticeComponent, + argTypes: { + className: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the notice wrapper.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + kind: { + control: { + type: 'select', + }, + description: 'The notice kind.', + options: ['error', 'info', 'success', 'warning'], + type: { + name: 'string', + required: true, + }, + }, + message: { + control: { + type: 'text', + }, + description: 'The notice body.', + type: { + name: 'string', + required: true, + }, + }, + }, +} as ComponentMeta<typeof NoticeComponent>; + +const Template: ComponentStory<typeof NoticeComponent> = (args) => ( + <NoticeComponent {...args} /> +); + +/** + * Notice stories - Error + */ +export const Error = Template.bind({}); +Error.args = { + kind: 'error', + message: 'Nisi provident sapiente.', +}; + +/** + * Notice stories - Info + */ +export const Info = Template.bind({}); +Info.args = { + kind: 'info', + message: 'Nisi provident sapiente.', +}; + +/** + * Notice stories - Success + */ +export const Success = Template.bind({}); +Success.args = { + kind: 'success', + message: 'Nisi provident sapiente.', +}; + +/** + * Notice stories - Warning + */ +export const Warning = Template.bind({}); +Warning.args = { + kind: 'warning', + message: 'Nisi provident sapiente.', +}; diff --git a/src/components/atoms/layout/notice.test.tsx b/src/components/atoms/layout/notice.test.tsx new file mode 100644 index 0000000..4501f8f --- /dev/null +++ b/src/components/atoms/layout/notice.test.tsx @@ -0,0 +1,11 @@ +import { render, screen } from '@test-utils'; +import Notice from './notice'; + +const message = 'Tenetur consequuntur tempore.'; + +describe('Notice', () => { + it('renders a message', () => { + render(<Notice kind="info" message={message} />); + expect(screen.getByText(message)).toBeInTheDocument(); + }); +}); diff --git a/src/components/atoms/layout/notice.tsx b/src/components/atoms/layout/notice.tsx new file mode 100644 index 0000000..a0d1d3e --- /dev/null +++ b/src/components/atoms/layout/notice.tsx @@ -0,0 +1,38 @@ +import { FC } from 'react'; +import styles from './notice.module.scss'; + +export type NoticeKind = 'error' | 'info' | 'success' | 'warning'; + +export type NoticeProps = { + /** + * Set additional classnames to the notice wrapper. + */ + className?: string; + /** + * The notice kind. + */ + kind: NoticeKind; + /** + * The notice body. + */ + message: string; +}; + +/** + * Notice component + * + * Render a colored message depending on notice kind. + */ +const Notice: FC<NoticeProps> = ({ className = '', kind, message }) => { + const kindClass = `wrapper--${kind}`; + + return message ? ( + <div className={`${styles.wrapper} ${styles[kindClass]} ${className}`}> + {message} + </div> + ) : ( + <></> + ); +}; + +export default Notice; diff --git a/src/components/atoms/layout/section.module.scss b/src/components/atoms/layout/section.module.scss new file mode 100644 index 0000000..012493a --- /dev/null +++ b/src/components/atoms/layout/section.module.scss @@ -0,0 +1,25 @@ +@use "@styles/abstracts/functions" as fun; +@use "@styles/abstracts/placeholders"; + +.wrapper { + @extend %grid; + + padding: var(--spacing-md) 0; + + &--borders { + border-bottom: fun.convert-px(1) solid var(--color-border); + } + + &--dark { + background: var(--color-bg-secondary); + } + + &--light { + background: var(--color-bg); + } +} + +.body, +.title { + grid-column: 2; +} diff --git a/src/components/atoms/layout/section.stories.tsx b/src/components/atoms/layout/section.stories.tsx new file mode 100644 index 0000000..530b2a0 --- /dev/null +++ b/src/components/atoms/layout/section.stories.tsx @@ -0,0 +1,102 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import Section from './section'; + +/** + * Section - Storybook Meta + */ +export default { + title: 'Atoms/Layout/Section', + component: Section, + args: { + variant: 'dark', + withBorder: true, + }, + argTypes: { + className: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the section element.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + content: { + control: { + type: 'text', + }, + description: 'The section content.', + type: { + name: 'string', + required: true, + }, + }, + title: { + control: { + type: 'text', + }, + description: 'The section title.', + type: { + name: 'string', + required: true, + }, + }, + variant: { + control: { + type: 'select', + }, + description: 'The section variant.', + options: ['light', 'dark'], + table: { + category: 'Styles', + defaultValue: { summary: 'dark' }, + }, + type: { + name: 'string', + required: false, + }, + }, + withBorder: { + control: { + type: 'boolean', + }, + description: 'Add a border at the bottom of the section.', + table: { + category: 'Styles', + defaultValue: { summary: true }, + }, + type: { + name: 'boolean', + required: false, + }, + }, + }, +} as ComponentMeta<typeof Section>; + +const Template: ComponentStory<typeof Section> = (args) => ( + <Section {...args} /> +); + +/** + * Section Stories - Light + */ +export const Light = Template.bind({}); +Light.args = { + title: 'A title', + content: 'The content.', + variant: 'light', +}; + +/** + * Section Stories - Dark + */ +export const Dark = Template.bind({}); +Dark.args = { + title: 'A title', + content: 'The content.', + variant: 'dark', +}; diff --git a/src/components/atoms/layout/section.test.tsx b/src/components/atoms/layout/section.test.tsx new file mode 100644 index 0000000..ca5f03a --- /dev/null +++ b/src/components/atoms/layout/section.test.tsx @@ -0,0 +1,17 @@ +import { render, screen } from '@test-utils'; +import Section from './section'; + +const title = 'Section title'; +const content = 'Section content.'; + +describe('Section', () => { + it('renders a title (h2)', () => { + render(<Section title={title} content={content} />); + expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(title); + }); + + it('renders a content', () => { + render(<Section title={title} content={content} />); + expect(screen.getByText(content)).toBeInTheDocument(); + }); +}); diff --git a/src/components/atoms/layout/section.tsx b/src/components/atoms/layout/section.tsx new file mode 100644 index 0000000..cb727ff --- /dev/null +++ b/src/components/atoms/layout/section.tsx @@ -0,0 +1,57 @@ +import { FC, ReactNode } from 'react'; +import Heading from '../headings/heading'; +import styles from './section.module.scss'; + +export type SectionVariant = 'dark' | 'light'; + +export type SectionProps = { + /** + * Set additional classnames to the section element. + */ + className?: string; + /** + * The section content. + */ + content: ReactNode; + /** + * The section title. + */ + title: string; + /** + * The section variant. + */ + variant?: SectionVariant; + /** + * Add a border at the bottom of the section. Default: true. + */ + withBorder?: boolean; +}; + +/** + * Section component + * + * Render a section element. + */ +const Section: FC<SectionProps> = ({ + className = '', + content, + title, + variant = 'dark', + withBorder = true, +}) => { + const borderClass = withBorder ? styles[`wrapper--borders`] : ''; + const variantClass = styles[`wrapper--${variant}`]; + + return ( + <section + className={`${styles.wrapper} ${borderClass} ${variantClass} ${className}`} + > + <Heading level={2} className={styles.title}> + {title} + </Heading> + <div className={styles.body}>{content}</div> + </section> + ); +}; + +export default Section; diff --git a/src/components/atoms/layout/sidebar.module.scss b/src/components/atoms/layout/sidebar.module.scss new file mode 100644 index 0000000..5d36f18 --- /dev/null +++ b/src/components/atoms/layout/sidebar.module.scss @@ -0,0 +1,12 @@ +@use "@styles/abstracts/functions" as fun; + +.wrapper { + > *:not(:first-child) { + margin-top: fun.convert-px(-2); + } +} + +.body { + position: sticky; + top: var(--spacing-xs); +} diff --git a/src/components/atoms/layout/sidebar.stories.tsx b/src/components/atoms/layout/sidebar.stories.tsx new file mode 100644 index 0000000..6876f95 --- /dev/null +++ b/src/components/atoms/layout/sidebar.stories.tsx @@ -0,0 +1,60 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import SidebarComponent from './sidebar'; + +/** + * Sidebar - Storybook Meta + */ +export default { + title: 'Atoms/Layout', + component: SidebarComponent, + argTypes: { + 'aria-label': { + control: { + type: 'text', + }, + description: 'An accessible name for the sidebar.', + table: { + category: 'Accessibility', + }, + type: { + name: 'string', + required: false, + }, + }, + children: { + control: { + type: 'text', + }, + description: 'The sidebar content.', + type: { + name: 'string', + required: true, + }, + }, + className: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the aside element.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + }, +} as ComponentMeta<typeof SidebarComponent>; + +const Template: ComponentStory<typeof SidebarComponent> = (args) => ( + <SidebarComponent {...args} /> +); + +/** + * Layout Stories - Sidebar + */ +export const Sidebar = Template.bind({}); +Sidebar.args = { + children: 'Some widgets.', +}; diff --git a/src/components/atoms/layout/sidebar.test.tsx b/src/components/atoms/layout/sidebar.test.tsx new file mode 100644 index 0000000..4c9459d --- /dev/null +++ b/src/components/atoms/layout/sidebar.test.tsx @@ -0,0 +1,11 @@ +import { render, screen } from '@test-utils'; +import Sidebar from './sidebar'; + +const children = 'A widget'; + +describe('Sidebar', () => { + it('renders an aside element', () => { + render(<Sidebar>{children}</Sidebar>); + expect(screen.getByRole('complementary')).toHaveTextContent(children); + }); +}); diff --git a/src/components/atoms/layout/sidebar.tsx b/src/components/atoms/layout/sidebar.tsx new file mode 100644 index 0000000..d86af37 --- /dev/null +++ b/src/components/atoms/layout/sidebar.tsx @@ -0,0 +1,32 @@ +import { FC, ReactNode } from 'react'; +import styles from './sidebar.module.scss'; + +export type SidebarProps = { + /** + * An accessible name for the sidebar. + */ + 'aria-label'?: string; + /** + * The sidebar body. + */ + children: ReactNode; + /** + * Set additional classnames to the aside element. + */ + className?: string; +}; + +/** + * Sidebar component + * + * Render an aside element. + */ +const Sidebar: FC<SidebarProps> = ({ children, className = '', ...props }) => { + return ( + <aside className={`${styles.wrapper} ${className}`} {...props}> + <div className={styles.body}>{children}</div> + </aside> + ); +}; + +export default Sidebar; |
