aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/atoms/layout
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/atoms/layout')
-rw-r--r--src/components/atoms/layout/copyright.module.scss32
-rw-r--r--src/components/atoms/layout/copyright.stories.tsx55
-rw-r--r--src/components/atoms/layout/copyright.test.tsx32
-rw-r--r--src/components/atoms/layout/copyright.tsx59
-rw-r--r--src/components/atoms/layout/main.stories.tsx52
-rw-r--r--src/components/atoms/layout/main.test.tsx12
-rw-r--r--src/components/atoms/layout/main.tsx23
-rw-r--r--src/components/atoms/layout/no-script.module.scss19
-rw-r--r--src/components/atoms/layout/no-script.stories.tsx46
-rw-r--r--src/components/atoms/layout/no-script.test.tsx11
-rw-r--r--src/components/atoms/layout/no-script.tsx21
-rw-r--r--src/components/atoms/layout/notice.module.scss28
-rw-r--r--src/components/atoms/layout/notice.stories.tsx40
-rw-r--r--src/components/atoms/layout/notice.test.tsx11
-rw-r--r--src/components/atoms/layout/notice.tsx30
-rw-r--r--src/components/atoms/layout/section.module.scss25
-rw-r--r--src/components/atoms/layout/section.stories.tsx85
-rw-r--r--src/components/atoms/layout/section.test.tsx17
-rw-r--r--src/components/atoms/layout/section.tsx57
19 files changed, 655 insertions, 0 deletions
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..3b315fa
--- /dev/null
+++ b/src/components/atoms/layout/copyright.stories.tsx
@@ -0,0 +1,55 @@
+import CCBySA from '@components/atoms/icons/cc-by-sa';
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { IntlProvider } from 'react-intl';
+import CopyrightComponent from './copyright';
+
+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) => (
+ <IntlProvider locale="en">
+ <CopyrightComponent {...args} />
+ </IntlProvider>
+);
+
+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..76252ee
--- /dev/null
+++ b/src/components/atoms/layout/copyright.tsx
@@ -0,0 +1,59 @@
+import { ReactNode, VFC } 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: VFC<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..64df890
--- /dev/null
+++ b/src/components/atoms/layout/main.stories.tsx
@@ -0,0 +1,52 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import MainComponent from './main';
+
+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} />
+);
+
+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..4549328
--- /dev/null
+++ b/src/components/atoms/layout/main.tsx
@@ -0,0 +1,23 @@
+import { FC } from 'react';
+
+export type MainProps = {
+ /**
+ * 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..474e2fb
--- /dev/null
+++ b/src/components/atoms/layout/no-script.stories.tsx
@@ -0,0 +1,46 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import NoScriptComponent from './no-script';
+
+export default {
+ title: 'Atoms/Layout',
+ component: NoScriptComponent,
+ 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 NoScriptComponent>;
+
+const Template: ComponentStory<typeof NoScriptComponent> = (args) => (
+ <NoScriptComponent {...args} />
+);
+
+export const NoScript = Template.bind({});
+NoScript.args = {
+ message: 'A noscript only message.',
+};
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..6358cf8
--- /dev/null
+++ b/src/components/atoms/layout/no-script.tsx
@@ -0,0 +1,21 @@
+import { VFC } 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: VFC<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..38ec7ee
--- /dev/null
+++ b/src/components/atoms/layout/notice.module.scss
@@ -0,0 +1,28 @@
+@use "@styles/abstracts/functions" as fun;
+
+.wrapper {
+ width: max-content;
+ 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..0555a2e
--- /dev/null
+++ b/src/components/atoms/layout/notice.stories.tsx
@@ -0,0 +1,40 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import NoticeComponent from './notice';
+
+export default {
+ title: 'Atoms/Layout',
+ component: NoticeComponent,
+ argTypes: {
+ 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} />
+);
+
+export const Notice = Template.bind({});
+Notice.args = {
+ kind: 'info',
+ 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..b6e09c5
--- /dev/null
+++ b/src/components/atoms/layout/notice.tsx
@@ -0,0 +1,30 @@
+import { VFC } from 'react';
+import styles from './notice.module.scss';
+
+export type NoticeKind = 'error' | 'info' | 'success' | 'warning';
+
+export type NoticeProps = {
+ /**
+ * The notice kind.
+ */
+ kind: NoticeKind;
+ /**
+ * The notice body.
+ */
+ message: string;
+};
+
+/**
+ * Notice component
+ *
+ * Render a colored message depending on notice kind.
+ */
+const Notice: VFC<NoticeProps> = ({ kind, message }) => {
+ const kindClass = `wrapper--${kind}`;
+
+ return (
+ <div className={`${styles.wrapper} ${styles[kindClass]}`}>{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..abbbeed
--- /dev/null
+++ b/src/components/atoms/layout/section.stories.tsx
@@ -0,0 +1,85 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import SectionComponent from './section';
+
+export default {
+ title: 'Atoms/Layout',
+ component: SectionComponent,
+ 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 SectionComponent>;
+
+const Template: ComponentStory<typeof SectionComponent> = (args) => (
+ <SectionComponent {...args} />
+);
+
+export const Section = Template.bind({});
+Section.args = {
+ title: 'A title',
+ content: 'The content.',
+};
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..f1bbb34
--- /dev/null
+++ b/src/components/atoms/layout/section.tsx
@@ -0,0 +1,57 @@
+import { ReactNode, VFC } 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: VFC<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;