aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/atoms/layout/section
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/atoms/layout/section')
-rw-r--r--src/components/atoms/layout/section/index.ts1
-rw-r--r--src/components/atoms/layout/section/section.module.scss25
-rw-r--r--src/components/atoms/layout/section/section.stories.tsx85
-rw-r--r--src/components/atoms/layout/section/section.test.tsx27
-rw-r--r--src/components/atoms/layout/section/section.tsx50
5 files changed, 188 insertions, 0 deletions
diff --git a/src/components/atoms/layout/section/index.ts b/src/components/atoms/layout/section/index.ts
new file mode 100644
index 0000000..2786cf0
--- /dev/null
+++ b/src/components/atoms/layout/section/index.ts
@@ -0,0 +1 @@
+export * from './section';
diff --git a/src/components/atoms/layout/section/section.module.scss b/src/components/atoms/layout/section/section.module.scss
new file mode 100644
index 0000000..771b8e3
--- /dev/null
+++ b/src/components/atoms/layout/section/section.module.scss
@@ -0,0 +1,25 @@
+@use "../../../../styles/abstracts/functions" as fun;
+@use "../../../../styles/abstracts/placeholders";
+
+.wrapper {
+ @extend %grid;
+
+ row-gap: var(--spacing-sm);
+ 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);
+ }
+
+ > * {
+ grid-column: 2;
+ }
+}
diff --git a/src/components/atoms/layout/section/section.stories.tsx b/src/components/atoms/layout/section/section.stories.tsx
new file mode 100644
index 0000000..0a3388b
--- /dev/null
+++ b/src/components/atoms/layout/section/section.stories.tsx
@@ -0,0 +1,85 @@
+import type { ComponentMeta, ComponentStory } from '@storybook/react';
+import { Heading } from '../../headings';
+import { Section } from './section';
+
+/**
+ * Section - Storybook Meta
+ */
+export default {
+ title: 'Atoms/Layout/Section',
+ component: Section,
+ args: {
+ hasBorder: true,
+ variant: 'light',
+ },
+ argTypes: {
+ children: {
+ description: 'The section content.',
+ type: {
+ name: 'function',
+ required: true,
+ },
+ },
+ hasBorder: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Add a border at the bottom of the section.',
+ table: {
+ category: 'Styles',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ variant: {
+ control: {
+ type: 'select',
+ },
+ description: 'The section variant.',
+ options: ['light', 'dark'],
+ table: {
+ category: 'Styles',
+ defaultValue: { summary: 'dark' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+} as ComponentMeta<typeof Section>;
+
+const Template: ComponentStory<typeof Section> = (args) => (
+ <Section {...args} />
+);
+
+/**
+ * Section Stories - Light
+ */
+export const Light = Template.bind({});
+Light.args = {
+ children: (
+ <>
+ <Heading level={2}>A section title</Heading>
+ <div>The body</div>
+ </>
+ ),
+ variant: 'light',
+};
+
+/**
+ * Section Stories - Dark
+ */
+export const Dark = Template.bind({});
+Dark.args = {
+ children: (
+ <>
+ <Heading level={2}>A section title</Heading>
+ <div>The body</div>
+ </>
+ ),
+ variant: 'dark',
+};
diff --git a/src/components/atoms/layout/section/section.test.tsx b/src/components/atoms/layout/section/section.test.tsx
new file mode 100644
index 0000000..85305c0
--- /dev/null
+++ b/src/components/atoms/layout/section/section.test.tsx
@@ -0,0 +1,27 @@
+import { describe, expect, it } from '@jest/globals';
+import { render, screen as rtlScreen } from '@testing-library/react';
+import { Section } from './section';
+
+const content = 'Section content.';
+
+describe('Section', () => {
+ it('renders its body', () => {
+ render(<Section>{content}</Section>);
+ expect(rtlScreen.getByText(content)).toBeInTheDocument();
+ });
+
+ it('renders a section with border', () => {
+ render(<Section hasBorder>{content}</Section>);
+ expect(rtlScreen.getByText(content)).toHaveClass('wrapper--borders');
+ });
+
+ it('renders a light section', () => {
+ render(<Section variant="light">{content}</Section>);
+ expect(rtlScreen.getByText(content)).toHaveClass('wrapper--light');
+ });
+
+ it('renders a dark section', () => {
+ render(<Section variant="dark">{content}</Section>);
+ expect(rtlScreen.getByText(content)).toHaveClass('wrapper--dark');
+ });
+});
diff --git a/src/components/atoms/layout/section/section.tsx b/src/components/atoms/layout/section/section.tsx
new file mode 100644
index 0000000..63c658a
--- /dev/null
+++ b/src/components/atoms/layout/section/section.tsx
@@ -0,0 +1,50 @@
+import {
+ forwardRef,
+ type ForwardRefRenderFunction,
+ type HTMLAttributes,
+ type ReactNode,
+} from 'react';
+import styles from './section.module.scss';
+
+export type SectionVariant = 'dark' | 'light';
+
+export type SectionProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
+ /**
+ * The section content.
+ */
+ children: ReactNode | ReactNode[];
+ /**
+ * Add a border at the bottom of the section.
+ *
+ * @default false
+ */
+ hasBorder?: boolean;
+ /**
+ * The section variant.
+ *
+ * @default 'light'
+ */
+ variant?: SectionVariant;
+};
+
+const SectionWithRef: ForwardRefRenderFunction<HTMLElement, SectionProps> = (
+ { children, className = '', hasBorder = false, variant = 'light', ...props },
+ ref
+) => {
+ const borderClass = hasBorder ? styles[`wrapper--borders`] : '';
+ const variantClass = styles[`wrapper--${variant}`];
+ const sectionClass = `${styles.wrapper} ${borderClass} ${variantClass} ${className}`;
+
+ return (
+ <section {...props} className={sectionClass} ref={ref}>
+ {children}
+ </section>
+ );
+};
+
+/**
+ * Section component
+ *
+ * Render a section element with a heading and a body.
+ */
+export const Section = forwardRef(SectionWithRef);