From 8370602f37ad6aa02485d85e5b179b76c3f15701 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Thu, 31 Mar 2022 15:39:55 +0200
Subject: chore: add a Heading component

---
 src/components/atoms/headings/heading.tsx | 21 +++++++++++++++++++++
 1 file changed, 21 insertions(+)
 create mode 100644 src/components/atoms/headings/heading.tsx

(limited to 'src/components/atoms/headings/heading.tsx')

diff --git a/src/components/atoms/headings/heading.tsx b/src/components/atoms/headings/heading.tsx
new file mode 100644
index 0000000..1535140
--- /dev/null
+++ b/src/components/atoms/headings/heading.tsx
@@ -0,0 +1,21 @@
+import { FC } from 'react';
+
+type HeadingProps = {
+  /**
+   * HTML heading level: 'h1', 'h2', 'h3', 'h4', 'h5' or 'h6'.
+   */
+  level: 1 | 2 | 3 | 4 | 5 | 6;
+};
+
+/**
+ * Heading component.
+ *
+ * Render an HTML heading element.
+ */
+const Heading: FC<HeadingProps> = ({ children, level }) => {
+  const TitleTag = `h${level}` as keyof JSX.IntrinsicElements;
+
+  return <TitleTag>{children}</TitleTag>;
+};
+
+export default Heading;
-- 
cgit v1.2.3


From 8a4fbf91b0ffdcb0ec38105f918ce6f90e6ec161 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Mon, 4 Apr 2022 15:45:08 +0200
Subject: chore: add a Branding component

---
 src/components/atoms/headings/heading.module.scss  | 57 +++++++++++++
 src/components/atoms/headings/heading.stories.tsx  | 32 +++++++
 src/components/atoms/headings/heading.test.tsx     | 10 +++
 src/components/atoms/headings/heading.tsx          | 35 +++++++-
 .../molecules/layout/branding.module.scss          | 48 +++++++++++
 .../molecules/layout/branding.stories.tsx          | 83 ++++++++++++++++++
 src/components/molecules/layout/branding.test.tsx  | 61 ++++++++++++++
 src/components/molecules/layout/branding.tsx       | 97 ++++++++++++++++++++++
 8 files changed, 419 insertions(+), 4 deletions(-)
 create mode 100644 src/components/atoms/headings/heading.module.scss
 create mode 100644 src/components/molecules/layout/branding.module.scss
 create mode 100644 src/components/molecules/layout/branding.stories.tsx
 create mode 100644 src/components/molecules/layout/branding.test.tsx
 create mode 100644 src/components/molecules/layout/branding.tsx

(limited to 'src/components/atoms/headings/heading.tsx')

diff --git a/src/components/atoms/headings/heading.module.scss b/src/components/atoms/headings/heading.module.scss
new file mode 100644
index 0000000..8620f6f
--- /dev/null
+++ b/src/components/atoms/headings/heading.module.scss
@@ -0,0 +1,57 @@
+@use "@styles/abstracts/functions" as fun;
+
+.heading {
+  color: var(--color-primary-dark);
+  font-family: var(--font-family-secondary);
+  letter-spacing: 0.01ex;
+
+  &--regular {
+    margin: 0;
+  }
+
+  &--margin {
+    margin: 0 0 var(--spacing-sm);
+
+    & + & {
+      margin-top: var(--spacing-md);
+    }
+  }
+
+  &--1 {
+    font-size: var(--font-size-3xl);
+    font-weight: 500;
+  }
+
+  &--2 {
+    padding-bottom: fun.convert-px(3);
+    background: linear-gradient(
+        to top,
+        var(--color-primary-dark) 0.3rem,
+        transparent 0.3rem
+      )
+      0 0 / 3rem 100% no-repeat;
+    font-size: var(--font-size-2xl);
+    font-weight: 500;
+    text-shadow: fun.convert-px(1) fun.convert-px(1) 0 var(--color-shadow-light);
+  }
+
+  &--3 {
+    font-size: var(--font-size-xl);
+    font-weight: 500;
+  }
+
+  &--4 {
+    font-size: var(--font-size-lg);
+    font-weight: 500;
+  }
+
+  &--5 {
+    font-size: var(--font-size-md);
+    font-weight: 600;
+  }
+
+  &--6 {
+    font-size: var(--font-size-md);
+    font-weight: 500;
+  }
+}
diff --git a/src/components/atoms/headings/heading.stories.tsx b/src/components/atoms/headings/heading.stories.tsx
index 9958af9..0b286fe 100644
--- a/src/components/atoms/headings/heading.stories.tsx
+++ b/src/components/atoms/headings/heading.stories.tsx
@@ -4,6 +4,10 @@ import HeadingComponent from './heading';
 export default {
   title: 'Atoms/Headings',
   component: HeadingComponent,
+  args: {
+    isFake: false,
+    withMargin: true,
+  },
   argTypes: {
     children: {
       description: 'Heading body.',
@@ -12,6 +16,20 @@ export default {
         required: true,
       },
     },
+    isFake: {
+      control: {
+        type: 'boolean',
+      },
+      description: 'Use an heading element or only its styles.',
+      table: {
+        category: 'Options',
+        defaultValue: { summary: false },
+      },
+      type: {
+        name: 'boolean',
+        required: false,
+      },
+    },
     level: {
       control: {
         type: 'select',
@@ -23,6 +41,20 @@ export default {
         required: true,
       },
     },
+    withMargin: {
+      control: {
+        type: 'boolean',
+      },
+      description: 'Adds margin.',
+      table: {
+        category: 'Options',
+        defaultValue: { summary: true },
+      },
+      type: {
+        name: 'boolean',
+        required: false,
+      },
+    },
   },
 } as ComponentMeta<typeof HeadingComponent>;
 
diff --git a/src/components/atoms/headings/heading.test.tsx b/src/components/atoms/headings/heading.test.tsx
index b83f7cd..6b6789a 100644
--- a/src/components/atoms/headings/heading.test.tsx
+++ b/src/components/atoms/headings/heading.test.tsx
@@ -43,4 +43,14 @@ describe('Heading', () => {
       'Level 6'
     );
   });
+
+  it('renders a text with heading styles', () => {
+    render(
+      <Heading isFake={true} level={2}>
+        Fake heading
+      </Heading>
+    );
+    expect(screen.queryByRole('heading', { level: 2 })).not.toBeInTheDocument();
+    expect(screen.getByText('Fake heading')).toHaveClass('heading');
+  });
 });
diff --git a/src/components/atoms/headings/heading.tsx b/src/components/atoms/headings/heading.tsx
index 1535140..77580cc 100644
--- a/src/components/atoms/headings/heading.tsx
+++ b/src/components/atoms/headings/heading.tsx
@@ -1,21 +1,48 @@
 import { FC } from 'react';
+import styles from './heading.module.scss';
 
 type HeadingProps = {
+  /**
+   * Adds additional classes.
+   */
+  additionalClasses?: string;
+  /**
+   * Use an heading element or only its styles. Default: false.
+   */
+  isFake?: boolean;
   /**
    * HTML heading level: 'h1', 'h2', 'h3', 'h4', 'h5' or 'h6'.
    */
   level: 1 | 2 | 3 | 4 | 5 | 6;
+  /**
+   * Adds margin. Default: true.
+   */
+  withMargin?: boolean;
 };
 
 /**
  * Heading component.
  *
- * Render an HTML heading element.
+ * Render an HTML heading element or a paragraph with heading styles.
  */
-const Heading: FC<HeadingProps> = ({ children, level }) => {
-  const TitleTag = `h${level}` as keyof JSX.IntrinsicElements;
+const Heading: FC<HeadingProps> = ({
+  children,
+  additionalClasses,
+  isFake = false,
+  level,
+  withMargin = true,
+}) => {
+  const TitleTag = isFake ? `p` : (`h${level}` as keyof JSX.IntrinsicElements);
+  const variantClass = withMargin ? 'heading--margin' : 'heading--regular';
+  const levelClass = `heading--${level}`;
 
-  return <TitleTag>{children}</TitleTag>;
+  return (
+    <TitleTag
+      className={`${styles.heading} ${styles[variantClass]} ${styles[levelClass]} ${additionalClasses}`}
+    >
+      {children}
+    </TitleTag>
+  );
 };
 
 export default Heading;
diff --git a/src/components/molecules/layout/branding.module.scss b/src/components/molecules/layout/branding.module.scss
new file mode 100644
index 0000000..aa18002
--- /dev/null
+++ b/src/components/molecules/layout/branding.module.scss
@@ -0,0 +1,48 @@
+@use "@styles/abstracts/functions" as fun;
+
+.wrapper {
+  display: grid;
+  grid-template-columns:
+    var(--logo-size, fun.convert-px(100))
+    minmax(0, 1fr);
+  grid-template-rows: 1fr min-content;
+  align-items: center;
+  column-gap: var(--spacing-sm);
+}
+
+.logo {
+  grid-row: span 2;
+}
+
+.title {
+  font-size: var(--font-size-2xl);
+}
+
+.baseline {
+  color: var(--color-fg-light);
+}
+
+.link {
+  background: linear-gradient(
+      to top,
+      var(--color-primary-light) fun.convert-px(5),
+      transparent fun.convert-px(5)
+    )
+    left / 0 100% no-repeat;
+  text-decoration: none;
+  transition: all 0.6s ease-out 0s;
+
+  &:hover,
+  &:focus {
+    background-size: 100% 100%;
+  }
+
+  &:focus {
+    color: var(--color-primary-light);
+  }
+
+  &:active {
+    background-size: 0 100%;
+    color: var(--color-primary-dark);
+  }
+}
diff --git a/src/components/molecules/layout/branding.stories.tsx b/src/components/molecules/layout/branding.stories.tsx
new file mode 100644
index 0000000..726ba26
--- /dev/null
+++ b/src/components/molecules/layout/branding.stories.tsx
@@ -0,0 +1,83 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { IntlProvider } from 'react-intl';
+import BrandingComponent from './branding';
+
+export default {
+  title: 'Molecules/Layout',
+  component: BrandingComponent,
+  args: {
+    isHome: false,
+  },
+  argTypes: {
+    baseline: {
+      control: {
+        type: 'text',
+      },
+      description: 'The Branding baseline.',
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
+    isHome: {
+      control: {
+        type: 'boolean',
+      },
+      description: 'Use H1 if the current page is homepage.',
+      table: {
+        category: 'Options',
+        defaultValue: { summary: false },
+      },
+      type: {
+        name: 'boolean',
+        required: false,
+      },
+    },
+    photo: {
+      control: {
+        type: 'text',
+      },
+      description: 'The Branding photo.',
+      type: {
+        name: 'string',
+        required: true,
+      },
+    },
+    title: {
+      control: {
+        type: 'text',
+      },
+      description: 'The Branding title.',
+      type: {
+        name: 'string',
+        required: true,
+      },
+    },
+    withLink: {
+      control: {
+        type: 'boolean',
+      },
+      description: 'Wraps the title with a link to homepage.',
+      table: {
+        category: 'Options',
+        defaultValue: { summary: false },
+      },
+      type: {
+        name: 'boolean',
+        required: false,
+      },
+    },
+  },
+} as ComponentMeta<typeof BrandingComponent>;
+
+const Template: ComponentStory<typeof BrandingComponent> = (args) => (
+  <IntlProvider locale="en">
+    <BrandingComponent {...args} />
+  </IntlProvider>
+);
+
+export const Branding = Template.bind({});
+Branding.args = {
+  title: 'Website title',
+  photo: 'http://placeimg.com/640/480',
+};
diff --git a/src/components/molecules/layout/branding.test.tsx b/src/components/molecules/layout/branding.test.tsx
new file mode 100644
index 0000000..4fe1e9a
--- /dev/null
+++ b/src/components/molecules/layout/branding.test.tsx
@@ -0,0 +1,61 @@
+import { render, screen } from '@test-utils';
+import Branding from './branding';
+
+describe('Branding', () => {
+  it('renders a photo', () => {
+    render(
+      <Branding
+        photo="http://placeimg.com/640/480/city"
+        title="Website title"
+      />
+    );
+    expect(
+      screen.getByRole('img', { name: 'Website title picture' })
+    ).toBeInTheDocument();
+  });
+
+  it('renders a logo', () => {
+    render(
+      <Branding photo="http://placeimg.com/640/480/city" title="Website name" />
+    );
+    expect(screen.getByTitle('Website name logo')).toBeInTheDocument();
+  });
+
+  it('renders a baseline', () => {
+    render(
+      <Branding
+        photo="http://placeimg.com/640/480"
+        title="Website title"
+        baseline="Website baseline"
+      />
+    );
+    expect(screen.getByText('Website baseline')).toBeInTheDocument();
+  });
+
+  it('renders a title wrapped with h1 element', () => {
+    render(
+      <Branding
+        photo="http://placeimg.com/640/480"
+        title="Website title"
+        isHome={true}
+      />
+    );
+    expect(
+      screen.getByRole('heading', { level: 1, name: 'Website title' })
+    ).toBeInTheDocument();
+  });
+
+  it('renders a title with h1 styles', () => {
+    render(
+      <Branding
+        photo="http://placeimg.com/640/480"
+        title="Website title"
+        isHome={false}
+      />
+    );
+    expect(
+      screen.queryByRole('heading', { level: 1, name: 'Website title' })
+    ).not.toBeInTheDocument();
+    expect(screen.getByText('Website title')).toHaveClass('heading--1');
+  });
+});
diff --git a/src/components/molecules/layout/branding.tsx b/src/components/molecules/layout/branding.tsx
new file mode 100644
index 0000000..efb2e34
--- /dev/null
+++ b/src/components/molecules/layout/branding.tsx
@@ -0,0 +1,97 @@
+import Heading from '@components/atoms/headings/heading';
+import Link from 'next/link';
+import { FC } from 'react';
+import { useIntl } from 'react-intl';
+import styles from './branding.module.scss';
+import FlippingLogo from './flipping-logo';
+
+type BrandingProps = {
+  /**
+   * The Branding baseline.
+   */
+  baseline?: string;
+  /**
+   * Use H1 if the current page is homepage. Default: false.
+   */
+  isHome?: boolean;
+  /**
+   * A photography URL.
+   */
+  photo: string;
+  /**
+   * The Branding title;
+   */
+  title: string;
+  /**
+   * Wraps the title with a link to homepage. Default: false.
+   */
+  withLink?: boolean;
+};
+
+/**
+ * Branding component
+ *
+ * Render the branding logo, title and optional baseline.
+ */
+const Branding: FC<BrandingProps> = ({
+  baseline,
+  isHome = false,
+  photo,
+  title,
+  withLink = false,
+}) => {
+  const intl = useIntl();
+  const altText = intl.formatMessage(
+    {
+      defaultMessage: '{website} picture',
+      description: 'Branding: photo alternative text',
+      id: 'dDK5oc',
+    },
+    { website: title }
+  );
+  const logoTitle = intl.formatMessage(
+    {
+      defaultMessage: '{website} logo',
+      description: 'Branding: logo title',
+      id: 'x55qsD',
+    },
+    { website: title }
+  );
+
+  return (
+    <div className={styles.wrapper}>
+      <FlippingLogo
+        additionalClasses={styles.logo}
+        altText={altText}
+        logoTitle={logoTitle}
+        photo={photo}
+      />
+      <Heading
+        isFake={!isHome}
+        level={1}
+        withMargin={false}
+        additionalClasses={styles.title}
+      >
+        {withLink ? (
+          <Link href="/">
+            <a className={styles.link}>{title}</a>
+          </Link>
+        ) : (
+          title
+        )}
+      </Heading>
+      {baseline && (
+        <Heading
+          isFake={true}
+          level={4}
+          withMargin={false}
+          additionalClasses={styles.baseline}
+        >
+          {baseline}
+        </Heading>
+      )}
+    </div>
+  );
+};
+
+export default Branding;
-- 
cgit v1.2.3


From 47e12259d512e476326e83929efebf036b57f7c1 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Wed, 6 Apr 2022 18:40:17 +0200
Subject: chore: add a Modal component

---
 src/components/atoms/headings/heading.stories.tsx  | 13 +++++
 src/components/atoms/headings/heading.tsx          |  6 +--
 src/components/atoms/icons/cog.module.scss         |  1 -
 .../atoms/icons/magnifying-glass.module.scss       |  1 -
 src/components/molecules/layout/modal.module.scss  | 21 ++++++++
 src/components/molecules/layout/modal.stories.tsx  | 57 ++++++++++++++++++++++
 src/components/molecules/layout/modal.test.tsx     |  9 ++++
 src/components/molecules/layout/modal.tsx          | 48 ++++++++++++++++++
 8 files changed, 151 insertions(+), 5 deletions(-)
 create mode 100644 src/components/molecules/layout/modal.module.scss
 create mode 100644 src/components/molecules/layout/modal.stories.tsx
 create mode 100644 src/components/molecules/layout/modal.test.tsx
 create mode 100644 src/components/molecules/layout/modal.tsx

(limited to 'src/components/atoms/headings/heading.tsx')

diff --git a/src/components/atoms/headings/heading.stories.tsx b/src/components/atoms/headings/heading.stories.tsx
index 0b286fe..cea3532 100644
--- a/src/components/atoms/headings/heading.stories.tsx
+++ b/src/components/atoms/headings/heading.stories.tsx
@@ -9,6 +9,19 @@ export default {
     withMargin: true,
   },
   argTypes: {
+    additionalClasses: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classes.',
+      table: {
+        category: 'Options',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     children: {
       description: 'Heading body.',
       type: {
diff --git a/src/components/atoms/headings/heading.tsx b/src/components/atoms/headings/heading.tsx
index 77580cc..136571d 100644
--- a/src/components/atoms/headings/heading.tsx
+++ b/src/components/atoms/headings/heading.tsx
@@ -1,7 +1,7 @@
 import { FC } from 'react';
 import styles from './heading.module.scss';
 
-type HeadingProps = {
+export type HeadingProps = {
   /**
    * Adds additional classes.
    */
@@ -33,12 +33,12 @@ const Heading: FC<HeadingProps> = ({
   withMargin = true,
 }) => {
   const TitleTag = isFake ? `p` : (`h${level}` as keyof JSX.IntrinsicElements);
-  const variantClass = withMargin ? 'heading--margin' : 'heading--regular';
   const levelClass = `heading--${level}`;
+  const marginClass = withMargin ? 'heading--margin' : 'heading--regular';
 
   return (
     <TitleTag
-      className={`${styles.heading} ${styles[variantClass]} ${styles[levelClass]} ${additionalClasses}`}
+      className={`${styles.heading} ${styles[levelClass]} ${styles[marginClass]} ${additionalClasses}`}
     >
       {children}
     </TitleTag>
diff --git a/src/components/atoms/icons/cog.module.scss b/src/components/atoms/icons/cog.module.scss
index 8c48fcc..5201598 100644
--- a/src/components/atoms/icons/cog.module.scss
+++ b/src/components/atoms/icons/cog.module.scss
@@ -1,7 +1,6 @@
 @use "@styles/abstracts/functions" as fun;
 
 .icon {
-  display: block;
   width: var(--icon-size, #{fun.convert-px(40)});
   fill: var(--color-primary-lighter);
   stroke: var(--color-primary-darker);
diff --git a/src/components/atoms/icons/magnifying-glass.module.scss b/src/components/atoms/icons/magnifying-glass.module.scss
index dca76fb..d14bec5 100644
--- a/src/components/atoms/icons/magnifying-glass.module.scss
+++ b/src/components/atoms/icons/magnifying-glass.module.scss
@@ -1,7 +1,6 @@
 @use "@styles/abstracts/functions" as fun;
 
 .icon {
-  display: block;
   width: var(--icon-size, #{fun.convert-px(40)});
 }
 
diff --git a/src/components/molecules/layout/modal.module.scss b/src/components/molecules/layout/modal.module.scss
new file mode 100644
index 0000000..2fff562
--- /dev/null
+++ b/src/components/molecules/layout/modal.module.scss
@@ -0,0 +1,21 @@
+@use "@styles/abstracts/functions" as fun;
+
+.wrapper {
+  padding: var(--spacing-md);
+  background: var(--color-bg-secondary);
+  border: fun.convert-px(4) solid;
+  border-image: radial-gradient(
+      ellipse at top,
+      var(--color-primary-lighter) 20%,
+      var(--color-primary) 100%
+    )
+    1;
+  box-shadow: fun.convert-px(2) fun.convert-px(-2) fun.convert-px(3)
+    fun.convert-px(-1) var(--color-shadow-dark);
+}
+
+.icon {
+  --icon-size: #{fun.convert-px(30)};
+
+  margin-right: var(--spacing-2xs);
+}
diff --git a/src/components/molecules/layout/modal.stories.tsx b/src/components/molecules/layout/modal.stories.tsx
new file mode 100644
index 0000000..396e89e
--- /dev/null
+++ b/src/components/molecules/layout/modal.stories.tsx
@@ -0,0 +1,57 @@
+import Cog from '@components/atoms/icons/cog';
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import ModalComponent from './modal';
+
+export default {
+  title: 'Molecules/Layout',
+  component: ModalComponent,
+  argTypes: {
+    children: {
+      control: {
+        type: 'text',
+      },
+      description: 'The modal body.',
+      type: {
+        name: 'string',
+        required: true,
+      },
+    },
+    icon: {
+      control: {
+        type: 'select',
+      },
+      description: 'The title icon.',
+      options: ['', 'cogs', 'search'],
+      table: {
+        category: 'Options',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
+    title: {
+      control: {
+        type: 'text',
+      },
+      description: 'The modal title.',
+      table: {
+        category: 'Options',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
+  },
+} as ComponentMeta<typeof ModalComponent>;
+
+const Template: ComponentStory<typeof ModalComponent> = (args) => (
+  <ModalComponent {...args} />
+);
+
+export const Modal = Template.bind({});
+Modal.args = {
+  children:
+    'Inventore natus dignissimos aut illum modi asperiores. Et voluptatibus delectus.',
+};
diff --git a/src/components/molecules/layout/modal.test.tsx b/src/components/molecules/layout/modal.test.tsx
new file mode 100644
index 0000000..14fb224
--- /dev/null
+++ b/src/components/molecules/layout/modal.test.tsx
@@ -0,0 +1,9 @@
+import { render, screen } from '@test-utils';
+import Modal from './modal';
+
+describe('Modal', () => {
+  it('renders a title', () => {
+    render(<Modal title="A custom title" />);
+    expect(screen.getByText('A custom title')).toBeInTheDocument();
+  });
+});
diff --git a/src/components/molecules/layout/modal.tsx b/src/components/molecules/layout/modal.tsx
new file mode 100644
index 0000000..4dc3b0a
--- /dev/null
+++ b/src/components/molecules/layout/modal.tsx
@@ -0,0 +1,48 @@
+import Heading from '@components/atoms/headings/heading';
+import dynamic from 'next/dynamic';
+import { FC, ReactNode } from 'react';
+import styles from './modal.module.scss';
+
+export type Icons = 'cogs' | 'search';
+
+export type ModalProps = {
+  icon?: Icons;
+  title?: string;
+};
+
+const CogIcon = dynamic<ReactNode>(() => import('@components/atoms/icons/cog'));
+const SearchIcon = dynamic<ReactNode>(
+  () => import('@components/atoms/icons/magnifying-glass')
+);
+
+/**
+ * Modal component
+ *
+ * Render a modal component with an optional title and icon.
+ */
+const Modal: FC<ModalProps> = ({ children, icon, title }) => {
+  const getIcon = (id: Icons) => {
+    switch (id) {
+      case 'cogs':
+        return <CogIcon />;
+      case 'search':
+        return <SearchIcon />;
+      default:
+        return <></>;
+    }
+  };
+
+  return (
+    <div className={styles.wrapper}>
+      {title && (
+        <Heading isFake={true} level={3}>
+          {icon && <span className={styles.icon}>{getIcon(icon)}</span>}
+          {title}
+        </Heading>
+      )}
+      {children}
+    </div>
+  );
+};
+
+export default Modal;
-- 
cgit v1.2.3


From a5df28fad0dae266a857ae110c43b3cb8b23c996 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Fri, 8 Apr 2022 19:41:40 +0200
Subject: refactor: use a consistent classname prop and avoid children prop

I was using the FunctionComponent type for some component that do not
use children. So I change the type to VoidFunctionComponent to avoid
mistakes.
I also rename all the "classes" or "additionalClasses" props to
"className" to keep consistency between each components.
---
 src/components/atoms/buttons/button-link.tsx       |  2 +-
 src/components/atoms/buttons/button.stories.tsx    | 13 +++++++++++
 src/components/atoms/buttons/button.tsx            | 10 ++++-----
 src/components/atoms/buttons/buttons.module.scss   |  4 ++++
 src/components/atoms/forms/toggle.module.scss      |  3 ++-
 src/components/atoms/forms/toggle.stories.tsx      | 14 ++++++++++++
 src/components/atoms/forms/toggle.tsx              | 26 ++++++++++++++++------
 src/components/atoms/headings/heading.stories.tsx  | 14 ++++++------
 src/components/atoms/headings/heading.tsx          | 14 +++++++-----
 src/components/atoms/images/logo.stories.tsx       | 15 +++++++++++++
 src/components/atoms/images/logo.tsx               |  4 ++--
 src/components/atoms/links/link.stories.tsx        | 13 +++++++++++
 src/components/atoms/links/link.tsx                | 14 +++++-------
 src/components/atoms/links/nav-link.tsx            |  4 ++--
 src/components/atoms/links/sharing-link.tsx        |  4 ++--
 src/components/atoms/links/social-link.tsx         | 10 +++++----
 .../atoms/lists/description-list.stories.tsx       |  6 ++---
 src/components/atoms/lists/description-list.tsx    | 13 ++++++-----
 src/components/atoms/lists/list.stories.tsx        |  6 ++---
 src/components/atoms/lists/list.tsx                | 15 +++++--------
 .../atoms/loaders/progress-bar.stories.tsx         |  8 ++++++-
 src/components/atoms/loaders/progress-bar.tsx      | 10 ++++-----
 src/components/atoms/loaders/spinner.stories.tsx   |  3 +++
 src/components/atoms/loaders/spinner.tsx           |  6 ++---
 .../molecules/buttons/back-to-top.stories.tsx      |  7 ++++--
 src/components/molecules/buttons/back-to-top.tsx   | 12 +++++-----
 .../molecules/buttons/help-button.stories.tsx      |  4 ++--
 src/components/molecules/buttons/help-button.tsx   | 10 ++++-----
 .../molecules/images/responsive-image.tsx          |  6 ++---
 src/components/molecules/layout/branding.tsx       | 10 ++++-----
 .../molecules/layout/flipping-logo.stories.tsx     | 23 ++++++++++++++-----
 src/components/molecules/layout/flipping-logo.tsx  | 12 +++++-----
 src/components/molecules/modals/modal.stories.tsx  | 13 +++++++++++
 src/components/molecules/modals/modal.tsx          | 22 +++++++++++++-----
 .../molecules/modals/tooltip.stories.tsx           | 23 +++++++++++++++++++
 src/components/molecules/modals/tooltip.tsx        | 15 ++++++++-----
 36 files changed, 270 insertions(+), 118 deletions(-)

(limited to 'src/components/atoms/headings/heading.tsx')

diff --git a/src/components/atoms/buttons/button-link.tsx b/src/components/atoms/buttons/button-link.tsx
index 47fe4b0..81229c8 100644
--- a/src/components/atoms/buttons/button-link.tsx
+++ b/src/components/atoms/buttons/button-link.tsx
@@ -2,7 +2,7 @@ import Link from 'next/link';
 import { FC } from 'react';
 import styles from './buttons.module.scss';
 
-type ButtonLinkProps = {
+export type ButtonLinkProps = {
   /**
    * ButtonLink accessible label.
    */
diff --git a/src/components/atoms/buttons/button.stories.tsx b/src/components/atoms/buttons/button.stories.tsx
index 9f4061b..1061d83 100644
--- a/src/components/atoms/buttons/button.stories.tsx
+++ b/src/components/atoms/buttons/button.stories.tsx
@@ -33,6 +33,19 @@ export default {
         required: true,
       },
     },
+    className: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the button wrapper.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     disabled: {
       control: {
         type: 'boolean',
diff --git a/src/components/atoms/buttons/button.tsx b/src/components/atoms/buttons/button.tsx
index ae4c894..b223046 100644
--- a/src/components/atoms/buttons/button.tsx
+++ b/src/components/atoms/buttons/button.tsx
@@ -3,9 +3,9 @@ import styles from './buttons.module.scss';
 
 export type ButtonProps = {
   /**
-   * Add additional classes to the button wrapper.
+   * Set additional classnames to the button wrapper.
    */
-  additionalClasses?: string;
+  className?: string;
   /**
    * Button accessible label.
    */
@@ -17,7 +17,7 @@ export type ButtonProps = {
   /**
    * Button kind. Default: secondary.
    */
-  kind?: 'primary' | 'secondary' | 'tertiary';
+  kind?: 'primary' | 'secondary' | 'tertiary' | 'neutral';
   /**
    * A callback function to handle click.
    */
@@ -38,7 +38,7 @@ export type ButtonProps = {
  * Use a button as call to action.
  */
 const Button: FC<ButtonProps> = ({
-  additionalClasses,
+  className = '',
   ariaLabel,
   children,
   disabled = false,
@@ -54,7 +54,7 @@ const Button: FC<ButtonProps> = ({
     <button
       type={type}
       disabled={disabled}
-      className={`${styles.btn} ${kindClass} ${shapeClass} ${additionalClasses}`}
+      className={`${styles.btn} ${kindClass} ${shapeClass} ${className}`}
       aria-label={ariaLabel}
       {...props}
     >
diff --git a/src/components/atoms/buttons/buttons.module.scss b/src/components/atoms/buttons/buttons.module.scss
index a1c3dba..87c92db 100644
--- a/src/components/atoms/buttons/buttons.module.scss
+++ b/src/components/atoms/buttons/buttons.module.scss
@@ -28,6 +28,10 @@
     cursor: wait;
   }
 
+  &--neutral {
+    background: inherit;
+  }
+
   &--primary {
     background: var(--color-primary);
     border: fun.convert-px(2) solid var(--color-bg);
diff --git a/src/components/atoms/forms/toggle.module.scss b/src/components/atoms/forms/toggle.module.scss
index 00e87a2..24b867e 100644
--- a/src/components/atoms/forms/toggle.module.scss
+++ b/src/components/atoms/forms/toggle.module.scss
@@ -6,10 +6,11 @@
 
   display: inline-flex;
   align-items: center;
+  width: 100%;
 }
 
 .title {
-  margin-right: var(--spacing-xs);
+  margin-right: auto;
 }
 
 .toggle {
diff --git a/src/components/atoms/forms/toggle.stories.tsx b/src/components/atoms/forms/toggle.stories.tsx
index 6e7323b..ea08694 100644
--- a/src/components/atoms/forms/toggle.stories.tsx
+++ b/src/components/atoms/forms/toggle.stories.tsx
@@ -34,6 +34,20 @@ export default {
         required: true,
       },
     },
+    labelSize: {
+      control: {
+        type: 'select',
+      },
+      description: 'The label size.',
+      options: ['medium', 'small'],
+      table: {
+        category: 'Options',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     name: {
       control: {
         type: 'text',
diff --git a/src/components/atoms/forms/toggle.tsx b/src/components/atoms/forms/toggle.tsx
index e8e8c0f..7ef40ed 100644
--- a/src/components/atoms/forms/toggle.tsx
+++ b/src/components/atoms/forms/toggle.tsx
@@ -1,9 +1,16 @@
-import { FC, ReactElement } from 'react';
+import { ReactNode, VFC } from 'react';
+import Label, { LabelProps } from './label';
 import styles from './toggle.module.scss';
 
 export type ToggleChoices = {
-  left: ReactElement | string;
-  right: ReactElement | string;
+  /**
+   * The left part of the toggle field (unchecked).
+   */
+  left: ReactNode;
+  /**
+   * The right part of the toggle field (checked).
+   */
+  right: ReactNode;
 };
 
 export type ToggleProps = {
@@ -19,6 +26,10 @@ export type ToggleProps = {
    * The toggle label.
    */
   label: string;
+  /**
+   * The label size.
+   */
+  labelSize?: LabelProps['size'];
   /**
    * The input name.
    */
@@ -38,13 +49,14 @@ export type ToggleProps = {
  *
  * Render a toggle with a label and two choices.
  */
-const Toggle: FC<ToggleProps> = ({
+const Toggle: VFC<ToggleProps> = ({
   choices,
   id,
   label,
+  labelSize,
   name,
-  value,
   setValue,
+  value,
 }) => {
   return (
     <>
@@ -56,12 +68,12 @@ const Toggle: FC<ToggleProps> = ({
         onChange={() => setValue(!value)}
         className={styles.checkbox}
       />
-      <label htmlFor={id} className={styles.label}>
+      <Label size={labelSize} htmlFor={id} className={styles.label}>
         <span className={styles.title}>{label}</span>
         {choices.left}
         <span className={styles.toggle}></span>
         {choices.right}
-      </label>
+      </Label>
     </>
   );
 };
diff --git a/src/components/atoms/headings/heading.stories.tsx b/src/components/atoms/headings/heading.stories.tsx
index cea3532..66a84dc 100644
--- a/src/components/atoms/headings/heading.stories.tsx
+++ b/src/components/atoms/headings/heading.stories.tsx
@@ -9,13 +9,13 @@ export default {
     withMargin: true,
   },
   argTypes: {
-    additionalClasses: {
+    className: {
       control: {
         type: 'text',
       },
-      description: 'Set additional classes.',
+      description: 'Set additional classnames.',
       table: {
-        category: 'Options',
+        category: 'Styles',
       },
       type: {
         name: 'string',
@@ -71,12 +71,12 @@ export default {
   },
 } as ComponentMeta<typeof HeadingComponent>;
 
-const Template: ComponentStory<typeof HeadingComponent> = (args) => {
-  const { level, ...props } = args;
-  return <HeadingComponent level={level || 1} {...props} />;
-};
+const Template: ComponentStory<typeof HeadingComponent> = (args) => (
+  <HeadingComponent {...args} />
+);
 
 export const Heading = Template.bind({});
 Heading.args = {
   children: 'Your title',
+  level: 1,
 };
diff --git a/src/components/atoms/headings/heading.tsx b/src/components/atoms/headings/heading.tsx
index 136571d..3048534 100644
--- a/src/components/atoms/headings/heading.tsx
+++ b/src/components/atoms/headings/heading.tsx
@@ -1,19 +1,21 @@
 import { FC } from 'react';
 import styles from './heading.module.scss';
 
+export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
+
 export type HeadingProps = {
   /**
-   * Adds additional classes.
+   * Set additional classnames.
    */
-  additionalClasses?: string;
+  className?: string;
   /**
    * Use an heading element or only its styles. Default: false.
    */
   isFake?: boolean;
   /**
-   * HTML heading level: 'h1', 'h2', 'h3', 'h4', 'h5' or 'h6'.
+   * HTML heading level.
    */
-  level: 1 | 2 | 3 | 4 | 5 | 6;
+  level: HeadingLevel;
   /**
    * Adds margin. Default: true.
    */
@@ -27,7 +29,7 @@ export type HeadingProps = {
  */
 const Heading: FC<HeadingProps> = ({
   children,
-  additionalClasses,
+  className,
   isFake = false,
   level,
   withMargin = true,
@@ -38,7 +40,7 @@ const Heading: FC<HeadingProps> = ({
 
   return (
     <TitleTag
-      className={`${styles.heading} ${styles[levelClass]} ${styles[marginClass]} ${additionalClasses}`}
+      className={`${styles.heading} ${styles[levelClass]} ${styles[marginClass]} ${className}`}
     >
       {children}
     </TitleTag>
diff --git a/src/components/atoms/images/logo.stories.tsx b/src/components/atoms/images/logo.stories.tsx
index afb5317..fbc7501 100644
--- a/src/components/atoms/images/logo.stories.tsx
+++ b/src/components/atoms/images/logo.stories.tsx
@@ -4,6 +4,21 @@ import LogoComponent from './logo';
 export default {
   title: 'Atoms/Images',
   component: LogoComponent,
+  argTypes: {
+    title: {
+      control: {
+        type: 'text',
+      },
+      description: 'The SVG title.',
+      table: {
+        category: 'Accessibility',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
+  },
 } as ComponentMeta<typeof LogoComponent>;
 
 const Template: ComponentStory<typeof LogoComponent> = (args) => (
diff --git a/src/components/atoms/images/logo.tsx b/src/components/atoms/images/logo.tsx
index 9ce2518..2e52110 100644
--- a/src/components/atoms/images/logo.tsx
+++ b/src/components/atoms/images/logo.tsx
@@ -1,4 +1,4 @@
-import { FC } from 'react';
+import { VFC } from 'react';
 import styles from './logo.module.scss';
 
 type LogoProps = {
@@ -13,7 +13,7 @@ type LogoProps = {
  *
  * Render a SVG logo.
  */
-const Logo: FC<LogoProps> = ({ title }) => {
+const Logo: VFC<LogoProps> = ({ title }) => {
   return (
     <svg
       viewBox="0 0 512 512"
diff --git a/src/components/atoms/links/link.stories.tsx b/src/components/atoms/links/link.stories.tsx
index e5a8b0a..569c874 100644
--- a/src/components/atoms/links/link.stories.tsx
+++ b/src/components/atoms/links/link.stories.tsx
@@ -15,6 +15,19 @@ export default {
         required: true,
       },
     },
+    className: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     external: {
       control: {
         type: 'boolean',
diff --git a/src/components/atoms/links/link.tsx b/src/components/atoms/links/link.tsx
index a61158f..87f11fc 100644
--- a/src/components/atoms/links/link.tsx
+++ b/src/components/atoms/links/link.tsx
@@ -2,11 +2,11 @@ import NextLink from 'next/link';
 import { FC } from 'react';
 import styles from './link.module.scss';
 
-type LinkProps = {
+export type LinkProps = {
   /**
-   * Set additional classes to the link.
+   * Set additional classnames to the link.
    */
-  classes?: string;
+  className?: string;
   /**
    * True if it is an external link. Default: false.
    */
@@ -28,24 +28,22 @@ type LinkProps = {
  */
 const Link: FC<LinkProps> = ({
   children,
-  classes,
+  className = '',
   href,
   lang,
   external = false,
 }) => {
-  const additionalClasses = classes || '';
-
   return external ? (
     <a
       href={href}
       hrefLang={lang}
-      className={`${styles.link} ${styles['link--external']} ${additionalClasses}`}
+      className={`${styles.link} ${styles['link--external']} ${className}`}
     >
       {children}
     </a>
   ) : (
     <NextLink href={href}>
-      <a className={`${styles.link} ${additionalClasses}`}>{children}</a>
+      <a className={`${styles.link} ${className}`}>{children}</a>
     </NextLink>
   );
 };
diff --git a/src/components/atoms/links/nav-link.tsx b/src/components/atoms/links/nav-link.tsx
index 7c6fede..25c0e7d 100644
--- a/src/components/atoms/links/nav-link.tsx
+++ b/src/components/atoms/links/nav-link.tsx
@@ -1,5 +1,5 @@
 import Link from 'next/link';
-import { FC, ReactNode } from 'react';
+import { VFC, ReactNode } from 'react';
 import styles from './nav-link.module.scss';
 
 export type NavLinkProps = {
@@ -22,7 +22,7 @@ export type NavLinkProps = {
  *
  * Render a navigation link.
  */
-const NavLink: FC<NavLinkProps> = ({ href, label, logo }) => {
+const NavLink: VFC<NavLinkProps> = ({ href, label, logo }) => {
   return (
     <Link href={href}>
       <a className={styles.link}>
diff --git a/src/components/atoms/links/sharing-link.tsx b/src/components/atoms/links/sharing-link.tsx
index ca53ef9..3cd2dd1 100644
--- a/src/components/atoms/links/sharing-link.tsx
+++ b/src/components/atoms/links/sharing-link.tsx
@@ -1,4 +1,4 @@
-import { FC } from 'react';
+import { VFC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './sharing-link.module.scss';
 
@@ -26,7 +26,7 @@ export type SharingLinkProps = {
  *
  * Render a sharing link.
  */
-const SharingLink: FC<SharingLinkProps> = ({ medium, url }) => {
+const SharingLink: VFC<SharingLinkProps> = ({ medium, url }) => {
   const intl = useIntl();
   const text = intl.formatMessage(
     {
diff --git a/src/components/atoms/links/social-link.tsx b/src/components/atoms/links/social-link.tsx
index 489c8b4..8c7c790 100644
--- a/src/components/atoms/links/social-link.tsx
+++ b/src/components/atoms/links/social-link.tsx
@@ -2,14 +2,16 @@ import GithubIcon from '@assets/images/social-media/github.svg';
 import GitlabIcon from '@assets/images/social-media/gitlab.svg';
 import LinkedInIcon from '@assets/images/social-media/linkedin.svg';
 import TwitterIcon from '@assets/images/social-media/twitter.svg';
-import { FC } from 'react';
+import { VFC } from 'react';
 import styles from './social-link.module.scss';
 
-type SocialLinkProps = {
+export type SocialWebsite = 'Github' | 'Gitlab' | 'LinkedIn' | 'Twitter';
+
+export type SocialLinkProps = {
   /**
    * The social website name.
    */
-  name: 'Github' | 'Gitlab' | 'LinkedIn' | 'Twitter';
+  name: SocialWebsite;
   /**
    * The social profile url.
    */
@@ -21,7 +23,7 @@ type SocialLinkProps = {
  *
  * Render a social icon link.
  */
-const SocialLink: FC<SocialLinkProps> = ({ name, url }) => {
+const SocialLink: VFC<SocialLinkProps> = ({ name, url }) => {
   /**
    * Retrieve a social link icon by id.
    * @param {string} id - The social website id.
diff --git a/src/components/atoms/lists/description-list.stories.tsx b/src/components/atoms/lists/description-list.stories.tsx
index e9a60cf..c65241d 100644
--- a/src/components/atoms/lists/description-list.stories.tsx
+++ b/src/components/atoms/lists/description-list.stories.tsx
@@ -7,13 +7,13 @@ export default {
   title: 'Atoms/Lists',
   component: DescriptionListComponent,
   argTypes: {
-    classes: {
+    className: {
       control: {
         type: 'text',
       },
-      description: 'Set additional classes to the list wrapper.',
+      description: 'Set additional classnames to the list wrapper',
       table: {
-        category: 'Options',
+        category: 'Styles',
       },
       type: {
         name: 'string',
diff --git a/src/components/atoms/lists/description-list.tsx b/src/components/atoms/lists/description-list.tsx
index df2880f..a5ab1d5 100644
--- a/src/components/atoms/lists/description-list.tsx
+++ b/src/components/atoms/lists/description-list.tsx
@@ -1,4 +1,4 @@
-import { FC } from 'react';
+import { VFC } from 'react';
 import styles from './description-list.module.scss';
 
 export type DescriptionListItem = {
@@ -18,9 +18,9 @@ export type DescriptionListItem = {
 
 export type DescriptionListProps = {
   /**
-   * Set additional classes to the list wrapper.
+   * Set additional classnames to the list wrapper.
    */
-  classes?: string;
+  className?: string;
   /**
    * The list items.
    */
@@ -32,7 +32,10 @@ export type DescriptionListProps = {
  *
  * Render a description list.
  */
-const DescriptionList: FC<DescriptionListProps> = ({ classes = '', items }) => {
+const DescriptionList: VFC<DescriptionListProps> = ({
+  className = '',
+  items,
+}) => {
   /**
    * Retrieve the description list items wrapped in a div element.
    *
@@ -54,7 +57,7 @@ const DescriptionList: FC<DescriptionListProps> = ({ classes = '', items }) => {
     });
   };
 
-  return <dl className={`${styles.list} ${classes}`}>{getItems(items)}</dl>;
+  return <dl className={`${styles.list} ${className}`}>{getItems(items)}</dl>;
 };
 
 export default DescriptionList;
diff --git a/src/components/atoms/lists/list.stories.tsx b/src/components/atoms/lists/list.stories.tsx
index 6256e81..30079cb 100644
--- a/src/components/atoms/lists/list.stories.tsx
+++ b/src/components/atoms/lists/list.stories.tsx
@@ -8,13 +8,13 @@ export default {
     kind: 'unordered',
   },
   argTypes: {
-    classes: {
+    className: {
       control: {
         type: 'text',
       },
-      description: 'Add additional classes to the list element.',
+      description: 'Set additional classnames to the list wrapper',
       table: {
-        category: 'Options',
+        category: 'Styles',
       },
       type: {
         name: 'string',
diff --git a/src/components/atoms/lists/list.tsx b/src/components/atoms/lists/list.tsx
index 8ce8437..7197c34 100644
--- a/src/components/atoms/lists/list.tsx
+++ b/src/components/atoms/lists/list.tsx
@@ -1,4 +1,4 @@
-import { FC } from 'react';
+import { VFC } from 'react';
 import styles from './list.module.scss';
 
 export type ListItem = {
@@ -18,9 +18,9 @@ export type ListItem = {
 
 export type ListProps = {
   /**
-   * Add additional classes to the list element.
+   * Set additional classnames to the list wrapper
    */
-  classes?: string;
+  className?: string;
   /**
    * An array of list items.
    */
@@ -36,9 +36,8 @@ export type ListProps = {
  *
  * Render either an ordered or an unordered list.
  */
-const List: FC<ListProps> = ({ classes, items, kind = 'unordered' }) => {
+const List: VFC<ListProps> = ({ className, items, kind = 'unordered' }) => {
   const ListTag = kind === 'ordered' ? 'ol' : 'ul';
-  const additionalClasses = classes || '';
   const kindClass = `list--${kind}`;
 
   /**
@@ -52,7 +51,7 @@ const List: FC<ListProps> = ({ classes, items, kind = 'unordered' }) => {
         {value}
         {child && (
           <ListTag
-            className={`${styles.list} ${styles[kindClass]} ${additionalClasses}`}
+            className={`${styles.list} ${styles[kindClass]} ${className}`}
           >
             {getItems(child)}
           </ListTag>
@@ -62,9 +61,7 @@ const List: FC<ListProps> = ({ classes, items, kind = 'unordered' }) => {
   };
 
   return (
-    <ListTag
-      className={`${styles.list} ${styles[kindClass]} ${additionalClasses}`}
-    >
+    <ListTag className={`${styles.list} ${styles[kindClass]} ${className}`}>
       {getItems(items)}
     </ListTag>
   );
diff --git a/src/components/atoms/loaders/progress-bar.stories.tsx b/src/components/atoms/loaders/progress-bar.stories.tsx
index 837a696..4fde5a7 100644
--- a/src/components/atoms/loaders/progress-bar.stories.tsx
+++ b/src/components/atoms/loaders/progress-bar.stories.tsx
@@ -5,11 +5,14 @@ export default {
   title: 'Atoms/Loaders',
   component: ProgressBarComponent,
   argTypes: {
-    ariaLabel: {
+    'aria-label': {
       control: {
         type: 'string',
       },
       description: 'An accessible name.',
+      table: {
+        category: 'Accessibility',
+      },
       type: {
         name: 'string',
         required: false,
@@ -30,6 +33,9 @@ export default {
         type: 'text',
       },
       description: 'An additional information to display.',
+      table: {
+        category: 'Options',
+      },
       type: {
         name: 'string',
         required: false,
diff --git a/src/components/atoms/loaders/progress-bar.tsx b/src/components/atoms/loaders/progress-bar.tsx
index fa4e09d..1b1ff06 100644
--- a/src/components/atoms/loaders/progress-bar.tsx
+++ b/src/components/atoms/loaders/progress-bar.tsx
@@ -1,11 +1,11 @@
-import { FC } from 'react';
+import { VFC } from 'react';
 import styles from './progress-bar.module.scss';
 
 export type ProgressBarProps = {
   /**
    * Accessible progress bar name.
    */
-  ariaLabel?: string;
+  'aria-label'?: string;
   /**
    * Current value.
    */
@@ -29,12 +29,12 @@ export type ProgressBarProps = {
  *
  * Render a progress bar.
  */
-const ProgressBar: FC<ProgressBarProps> = ({
-  ariaLabel,
+const ProgressBar: VFC<ProgressBarProps> = ({
   current,
   info,
   min,
   max,
+  ...props
 }) => {
   return (
     <div className={styles.progress}>
@@ -46,7 +46,7 @@ const ProgressBar: FC<ProgressBarProps> = ({
         aria-valuemin={min}
         aria-valuemax={max}
         aria-valuenow={current}
-        aria-label={ariaLabel}
+        {...props}
       ></progress>
     </div>
   );
diff --git a/src/components/atoms/loaders/spinner.stories.tsx b/src/components/atoms/loaders/spinner.stories.tsx
index 86c316e..5006ce4 100644
--- a/src/components/atoms/loaders/spinner.stories.tsx
+++ b/src/components/atoms/loaders/spinner.stories.tsx
@@ -11,6 +11,9 @@ export default {
         type: 'text',
       },
       description: 'Loading message.',
+      table: {
+        category: 'Options',
+      },
       type: {
         name: 'string',
         required: false,
diff --git a/src/components/atoms/loaders/spinner.tsx b/src/components/atoms/loaders/spinner.tsx
index 57b0a43..bff0f25 100644
--- a/src/components/atoms/loaders/spinner.tsx
+++ b/src/components/atoms/loaders/spinner.tsx
@@ -1,8 +1,8 @@
-import { FC } from 'react';
+import { VFC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './spinner.module.scss';
 
-type SpinnerProps = {
+export type SpinnerProps = {
   /**
    * The loading message. Default: "Loading...".
    */
@@ -14,7 +14,7 @@ type SpinnerProps = {
  *
  * Render a loading message with animation.
  */
-const Spinner: FC<SpinnerProps> = ({ message }) => {
+const Spinner: VFC<SpinnerProps> = ({ message }) => {
   const intl = useIntl();
 
   return (
diff --git a/src/components/molecules/buttons/back-to-top.stories.tsx b/src/components/molecules/buttons/back-to-top.stories.tsx
index f1a36e5..fe58293 100644
--- a/src/components/molecules/buttons/back-to-top.stories.tsx
+++ b/src/components/molecules/buttons/back-to-top.stories.tsx
@@ -6,11 +6,14 @@ export default {
   title: 'Molecules/Buttons',
   component: BackToTopComponent,
   argTypes: {
-    additionalClasses: {
+    className: {
       control: {
         type: 'text',
       },
-      description: 'Add additional classes to the button wrapper.',
+      description: 'Set additional classnames to the button wrapper.',
+      table: {
+        category: 'Styles',
+      },
       type: {
         name: 'string',
         required: false,
diff --git a/src/components/molecules/buttons/back-to-top.tsx b/src/components/molecules/buttons/back-to-top.tsx
index f25d3e9..56c5247 100644
--- a/src/components/molecules/buttons/back-to-top.tsx
+++ b/src/components/molecules/buttons/back-to-top.tsx
@@ -1,14 +1,14 @@
 import ButtonLink from '@components/atoms/buttons/button-link';
 import Arrow from '@components/atoms/icons/arrow';
-import { FC } from 'react';
+import { VFC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './back-to-top.module.scss';
 
-type BackToTopProps = {
+export type BackToTopProps = {
   /**
-   * Add additional classes to the button wrapper.
+   * Set additional classnames to the button wrapper.
    */
-  additionalClasses?: string;
+  className?: string;
   /**
    * An element id (without hashtag) to use as anchor.
    */
@@ -20,7 +20,7 @@ type BackToTopProps = {
  *
  * Render a back to top link.
  */
-const BackToTop: FC<BackToTopProps> = ({ additionalClasses, target }) => {
+const BackToTop: VFC<BackToTopProps> = ({ className = '', target }) => {
   const intl = useIntl();
   const linkName = intl.formatMessage({
     defaultMessage: 'Back to top',
@@ -29,7 +29,7 @@ const BackToTop: FC<BackToTopProps> = ({ additionalClasses, target }) => {
   });
 
   return (
-    <div className={`${styles.wrapper} ${additionalClasses}`}>
+    <div className={`${styles.wrapper} ${className}`}>
       <ButtonLink shape="square" target={`#${target}`} aria-label={linkName}>
         <Arrow direction="top" />
       </ButtonLink>
diff --git a/src/components/molecules/buttons/help-button.stories.tsx b/src/components/molecules/buttons/help-button.stories.tsx
index 7ed953e..cfc1b0b 100644
--- a/src/components/molecules/buttons/help-button.stories.tsx
+++ b/src/components/molecules/buttons/help-button.stories.tsx
@@ -6,11 +6,11 @@ export default {
   title: 'Molecules/Buttons',
   component: HelpButtonComponent,
   argTypes: {
-    classes: {
+    className: {
       control: {
         type: 'text',
       },
-      description: 'Set additional classes to the button.',
+      description: 'Set additional classnames to the button wrapper.',
       table: {
         category: 'Options',
       },
diff --git a/src/components/molecules/buttons/help-button.tsx b/src/components/molecules/buttons/help-button.tsx
index d933829..aeb84ec 100644
--- a/src/components/molecules/buttons/help-button.tsx
+++ b/src/components/molecules/buttons/help-button.tsx
@@ -1,13 +1,13 @@
 import Button, { ButtonProps } from '@components/atoms/buttons/button';
-import { FC } from 'react';
+import { VFC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './help-button.module.scss';
 
 export type HelpButtonProps = Pick<ButtonProps, 'onClick'> & {
   /**
-   * Set additional classes to the button.
+   * Set additional classnames to the button wrapper.
    */
-  classes?: string;
+  className?: string;
 };
 
 /**
@@ -15,7 +15,7 @@ export type HelpButtonProps = Pick<ButtonProps, 'onClick'> & {
  *
  * Render a button with an interrogation mark icon.
  */
-const HelpButton: FC<HelpButtonProps> = ({ classes = '', onClick }) => {
+const HelpButton: VFC<HelpButtonProps> = ({ className = '', onClick }) => {
   const intl = useIntl();
   const text = intl.formatMessage({
     defaultMessage: 'Help',
@@ -26,7 +26,7 @@ const HelpButton: FC<HelpButtonProps> = ({ classes = '', onClick }) => {
   return (
     <Button
       shape="circle"
-      additionalClasses={`${styles.btn} ${classes}`}
+      className={`${styles.btn} ${className}`}
       onClick={onClick}
     >
       <span className="screen-reader-text">{text}</span>
diff --git a/src/components/molecules/images/responsive-image.tsx b/src/components/molecules/images/responsive-image.tsx
index db2f5ab..9f96f18 100644
--- a/src/components/molecules/images/responsive-image.tsx
+++ b/src/components/molecules/images/responsive-image.tsx
@@ -1,6 +1,6 @@
 import Link from '@components/atoms/links/link';
 import Image, { ImageProps } from 'next/image';
-import { FC } from 'react';
+import { VFC } from 'react';
 import styles from './responsive-image.module.scss';
 
 type ResponsiveImageProps = Omit<ImageProps, 'alt' | 'width' | 'height'> & {
@@ -31,7 +31,7 @@ type ResponsiveImageProps = Omit<ImageProps, 'alt' | 'width' | 'height'> & {
  *
  * Render a responsive image wrapped in a figure element.
  */
-const ResponsiveImage: FC<ResponsiveImageProps> = ({
+const ResponsiveImage: VFC<ResponsiveImageProps> = ({
   alt,
   caption,
   layout,
@@ -42,7 +42,7 @@ const ResponsiveImage: FC<ResponsiveImageProps> = ({
   return (
     <figure className={styles.wrapper}>
       {target ? (
-        <Link href={target} classes={styles.link}>
+        <Link href={target} className={styles.link}>
           <Image alt={alt} layout={layout || 'intrinsic'} {...props} />
           {caption && (
             <figcaption className={styles.caption}>{caption}</figcaption>
diff --git a/src/components/molecules/layout/branding.tsx b/src/components/molecules/layout/branding.tsx
index efb2e34..9f564bf 100644
--- a/src/components/molecules/layout/branding.tsx
+++ b/src/components/molecules/layout/branding.tsx
@@ -1,6 +1,6 @@
 import Heading from '@components/atoms/headings/heading';
 import Link from 'next/link';
-import { FC } from 'react';
+import { VFC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './branding.module.scss';
 import FlippingLogo from './flipping-logo';
@@ -33,7 +33,7 @@ type BrandingProps = {
  *
  * Render the branding logo, title and optional baseline.
  */
-const Branding: FC<BrandingProps> = ({
+const Branding: VFC<BrandingProps> = ({
   baseline,
   isHome = false,
   photo,
@@ -61,7 +61,7 @@ const Branding: FC<BrandingProps> = ({
   return (
     <div className={styles.wrapper}>
       <FlippingLogo
-        additionalClasses={styles.logo}
+        className={styles.logo}
         altText={altText}
         logoTitle={logoTitle}
         photo={photo}
@@ -70,7 +70,7 @@ const Branding: FC<BrandingProps> = ({
         isFake={!isHome}
         level={1}
         withMargin={false}
-        additionalClasses={styles.title}
+        className={styles.title}
       >
         {withLink ? (
           <Link href="/">
@@ -85,7 +85,7 @@ const Branding: FC<BrandingProps> = ({
           isFake={true}
           level={4}
           withMargin={false}
-          additionalClasses={styles.baseline}
+          className={styles.baseline}
         >
           {baseline}
         </Heading>
diff --git a/src/components/molecules/layout/flipping-logo.stories.tsx b/src/components/molecules/layout/flipping-logo.stories.tsx
index 1508269..1ac8de8 100644
--- a/src/components/molecules/layout/flipping-logo.stories.tsx
+++ b/src/components/molecules/layout/flipping-logo.stories.tsx
@@ -5,11 +5,21 @@ export default {
   title: 'Molecules/Layout',
   component: FlippingLogoComponent,
   argTypes: {
-    additionalClasses: {
+    altText: {
+      control: {
+        type: 'text',
+      },
+      description: 'Photo alternative text.',
+      type: {
+        name: 'string',
+        required: true,
+      },
+    },
+    className: {
       control: {
         type: 'text',
       },
-      description: 'Adds additional classes to the logo wrapper.',
+      description: 'Set additional classnames to the logo wrapper.',
       table: {
         category: 'Options',
       },
@@ -18,14 +28,17 @@ export default {
         required: false,
       },
     },
-    altText: {
+    logoTitle: {
       control: {
         type: 'text',
       },
-      description: 'Photo alternative text.',
+      description: 'An accessible name for the logo.',
+      table: {
+        category: 'Accessibility',
+      },
       type: {
         name: 'string',
-        required: true,
+        required: false,
       },
     },
     photo: {
diff --git a/src/components/molecules/layout/flipping-logo.tsx b/src/components/molecules/layout/flipping-logo.tsx
index 7bb7afc..6f7645f 100644
--- a/src/components/molecules/layout/flipping-logo.tsx
+++ b/src/components/molecules/layout/flipping-logo.tsx
@@ -1,13 +1,13 @@
 import Logo from '@components/atoms/images/logo';
 import Image from 'next/image';
-import { FC } from 'react';
+import { VFC } from 'react';
 import styles from './flipping-logo.module.scss';
 
 type FlippingLogoProps = {
   /**
-   * Adds additional classes to the logo wrapper.
+   * Set additional classnames to the logo wrapper.
    */
-  additionalClasses?: string;
+  className?: string;
   /**
    * Photo alternative text.
    */
@@ -27,14 +27,14 @@ type FlippingLogoProps = {
  *
  * Render a logo and a photo with a flipping effect.
  */
-const FlippingLogo: FC<FlippingLogoProps> = ({
-  additionalClasses,
+const FlippingLogo: VFC<FlippingLogoProps> = ({
+  className = '',
   altText,
   logoTitle,
   photo,
 }) => {
   return (
-    <div className={`${styles.logo} ${additionalClasses}`}>
+    <div className={`${styles.logo} ${className}`}>
       <div className={styles.logo__front}>
         <Image src={photo} alt={altText} layout="fill" objectFit="cover" />
       </div>
diff --git a/src/components/molecules/modals/modal.stories.tsx b/src/components/molecules/modals/modal.stories.tsx
index b68a24b..bd7d9f4 100644
--- a/src/components/molecules/modals/modal.stories.tsx
+++ b/src/components/molecules/modals/modal.stories.tsx
@@ -15,6 +15,19 @@ export default {
         required: true,
       },
     },
+    className: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the modal.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     icon: {
       control: {
         type: 'select',
diff --git a/src/components/molecules/modals/modal.tsx b/src/components/molecules/modals/modal.tsx
index 4dc3b0a..ce12e7a 100644
--- a/src/components/molecules/modals/modal.tsx
+++ b/src/components/molecules/modals/modal.tsx
@@ -1,17 +1,29 @@
 import Heading from '@components/atoms/headings/heading';
+import { CogProps } from '@components/atoms/icons/cog';
+import { MagnifyingGlassProps } from '@components/atoms/icons/magnifying-glass';
 import dynamic from 'next/dynamic';
-import { FC, ReactNode } from 'react';
+import { FC } from 'react';
 import styles from './modal.module.scss';
 
 export type Icons = 'cogs' | 'search';
 
 export type ModalProps = {
+  /**
+   * Set additional classnames.
+   */
+  className?: string;
+  /**
+   * A icon to illustrate the modal.
+   */
   icon?: Icons;
+  /**
+   * The modal title.
+   */
   title?: string;
 };
 
-const CogIcon = dynamic<ReactNode>(() => import('@components/atoms/icons/cog'));
-const SearchIcon = dynamic<ReactNode>(
+const CogIcon = dynamic<CogProps>(() => import('@components/atoms/icons/cog'));
+const SearchIcon = dynamic<MagnifyingGlassProps>(
   () => import('@components/atoms/icons/magnifying-glass')
 );
 
@@ -20,7 +32,7 @@ const SearchIcon = dynamic<ReactNode>(
  *
  * Render a modal component with an optional title and icon.
  */
-const Modal: FC<ModalProps> = ({ children, icon, title }) => {
+const Modal: FC<ModalProps> = ({ children, className = '', icon, title }) => {
   const getIcon = (id: Icons) => {
     switch (id) {
       case 'cogs':
@@ -33,7 +45,7 @@ const Modal: FC<ModalProps> = ({ children, icon, title }) => {
   };
 
   return (
-    <div className={styles.wrapper}>
+    <div className={`${styles.wrapper} ${className}`}>
       {title && (
         <Heading isFake={true} level={3}>
           {icon && <span className={styles.icon}>{getIcon(icon)}</span>}
diff --git a/src/components/molecules/modals/tooltip.stories.tsx b/src/components/molecules/modals/tooltip.stories.tsx
index a57cf34..63fc71d 100644
--- a/src/components/molecules/modals/tooltip.stories.tsx
+++ b/src/components/molecules/modals/tooltip.stories.tsx
@@ -5,6 +5,19 @@ export default {
   title: 'Molecules/Modals',
   component: TooltipComponent,
   argTypes: {
+    className: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the tooltip.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     content: {
       control: {
         type: 'text',
@@ -15,6 +28,16 @@ export default {
         required: true,
       },
     },
+    icon: {
+      control: {
+        type: 'text',
+      },
+      description: 'The tooltip icon.',
+      type: {
+        name: 'string',
+        required: true,
+      },
+    },
     title: {
       control: {
         type: 'text',
diff --git a/src/components/molecules/modals/tooltip.tsx b/src/components/molecules/modals/tooltip.tsx
index ceb0b14..73f36e7 100644
--- a/src/components/molecules/modals/tooltip.tsx
+++ b/src/components/molecules/modals/tooltip.tsx
@@ -1,12 +1,12 @@
 import List, { type ListItem } from '@components/atoms/lists/list';
-import { FC, ReactNode } from 'react';
+import { ReactNode, VFC } from 'react';
 import styles from './tooltip.module.scss';
 
 export type TooltipProps = {
   /**
-   * Set additional classes to the tooltip wrapper.
+   * Set additional classnames to the tooltip wrapper.
    */
-  classes?: string;
+  className?: string;
   /**
    * The tooltip body.
    */
@@ -26,7 +26,12 @@ export type TooltipProps = {
  *
  * Render a tooltip modal.
  */
-const Tooltip: FC<TooltipProps> = ({ classes = '', content, icon, title }) => {
+const Tooltip: VFC<TooltipProps> = ({
+  className = '',
+  content,
+  icon,
+  title,
+}) => {
   /**
    * Format an array of strings to an array of object with id and value.
    *
@@ -40,7 +45,7 @@ const Tooltip: FC<TooltipProps> = ({ classes = '', content, icon, title }) => {
   };
 
   return (
-    <div className={`${styles.wrapper} ${classes}`}>
+    <div className={`${styles.wrapper} ${className}`}>
       <div className={styles.title}>
         <span className={styles.icon}>{icon}</span>
         {title}
-- 
cgit v1.2.3


From b836f0a9f8b783e3328983ad087aa2b7b297b43a Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Fri, 15 Apr 2022 14:38:54 +0200
Subject: chore: add a CommentForm component

---
 src/components/atoms/buttons/button.stories.tsx    |   4 +-
 src/components/atoms/buttons/button.tsx            |   6 +-
 src/components/atoms/buttons/buttons.module.scss   |   8 +-
 src/components/atoms/headings/heading.tsx          |   6 +
 .../organisms/forms/comment-form.module.scss       |   8 +
 .../organisms/forms/comment-form.stories.tsx       |  86 ++++++++++
 .../organisms/forms/comment-form.test.tsx          |  20 +++
 src/components/organisms/forms/comment-form.tsx    | 174 +++++++++++++++++++++
 8 files changed, 304 insertions(+), 8 deletions(-)
 create mode 100644 src/components/organisms/forms/comment-form.module.scss
 create mode 100644 src/components/organisms/forms/comment-form.stories.tsx
 create mode 100644 src/components/organisms/forms/comment-form.test.tsx
 create mode 100644 src/components/organisms/forms/comment-form.tsx

(limited to 'src/components/atoms/headings/heading.tsx')

diff --git a/src/components/atoms/buttons/button.stories.tsx b/src/components/atoms/buttons/button.stories.tsx
index 1061d83..d47a1ea 100644
--- a/src/components/atoms/buttons/button.stories.tsx
+++ b/src/components/atoms/buttons/button.stories.tsx
@@ -65,7 +65,7 @@ export default {
         type: 'select',
       },
       description: 'Button kind.',
-      options: ['primary', 'secondary', 'tertiary'],
+      options: ['primary', 'secondary', 'tertiary', 'neutral'],
       table: {
         category: 'Options',
         defaultValue: { summary: 'secondary' },
@@ -93,7 +93,7 @@ export default {
         type: 'select',
       },
       description: 'The link shape.',
-      options: ['circle', 'rectangle', 'square'],
+      options: ['circle', 'rectangle', 'square', 'initial'],
       table: {
         category: 'Options',
         defaultValue: { summary: 'rectangle' },
diff --git a/src/components/atoms/buttons/button.tsx b/src/components/atoms/buttons/button.tsx
index b223046..545c5c5 100644
--- a/src/components/atoms/buttons/button.tsx
+++ b/src/components/atoms/buttons/button.tsx
@@ -9,7 +9,7 @@ export type ButtonProps = {
   /**
    * Button accessible label.
    */
-  ariaLabel?: string;
+  'aria-label'?: string;
   /**
    * Button state. Default: false.
    */
@@ -25,7 +25,7 @@ export type ButtonProps = {
   /**
    * Button shape. Default: rectangle.
    */
-  shape?: 'circle' | 'rectangle' | 'square';
+  shape?: 'circle' | 'rectangle' | 'square' | 'initial';
   /**
    * Button type attribute. Default: button.
    */
@@ -39,7 +39,6 @@ export type ButtonProps = {
  */
 const Button: FC<ButtonProps> = ({
   className = '',
-  ariaLabel,
   children,
   disabled = false,
   kind = 'secondary',
@@ -55,7 +54,6 @@ const Button: FC<ButtonProps> = ({
       type={type}
       disabled={disabled}
       className={`${styles.btn} ${kindClass} ${shapeClass} ${className}`}
-      aria-label={ariaLabel}
       {...props}
     >
       {children}
diff --git a/src/components/atoms/buttons/buttons.module.scss b/src/components/atoms/buttons/buttons.module.scss
index 87c92db..8e3e196 100644
--- a/src/components/atoms/buttons/buttons.module.scss
+++ b/src/components/atoms/buttons/buttons.module.scss
@@ -10,6 +10,10 @@
   font-weight: 600;
   transition: all 0.3s ease-in-out 0s;
 
+  &--initial {
+    border-radius: 0;
+  }
+
   &--rectangle {
     padding: var(--spacing-2xs) var(--spacing-md);
   }
@@ -107,7 +111,7 @@
             fun.convert-px(-4) var(--color-shadow-light),
           fun.convert-px(7) fun.convert-px(10) fun.convert-px(12)
             fun.convert-px(-3) var(--color-shadow-light);
-        transform: scale(1.1);
+        transform: scale(var(--scale-up, 1.1));
       }
 
       &:focus {
@@ -119,7 +123,7 @@
         box-shadow: 0 0 0 0 var(--color-shadow);
         color: var(--color-primary-dark);
         text-decoration: underline transparent 0;
-        transform: scale(0.94);
+        transform: scale(var(--scale-down, 0.94));
       }
     }
   }
diff --git a/src/components/atoms/headings/heading.tsx b/src/components/atoms/headings/heading.tsx
index 3048534..4703b5d 100644
--- a/src/components/atoms/headings/heading.tsx
+++ b/src/components/atoms/headings/heading.tsx
@@ -8,6 +8,10 @@ export type HeadingProps = {
    * Set additional classnames.
    */
   className?: string;
+  /**
+   * The heading id.
+   */
+  id?: string;
   /**
    * Use an heading element or only its styles. Default: false.
    */
@@ -30,6 +34,7 @@ export type HeadingProps = {
 const Heading: FC<HeadingProps> = ({
   children,
   className,
+  id,
   isFake = false,
   level,
   withMargin = true,
@@ -41,6 +46,7 @@ const Heading: FC<HeadingProps> = ({
   return (
     <TitleTag
       className={`${styles.heading} ${styles[levelClass]} ${styles[marginClass]} ${className}`}
+      id={id}
     >
       {children}
     </TitleTag>
diff --git a/src/components/organisms/forms/comment-form.module.scss b/src/components/organisms/forms/comment-form.module.scss
new file mode 100644
index 0000000..f3f2646
--- /dev/null
+++ b/src/components/organisms/forms/comment-form.module.scss
@@ -0,0 +1,8 @@
+.field {
+  width: 100%;
+}
+
+.button {
+  display: block;
+  margin: auto;
+}
diff --git a/src/components/organisms/forms/comment-form.stories.tsx b/src/components/organisms/forms/comment-form.stories.tsx
new file mode 100644
index 0000000..1ab7cf2
--- /dev/null
+++ b/src/components/organisms/forms/comment-form.stories.tsx
@@ -0,0 +1,86 @@
+import Notice from '@components/atoms/layout/notice';
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { IntlProvider } from 'react-intl';
+import CommentFormComponent from './comment-form';
+
+export default {
+  title: 'Organisms/Forms',
+  component: CommentFormComponent,
+  argTypes: {
+    className: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the form wrapper.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
+    Notice: {
+      control: {
+        type: null,
+      },
+      description: 'A component to display a success or error message.',
+      table: {
+        category: 'Options',
+      },
+      type: {
+        name: 'function',
+        required: false,
+      },
+    },
+    saveComment: {
+      control: {
+        type: null,
+      },
+      description: 'A callback function to process the comment form data.',
+      type: {
+        name: 'function',
+        required: true,
+      },
+    },
+    title: {
+      control: {
+        type: 'text',
+      },
+      description: 'The form title.',
+      table: {
+        category: 'Options',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
+    titleLevel: {
+      control: {
+        type: 'number',
+      },
+      description: 'The title level (hn).',
+      table: {
+        category: 'Options',
+      },
+      type: {
+        name: 'number',
+        required: false,
+      },
+    },
+  },
+} as ComponentMeta<typeof CommentFormComponent>;
+
+const Template: ComponentStory<typeof CommentFormComponent> = (args) => (
+  <IntlProvider locale="en">
+    <CommentFormComponent {...args} />
+  </IntlProvider>
+);
+
+export const CommentForm = Template.bind({});
+CommentForm.args = {
+  saveComment: (reset: () => void) => {
+    reset();
+  },
+};
diff --git a/src/components/organisms/forms/comment-form.test.tsx b/src/components/organisms/forms/comment-form.test.tsx
new file mode 100644
index 0000000..0d387b5
--- /dev/null
+++ b/src/components/organisms/forms/comment-form.test.tsx
@@ -0,0 +1,20 @@
+import { render, screen } from '@test-utils';
+import CommentForm from './comment-form';
+
+const title = 'Cum voluptas voluptatibus';
+
+describe('CommentForm', () => {
+  it('renders a form', () => {
+    render(<CommentForm saveComment={() => null} />);
+    expect(screen.getByRole('form')).toBeInTheDocument();
+  });
+
+  it('renders an optional title', () => {
+    render(
+      <CommentForm saveComment={() => null} title={title} titleLevel={2} />
+    );
+    expect(
+      screen.getByRole('heading', { level: 2, name: title })
+    ).toBeInTheDocument();
+  });
+});
diff --git a/src/components/organisms/forms/comment-form.tsx b/src/components/organisms/forms/comment-form.tsx
new file mode 100644
index 0000000..6acbf94
--- /dev/null
+++ b/src/components/organisms/forms/comment-form.tsx
@@ -0,0 +1,174 @@
+import Button from '@components/atoms/buttons/button';
+import Form from '@components/atoms/forms/form';
+import Heading, { type HeadingLevel } from '@components/atoms/headings/heading';
+import Spinner from '@components/atoms/loaders/spinner';
+import LabelledField from '@components/molecules/forms/labelled-field';
+import { ReactNode, useState, VFC } from 'react';
+import { useIntl } from 'react-intl';
+import styles from './comment-form.module.scss';
+
+export type CommentFormProps = {
+  /**
+   * Set additional classnames to the form wrapper.
+   */
+  className?: string;
+  /**
+   * Pass a component to print a success/error message.
+   */
+  Notice?: ReactNode;
+  /**
+   * A callback function to save comment. It takes a function as parameter to
+   * reset the form.
+   */
+  saveComment: (reset: () => void) => void;
+  /**
+   * The form title.
+   */
+  title?: string;
+  /**
+   * The title level.
+   */
+  titleLevel?: HeadingLevel;
+};
+
+const CommentForm: VFC<CommentFormProps> = ({
+  className = '',
+  Notice,
+  saveComment,
+  title,
+  titleLevel = 2,
+}) => {
+  const intl = useIntl();
+  const [name, setName] = useState<string>('');
+  const [email, setEmail] = useState<string>('');
+  const [website, setWebsite] = useState<string>('');
+  const [comment, setComment] = useState<string>('');
+  const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
+
+  /**
+   * Reset all the form fields.
+   */
+  const resetForm = () => {
+    setName('');
+    setEmail('');
+    setWebsite('');
+    setComment('');
+    setIsSubmitting(false);
+  };
+
+  const nameLabel = intl.formatMessage({
+    defaultMessage: 'Name:',
+    description: 'CommentForm: name label',
+    id: 'ZIrTee',
+  });
+
+  const emailLabel = intl.formatMessage({
+    defaultMessage: 'Email:',
+    description: 'CommentForm: email label',
+    id: 'Bh7z5v',
+  });
+
+  const websiteLabel = intl.formatMessage({
+    defaultMessage: 'Website:',
+    description: 'CommentForm: website label',
+    id: 'u41qSk',
+  });
+
+  const commentLabel = intl.formatMessage({
+    defaultMessage: 'Comment:',
+    description: 'CommentForm: comment label',
+    id: 'A8hGaK',
+  });
+
+  const formTitle = intl.formatMessage({
+    defaultMessage: 'Comment form',
+    description: 'CommentForm: aria label',
+    id: 'dz2kDV',
+  });
+
+  const formAriaLabel = title ? undefined : formTitle;
+  const formId = 'comment-form-title';
+  const formLabelledBy = title ? formId : undefined;
+
+  /**
+   * Handle form submit.
+   */
+  const submitHandler = () => {
+    setIsSubmitting(true);
+    saveComment(resetForm);
+  };
+
+  return (
+    <Form
+      onSubmit={submitHandler}
+      className={className}
+      aria-label={formAriaLabel}
+      aria-labelledby={formLabelledBy}
+    >
+      {title && (
+        <Heading id={formId} level={titleLevel}>
+          {title}
+        </Heading>
+      )}
+      <LabelledField
+        type="text"
+        id="commenter-name"
+        name="commenter-name"
+        label={nameLabel}
+        required={true}
+        value={name}
+        setValue={setName}
+        className={styles.field}
+      />
+      <LabelledField
+        type="email"
+        id="commenter-email"
+        name="commenter-email"
+        label={emailLabel}
+        required={true}
+        value={email}
+        setValue={setEmail}
+        className={styles.field}
+      />
+      <LabelledField
+        type="text"
+        id="commenter-website"
+        name="commenter-website"
+        label={websiteLabel}
+        required={false}
+        value={website}
+        setValue={setWebsite}
+        className={styles.field}
+      />
+      <LabelledField
+        type="textarea"
+        id="commenter-comment"
+        name="commenter-comment"
+        label={commentLabel}
+        required={true}
+        value={comment}
+        setValue={setComment}
+        className={styles.field}
+      />
+      <Button type="submit" kind="primary" className={styles.button}>
+        {intl.formatMessage({
+          defaultMessage: 'Publish',
+          description: 'CommentForm: submit button',
+          id: 'OL0Yzx',
+        })}
+      </Button>
+      {isSubmitting && (
+        <Spinner
+          message={intl.formatMessage({
+            defaultMessage: 'Submitting...',
+            description: 'CommentForm: spinner message on submit',
+            id: 'IY5ew6',
+          })}
+        />
+      )}
+      {Notice}
+    </Form>
+  );
+};
+
+export default CommentForm;
-- 
cgit v1.2.3


From 5a6e4eea16047083e2de0e91a1b3ed9be8d6eb68 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Sat, 16 Apr 2022 16:08:49 +0200
Subject: refactor: support React 18

I replaced the deprecated VFC type with FC type and made all children
explicits.
Formatjs is still not compatible with React 18 so I need to skip type
checking when comitting. There are some type errors because of
IntlProvider in Storybook stories.
---
 jest.setup.js                                      |  1 +
 src/components/atoms/buttons/button-link.tsx       |  6 +-
 src/components/atoms/buttons/button.tsx            |  6 +-
 src/components/atoms/forms/checkbox.tsx            |  4 +-
 src/components/atoms/forms/field.tsx               |  4 +-
 src/components/atoms/forms/form.test.tsx           |  6 +-
 src/components/atoms/forms/form.tsx                |  6 +-
 src/components/atoms/forms/label.tsx               |  6 +-
 src/components/atoms/forms/select.tsx              |  4 +-
 src/components/atoms/headings/heading.tsx          |  6 +-
 src/components/atoms/icons/arrow.tsx               |  4 +-
 src/components/atoms/icons/career.tsx              |  4 +-
 src/components/atoms/icons/cc-by-sa.tsx            |  4 +-
 src/components/atoms/icons/close.tsx               |  4 +-
 src/components/atoms/icons/cog.tsx                 |  4 +-
 src/components/atoms/icons/computer-screen.tsx     |  4 +-
 src/components/atoms/icons/envelop.tsx             |  4 +-
 src/components/atoms/icons/hamburger.tsx           |  2 +-
 src/components/atoms/icons/home.tsx                |  4 +-
 src/components/atoms/icons/magnifying-glass.tsx    |  4 +-
 src/components/atoms/icons/moon.tsx                |  6 +-
 src/components/atoms/icons/plus-minus.tsx          |  2 +-
 src/components/atoms/icons/posts-stack.tsx         |  4 +-
 src/components/atoms/icons/sun.tsx                 |  6 +-
 src/components/atoms/images/logo.tsx               |  6 +-
 src/components/atoms/layout/copyright.tsx          |  4 +-
 src/components/atoms/layout/main.tsx               |  6 +-
 src/components/atoms/layout/no-script.tsx          |  4 +-
 src/components/atoms/layout/notice.tsx             |  4 +-
 src/components/atoms/layout/section.tsx            |  4 +-
 src/components/atoms/links/link.module.scss        | 20 ++++--
 src/components/atoms/links/link.tsx                |  6 +-
 src/components/atoms/links/nav-link.tsx            |  4 +-
 src/components/atoms/links/sharing-link.tsx        |  4 +-
 src/components/atoms/links/social-link.tsx         |  4 +-
 src/components/atoms/lists/description-list.tsx    |  4 +-
 src/components/atoms/lists/list.tsx                |  4 +-
 src/components/atoms/loaders/progress-bar.tsx      |  4 +-
 src/components/atoms/loaders/spinner.tsx           |  4 +-
 src/components/molecules/buttons/back-to-top.tsx   | 14 ++--
 .../molecules/buttons/heading-button.tsx           |  4 +-
 src/components/molecules/buttons/help-button.tsx   |  6 +-
 .../molecules/forms/ackee-select.test.tsx          | 16 +++--
 src/components/molecules/forms/ackee-select.tsx    |  6 +-
 src/components/molecules/forms/labelled-field.tsx  |  4 +-
 src/components/molecules/forms/labelled-select.tsx | 15 +++--
 src/components/molecules/forms/motion-toggle.tsx   |  8 +--
 .../molecules/forms/prism-theme-toggle.tsx         |  8 +--
 .../molecules/forms/select-with-tooltip.tsx        | 10 +--
 src/components/molecules/forms/theme-toggle.tsx    |  8 +--
 src/components/molecules/forms/toggle.tsx          | 18 ++---
 .../molecules/images/responsive-image.tsx          | 10 +--
 src/components/molecules/layout/branding.tsx       | 12 ++--
 src/components/molecules/layout/card.tsx           |  8 +--
 src/components/molecules/layout/flipping-logo.tsx  | 10 +--
 src/components/molecules/layout/meta.tsx           |  4 +-
 src/components/molecules/layout/widget.tsx         | 10 ++-
 src/components/molecules/modals/modal.test.tsx     | 13 +++-
 src/components/molecules/modals/modal.tsx          | 14 ++--
 src/components/molecules/modals/tooltip.tsx        |  4 +-
 src/components/molecules/nav/breadcrumb.tsx        |  4 +-
 src/components/molecules/nav/nav.tsx               |  4 +-
 .../organisms/forms/comment-form.stories.tsx       |  1 -
 src/components/organisms/forms/comment-form.tsx    |  4 +-
 src/components/organisms/forms/contact-form.tsx    |  4 +-
 src/components/organisms/forms/search-form.tsx     | 11 +++-
 src/components/organisms/layout/cards-list.tsx     |  4 +-
 src/components/organisms/layout/footer.tsx         | 13 ++--
 src/components/organisms/layout/overview.tsx       |  4 +-
 .../organisms/layout/summary.stories.tsx           |  2 +-
 src/components/organisms/layout/summary.test.tsx   |  2 +-
 src/components/organisms/layout/summary.tsx        | 77 +++++++++++++---------
 src/components/organisms/modals/search-modal.tsx   |  8 +--
 src/components/organisms/modals/settings-modal.tsx | 14 ++--
 src/components/organisms/toolbar/main-nav.tsx      | 18 +++--
 src/components/organisms/toolbar/search.tsx        | 16 ++---
 src/components/organisms/toolbar/settings.tsx      | 14 ++--
 src/components/organisms/toolbar/toolbar.tsx       |  4 +-
 src/components/organisms/widgets/image-widget.tsx  | 34 ++++------
 .../organisms/widgets/links-list-widget.tsx        |  9 ++-
 src/components/organisms/widgets/sharing.tsx       |  6 +-
 81 files changed, 353 insertions(+), 289 deletions(-)

(limited to 'src/components/atoms/headings/heading.tsx')

diff --git a/jest.setup.js b/jest.setup.js
index 5c5ecbc..d50c988 100644
--- a/jest.setup.js
+++ b/jest.setup.js
@@ -2,3 +2,4 @@ import '@testing-library/jest-dom/extend-expect';
 import './__tests__/jest/__mocks__/matchMedia.mock';
 
 jest.mock('next/dist/client/router', () => require('next-router-mock'));
+jest.mock('next/dynamic', () => () => 'dynamic-import');
diff --git a/src/components/atoms/buttons/button-link.tsx b/src/components/atoms/buttons/button-link.tsx
index 77a7f7b..906fa76 100644
--- a/src/components/atoms/buttons/button-link.tsx
+++ b/src/components/atoms/buttons/button-link.tsx
@@ -1,5 +1,5 @@
 import Link from 'next/link';
-import { FC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './buttons.module.scss';
 
 export type ButtonLinkProps = {
@@ -7,6 +7,10 @@ export type ButtonLinkProps = {
    * ButtonLink accessible label.
    */
   'aria-label'?: string;
+  /**
+   * The button link body.
+   */
+  children: ReactNode;
   /**
    * Set additional classnames to the button link.
    */
diff --git a/src/components/atoms/buttons/button.tsx b/src/components/atoms/buttons/button.tsx
index 545c5c5..a6eef8b 100644
--- a/src/components/atoms/buttons/button.tsx
+++ b/src/components/atoms/buttons/button.tsx
@@ -1,7 +1,11 @@
-import { FC, MouseEventHandler } from 'react';
+import { FC, MouseEventHandler, ReactNode } from 'react';
 import styles from './buttons.module.scss';
 
 export type ButtonProps = {
+  /**
+   * The button body.
+   */
+  children: ReactNode;
   /**
    * Set additional classnames to the button wrapper.
    */
diff --git a/src/components/atoms/forms/checkbox.tsx b/src/components/atoms/forms/checkbox.tsx
index 8babcc8..aec97f0 100644
--- a/src/components/atoms/forms/checkbox.tsx
+++ b/src/components/atoms/forms/checkbox.tsx
@@ -1,4 +1,4 @@
-import { SetStateAction, VFC } from 'react';
+import { FC, SetStateAction } from 'react';
 
 export type CheckboxProps = {
   /**
@@ -32,7 +32,7 @@ export type CheckboxProps = {
  *
  * Render a checkbox type input.
  */
-const Checkbox: VFC<CheckboxProps> = ({ value, setValue, ...props }) => {
+const Checkbox: FC<CheckboxProps> = ({ value, setValue, ...props }) => {
   return (
     <input
       type="checkbox"
diff --git a/src/components/atoms/forms/field.tsx b/src/components/atoms/forms/field.tsx
index 2e75d0f..e45a8a7 100644
--- a/src/components/atoms/forms/field.tsx
+++ b/src/components/atoms/forms/field.tsx
@@ -1,4 +1,4 @@
-import { ChangeEvent, SetStateAction, VFC } from 'react';
+import { ChangeEvent, FC, SetStateAction } from 'react';
 import styles from './forms.module.scss';
 
 export type FieldType =
@@ -72,7 +72,7 @@ export type FieldProps = {
  *
  * Render either an input or a textarea.
  */
-const Field: VFC<FieldProps> = ({
+const Field: FC<FieldProps> = ({
   className = '',
   setValue,
   type,
diff --git a/src/components/atoms/forms/form.test.tsx b/src/components/atoms/forms/form.test.tsx
index 9cd3c58..8b534f1 100644
--- a/src/components/atoms/forms/form.test.tsx
+++ b/src/components/atoms/forms/form.test.tsx
@@ -3,7 +3,11 @@ import Form from './form';
 
 describe('Form', () => {
   it('renders a form', () => {
-    render(<Form aria-label="Jest form" onSubmit={() => null}></Form>);
+    render(
+      <Form aria-label="Jest form" onSubmit={() => null}>
+        Fields
+      </Form>
+    );
     expect(screen.getByRole('form', { name: 'Jest form' })).toBeInTheDocument();
   });
 });
diff --git a/src/components/atoms/forms/form.tsx b/src/components/atoms/forms/form.tsx
index 8e80930..ef8dce4 100644
--- a/src/components/atoms/forms/form.tsx
+++ b/src/components/atoms/forms/form.tsx
@@ -1,4 +1,4 @@
-import { Children, FC, FormEvent, Fragment } from 'react';
+import { Children, FC, FormEvent, Fragment, ReactNode } from 'react';
 import styles from './forms.module.scss';
 
 export type FormProps = {
@@ -10,6 +10,10 @@ export type FormProps = {
    * One or more ids that refers to the form name.
    */
   'aria-labelledby'?: string;
+  /**
+   * The form body.
+   */
+  children: ReactNode;
   /**
    * Set additional classnames to the form wrapper.
    */
diff --git a/src/components/atoms/forms/label.tsx b/src/components/atoms/forms/label.tsx
index 8d57ee2..ce4c70f 100644
--- a/src/components/atoms/forms/label.tsx
+++ b/src/components/atoms/forms/label.tsx
@@ -1,7 +1,11 @@
-import { FC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './label.module.scss';
 
 export type LabelProps = {
+  /**
+   * The label body.
+   */
+  children: ReactNode;
   /**
    * Add classnames to the label.
    */
diff --git a/src/components/atoms/forms/select.tsx b/src/components/atoms/forms/select.tsx
index 25e86e0..dbe9b37 100644
--- a/src/components/atoms/forms/select.tsx
+++ b/src/components/atoms/forms/select.tsx
@@ -1,4 +1,4 @@
-import { ChangeEvent, SetStateAction, VFC } from 'react';
+import { ChangeEvent, FC, SetStateAction } from 'react';
 import styles from './forms.module.scss';
 
 export type SelectOptions = {
@@ -60,7 +60,7 @@ export type SelectProps = {
  *
  * Render a HTML select element.
  */
-const Select: VFC<SelectProps> = ({
+const Select: FC<SelectProps> = ({
   className = '',
   options,
   setValue,
diff --git a/src/components/atoms/headings/heading.tsx b/src/components/atoms/headings/heading.tsx
index 4703b5d..c5bf4ca 100644
--- a/src/components/atoms/headings/heading.tsx
+++ b/src/components/atoms/headings/heading.tsx
@@ -1,9 +1,13 @@
-import { FC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './heading.module.scss';
 
 export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
 
 export type HeadingProps = {
+  /**
+   * The heading body.
+   */
+  children: ReactNode;
   /**
    * Set additional classnames.
    */
diff --git a/src/components/atoms/icons/arrow.tsx b/src/components/atoms/icons/arrow.tsx
index 5f3c460..2962530 100644
--- a/src/components/atoms/icons/arrow.tsx
+++ b/src/components/atoms/icons/arrow.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './arrow.module.scss';
 
 export type ArrowDirection = 'top' | 'right' | 'bottom' | 'left';
@@ -19,7 +19,7 @@ export type ArrowProps = {
  *
  * Render a svg arrow icon.
  */
-const Arrow: VFC<ArrowProps> = ({ className = '', direction }) => {
+const Arrow: FC<ArrowProps> = ({ className = '', direction }) => {
   const directionClass = styles[`icon--${direction}`];
   const classes = `${styles.icon} ${directionClass} ${className}`;
 
diff --git a/src/components/atoms/icons/career.tsx b/src/components/atoms/icons/career.tsx
index 28edcc7..f28d399 100644
--- a/src/components/atoms/icons/career.tsx
+++ b/src/components/atoms/icons/career.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './career.module.scss';
 
 export type CareerProps = {
@@ -13,7 +13,7 @@ export type CareerProps = {
  *
  * Render a career svg icon.
  */
-const Career: VFC<CareerProps> = ({ className = '' }) => {
+const Career: FC<CareerProps> = ({ className = '' }) => {
   return (
     <svg
       viewBox="0 0 100 100"
diff --git a/src/components/atoms/icons/cc-by-sa.tsx b/src/components/atoms/icons/cc-by-sa.tsx
index 552504e..8239154 100644
--- a/src/components/atoms/icons/cc-by-sa.tsx
+++ b/src/components/atoms/icons/cc-by-sa.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './cc-by-sa.module.scss';
 
@@ -14,7 +14,7 @@ export type CCBySAProps = {
  *
  * Render a CC BY SA svg icon.
  */
-const CCBySA: VFC<CCBySAProps> = ({ className = '' }) => {
+const CCBySA: FC<CCBySAProps> = ({ className = '' }) => {
   const intl = useIntl();
 
   return (
diff --git a/src/components/atoms/icons/close.tsx b/src/components/atoms/icons/close.tsx
index eb9ce7c..3e0adb5 100644
--- a/src/components/atoms/icons/close.tsx
+++ b/src/components/atoms/icons/close.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './close.module.scss';
 
 export type CloseProps = {
@@ -13,7 +13,7 @@ export type CloseProps = {
  *
  * Render a close svg icon.
  */
-const Close: VFC<CloseProps> = ({ className = '' }) => {
+const Close: FC<CloseProps> = ({ className = '' }) => {
   return (
     <svg
       viewBox="0 0 100 100"
diff --git a/src/components/atoms/icons/cog.tsx b/src/components/atoms/icons/cog.tsx
index df6d54d..9e78a7b 100644
--- a/src/components/atoms/icons/cog.tsx
+++ b/src/components/atoms/icons/cog.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './cog.module.scss';
 
 export type CogProps = {
@@ -13,7 +13,7 @@ export type CogProps = {
  *
  * Render a cog svg icon.
  */
-const Cog: VFC<CogProps> = ({ className = '' }) => {
+const Cog: FC<CogProps> = ({ className = '' }) => {
   return (
     <svg
       viewBox="0 0 100 100"
diff --git a/src/components/atoms/icons/computer-screen.tsx b/src/components/atoms/icons/computer-screen.tsx
index 310836f..8786139 100644
--- a/src/components/atoms/icons/computer-screen.tsx
+++ b/src/components/atoms/icons/computer-screen.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './computer-screen.module.scss';
 
 export type ComputerScreenProps = {
@@ -13,7 +13,7 @@ export type ComputerScreenProps = {
  *
  * Render a computer screen svg icon.
  */
-const ComputerScreen: VFC<ComputerScreenProps> = ({ className = '' }) => {
+const ComputerScreen: FC<ComputerScreenProps> = ({ className = '' }) => {
   return (
     <svg
       viewBox="0 0 100 100"
diff --git a/src/components/atoms/icons/envelop.tsx b/src/components/atoms/icons/envelop.tsx
index 7b50d1d..84dca97 100644
--- a/src/components/atoms/icons/envelop.tsx
+++ b/src/components/atoms/icons/envelop.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './envelop.module.scss';
 
 export type EnvelopProps = {
@@ -13,7 +13,7 @@ export type EnvelopProps = {
  *
  * Render an envelop svg icon.
  */
-const Envelop: VFC<EnvelopProps> = ({ className = '' }) => {
+const Envelop: FC<EnvelopProps> = ({ className = '' }) => {
   return (
     <svg
       viewBox="0 0 100 100"
diff --git a/src/components/atoms/icons/hamburger.tsx b/src/components/atoms/icons/hamburger.tsx
index 7e7c2c9..93aed2a 100644
--- a/src/components/atoms/icons/hamburger.tsx
+++ b/src/components/atoms/icons/hamburger.tsx
@@ -1,7 +1,7 @@
 import { FC } from 'react';
 import styles from './hamburger.module.scss';
 
-type HamburgerProps = {
+export type HamburgerProps = {
   /**
    * Set additional classnames to the icon wrapper.
    */
diff --git a/src/components/atoms/icons/home.tsx b/src/components/atoms/icons/home.tsx
index 71bbc4a..3b6732d 100644
--- a/src/components/atoms/icons/home.tsx
+++ b/src/components/atoms/icons/home.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './home.module.scss';
 
 export type HomeProps = {
@@ -13,7 +13,7 @@ export type HomeProps = {
  *
  * Render a home svg icon.
  */
-const Home: VFC<HomeProps> = ({ className = '' }) => {
+const Home: FC<HomeProps> = ({ className = '' }) => {
   return (
     <svg
       viewBox="0 0 100 100"
diff --git a/src/components/atoms/icons/magnifying-glass.tsx b/src/components/atoms/icons/magnifying-glass.tsx
index 445ef10..1ca2a44 100644
--- a/src/components/atoms/icons/magnifying-glass.tsx
+++ b/src/components/atoms/icons/magnifying-glass.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './magnifying-glass.module.scss';
 
 export type MagnifyingGlassProps = {
@@ -13,7 +13,7 @@ export type MagnifyingGlassProps = {
  *
  * Render a magnifying glass svg icon.
  */
-const MagnifyingGlass: VFC<MagnifyingGlassProps> = ({ className = '' }) => {
+const MagnifyingGlass: FC<MagnifyingGlassProps> = ({ className = '' }) => {
   return (
     <svg
       viewBox="0 0 100 100"
diff --git a/src/components/atoms/icons/moon.tsx b/src/components/atoms/icons/moon.tsx
index 4f52319..ec4fa0c 100644
--- a/src/components/atoms/icons/moon.tsx
+++ b/src/components/atoms/icons/moon.tsx
@@ -1,7 +1,7 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './moon.module.scss';
 
-type MoonProps = {
+export type MoonProps = {
   /**
    * Set additional classnames to the icon.
    */
@@ -12,7 +12,7 @@ type MoonProps = {
   title?: string;
 };
 
-const Moon: VFC<MoonProps> = ({ className = '', title }) => {
+const Moon: FC<MoonProps> = ({ className = '', title }) => {
   return (
     <svg
       className={`${styles.icon} ${className}`}
diff --git a/src/components/atoms/icons/plus-minus.tsx b/src/components/atoms/icons/plus-minus.tsx
index 78aa14a..e8897b7 100644
--- a/src/components/atoms/icons/plus-minus.tsx
+++ b/src/components/atoms/icons/plus-minus.tsx
@@ -1,7 +1,7 @@
 import { FC } from 'react';
 import styles from './plus-minus.module.scss';
 
-type PlusMinusProps = {
+export type PlusMinusProps = {
   /**
    * Set additional classnames to the icon.
    */
diff --git a/src/components/atoms/icons/posts-stack.tsx b/src/components/atoms/icons/posts-stack.tsx
index 1998d25..ab21323 100644
--- a/src/components/atoms/icons/posts-stack.tsx
+++ b/src/components/atoms/icons/posts-stack.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './posts-stack.module.scss';
 
 export type PostsStackProps = {
@@ -13,7 +13,7 @@ export type PostsStackProps = {
  *
  * Render a posts stack svg icon.
  */
-const PostsStack: VFC<PostsStackProps> = ({ className = '' }) => {
+const PostsStack: FC<PostsStackProps> = ({ className = '' }) => {
   return (
     <svg
       viewBox="0 0 100 100"
diff --git a/src/components/atoms/icons/sun.tsx b/src/components/atoms/icons/sun.tsx
index fa9d922..ca31747 100644
--- a/src/components/atoms/icons/sun.tsx
+++ b/src/components/atoms/icons/sun.tsx
@@ -1,7 +1,7 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './sun.module.scss';
 
-type SunProps = {
+export type SunProps = {
   /**
    * Set additional classnames to the icon.
    */
@@ -17,7 +17,7 @@ type SunProps = {
  *
  * Render a svg sun icon.
  */
-const Sun: VFC<SunProps> = ({ className = '', title }) => {
+const Sun: FC<SunProps> = ({ className = '', title }) => {
   return (
     <svg
       className={`${styles.sun} ${className}`}
diff --git a/src/components/atoms/images/logo.tsx b/src/components/atoms/images/logo.tsx
index 2e52110..3978882 100644
--- a/src/components/atoms/images/logo.tsx
+++ b/src/components/atoms/images/logo.tsx
@@ -1,7 +1,7 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './logo.module.scss';
 
-type LogoProps = {
+export type LogoProps = {
   /**
    * SVG Image title.
    */
@@ -13,7 +13,7 @@ type LogoProps = {
  *
  * Render a SVG logo.
  */
-const Logo: VFC<LogoProps> = ({ title }) => {
+const Logo: FC<LogoProps> = ({ title }) => {
   return (
     <svg
       viewBox="0 0 512 512"
diff --git a/src/components/atoms/layout/copyright.tsx b/src/components/atoms/layout/copyright.tsx
index 76252ee..f70695d 100644
--- a/src/components/atoms/layout/copyright.tsx
+++ b/src/components/atoms/layout/copyright.tsx
@@ -1,4 +1,4 @@
-import { ReactNode, VFC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './copyright.module.scss';
 
 export type CopyrightDates = {
@@ -32,7 +32,7 @@ export type CopyrightProps = {
  *
  * Renders a copyright information (owner, dates, license icon).
  */
-const Copyright: VFC<CopyrightProps> = ({ owner, dates, icon }) => {
+const Copyright: FC<CopyrightProps> = ({ owner, dates, icon }) => {
   const getFormattedDate = (date: string) => {
     const datetime = new Date(date).toISOString();
 
diff --git a/src/components/atoms/layout/main.tsx b/src/components/atoms/layout/main.tsx
index 4549328..d92a5c7 100644
--- a/src/components/atoms/layout/main.tsx
+++ b/src/components/atoms/layout/main.tsx
@@ -1,6 +1,10 @@
-import { FC } from 'react';
+import { FC, ReactNode } from 'react';
 
 export type MainProps = {
+  /**
+   * The main body.
+   */
+  children: ReactNode;
   /**
    * Set additional classnames to the main element.
    */
diff --git a/src/components/atoms/layout/no-script.tsx b/src/components/atoms/layout/no-script.tsx
index 6358cf8..a503e0c 100644
--- a/src/components/atoms/layout/no-script.tsx
+++ b/src/components/atoms/layout/no-script.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './no-script.module.scss';
 
 export type NoScriptProps = {
@@ -12,7 +12,7 @@ export type NoScriptProps = {
   position?: 'initial' | 'top';
 };
 
-const NoScript: VFC<NoScriptProps> = ({ message, position = 'initial' }) => {
+const NoScript: FC<NoScriptProps> = ({ message, position = 'initial' }) => {
   const positionClass = styles[`noscript--${position}`];
 
   return <div className={`${styles.noscript} ${positionClass}`}>{message}</div>;
diff --git a/src/components/atoms/layout/notice.tsx b/src/components/atoms/layout/notice.tsx
index b6e09c5..115bd9c 100644
--- a/src/components/atoms/layout/notice.tsx
+++ b/src/components/atoms/layout/notice.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './notice.module.scss';
 
 export type NoticeKind = 'error' | 'info' | 'success' | 'warning';
@@ -19,7 +19,7 @@ export type NoticeProps = {
  *
  * Render a colored message depending on notice kind.
  */
-const Notice: VFC<NoticeProps> = ({ kind, message }) => {
+const Notice: FC<NoticeProps> = ({ kind, message }) => {
   const kindClass = `wrapper--${kind}`;
 
   return (
diff --git a/src/components/atoms/layout/section.tsx b/src/components/atoms/layout/section.tsx
index f1bbb34..cb727ff 100644
--- a/src/components/atoms/layout/section.tsx
+++ b/src/components/atoms/layout/section.tsx
@@ -1,4 +1,4 @@
-import { ReactNode, VFC } from 'react';
+import { FC, ReactNode } from 'react';
 import Heading from '../headings/heading';
 import styles from './section.module.scss';
 
@@ -32,7 +32,7 @@ export type SectionProps = {
  *
  * Render a section element.
  */
-const Section: VFC<SectionProps> = ({
+const Section: FC<SectionProps> = ({
   className = '',
   content,
   title,
diff --git a/src/components/atoms/links/link.module.scss b/src/components/atoms/links/link.module.scss
index e7ead86..d23667a 100644
--- a/src/components/atoms/links/link.module.scss
+++ b/src/components/atoms/links/link.module.scss
@@ -5,7 +5,9 @@
   &[hreflang] {
     &::after {
       display: inline-block;
-      content: "\0000a0["attr(hreflang) "]";
+      /* Prettier is removing spacing between content parts. */
+      /* prettier-ignore */
+      content: "\0000a0[" attr(hreflang) "]";
       font-size: var(--font-size-sm);
     }
   }
@@ -13,22 +15,30 @@
   &--external {
     &::after {
       display: inline-block;
-      content: "\0000a0"url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+      /* Prettier is removing spacing between content parts. */
+      /* prettier-ignore */
+      content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
     }
 
     &:focus:not(:active)::after {
-      content: "\0000a0"url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_white}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
+      /* Prettier is removing spacing between content parts. */
+      /* prettier-ignore */
+      content: "\0000a0" url(fun.encode-svg('<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_white}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'));
     }
 
     &[hreflang] {
       &::after {
-        content: "\0000a0["attr(hreflang) "]\0000a0"url(fun.encode-svg(
+        /* Prettier is removing spacing between content parts. */
+        /* prettier-ignore */
+        content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg(
             '<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_blue}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_blue}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'
           ));
       }
 
       &:focus:not(:active)::after {
-        content: "\0000a0["attr(hreflang) "]\0000a0"url(fun.encode-svg(
+        /* Prettier is removing spacing between content parts. */
+        /* prettier-ignore */
+        content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg(
             '<svg width="13" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><path fill="#{var.$light-theme_white}" d="M100 0 59.543 5.887l20.8 6.523-51.134 51.134 7.249 7.248L87.59 19.66l6.522 20.798z"/><path fill="#{var.$light-theme_white}" d="M4 10a4 4 0 0 0-4 4v82a4 4 0 0 0 4 4h82a4 4 0 0 0 4-4V62.314h-8V92H8V18h29.686v-8z"/></svg>'
           ));
       }
diff --git a/src/components/atoms/links/link.tsx b/src/components/atoms/links/link.tsx
index 87f11fc..674c07b 100644
--- a/src/components/atoms/links/link.tsx
+++ b/src/components/atoms/links/link.tsx
@@ -1,8 +1,12 @@
 import NextLink from 'next/link';
-import { FC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './link.module.scss';
 
 export type LinkProps = {
+  /**
+   * The link body.
+   */
+  children: ReactNode;
   /**
    * Set additional classnames to the link.
    */
diff --git a/src/components/atoms/links/nav-link.tsx b/src/components/atoms/links/nav-link.tsx
index 25c0e7d..7c6fede 100644
--- a/src/components/atoms/links/nav-link.tsx
+++ b/src/components/atoms/links/nav-link.tsx
@@ -1,5 +1,5 @@
 import Link from 'next/link';
-import { VFC, ReactNode } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './nav-link.module.scss';
 
 export type NavLinkProps = {
@@ -22,7 +22,7 @@ export type NavLinkProps = {
  *
  * Render a navigation link.
  */
-const NavLink: VFC<NavLinkProps> = ({ href, label, logo }) => {
+const NavLink: FC<NavLinkProps> = ({ href, label, logo }) => {
   return (
     <Link href={href}>
       <a className={styles.link}>
diff --git a/src/components/atoms/links/sharing-link.tsx b/src/components/atoms/links/sharing-link.tsx
index 3cd2dd1..ca53ef9 100644
--- a/src/components/atoms/links/sharing-link.tsx
+++ b/src/components/atoms/links/sharing-link.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './sharing-link.module.scss';
 
@@ -26,7 +26,7 @@ export type SharingLinkProps = {
  *
  * Render a sharing link.
  */
-const SharingLink: VFC<SharingLinkProps> = ({ medium, url }) => {
+const SharingLink: FC<SharingLinkProps> = ({ medium, url }) => {
   const intl = useIntl();
   const text = intl.formatMessage(
     {
diff --git a/src/components/atoms/links/social-link.tsx b/src/components/atoms/links/social-link.tsx
index 8c7c790..464bc60 100644
--- a/src/components/atoms/links/social-link.tsx
+++ b/src/components/atoms/links/social-link.tsx
@@ -2,7 +2,7 @@ import GithubIcon from '@assets/images/social-media/github.svg';
 import GitlabIcon from '@assets/images/social-media/gitlab.svg';
 import LinkedInIcon from '@assets/images/social-media/linkedin.svg';
 import TwitterIcon from '@assets/images/social-media/twitter.svg';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './social-link.module.scss';
 
 export type SocialWebsite = 'Github' | 'Gitlab' | 'LinkedIn' | 'Twitter';
@@ -23,7 +23,7 @@ export type SocialLinkProps = {
  *
  * Render a social icon link.
  */
-const SocialLink: VFC<SocialLinkProps> = ({ name, url }) => {
+const SocialLink: FC<SocialLinkProps> = ({ name, url }) => {
   /**
    * Retrieve a social link icon by id.
    * @param {string} id - The social website id.
diff --git a/src/components/atoms/lists/description-list.tsx b/src/components/atoms/lists/description-list.tsx
index 0a92465..a60a6a1 100644
--- a/src/components/atoms/lists/description-list.tsx
+++ b/src/components/atoms/lists/description-list.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './description-list.module.scss';
 
 export type DescriptionListItem = {
@@ -52,7 +52,7 @@ export type DescriptionListProps = {
  *
  * Render a description list.
  */
-const DescriptionList: VFC<DescriptionListProps> = ({
+const DescriptionList: FC<DescriptionListProps> = ({
   className = '',
   descriptionClassName = '',
   groupClassName = '',
diff --git a/src/components/atoms/lists/list.tsx b/src/components/atoms/lists/list.tsx
index d100a31..6726802 100644
--- a/src/components/atoms/lists/list.tsx
+++ b/src/components/atoms/lists/list.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './list.module.scss';
 
 export type ListItem = {
@@ -44,7 +44,7 @@ export type ListProps = {
  *
  * Render either an ordered or an unordered list.
  */
-const List: VFC<ListProps> = ({
+const List: FC<ListProps> = ({
   className = '',
   items,
   itemsClassName = '',
diff --git a/src/components/atoms/loaders/progress-bar.tsx b/src/components/atoms/loaders/progress-bar.tsx
index 1b1ff06..9bac847 100644
--- a/src/components/atoms/loaders/progress-bar.tsx
+++ b/src/components/atoms/loaders/progress-bar.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './progress-bar.module.scss';
 
 export type ProgressBarProps = {
@@ -29,7 +29,7 @@ export type ProgressBarProps = {
  *
  * Render a progress bar.
  */
-const ProgressBar: VFC<ProgressBarProps> = ({
+const ProgressBar: FC<ProgressBarProps> = ({
   current,
   info,
   min,
diff --git a/src/components/atoms/loaders/spinner.tsx b/src/components/atoms/loaders/spinner.tsx
index bff0f25..6655141 100644
--- a/src/components/atoms/loaders/spinner.tsx
+++ b/src/components/atoms/loaders/spinner.tsx
@@ -1,4 +1,4 @@
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './spinner.module.scss';
 
@@ -14,7 +14,7 @@ export type SpinnerProps = {
  *
  * Render a loading message with animation.
  */
-const Spinner: VFC<SpinnerProps> = ({ message }) => {
+const Spinner: FC<SpinnerProps> = ({ message }) => {
   const intl = useIntl();
 
   return (
diff --git a/src/components/molecules/buttons/back-to-top.tsx b/src/components/molecules/buttons/back-to-top.tsx
index 8a52231..bd1925a 100644
--- a/src/components/molecules/buttons/back-to-top.tsx
+++ b/src/components/molecules/buttons/back-to-top.tsx
@@ -1,18 +1,16 @@
-import ButtonLink from '@components/atoms/buttons/button-link';
+import ButtonLink, {
+  type ButtonLinkProps,
+} from '@components/atoms/buttons/button-link';
 import Arrow from '@components/atoms/icons/arrow';
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './back-to-top.module.scss';
 
-export type BackToTopProps = {
+export type BackToTopProps = Pick<ButtonLinkProps, 'target'> & {
   /**
    * Set additional classnames to the button wrapper.
    */
   className?: string;
-  /**
-   * An element id (without hashtag) to use as anchor.
-   */
-  target: string;
 };
 
 /**
@@ -20,7 +18,7 @@ export type BackToTopProps = {
  *
  * Render a back to top link.
  */
-const BackToTop: VFC<BackToTopProps> = ({ className = '', target }) => {
+const BackToTop: FC<BackToTopProps> = ({ className = '', target }) => {
   const intl = useIntl();
   const linkName = intl.formatMessage({
     defaultMessage: 'Back to top',
diff --git a/src/components/molecules/buttons/heading-button.tsx b/src/components/molecules/buttons/heading-button.tsx
index fc79749..0ed9a76 100644
--- a/src/components/molecules/buttons/heading-button.tsx
+++ b/src/components/molecules/buttons/heading-button.tsx
@@ -1,6 +1,6 @@
 import Heading, { type HeadingProps } from '@components/atoms/headings/heading';
 import PlusMinus from '@components/atoms/icons/plus-minus';
-import { SetStateAction, VFC } from 'react';
+import { FC, SetStateAction } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './heading-button.module.scss';
 
@@ -28,7 +28,7 @@ export type HeadingButtonProps = Pick<HeadingProps, 'level'> & {
  *
  * Render a button as accordion title to toggle body.
  */
-const HeadingButton: VFC<HeadingButtonProps> = ({
+const HeadingButton: FC<HeadingButtonProps> = ({
   className = '',
   expanded,
   level,
diff --git a/src/components/molecules/buttons/help-button.tsx b/src/components/molecules/buttons/help-button.tsx
index aeb84ec..f19322f 100644
--- a/src/components/molecules/buttons/help-button.tsx
+++ b/src/components/molecules/buttons/help-button.tsx
@@ -1,5 +1,5 @@
-import Button, { ButtonProps } from '@components/atoms/buttons/button';
-import { VFC } from 'react';
+import Button, { type ButtonProps } from '@components/atoms/buttons/button';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './help-button.module.scss';
 
@@ -15,7 +15,7 @@ export type HelpButtonProps = Pick<ButtonProps, 'onClick'> & {
  *
  * Render a button with an interrogation mark icon.
  */
-const HelpButton: VFC<HelpButtonProps> = ({ className = '', onClick }) => {
+const HelpButton: FC<HelpButtonProps> = ({ className = '', onClick }) => {
   const intl = useIntl();
   const text = intl.formatMessage({
     defaultMessage: 'Help',
diff --git a/src/components/molecules/forms/ackee-select.test.tsx b/src/components/molecules/forms/ackee-select.test.tsx
index e1e6b2d..ec27922 100644
--- a/src/components/molecules/forms/ackee-select.test.tsx
+++ b/src/components/molecules/forms/ackee-select.test.tsx
@@ -1,5 +1,5 @@
-import userEvent from '@testing-library/user-event';
-import { render, screen } from '@test-utils';
+import user from '@testing-library/user-event';
+import { act, render, screen } from '@test-utils';
 import AckeeSelect from './ackee-select';
 
 describe('Select', () => {
@@ -9,13 +9,15 @@ describe('Select', () => {
     expect(screen.queryByRole('combobox')).not.toHaveValue('partial');
   });
 
-  it('should correctly change value when user choose another option', () => {
+  it('should correctly change value when user choose another option', async () => {
     render(<AckeeSelect initialValue="full" />);
 
-    userEvent.selectOptions(
-      screen.getByRole('combobox'),
-      screen.getByRole('option', { name: 'Partial' })
-    );
+    await act(async () => {
+      await user.selectOptions(
+        screen.getByRole('combobox'),
+        screen.getByRole('option', { name: 'Partial' })
+      );
+    });
 
     expect(screen.getByRole('combobox')).toHaveValue('partial');
     expect(screen.queryByRole('combobox')).not.toHaveValue('full');
diff --git a/src/components/molecules/forms/ackee-select.tsx b/src/components/molecules/forms/ackee-select.tsx
index 4a8410c..101e5b5 100644
--- a/src/components/molecules/forms/ackee-select.tsx
+++ b/src/components/molecules/forms/ackee-select.tsx
@@ -1,8 +1,8 @@
 import { SelectOptions } from '@components/atoms/forms/select';
-import { Dispatch, SetStateAction, useState, VFC } from 'react';
+import { Dispatch, FC, SetStateAction, useState } from 'react';
 import { useIntl } from 'react-intl';
 import SelectWithTooltip, {
-  SelectWithTooltipProps,
+  type SelectWithTooltipProps,
 } from './select-with-tooltip';
 
 export type AckeeOptions = 'full' | 'partial';
@@ -22,7 +22,7 @@ export type AckeeSelectProps = Pick<
  *
  * Render a select to set Ackee settings.
  */
-const AckeeSelect: VFC<AckeeSelectProps> = ({ initialValue, ...props }) => {
+const AckeeSelect: FC<AckeeSelectProps> = ({ initialValue, ...props }) => {
   const intl = useIntl();
   const [value, setValue] = useState<AckeeOptions>(initialValue);
 
diff --git a/src/components/molecules/forms/labelled-field.tsx b/src/components/molecules/forms/labelled-field.tsx
index 08d0126..ecc9255 100644
--- a/src/components/molecules/forms/labelled-field.tsx
+++ b/src/components/molecules/forms/labelled-field.tsx
@@ -1,6 +1,6 @@
 import Field, { type FieldProps } from '@components/atoms/forms/field';
 import Label from '@components/atoms/forms/label';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './labelled-field.module.scss';
 
 export type LabelledFieldProps = FieldProps & {
@@ -23,7 +23,7 @@ export type LabelledFieldProps = FieldProps & {
  *
  * Render a field tied to a label.
  */
-const LabelledField: VFC<LabelledFieldProps> = ({
+const LabelledField: FC<LabelledFieldProps> = ({
   hideLabel = false,
   id,
   label,
diff --git a/src/components/molecules/forms/labelled-select.tsx b/src/components/molecules/forms/labelled-select.tsx
index 7d4237a..23057d0 100644
--- a/src/components/molecules/forms/labelled-select.tsx
+++ b/src/components/molecules/forms/labelled-select.tsx
@@ -1,6 +1,6 @@
-import Label, { LabelProps } from '@components/atoms/forms/label';
+import Label, { type LabelProps } from '@components/atoms/forms/label';
 import Select, { type SelectProps } from '@components/atoms/forms/select';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './labelled-select.module.scss';
 
 export type LabelledSelectProps = Omit<
@@ -14,7 +14,7 @@ export type LabelledSelectProps = Omit<
   /**
    * Set additional classnames to the label.
    */
-  labelClassName?: string;
+  labelClassName?: LabelProps['className'];
   /**
    * The label position. Default: top.
    */
@@ -26,10 +26,15 @@ export type LabelledSelectProps = Omit<
   /**
    * Set additional classnames to the select field.
    */
-  selectClassName?: string;
+  selectClassName?: SelectProps['className'];
 };
 
-const LabelledSelect: VFC<LabelledSelectProps> = ({
+/**
+ * LabelledSelect component
+ *
+ * Render a select with a label.
+ */
+const LabelledSelect: FC<LabelledSelectProps> = ({
   id,
   label,
   labelClassName = '',
diff --git a/src/components/molecules/forms/motion-toggle.tsx b/src/components/molecules/forms/motion-toggle.tsx
index 9f30b42..24b54ae 100644
--- a/src/components/molecules/forms/motion-toggle.tsx
+++ b/src/components/molecules/forms/motion-toggle.tsx
@@ -1,8 +1,8 @@
 import Toggle, {
-  ToggleChoices,
-  ToggleProps,
+  type ToggleChoices,
+  type ToggleProps,
 } from '@components/molecules/forms/toggle';
-import { useState, VFC } from 'react';
+import { FC, useState } from 'react';
 import { useIntl } from 'react-intl';
 
 export type MotionToggleProps = Pick<ToggleProps, 'labelClassName' | 'value'>;
@@ -12,7 +12,7 @@ export type MotionToggleProps = Pick<ToggleProps, 'labelClassName' | 'value'>;
  *
  * Render a Toggle component to set reduce motion.
  */
-const MotionToggle: VFC<MotionToggleProps> = ({ value, ...props }) => {
+const MotionToggle: FC<MotionToggleProps> = ({ value, ...props }) => {
   const intl = useIntl();
   const [isDeactivated, setIsDeactivated] = useState<boolean>(value);
   const reduceMotionLabel = intl.formatMessage({
diff --git a/src/components/molecules/forms/prism-theme-toggle.tsx b/src/components/molecules/forms/prism-theme-toggle.tsx
index daee6bd..0b9c447 100644
--- a/src/components/molecules/forms/prism-theme-toggle.tsx
+++ b/src/components/molecules/forms/prism-theme-toggle.tsx
@@ -1,10 +1,10 @@
 import Moon from '@components/atoms/icons/moon';
 import Sun from '@components/atoms/icons/sun';
 import Toggle, {
-  ToggleChoices,
-  ToggleProps,
+  type ToggleChoices,
+  type ToggleProps,
 } from '@components/molecules/forms/toggle';
-import { useState, VFC } from 'react';
+import { FC, useState } from 'react';
 import { useIntl } from 'react-intl';
 
 export type PrismThemeToggleProps = Pick<
@@ -17,7 +17,7 @@ export type PrismThemeToggleProps = Pick<
  *
  * Render a Toggle component to set code blocks theme.
  */
-const PrismThemeToggle: VFC<PrismThemeToggleProps> = ({ value, ...props }) => {
+const PrismThemeToggle: FC<PrismThemeToggleProps> = ({ value, ...props }) => {
   const intl = useIntl();
   const [isDarkTheme, setIsDarkTheme] = useState<boolean>(value);
   const themeLabel = intl.formatMessage({
diff --git a/src/components/molecules/forms/select-with-tooltip.tsx b/src/components/molecules/forms/select-with-tooltip.tsx
index f537e1e..cf7b041 100644
--- a/src/components/molecules/forms/select-with-tooltip.tsx
+++ b/src/components/molecules/forms/select-with-tooltip.tsx
@@ -1,4 +1,4 @@
-import { useState, VFC } from 'react';
+import { FC, useState } from 'react';
 import HelpButton from '../buttons/help-button';
 import Tooltip, { type TooltipProps } from '../modals/tooltip';
 import LabelledSelect, { type LabelledSelectProps } from './labelled-select';
@@ -9,14 +9,10 @@ export type SelectWithTooltipProps = Omit<
   'labelPosition'
 > &
   Pick<TooltipProps, 'title' | 'content'> & {
-    /**
-     * The select label.
-     */
-    label: string;
     /**
      * Set additional classnames to the tooltip wrapper.
      */
-    tooltipClassName?: string;
+    tooltipClassName?: TooltipProps['className'];
   };
 
 /**
@@ -24,7 +20,7 @@ export type SelectWithTooltipProps = Omit<
  *
  * Render a select with a button to display a tooltip about options.
  */
-const SelectWithTooltip: VFC<SelectWithTooltipProps> = ({
+const SelectWithTooltip: FC<SelectWithTooltipProps> = ({
   title,
   content,
   id,
diff --git a/src/components/molecules/forms/theme-toggle.tsx b/src/components/molecules/forms/theme-toggle.tsx
index eb56ce9..10c6c47 100644
--- a/src/components/molecules/forms/theme-toggle.tsx
+++ b/src/components/molecules/forms/theme-toggle.tsx
@@ -1,10 +1,10 @@
 import Moon from '@components/atoms/icons/moon';
 import Sun from '@components/atoms/icons/sun';
 import Toggle, {
-  ToggleChoices,
-  ToggleProps,
+  type ToggleChoices,
+  type ToggleProps,
 } from '@components/molecules/forms/toggle';
-import { useState, VFC } from 'react';
+import { FC, useState } from 'react';
 import { useIntl } from 'react-intl';
 
 export type ThemeToggleProps = Pick<ToggleProps, 'labelClassName' | 'value'>;
@@ -14,7 +14,7 @@ export type ThemeToggleProps = Pick<ToggleProps, 'labelClassName' | 'value'>;
  *
  * Render a Toggle component to set theme.
  */
-const ThemeToggle: VFC<ThemeToggleProps> = ({ value, ...props }) => {
+const ThemeToggle: FC<ThemeToggleProps> = ({ value, ...props }) => {
   const intl = useIntl();
   const [isDarkTheme, setIsDarkTheme] = useState<boolean>(value);
   const themeLabel = intl.formatMessage({
diff --git a/src/components/molecules/forms/toggle.tsx b/src/components/molecules/forms/toggle.tsx
index dff2d2d..288062d 100644
--- a/src/components/molecules/forms/toggle.tsx
+++ b/src/components/molecules/forms/toggle.tsx
@@ -1,6 +1,6 @@
-import Checkbox from '@components/atoms/forms/checkbox';
+import Checkbox, { type CheckboxProps } from '@components/atoms/forms/checkbox';
 import Label, { type LabelProps } from '@components/atoms/forms/label';
-import { ReactNode, VFC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './toggle.module.scss';
 
 export type ToggleChoices = {
@@ -14,15 +14,11 @@ export type ToggleChoices = {
   right: ReactNode;
 };
 
-export type ToggleProps = {
+export type ToggleProps = Pick<CheckboxProps, 'id' | 'name'> & {
   /**
    * The toggle choices.
    */
   choices: ToggleChoices;
-  /**
-   * The input id.
-   */
-  id: string;
   /**
    * The toggle label.
    */
@@ -30,15 +26,11 @@ export type ToggleProps = {
   /**
    * Set additional classnames to the label.
    */
-  labelClassName?: string;
+  labelClassName?: LabelProps['className'];
   /**
    * The label size.
    */
   labelSize?: LabelProps['size'];
-  /**
-   * The input name.
-   */
-  name: string;
   /**
    * The toggle value. True if checked.
    */
@@ -54,7 +46,7 @@ export type ToggleProps = {
  *
  * Render a toggle with a label and two choices.
  */
-const Toggle: VFC<ToggleProps> = ({
+const Toggle: FC<ToggleProps> = ({
   choices,
   id,
   label,
diff --git a/src/components/molecules/images/responsive-image.tsx b/src/components/molecules/images/responsive-image.tsx
index 1d8787e..31cbcd1 100644
--- a/src/components/molecules/images/responsive-image.tsx
+++ b/src/components/molecules/images/responsive-image.tsx
@@ -1,6 +1,6 @@
-import Link from '@components/atoms/links/link';
-import Image, { ImageProps } from 'next/image';
-import { VFC } from 'react';
+import Link, { type LinkProps } from '@components/atoms/links/link';
+import Image, { type ImageProps } from 'next/image';
+import { FC } from 'react';
 import styles from './responsive-image.module.scss';
 
 export type ResponsiveImageProps = Omit<
@@ -26,7 +26,7 @@ export type ResponsiveImageProps = Omit<
   /**
    * A link target.
    */
-  target?: string;
+  target?: LinkProps['href'];
   /**
    * The image width.
    */
@@ -38,7 +38,7 @@ export type ResponsiveImageProps = Omit<
  *
  * Render a responsive image wrapped in a figure element.
  */
-const ResponsiveImage: VFC<ResponsiveImageProps> = ({
+const ResponsiveImage: FC<ResponsiveImageProps> = ({
   alt,
   caption,
   className = '',
diff --git a/src/components/molecules/layout/branding.tsx b/src/components/molecules/layout/branding.tsx
index 9f564bf..9fe89e7 100644
--- a/src/components/molecules/layout/branding.tsx
+++ b/src/components/molecules/layout/branding.tsx
@@ -1,11 +1,11 @@
 import Heading from '@components/atoms/headings/heading';
 import Link from 'next/link';
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './branding.module.scss';
-import FlippingLogo from './flipping-logo';
+import FlippingLogo, { type FlippingLogoProps } from './flipping-logo';
 
-type BrandingProps = {
+export type BrandingProps = Pick<FlippingLogoProps, 'photo'> & {
   /**
    * The Branding baseline.
    */
@@ -14,10 +14,6 @@ type BrandingProps = {
    * Use H1 if the current page is homepage. Default: false.
    */
   isHome?: boolean;
-  /**
-   * A photography URL.
-   */
-  photo: string;
   /**
    * The Branding title;
    */
@@ -33,7 +29,7 @@ type BrandingProps = {
  *
  * Render the branding logo, title and optional baseline.
  */
-const Branding: VFC<BrandingProps> = ({
+const Branding: FC<BrandingProps> = ({
   baseline,
   isHome = false,
   photo,
diff --git a/src/components/molecules/layout/card.tsx b/src/components/molecules/layout/card.tsx
index 23a0e54..89f100e 100644
--- a/src/components/molecules/layout/card.tsx
+++ b/src/components/molecules/layout/card.tsx
@@ -1,11 +1,11 @@
 import ButtonLink from '@components/atoms/buttons/button-link';
 import Heading, { type HeadingLevel } from '@components/atoms/headings/heading';
 import DescriptionList, {
-  DescriptionListItem,
+  type DescriptionListItem,
 } from '@components/atoms/lists/description-list';
-import { VFC } from 'react';
+import { FC } from 'react';
 import ResponsiveImage, {
-  ResponsiveImageProps,
+  type ResponsiveImageProps,
 } from '../images/responsive-image';
 import styles from './card.module.scss';
 
@@ -68,7 +68,7 @@ export type CardProps = {
  *
  * Render a link with minimal information about its content.
  */
-const Card: VFC<CardProps> = ({
+const Card: FC<CardProps> = ({
   className = '',
   cover,
   coverFit = 'cover',
diff --git a/src/components/molecules/layout/flipping-logo.tsx b/src/components/molecules/layout/flipping-logo.tsx
index 6f7645f..4a216ef 100644
--- a/src/components/molecules/layout/flipping-logo.tsx
+++ b/src/components/molecules/layout/flipping-logo.tsx
@@ -1,9 +1,9 @@
-import Logo from '@components/atoms/images/logo';
+import Logo, { type LogoProps } from '@components/atoms/images/logo';
 import Image from 'next/image';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './flipping-logo.module.scss';
 
-type FlippingLogoProps = {
+export type FlippingLogoProps = {
   /**
    * Set additional classnames to the logo wrapper.
    */
@@ -15,7 +15,7 @@ type FlippingLogoProps = {
   /**
    * Logo image title.
    */
-  logoTitle?: string;
+  logoTitle?: LogoProps['title'];
   /**
    * Photo url.
    */
@@ -27,7 +27,7 @@ type FlippingLogoProps = {
  *
  * Render a logo and a photo with a flipping effect.
  */
-const FlippingLogo: VFC<FlippingLogoProps> = ({
+const FlippingLogo: FC<FlippingLogoProps> = ({
   className = '',
   altText,
   logoTitle,
diff --git a/src/components/molecules/layout/meta.tsx b/src/components/molecules/layout/meta.tsx
index 218ebd9..fcce473 100644
--- a/src/components/molecules/layout/meta.tsx
+++ b/src/components/molecules/layout/meta.tsx
@@ -2,7 +2,7 @@ import DescriptionList, {
   type DescriptionListProps,
   type DescriptionListItem,
 } from '@components/atoms/lists/description-list';
-import { ReactNode, VFC } from 'react';
+import { FC, ReactNode } from 'react';
 
 export type MetaItem = {
   /**
@@ -43,7 +43,7 @@ export type MetaProps = {
  *
  * Renders the page metadata.
  */
-const Meta: VFC<MetaProps> = ({ data, ...props }) => {
+const Meta: FC<MetaProps> = ({ data, ...props }) => {
   /**
    * Transform the metadata to description list item format.
    *
diff --git a/src/components/molecules/layout/widget.tsx b/src/components/molecules/layout/widget.tsx
index c04362a..feb2add 100644
--- a/src/components/molecules/layout/widget.tsx
+++ b/src/components/molecules/layout/widget.tsx
@@ -1,11 +1,17 @@
-import { FC, useState } from 'react';
-import HeadingButton, { HeadingButtonProps } from '../buttons/heading-button';
+import { FC, ReactNode, useState } from 'react';
+import HeadingButton, {
+  type HeadingButtonProps,
+} from '../buttons/heading-button';
 import styles from './widget.module.scss';
 
 export type WidgetProps = Pick<
   HeadingButtonProps,
   'expanded' | 'level' | 'title'
 > & {
+  /**
+   * The widget body.
+   */
+  children: ReactNode;
   /**
    * Set additional classnames to the widget wrapper.
    */
diff --git a/src/components/molecules/modals/modal.test.tsx b/src/components/molecules/modals/modal.test.tsx
index 14fb224..9a0e237 100644
--- a/src/components/molecules/modals/modal.test.tsx
+++ b/src/components/molecules/modals/modal.test.tsx
@@ -1,9 +1,18 @@
 import { render, screen } from '@test-utils';
 import Modal from './modal';
 
+const title = 'A custom title';
+const children =
+  'Labore ullam delectus sit modi quam dolores. Ratione id sint aliquid facilis ipsum. Unde necessitatibus provident minus.';
+
 describe('Modal', () => {
   it('renders a title', () => {
-    render(<Modal title="A custom title" />);
-    expect(screen.getByText('A custom title')).toBeInTheDocument();
+    render(<Modal title={title}>{children}</Modal>);
+    expect(screen.getByText(title)).toBeInTheDocument();
+  });
+
+  it('renders the modal body', () => {
+    render(<Modal title={title}>{children}</Modal>);
+    expect(screen.getByText(children)).toBeInTheDocument();
   });
 });
diff --git a/src/components/molecules/modals/modal.tsx b/src/components/molecules/modals/modal.tsx
index 52ada57..58f5fa0 100644
--- a/src/components/molecules/modals/modal.tsx
+++ b/src/components/molecules/modals/modal.tsx
@@ -1,13 +1,17 @@
-import Heading from '@components/atoms/headings/heading';
-import { CogProps } from '@components/atoms/icons/cog';
-import { MagnifyingGlassProps } from '@components/atoms/icons/magnifying-glass';
+import Heading, { type HeadingProps } from '@components/atoms/headings/heading';
+import { type CogProps } from '@components/atoms/icons/cog';
+import { type MagnifyingGlassProps } from '@components/atoms/icons/magnifying-glass';
 import dynamic from 'next/dynamic';
-import { FC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './modal.module.scss';
 
 export type Icons = 'cogs' | 'search';
 
 export type ModalProps = {
+  /**
+   * The modal body.
+   */
+  children: ReactNode;
   /**
    * Set additional classnames.
    */
@@ -15,7 +19,7 @@ export type ModalProps = {
   /**
    * Set additional classnames to the heading.
    */
-  headingClassName?: string;
+  headingClassName?: HeadingProps['className'];
   /**
    * A icon to illustrate the modal.
    */
diff --git a/src/components/molecules/modals/tooltip.tsx b/src/components/molecules/modals/tooltip.tsx
index 73f36e7..80721f3 100644
--- a/src/components/molecules/modals/tooltip.tsx
+++ b/src/components/molecules/modals/tooltip.tsx
@@ -1,5 +1,5 @@
 import List, { type ListItem } from '@components/atoms/lists/list';
-import { ReactNode, VFC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './tooltip.module.scss';
 
 export type TooltipProps = {
@@ -26,7 +26,7 @@ export type TooltipProps = {
  *
  * Render a tooltip modal.
  */
-const Tooltip: VFC<TooltipProps> = ({
+const Tooltip: FC<TooltipProps> = ({
   className = '',
   content,
   icon,
diff --git a/src/components/molecules/nav/breadcrumb.tsx b/src/components/molecules/nav/breadcrumb.tsx
index 33af735..6dc86a0 100644
--- a/src/components/molecules/nav/breadcrumb.tsx
+++ b/src/components/molecules/nav/breadcrumb.tsx
@@ -1,7 +1,7 @@
 import Link from '@components/atoms/links/link';
 import { settings } from '@utils/config';
 import Script from 'next/script';
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import { BreadcrumbList, ListItem, WithContext } from 'schema-dts';
 import styles from './breadcrumb.module.scss';
@@ -37,7 +37,7 @@ export type BreadcrumbProps = {
  *
  * Render a breadcrumb navigation.
  */
-const Breadcrumb: VFC<BreadcrumbProps> = ({ items, ...props }) => {
+const Breadcrumb: FC<BreadcrumbProps> = ({ items, ...props }) => {
   const intl = useIntl();
 
   /**
diff --git a/src/components/molecules/nav/nav.tsx b/src/components/molecules/nav/nav.tsx
index 6ef9158..2666ea2 100644
--- a/src/components/molecules/nav/nav.tsx
+++ b/src/components/molecules/nav/nav.tsx
@@ -1,6 +1,6 @@
 import Link from '@components/atoms/links/link';
 import NavLink from '@components/atoms/links/nav-link';
-import { ReactNode, VFC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './nav.module.scss';
 
 export type NavItem = {
@@ -46,7 +46,7 @@ export type NavProps = {
  *
  * Render the nav links.
  */
-const Nav: VFC<NavProps> = ({
+const Nav: FC<NavProps> = ({
   className = '',
   items,
   kind,
diff --git a/src/components/organisms/forms/comment-form.stories.tsx b/src/components/organisms/forms/comment-form.stories.tsx
index 1ab7cf2..670176c 100644
--- a/src/components/organisms/forms/comment-form.stories.tsx
+++ b/src/components/organisms/forms/comment-form.stories.tsx
@@ -1,4 +1,3 @@
-import Notice from '@components/atoms/layout/notice';
 import { ComponentMeta, ComponentStory } from '@storybook/react';
 import { IntlProvider } from 'react-intl';
 import CommentFormComponent from './comment-form';
diff --git a/src/components/organisms/forms/comment-form.tsx b/src/components/organisms/forms/comment-form.tsx
index 6acbf94..d7cb0f5 100644
--- a/src/components/organisms/forms/comment-form.tsx
+++ b/src/components/organisms/forms/comment-form.tsx
@@ -3,7 +3,7 @@ import Form from '@components/atoms/forms/form';
 import Heading, { type HeadingLevel } from '@components/atoms/headings/heading';
 import Spinner from '@components/atoms/loaders/spinner';
 import LabelledField from '@components/molecules/forms/labelled-field';
-import { ReactNode, useState, VFC } from 'react';
+import { FC, ReactNode, useState } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './comment-form.module.scss';
 
@@ -31,7 +31,7 @@ export type CommentFormProps = {
   titleLevel?: HeadingLevel;
 };
 
-const CommentForm: VFC<CommentFormProps> = ({
+const CommentForm: FC<CommentFormProps> = ({
   className = '',
   Notice,
   saveComment,
diff --git a/src/components/organisms/forms/contact-form.tsx b/src/components/organisms/forms/contact-form.tsx
index 994244a..4a6902b 100644
--- a/src/components/organisms/forms/contact-form.tsx
+++ b/src/components/organisms/forms/contact-form.tsx
@@ -2,7 +2,7 @@ import Button from '@components/atoms/buttons/button';
 import Form from '@components/atoms/forms/form';
 import Spinner from '@components/atoms/loaders/spinner';
 import LabelledField from '@components/molecules/forms/labelled-field';
-import { ReactNode, useState, VFC } from 'react';
+import { FC, ReactNode, useState } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './contact-form.module.scss';
 
@@ -27,7 +27,7 @@ export type ContactFormProps = {
  *
  * Render a contact form.
  */
-const ContactForm: VFC<ContactFormProps> = ({
+const ContactForm: FC<ContactFormProps> = ({
   className = '',
   Notice,
   sendMail,
diff --git a/src/components/organisms/forms/search-form.tsx b/src/components/organisms/forms/search-form.tsx
index 351d93c..18b7c08 100644
--- a/src/components/organisms/forms/search-form.tsx
+++ b/src/components/organisms/forms/search-form.tsx
@@ -2,15 +2,20 @@ import Button from '@components/atoms/buttons/button';
 import Form from '@components/atoms/forms/form';
 import MagnifyingGlass from '@components/atoms/icons/magnifying-glass';
 import LabelledField, {
-  LabelledFieldProps,
+  type LabelledFieldProps,
 } from '@components/molecules/forms/labelled-field';
-import { useState, VFC } from 'react';
+import { FC, useState } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './search-form.module.scss';
 
 export type SearchFormProps = Pick<LabelledFieldProps, 'hideLabel'>;
 
-const SearchForm: VFC<SearchFormProps> = ({ hideLabel }) => {
+/**
+ * SearchForm component
+ *
+ * Render a search form.
+ */
+const SearchForm: FC<SearchFormProps> = ({ hideLabel }) => {
   const intl = useIntl();
   const fieldLabel = intl.formatMessage({
     defaultMessage: 'Search for:',
diff --git a/src/components/organisms/layout/cards-list.tsx b/src/components/organisms/layout/cards-list.tsx
index a53df0d..33ffe23 100644
--- a/src/components/organisms/layout/cards-list.tsx
+++ b/src/components/organisms/layout/cards-list.tsx
@@ -3,7 +3,7 @@ import List, {
   type ListProps,
 } from '@components/atoms/lists/list';
 import Card, { type CardProps } from '@components/molecules/layout/card';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './cards-list.module.scss';
 
 export type CardsListItem = Omit<
@@ -37,7 +37,7 @@ export type CardsListProps = {
  *
  * Return a list of Card components.
  */
-const CardsList: VFC<CardsListProps> = ({
+const CardsList: FC<CardsListProps> = ({
   coverFit,
   items,
   kind = 'unordered',
diff --git a/src/components/organisms/layout/footer.tsx b/src/components/organisms/layout/footer.tsx
index c9cb067..15bfa24 100644
--- a/src/components/organisms/layout/footer.tsx
+++ b/src/components/organisms/layout/footer.tsx
@@ -1,7 +1,9 @@
-import Copyright, { CopyrightProps } from '@components/atoms/layout/copyright';
+import Copyright, {
+  type CopyrightProps,
+} from '@components/atoms/layout/copyright';
 import BackToTop from '@components/molecules/buttons/back-to-top';
 import Nav, { type NavItem } from '@components/molecules/nav/nav';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './footer.module.scss';
 
 export type FooterProps = {
@@ -28,12 +30,7 @@ export type FooterProps = {
  *
  * Renders a footer with copyright and nav;
  */
-const Footer: VFC<FooterProps> = ({
-  className,
-  copyright,
-  navItems,
-  topId,
-}) => {
+const Footer: FC<FooterProps> = ({ className, copyright, navItems, topId }) => {
   return (
     <footer className={`${styles.wrapper} ${className}`}>
       <Copyright
diff --git a/src/components/organisms/layout/overview.tsx b/src/components/organisms/layout/overview.tsx
index 3f83342..42d0431 100644
--- a/src/components/organisms/layout/overview.tsx
+++ b/src/components/organisms/layout/overview.tsx
@@ -2,7 +2,7 @@ import ResponsiveImage, {
   type ResponsiveImageProps,
 } from '@components/molecules/images/responsive-image';
 import Meta, { type MetaMap } from '@components/molecules/layout/meta';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './overview.module.scss';
 
 export type OverviewProps = {
@@ -15,7 +15,7 @@ export type OverviewProps = {
  *
  * Render an overview.
  */
-const Overview: VFC<OverviewProps> = ({ cover, meta }) => {
+const Overview: FC<OverviewProps> = ({ cover, meta }) => {
   return (
     <div className={styles.wrapper}>
       {cover && (
diff --git a/src/components/organisms/layout/summary.stories.tsx b/src/components/organisms/layout/summary.stories.tsx
index 5214d70..b33acde 100644
--- a/src/components/organisms/layout/summary.stories.tsx
+++ b/src/components/organisms/layout/summary.stories.tsx
@@ -103,7 +103,7 @@ Summary.args = {
   cover: {
     alt: 'A cover',
     height: 480,
-    url: 'http://placeimg.com/640/480',
+    src: 'http://placeimg.com/640/480',
     width: 640,
   },
   excerpt:
diff --git a/src/components/organisms/layout/summary.test.tsx b/src/components/organisms/layout/summary.test.tsx
index ce87c0c..4944805 100644
--- a/src/components/organisms/layout/summary.test.tsx
+++ b/src/components/organisms/layout/summary.test.tsx
@@ -4,7 +4,7 @@ import Summary from './summary';
 const cover = {
   alt: 'A cover',
   height: 480,
-  url: 'http://placeimg.com/640/480',
+  src: 'http://placeimg.com/640/480',
   width: 640,
 };
 
diff --git a/src/components/organisms/layout/summary.tsx b/src/components/organisms/layout/summary.tsx
index 3624e5d..733a660 100644
--- a/src/components/organisms/layout/summary.tsx
+++ b/src/components/organisms/layout/summary.tsx
@@ -2,18 +2,18 @@ import ButtonLink from '@components/atoms/buttons/button-link';
 import Heading, { type HeadingLevel } from '@components/atoms/headings/heading';
 import Arrow from '@components/atoms/icons/arrow';
 import Link from '@components/atoms/links/link';
-import ResponsiveImage from '@components/molecules/images/responsive-image';
+import ResponsiveImage, {
+  type ResponsiveImageProps,
+} from '@components/molecules/images/responsive-image';
 import Meta, { type MetaItem } from '@components/molecules/layout/meta';
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './summary.module.scss';
 
-export type Cover = {
-  alt: string;
-  height: number;
-  url: string;
-  width: number;
-};
+export type Cover = Pick<
+  ResponsiveImageProps,
+  'alt' | 'src' | 'width' | 'height'
+>;
 
 export type RequiredMetaKey = 'publication';
 
@@ -35,11 +35,29 @@ export type OptionalMeta = {
 export type Meta = RequiredMeta & OptionalMeta;
 
 export type SummaryProps = {
+  /**
+   * The post cover.
+   */
   cover?: Cover;
+  /**
+   * The post excerpt.
+   */
   excerpt: string;
+  /**
+   * The post meta.
+   */
   meta: Meta;
+  /**
+   * The post title.
+   */
   title: string;
+  /**
+   * The heading level (hn).
+   */
   titleLevel?: HeadingLevel;
+  /**
+   * The post url.
+   */
   url: string;
 };
 
@@ -48,7 +66,7 @@ export type SummaryProps = {
  *
  * Render a page summary.
  */
-const Summary: VFC<SummaryProps> = ({
+const Summary: FC<SummaryProps> = ({
   cover,
   excerpt,
   meta,
@@ -57,18 +75,23 @@ const Summary: VFC<SummaryProps> = ({
   url,
 }) => {
   const intl = useIntl();
+  const readMore = intl.formatMessage(
+    {
+      defaultMessage: 'Read more<a11y> about {title}</a11y>',
+      description: 'Summary: read more link',
+      id: 'Zpgv+f',
+    },
+    {
+      title,
+      a11y: (chunks: string) => (
+        <span className="screen-reader-text">{chunks}</span>
+      ),
+    }
+  );
 
   return (
     <article className={styles.wrapper}>
-      {cover && (
-        <ResponsiveImage
-          alt={cover.alt}
-          src={cover.url}
-          width={cover.width}
-          height={cover.height}
-          className={styles.cover}
-        />
-      )}
+      {cover && <ResponsiveImage className={styles.cover} {...cover} />}
       <header className={styles.header}>
         <Link href={url}>
           <Heading level={titleLevel} className={styles.title}>
@@ -79,20 +102,10 @@ const Summary: VFC<SummaryProps> = ({
       <div className={styles.body}>
         {excerpt}
         <ButtonLink target={url} className={styles['read-more']}>
-          {intl.formatMessage(
-            {
-              defaultMessage: 'Read more<a11y> about {title}</a11y>',
-              description: 'Summary: read more link',
-              id: 'Zpgv+f',
-            },
-            {
-              title,
-              a11y: (chunks: string) => (
-                <span className="screen-reader-text">{chunks}</span>
-              ),
-            }
-          )}
-          <Arrow direction="right" />
+          <>
+            {readMore}
+            <Arrow direction="right" />
+          </>
         </ButtonLink>
       </div>
       <footer className={styles.footer}>
diff --git a/src/components/organisms/modals/search-modal.tsx b/src/components/organisms/modals/search-modal.tsx
index 883b783..0e0ceed 100644
--- a/src/components/organisms/modals/search-modal.tsx
+++ b/src/components/organisms/modals/search-modal.tsx
@@ -1,5 +1,5 @@
-import Modal from '@components/molecules/modals/modal';
-import { VFC } from 'react';
+import Modal, { type ModalProps } from '@components/molecules/modals/modal';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import SearchForm from '../forms/search-form';
 import styles from './search-modal.module.scss';
@@ -8,7 +8,7 @@ export type SearchModalProps = {
   /**
    * Set additional classnames to modal wrapper.
    */
-  className?: string;
+  className?: ModalProps['className'];
 };
 
 /**
@@ -16,7 +16,7 @@ export type SearchModalProps = {
  *
  * Render a search form modal.
  */
-const SearchModal: VFC<SearchModalProps> = ({ className }) => {
+const SearchModal: FC<SearchModalProps> = ({ className }) => {
   const intl = useIntl();
   const modalTitle = intl.formatMessage({
     defaultMessage: 'Search',
diff --git a/src/components/organisms/modals/settings-modal.tsx b/src/components/organisms/modals/settings-modal.tsx
index 25d6f6f..20d2605 100644
--- a/src/components/organisms/modals/settings-modal.tsx
+++ b/src/components/organisms/modals/settings-modal.tsx
@@ -1,10 +1,12 @@
 import Form from '@components/atoms/forms/form';
-import AckeeSelect from '@components/molecules/forms/ackee-select';
+import AckeeSelect, {
+  type AckeeSelectProps,
+} from '@components/molecules/forms/ackee-select';
 import MotionToggle from '@components/molecules/forms/motion-toggle';
 import PrismThemeToggle from '@components/molecules/forms/prism-theme-toggle';
 import ThemeToggle from '@components/molecules/forms/theme-toggle';
-import Modal from '@components/molecules/modals/modal';
-import { VFC } from 'react';
+import Modal, { type ModalProps } from '@components/molecules/modals/modal';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './settings-modal.module.scss';
 
@@ -12,11 +14,11 @@ export type SettingsModalProps = {
   /**
    * Set additional classnames to the modal wrapper.
    */
-  className?: string;
+  className?: ModalProps['className'];
   /**
    * Set additional classnames to the tooltip wrapper.
    */
-  tooltipClassName?: string;
+  tooltipClassName?: AckeeSelectProps['tooltipClassName'];
 };
 
 /**
@@ -24,7 +26,7 @@ export type SettingsModalProps = {
  *
  * Render a modal with settings options.
  */
-const SettingsModal: VFC<SettingsModalProps> = ({
+const SettingsModal: FC<SettingsModalProps> = ({
   className = '',
   tooltipClassName = '',
 }) => {
diff --git a/src/components/organisms/toolbar/main-nav.tsx b/src/components/organisms/toolbar/main-nav.tsx
index 50bbe8b..35e3fd6 100644
--- a/src/components/organisms/toolbar/main-nav.tsx
+++ b/src/components/organisms/toolbar/main-nav.tsx
@@ -1,17 +1,20 @@
 import Checkbox, { type CheckboxProps } from '@components/atoms/forms/checkbox';
 import Label from '@components/atoms/forms/label';
 import Hamburger from '@components/atoms/icons/hamburger';
-import Nav, { type NavItem } from '@components/molecules/nav/nav';
-import { VFC } from 'react';
+import Nav, {
+  type NavProps,
+  type NavItem,
+} from '@components/molecules/nav/nav';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
-import sharedStyles from './toolbar-items.module.scss';
 import mainNavStyles from './main-nav.module.scss';
+import sharedStyles from './toolbar-items.module.scss';
 
 export type MainNavProps = {
   /**
    * Set additional classnames to the nav element.
    */
-  className?: string;
+  className?: NavProps['className'];
   /**
    * The button state.
    */
@@ -26,7 +29,12 @@ export type MainNavProps = {
   setIsActive: CheckboxProps['setValue'];
 };
 
-const MainNav: VFC<MainNavProps> = ({
+/**
+ * MainNav component
+ *
+ * Render the main navigation.
+ */
+const MainNav: FC<MainNavProps> = ({
   className = '',
   isActive,
   items,
diff --git a/src/components/organisms/toolbar/search.tsx b/src/components/organisms/toolbar/search.tsx
index 070bce0..72cd576 100644
--- a/src/components/organisms/toolbar/search.tsx
+++ b/src/components/organisms/toolbar/search.tsx
@@ -1,17 +1,17 @@
-import Checkbox, { CheckboxProps } from '@components/atoms/forms/checkbox';
+import Checkbox, { type CheckboxProps } from '@components/atoms/forms/checkbox';
 import Label from '@components/atoms/forms/label';
 import MagnifyingGlass from '@components/atoms/icons/magnifying-glass';
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
-import SearchModal from '../modals/search-modal';
-import sharedStyles from './toolbar-items.module.scss';
+import SearchModal, { type SearchModalProps } from '../modals/search-modal';
 import searchStyles from './search.module.scss';
+import sharedStyles from './toolbar-items.module.scss';
 
 export type SearchProps = {
   /**
    * Set additional classnames to the modal wrapper.
    */
-  className?: string;
+  className?: SearchModalProps['className'];
   /**
    * The button state.
    */
@@ -22,11 +22,7 @@ export type SearchProps = {
   setIsActive: CheckboxProps['setValue'];
 };
 
-const Search: VFC<SearchProps> = ({
-  className = '',
-  isActive,
-  setIsActive,
-}) => {
+const Search: FC<SearchProps> = ({ className = '', isActive, setIsActive }) => {
   const intl = useIntl();
   const label = isActive
     ? intl.formatMessage({
diff --git a/src/components/organisms/toolbar/settings.tsx b/src/components/organisms/toolbar/settings.tsx
index 88539fb..3b10226 100644
--- a/src/components/organisms/toolbar/settings.tsx
+++ b/src/components/organisms/toolbar/settings.tsx
@@ -1,11 +1,13 @@
-import Checkbox, { CheckboxProps } from '@components/atoms/forms/checkbox';
+import Checkbox, { type CheckboxProps } from '@components/atoms/forms/checkbox';
 import Label from '@components/atoms/forms/label';
 import Cog from '@components/atoms/icons/cog';
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
-import SettingsModal from '../modals/settings-modal';
-import sharedStyles from './toolbar-items.module.scss';
+import SettingsModal, {
+  type SettingsModalProps,
+} from '../modals/settings-modal';
 import settingsStyles from './settings.module.scss';
+import sharedStyles from './toolbar-items.module.scss';
 
 export type SettingsProps = {
   /**
@@ -23,10 +25,10 @@ export type SettingsProps = {
   /**
    * Set additional classnames to the tooltip wrapper.
    */
-  tooltipClassName?: string;
+  tooltipClassName?: SettingsModalProps['tooltipClassName'];
 };
 
-const Settings: VFC<SettingsProps> = ({
+const Settings: FC<SettingsProps> = ({
   className = '',
   isActive,
   setIsActive,
diff --git a/src/components/organisms/toolbar/toolbar.tsx b/src/components/organisms/toolbar/toolbar.tsx
index 81e80cf..f1ce530 100644
--- a/src/components/organisms/toolbar/toolbar.tsx
+++ b/src/components/organisms/toolbar/toolbar.tsx
@@ -1,4 +1,4 @@
-import { useState, VFC } from 'react';
+import { FC, useState } from 'react';
 import MainNav, { type MainNavProps } from '../toolbar/main-nav';
 import Search from '../toolbar/search';
 import Settings from '../toolbar/settings';
@@ -20,7 +20,7 @@ export type ToolbarProps = {
  *
  * Render the website toolbar.
  */
-const Toolbar: VFC<ToolbarProps> = ({ className = '', nav }) => {
+const Toolbar: FC<ToolbarProps> = ({ className = '', nav }) => {
   const [isNavOpened, setIsNavOpened] = useState<boolean>(false);
   const [isSettingsOpened, setIsSettingsOpened] = useState<boolean>(false);
   const [isSearchOpened, setIsSearchOpened] = useState<boolean>(false);
diff --git a/src/components/organisms/widgets/image-widget.tsx b/src/components/organisms/widgets/image-widget.tsx
index 928d5ea..ba04c6a 100644
--- a/src/components/organisms/widgets/image-widget.tsx
+++ b/src/components/organisms/widgets/image-widget.tsx
@@ -1,28 +1,16 @@
-import ResponsiveImage from '@components/molecules/images/responsive-image';
+import ResponsiveImage, {
+  type ResponsiveImageProps,
+} from '@components/molecules/images/responsive-image';
 import Widget, { type WidgetProps } from '@components/molecules/layout/widget';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './image-widget.module.scss';
 
 export type Alignment = 'left' | 'center' | 'right';
 
-export type Image = {
-  /**
-   * An alternative text for the image.
-   */
-  alt: string;
-  /**
-   * The image height.
-   */
-  height: number;
-  /**
-   * The image source.
-   */
-  src: string;
-  /**
-   * The image width.
-   */
-  width: number;
-};
+export type Image = Pick<
+  ResponsiveImageProps,
+  'alt' | 'height' | 'src' | 'width'
+>;
 
 export type ImageWidgetProps = Pick<
   WidgetProps,
@@ -35,7 +23,7 @@ export type ImageWidgetProps = Pick<
   /**
    * Add a caption to the image.
    */
-  description?: string;
+  description?: ResponsiveImageProps['caption'];
   /**
    * An object describing the image.
    */
@@ -43,7 +31,7 @@ export type ImageWidgetProps = Pick<
   /**
    * Add a link to the image.
    */
-  url?: string;
+  url?: ResponsiveImageProps['target'];
 };
 
 /**
@@ -51,7 +39,7 @@ export type ImageWidgetProps = Pick<
  *
  * Renders a widget that print an image and an optional text.
  */
-const ImageWidget: VFC<ImageWidgetProps> = ({
+const ImageWidget: FC<ImageWidgetProps> = ({
   alignment = 'left',
   description,
   img,
diff --git a/src/components/organisms/widgets/links-list-widget.tsx b/src/components/organisms/widgets/links-list-widget.tsx
index 155354e..559d0b6 100644
--- a/src/components/organisms/widgets/links-list-widget.tsx
+++ b/src/components/organisms/widgets/links-list-widget.tsx
@@ -1,8 +1,11 @@
 import Link from '@components/atoms/links/link';
-import List, { ListProps, type ListItem } from '@components/atoms/lists/list';
+import List, {
+  type ListProps,
+  type ListItem,
+} from '@components/atoms/lists/list';
 import Widget, { type WidgetProps } from '@components/molecules/layout/widget';
 import { slugify } from '@utils/helpers/slugify';
-import { VFC } from 'react';
+import { FC } from 'react';
 import styles from './links-list-widget.module.scss';
 
 export type LinksListItems = {
@@ -33,7 +36,7 @@ export type LinksListWidgetProps = Pick<WidgetProps, 'level' | 'title'> &
  *
  * Render a list of links inside a widget.
  */
-const LinksListWidget: VFC<LinksListWidgetProps> = ({
+const LinksListWidget: FC<LinksListWidgetProps> = ({
   items,
   kind = 'unordered',
   ...props
diff --git a/src/components/organisms/widgets/sharing.tsx b/src/components/organisms/widgets/sharing.tsx
index ccd3a21..571c1a4 100644
--- a/src/components/organisms/widgets/sharing.tsx
+++ b/src/components/organisms/widgets/sharing.tsx
@@ -2,7 +2,7 @@ import SharingLink, {
   type SharingMedium,
 } from '@components/atoms/links/sharing-link';
 import Widget, { type WidgetProps } from '@components/molecules/layout/widget';
-import { VFC } from 'react';
+import { FC } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './sharing.module.scss';
 
@@ -21,7 +21,7 @@ export type SharingData = {
   url: string;
 };
 
-export type SharingProps = WidgetProps & {
+export type SharingProps = Omit<WidgetProps, 'children'> & {
   /**
    * The page data to share.
    */
@@ -37,7 +37,7 @@ export type SharingProps = WidgetProps & {
  *
  * Render a list of sharing links inside a widget.
  */
-const Sharing: VFC<SharingProps> = ({ data, media, ...props }) => {
+const Sharing: FC<SharingProps> = ({ data, media, ...props }) => {
   const intl = useIntl();
 
   /**
-- 
cgit v1.2.3


From 7d9f874364ec6e255e62eb3027011bfed267904c Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Tue, 17 May 2022 18:37:38 +0200
Subject: chore: adjust articles styles

* change animation on article card hover
* change comments section alignment
---
 src/components/atoms/buttons/buttons.module.scss   |  2 +-
 src/components/atoms/headings/heading.module.scss  | 16 +++-
 src/components/atoms/headings/heading.stories.tsx  | 26 +++++++
 src/components/atoms/headings/heading.tsx          | 58 ++++++++++++---
 .../molecules/buttons/heading-button.module.scss   | 15 ++--
 .../molecules/buttons/heading-button.stories.tsx   | 73 ++++++-------------
 src/components/organisms/forms/comment-form.tsx    | 14 +++-
 .../organisms/layout/summary.module.scss           |  7 +-
 .../templates/page/page-layout.module.scss         |  9 ++-
 src/components/templates/page/page-layout.tsx      | 85 ++++++++++++++--------
 src/pages/article/[slug].tsx                       | 18 ++++-
 src/styles/pages/article.module.scss               |  1 +
 12 files changed, 211 insertions(+), 113 deletions(-)

(limited to 'src/components/atoms/headings/heading.tsx')

diff --git a/src/components/atoms/buttons/buttons.module.scss b/src/components/atoms/buttons/buttons.module.scss
index 36c66b6..2444bb1 100644
--- a/src/components/atoms/buttons/buttons.module.scss
+++ b/src/components/atoms/buttons/buttons.module.scss
@@ -16,7 +16,7 @@
   }
 
   &--rectangle {
-    padding: var(--spacing-2xs) var(--spacing-md);
+    padding: var(--spacing-2xs) var(--spacing-sm);
   }
 
   &--square,
diff --git a/src/components/atoms/headings/heading.module.scss b/src/components/atoms/headings/heading.module.scss
index 8620f6f..a420bc1 100644
--- a/src/components/atoms/headings/heading.module.scss
+++ b/src/components/atoms/headings/heading.module.scss
@@ -6,11 +6,23 @@
   letter-spacing: 0.01ex;
 
   &--regular {
-    margin: 0;
+    margin-bottom: 0;
+    margin-top: 0;
+  }
+
+  &--left {
+    text-align: left;
+  }
+
+  &--center {
+    width: fit-content;
+    margin-left: auto;
+    margin-right: auto;
   }
 
   &--margin {
-    margin: 0 0 var(--spacing-sm);
+    margin-top: 0;
+    margin-bottom: var(--spacing-sm);
 
     & + & {
       margin-top: var(--spacing-md);
diff --git a/src/components/atoms/headings/heading.stories.tsx b/src/components/atoms/headings/heading.stories.tsx
index da5a718..0e3885d 100644
--- a/src/components/atoms/headings/heading.stories.tsx
+++ b/src/components/atoms/headings/heading.stories.tsx
@@ -8,10 +8,26 @@ export default {
   title: 'Atoms/Typography/Headings',
   component: Heading,
   args: {
+    alignment: 'left',
     isFake: false,
     withMargin: true,
   },
   argTypes: {
+    alignment: {
+      control: {
+        type: 'select',
+      },
+      description: 'The title alignment.',
+      options: ['center', 'left'],
+      table: {
+        category: 'Options',
+        defaultValue: { summary: 'left' },
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     className: {
       control: {
         type: 'text',
@@ -32,6 +48,16 @@ export default {
         required: true,
       },
     },
+    id: {
+      control: {
+        type: 'text',
+      },
+      description: 'An unique id.',
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     isFake: {
       control: {
         type: 'boolean',
diff --git a/src/components/atoms/headings/heading.tsx b/src/components/atoms/headings/heading.tsx
index c5bf4ca..e385249 100644
--- a/src/components/atoms/headings/heading.tsx
+++ b/src/components/atoms/headings/heading.tsx
@@ -1,9 +1,19 @@
-import { FC, ReactNode } from 'react';
+import {
+  createElement,
+  ForwardedRef,
+  forwardRef,
+  ForwardRefRenderFunction,
+  ReactNode,
+} from 'react';
 import styles from './heading.module.scss';
 
 export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
 
 export type HeadingProps = {
+  /**
+   * The title alignment. Default: left;
+   */
+  alignment?: 'center' | 'left';
   /**
    * The heading body.
    */
@@ -30,31 +40,55 @@ export type HeadingProps = {
   withMargin?: boolean;
 };
 
+type TitleTagProps = Pick<HeadingProps, 'children' | 'className' | 'id'> & {
+  tagName: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p';
+};
+
+const TitleTag = forwardRef<
+  HTMLHeadingElement | HTMLParagraphElement,
+  TitleTagProps
+>(
+  (
+    { children, tagName, ...props },
+    ref: ForwardedRef<HTMLHeadingElement | HTMLParagraphElement>
+  ) => {
+    return createElement(tagName, { ...props, ref }, children);
+  }
+);
+TitleTag.displayName = 'TitleTag';
+
 /**
  * Heading component.
  *
  * Render an HTML heading element or a paragraph with heading styles.
  */
-const Heading: FC<HeadingProps> = ({
-  children,
-  className,
-  id,
-  isFake = false,
-  level,
-  withMargin = true,
-}) => {
-  const TitleTag = isFake ? `p` : (`h${level}` as keyof JSX.IntrinsicElements);
+const Heading: ForwardRefRenderFunction<HTMLDivElement, HeadingProps> = (
+  {
+    alignment = 'left',
+    children,
+    className,
+    id,
+    isFake = false,
+    level,
+    withMargin = true,
+  },
+  ref: ForwardedRef<HTMLHeadingElement | HTMLParagraphElement>
+) => {
+  const tagName = isFake ? 'p' : (`h${level}` as TitleTagProps['tagName']);
   const levelClass = `heading--${level}`;
+  const alignmentClass = `heading--${alignment}`;
   const marginClass = withMargin ? 'heading--margin' : 'heading--regular';
 
   return (
     <TitleTag
-      className={`${styles.heading} ${styles[levelClass]} ${styles[marginClass]} ${className}`}
+      tagName={tagName}
+      className={`${styles.heading} ${styles[levelClass]} ${styles[alignmentClass]} ${styles[marginClass]} ${className}`}
       id={id}
+      ref={ref}
     >
       {children}
     </TitleTag>
   );
 };
 
-export default Heading;
+export default forwardRef(Heading);
diff --git a/src/components/molecules/buttons/heading-button.module.scss b/src/components/molecules/buttons/heading-button.module.scss
index 9c278e4..3c69221 100644
--- a/src/components/molecules/buttons/heading-button.module.scss
+++ b/src/components/molecules/buttons/heading-button.module.scss
@@ -20,6 +20,14 @@
   border-bottom: fun.convert-px(2) solid var(--color-primary-dark);
   cursor: pointer;
 
+  .heading {
+    padding: var(--spacing-2xs) 0;
+    background: none;
+    font-size: var(--font-size-xl);
+    font-weight: 500;
+    text-align: left;
+  }
+
   &:hover,
   &:focus {
     .icon {
@@ -34,10 +42,3 @@
     }
   }
 }
-
-.heading {
-  padding: var(--spacing-2xs) 0;
-  background: none;
-  font-size: var(--font-size-xl);
-  text-align: left;
-}
diff --git a/src/components/molecules/buttons/heading-button.stories.tsx b/src/components/molecules/buttons/heading-button.stories.tsx
index b0e1465..d1b5ed4 100644
--- a/src/components/molecules/buttons/heading-button.stories.tsx
+++ b/src/components/molecules/buttons/heading-button.stories.tsx
@@ -1,6 +1,5 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
 import { useState } from 'react';
-import { IntlProvider } from 'react-intl';
 import HeadingButtonComponent from './heading-button';
 
 /**
@@ -10,6 +9,19 @@ export default {
   title: 'Molecules/Buttons/HeadingButton',
   component: HeadingButtonComponent,
   argTypes: {
+    className: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the button.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     expanded: {
       control: {
         type: null,
@@ -53,13 +65,6 @@ export default {
       },
     },
   },
-  decorators: [
-    (Story) => (
-      <IntlProvider locale="en">
-        <Story />
-      </IntlProvider>
-    ),
-  ],
 } as ComponentMeta<typeof HeadingButtonComponent>;
 
 const Template: ComponentStory<typeof HeadingButtonComponent> = ({
@@ -79,55 +84,21 @@ const Template: ComponentStory<typeof HeadingButtonComponent> = ({
 };
 
 /**
- * Heading Button Stories - Level 1
+ * Heading Button Stories - Expanded
  */
-export const Level1 = Template.bind({});
-Level1.args = {
-  level: 1,
-  title: 'Your title',
-};
-
-/**
- * Heading Button Stories - Level 2
- */
-export const Level2 = Template.bind({});
-Level2.args = {
+export const Expanded = Template.bind({});
+Expanded.args = {
+  expanded: true,
   level: 2,
   title: 'Your title',
 };
 
 /**
- * Heading Button Stories - Level 3
+ * Heading Button Stories - Collapsed
  */
-export const Level3 = Template.bind({});
-Level3.args = {
-  level: 3,
-  title: 'Your title',
-};
-
-/**
- * Heading Button Stories - Level 4
- */
-export const Level4 = Template.bind({});
-Level4.args = {
-  level: 4,
-  title: 'Your title',
-};
-
-/**
- * Heading Button Stories - Level 5
- */
-export const Level5 = Template.bind({});
-Level5.args = {
-  level: 5,
-  title: 'Your title',
-};
-
-/**
- * Heading Button Stories - Level 6
- */
-export const Level6 = Template.bind({});
-Level6.args = {
-  level: 6,
+export const Collapsed = Template.bind({});
+Collapsed.args = {
+  expanded: false,
+  level: 2,
   title: 'Your title',
 };
diff --git a/src/components/organisms/forms/comment-form.tsx b/src/components/organisms/forms/comment-form.tsx
index 5ff4ea4..b2c725f 100644
--- a/src/components/organisms/forms/comment-form.tsx
+++ b/src/components/organisms/forms/comment-form.tsx
@@ -1,6 +1,9 @@
 import Button from '@components/atoms/buttons/button';
 import Form, { type FormProps } from '@components/atoms/forms/form';
-import Heading, { type HeadingLevel } from '@components/atoms/headings/heading';
+import Heading, {
+  type HeadingProps,
+  type HeadingLevel,
+} from '@components/atoms/headings/heading';
 import Spinner from '@components/atoms/loaders/spinner';
 import LabelledField from '@components/molecules/forms/labelled-field';
 import { FC, ReactNode, useState } from 'react';
@@ -34,7 +37,11 @@ export type CommentFormProps = Pick<FormProps, 'className'> & {
    */
   title?: string;
   /**
-   * The title level.
+   * The form title alignment. Default: left.
+   */
+  titleAlignment?: HeadingProps['alignment'];
+  /**
+   * The title level. Default: 2.
    */
   titleLevel?: HeadingLevel;
 };
@@ -44,6 +51,7 @@ const CommentForm: FC<CommentFormProps> = ({
   parentId,
   saveComment,
   title,
+  titleAlignment,
   titleLevel = 2,
   ...props
 }) => {
@@ -117,7 +125,7 @@ const CommentForm: FC<CommentFormProps> = ({
       {...props}
     >
       {title && (
-        <Heading id={formId} level={titleLevel}>
+        <Heading id={formId} level={titleLevel} alignment={titleAlignment}>
           {title}
         </Heading>
       )}
diff --git a/src/components/organisms/layout/summary.module.scss b/src/components/organisms/layout/summary.module.scss
index 7e86dd2..62dfc0e 100644
--- a/src/components/organisms/layout/summary.module.scss
+++ b/src/components/organisms/layout/summary.module.scss
@@ -29,8 +29,11 @@
 
   &:hover {
     .icon {
-      transform: scaleX(1.4);
-      transform-origin: left;
+      --icon-size: #{fun.convert-px(35)};
+
+      :global {
+        animation: pulse 1.5s ease-in-out 0.2s infinite;
+      }
     }
   }
 }
diff --git a/src/components/templates/page/page-layout.module.scss b/src/components/templates/page/page-layout.module.scss
index 83e5a80..c6b4e8d 100644
--- a/src/components/templates/page/page-layout.module.scss
+++ b/src/components/templates/page/page-layout.module.scss
@@ -86,7 +86,14 @@
 
   &__section {
     grid-column: 2;
-    margin: var(--spacing-md) 0 0;
+
+    &:first-child {
+      margin: var(--spacing-md) 0 0;
+    }
+  }
+
+  &__no-comments {
+    text-align: center;
   }
 
   &__form {
diff --git a/src/components/templates/page/page-layout.tsx b/src/components/templates/page/page-layout.tsx
index f3f3ea8..2ff2084 100644
--- a/src/components/templates/page/page-layout.tsx
+++ b/src/components/templates/page/page-layout.tsx
@@ -1,6 +1,7 @@
 import Heading from '@components/atoms/headings/heading';
 import Notice, { type NoticeKind } from '@components/atoms/layout/notice';
 import Sidebar from '@components/atoms/layout/sidebar';
+import { MetaData } from '@components/molecules/layout/meta';
 import PageFooter, {
   type PageFooterProps,
 } from '@components/molecules/layout/page-footer';
@@ -118,10 +119,10 @@ const PageLayout: FC<PageLayoutProps> = ({
   const isMounted = useIsMounted(bodyRef);
 
   const hasComments = Array.isArray(comments) && comments.length > 0;
-  const hasCommentsSection = hasComments || allowComments;
-  const articleModifier = hasCommentsSection
-    ? 'article--has-comments'
-    : 'article--no-comments';
+  const articleModifier =
+    hasComments || allowComments
+      ? 'article--has-comments'
+      : 'article--no-comments';
 
   const [status, setStatus] = useState<NoticeKind>('info');
   const [statusMessage, setStatusMessage] = useState<string>('');
@@ -175,6 +176,15 @@ const PageLayout: FC<PageLayoutProps> = ({
     }
   };
 
+  /**
+   * Check if meta properties are defined.
+   *
+   * @param {MetaData} meta - The metadata.
+   */
+  const hasMeta = (meta: MetaData) => {
+    return Object.values(meta).every((value) => value === null);
+  };
+
   return (
     <Layout
       breadcrumbSchema={breadcrumbSchema}
@@ -218,7 +228,9 @@ const PageLayout: FC<PageLayoutProps> = ({
           {children}
         </div>
       )}
-      <PageFooter meta={footerMeta} className={styles.footer} />
+      {footerMeta && hasMeta(footerMeta) && (
+        <PageFooter meta={footerMeta} className={styles.footer} />
+      )}
       <Sidebar
         className={`${styles.sidebar} ${styles['sidebar--last']}`}
         aria-label={intl.formatMessage({
@@ -229,45 +241,54 @@ const PageLayout: FC<PageLayoutProps> = ({
       >
         {widgets}
       </Sidebar>
-      {hasCommentsSection && (
-        <div className={styles.comments}>
-          {hasComments && (
-            <section className={styles.comments__section}>
-              <Heading level={2}>{commentsTitle}</Heading>
+      {allowComments && (
+        <div className={styles.comments} id="comments">
+          <section className={styles.comments__section}>
+            <Heading level={2} alignment="center">
+              {commentsTitle}
+            </Heading>
+            {hasComments ? (
               <CommentsList
                 comments={comments}
                 depth={1}
                 Notice={
-                  isReplyRef.current === true ? (
+                  isReplyRef.current === true && (
                     <Notice
                       kind={status}
                       message={statusMessage}
                       className={styles.notice}
                     />
-                  ) : undefined
+                  )
                 }
                 saveComment={saveComment}
               />
-            </section>
-          )}
-          {allowComments && (
-            <section className={styles.comments__section}>
-              <CommentForm
-                className={styles.comments__form}
-                saveComment={saveComment}
-                title={commentFormTitle}
-                Notice={
-                  isReplyRef.current === false ? (
-                    <Notice
-                      kind={status}
-                      message={statusMessage}
-                      className={styles.notice}
-                    />
-                  ) : undefined
-                }
-              />
-            </section>
-          )}
+            ) : (
+              <p className={styles['comments__no-comments']}>
+                {intl.formatMessage({
+                  defaultMessage: 'No comments.',
+                  id: 'sBwfCy',
+                  description: 'PageLayout: no comments text',
+                })}
+              </p>
+            )}
+          </section>
+          <section className={styles.comments__section}>
+            <CommentForm
+              className={styles.comments__form}
+              saveComment={saveComment}
+              title={commentFormTitle}
+              titleAlignment="center"
+              Notice={
+                isReplyRef.current === false && (
+                  <Notice
+                    kind={status}
+                    message={statusMessage}
+                    className={styles.notice}
+                  />
+                )
+              }
+            />
+          </section>
         </div>
       )}
     </Layout>
diff --git a/src/pages/article/[slug].tsx b/src/pages/article/[slug].tsx
index 995e3a9..a0fb7fc 100644
--- a/src/pages/article/[slug].tsx
+++ b/src/pages/article/[slug].tsx
@@ -15,7 +15,10 @@ import { type Article, type Comment } from '@ts/types/app';
 import { loadTranslation, type Messages } from '@utils/helpers/i18n';
 import useAddPrismClassAttr from '@utils/hooks/use-add-prism-class-attr';
 import useBreadcrumb from '@utils/hooks/use-breadcrumb';
-import usePrismPlugins, { PrismPlugin } from '@utils/hooks/use-prism-plugins';
+import usePrismPlugins, {
+  type PrismPlugin,
+} from '@utils/hooks/use-prism-plugins';
+import useReadingTime from '@utils/hooks/use-reading-time';
 import useSettings from '@utils/hooks/use-settings';
 import { GetStaticPaths, GetStaticProps, NextPage } from 'next';
 import Head from 'next/head';
@@ -38,7 +41,16 @@ type ArticlePageProps = {
  */
 const ArticlePage: NextPage<ArticlePageProps> = ({ comments, post }) => {
   const { content, id, intro, meta, slug, title } = post;
-  const { author, commentsCount, cover, dates, seo, thematics, topics } = meta;
+  const {
+    author,
+    commentsCount,
+    cover,
+    dates,
+    seo,
+    thematics,
+    topics,
+    wordsCount,
+  } = meta;
   const { data } = useSWR(() => id, getPostComments, {
     fallbackData: comments,
   });
@@ -46,11 +58,13 @@ const ArticlePage: NextPage<ArticlePageProps> = ({ comments, post }) => {
     title,
     url: `/article/${slug}`,
   });
+  const readingTime = useReadingTime(wordsCount || 0, true);
 
   const headerMeta: PageLayoutProps['headerMeta'] = {
     author: author?.name,
     publication: { date: dates.publication },
     update: dates.update ? { date: dates.update } : undefined,
+    readingTime,
     thematics:
       thematics &&
       thematics.map((thematic) => (
diff --git a/src/styles/pages/article.module.scss b/src/styles/pages/article.module.scss
index a42c633..04acad9 100644
--- a/src/styles/pages/article.module.scss
+++ b/src/styles/pages/article.module.scss
@@ -31,6 +31,7 @@
   @include mix.media("screen") {
     @include mix.dimensions("md") {
       width: min-content;
+      gap: var(--spacing-2xs);
     }
   }
 }
-- 
cgit v1.2.3

