From 06396f8e942c58254ee4e87f610d3e33197e0d73 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Thu, 7 Apr 2022 19:00:13 +0200
Subject: chore: add a DescriptionList component

---
 src/components/atoms/lists/description-list.tsx | 60 +++++++++++++++++++++++++
 1 file changed, 60 insertions(+)
 create mode 100644 src/components/atoms/lists/description-list.tsx

(limited to 'src/components/atoms/lists/description-list.tsx')

diff --git a/src/components/atoms/lists/description-list.tsx b/src/components/atoms/lists/description-list.tsx
new file mode 100644
index 0000000..df2880f
--- /dev/null
+++ b/src/components/atoms/lists/description-list.tsx
@@ -0,0 +1,60 @@
+import { FC } from 'react';
+import styles from './description-list.module.scss';
+
+export type DescriptionListItem = {
+  /**
+   * The item id.
+   */
+  id: string;
+  /**
+   * A list term.
+   */
+  term: string;
+  /**
+   * An array of values for the list term.
+   */
+  value: any[];
+};
+
+export type DescriptionListProps = {
+  /**
+   * Set additional classes to the list wrapper.
+   */
+  classes?: string;
+  /**
+   * The list items.
+   */
+  items: DescriptionListItem[];
+};
+
+/**
+ * DescriptionList component
+ *
+ * Render a description list.
+ */
+const DescriptionList: FC<DescriptionListProps> = ({ classes = '', items }) => {
+  /**
+   * Retrieve the description list items wrapped in a div element.
+   *
+   * @param {DescriptionListItem[]} listItems - An array of term and description couples.
+   * @returns {JSX.Element[]} The description list items.
+   */
+  const getItems = (listItems: DescriptionListItem[]): JSX.Element[] => {
+    return listItems.map(({ id, term, value }) => {
+      return (
+        <div key={id} className={styles.list__item}>
+          <dt className={styles.list__term}>{term}</dt>
+          {value.map((currentValue, index) => (
+            <dd key={`${id}-${index}`} className={styles.list__description}>
+              {currentValue}
+            </dd>
+          ))}
+        </div>
+      );
+    });
+  };
+
+  return <dl className={`${styles.list} ${classes}`}>{getItems(items)}</dl>;
+};
+
+export default DescriptionList;
-- 
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/lists/description-list.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 47e35fcd7c2c346f4799630bf6521d6a4bf49e85 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Wed, 13 Apr 2022 19:28:16 +0200
Subject: chore: add a Card component

---
 .../atoms/lists/description-list.module.scss       |  42 +++++---
 .../atoms/lists/description-list.stories.tsx       |  18 ++++
 src/components/atoms/lists/description-list.tsx    |  45 +++++++-
 .../molecules/images/responsive-image.tsx          |   5 +-
 src/components/molecules/layout/card.module.scss   |  77 ++++++++++++++
 src/components/molecules/layout/card.stories.tsx   | 102 ++++++++++++++++++
 src/components/molecules/layout/card.test.tsx      |  52 ++++++++++
 src/components/molecules/layout/card.tsx           | 114 +++++++++++++++++++++
 8 files changed, 435 insertions(+), 20 deletions(-)
 create mode 100644 src/components/molecules/layout/card.module.scss
 create mode 100644 src/components/molecules/layout/card.stories.tsx
 create mode 100644 src/components/molecules/layout/card.test.tsx
 create mode 100644 src/components/molecules/layout/card.tsx

(limited to 'src/components/atoms/lists/description-list.tsx')

diff --git a/src/components/atoms/lists/description-list.module.scss b/src/components/atoms/lists/description-list.module.scss
index 4758816..caa2711 100644
--- a/src/components/atoms/lists/description-list.module.scss
+++ b/src/components/atoms/lists/description-list.module.scss
@@ -6,18 +6,6 @@
   gap: var(--spacing-2xs);
   margin: 0;
 
-  &__item {
-    display: flex;
-    flex-flow: column wrap;
-    gap: var(--spacing-2xs);
-
-    @include mix.media("screen") {
-      @include mix.dimensions("sm") {
-        flex-flow: row wrap;
-      }
-    }
-  }
-
   &__term {
     flex: 0 0 max-content;
     color: var(--color-fg-light);
@@ -27,16 +15,40 @@
   &__description {
     flex: 0 0 auto;
     margin: 0;
+  }
+
+  &__item {
+    display: flex;
+  }
+
+  &--inline &__item {
+    flex-flow: column wrap;
 
     @include mix.media("screen") {
-      @include mix.dimensions("sm") {
-        &:not(:first-of-type) {
+      @include mix.dimensions("xs") {
+        flex-flow: row wrap;
+        gap: var(--spacing-2xs);
+
+        .list__description:not(:first-of-type) {
           &::before {
             content: "/";
-            margin: 0 var(--spacing-2xs);
+            margin-right: var(--spacing-2xs);
           }
         }
       }
     }
   }
+
+  &--column#{&}--responsive {
+    @include mix.media("screen") {
+      @include mix.dimensions("xs") {
+        flex-flow: row wrap;
+        gap: var(--spacing-lg);
+      }
+    }
+  }
+
+  &--column &__item {
+    flex-flow: column wrap;
+  }
 }
diff --git a/src/components/atoms/lists/description-list.stories.tsx b/src/components/atoms/lists/description-list.stories.tsx
index c65241d..66d94af 100644
--- a/src/components/atoms/lists/description-list.stories.tsx
+++ b/src/components/atoms/lists/description-list.stories.tsx
@@ -6,6 +6,9 @@ import DescriptionListComponent, {
 export default {
   title: 'Atoms/Lists',
   component: DescriptionListComponent,
+  args: {
+    layout: 'column',
+  },
   argTypes: {
     className: {
       control: {
@@ -31,6 +34,21 @@ export default {
         value: {},
       },
     },
+    layout: {
+      control: {
+        type: 'select',
+      },
+      description: 'The list layout.',
+      options: ['column', 'inline'],
+      table: {
+        category: 'Options',
+        defaultValue: { summary: 'column' },
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
   },
 } as ComponentMeta<typeof DescriptionListComponent>;
 
diff --git a/src/components/atoms/lists/description-list.tsx b/src/components/atoms/lists/description-list.tsx
index a5ab1d5..0a92465 100644
--- a/src/components/atoms/lists/description-list.tsx
+++ b/src/components/atoms/lists/description-list.tsx
@@ -21,10 +21,30 @@ export type DescriptionListProps = {
    * Set additional classnames to the list wrapper.
    */
   className?: string;
+  /**
+   * Set additional classnames to the `dd` element.
+   */
+  descriptionClassName?: string;
+  /**
+   * Set additional classnames to the `dt`/`dd` couple wrapper.
+   */
+  groupClassName?: string;
   /**
    * The list items.
    */
   items: DescriptionListItem[];
+  /**
+   * The list items layout. Default: column.
+   */
+  layout?: 'inline' | 'column';
+  /**
+   * Define if the layout should automatically create rows/columns.
+   */
+  responsiveLayout?: boolean;
+  /**
+   * Set additional classnames to the `dt` element.
+   */
+  termClassName?: string;
 };
 
 /**
@@ -34,8 +54,16 @@ export type DescriptionListProps = {
  */
 const DescriptionList: VFC<DescriptionListProps> = ({
   className = '',
+  descriptionClassName = '',
+  groupClassName = '',
   items,
+  layout = 'column',
+  responsiveLayout = false,
+  termClassName = '',
 }) => {
+  const layoutModifier = `list--${layout}`;
+  const responsiveModifier = responsiveLayout ? 'list--responsive' : '';
+
   /**
    * Retrieve the description list items wrapped in a div element.
    *
@@ -45,10 +73,13 @@ const DescriptionList: VFC<DescriptionListProps> = ({
   const getItems = (listItems: DescriptionListItem[]): JSX.Element[] => {
     return listItems.map(({ id, term, value }) => {
       return (
-        <div key={id} className={styles.list__item}>
-          <dt className={styles.list__term}>{term}</dt>
+        <div key={id} className={`${styles.list__item} ${groupClassName}`}>
+          <dt className={`${styles.list__term} ${termClassName}`}>{term}</dt>
           {value.map((currentValue, index) => (
-            <dd key={`${id}-${index}`} className={styles.list__description}>
+            <dd
+              key={`${id}-${index}`}
+              className={`${styles.list__description} ${descriptionClassName}`}
+            >
               {currentValue}
             </dd>
           ))}
@@ -57,7 +88,13 @@ const DescriptionList: VFC<DescriptionListProps> = ({
     });
   };
 
-  return <dl className={`${styles.list} ${className}`}>{getItems(items)}</dl>;
+  return (
+    <dl
+      className={`${styles.list} ${styles[layoutModifier]} ${styles[responsiveModifier]} ${className}`}
+    >
+      {getItems(items)}
+    </dl>
+  );
 };
 
 export default DescriptionList;
diff --git a/src/components/molecules/images/responsive-image.tsx b/src/components/molecules/images/responsive-image.tsx
index 3d54e95..1d8787e 100644
--- a/src/components/molecules/images/responsive-image.tsx
+++ b/src/components/molecules/images/responsive-image.tsx
@@ -3,7 +3,10 @@ import Image, { ImageProps } from 'next/image';
 import { VFC } from 'react';
 import styles from './responsive-image.module.scss';
 
-type ResponsiveImageProps = Omit<ImageProps, 'alt' | 'width' | 'height'> & {
+export type ResponsiveImageProps = Omit<
+  ImageProps,
+  'alt' | 'width' | 'height'
+> & {
   /**
    * An alternative text.
    */
diff --git a/src/components/molecules/layout/card.module.scss b/src/components/molecules/layout/card.module.scss
new file mode 100644
index 0000000..2b1b7dc
--- /dev/null
+++ b/src/components/molecules/layout/card.module.scss
@@ -0,0 +1,77 @@
+@use "@styles/abstracts/functions" as fun;
+
+.wrapper {
+  --scale-up: 1.05;
+  --scale-down: 0.95;
+
+  display: flex;
+  flex-flow: column wrap;
+  max-width: var(--card-width, 40ch);
+  padding: 0;
+  text-align: center;
+
+  .article {
+    flex: 1;
+    display: flex;
+    flex-flow: column nowrap;
+    justify-content: flex-start;
+  }
+
+  .footer {
+    margin-top: var(--spacing-md);
+  }
+
+  .cover {
+    align-self: flex-start;
+    max-height: fun.convert-px(150);
+    margin: auto;
+    border-bottom: fun.convert-px(1) solid var(--color-border);
+  }
+
+  .title,
+  .tagline,
+  .footer {
+    padding: 0 var(--spacing-md);
+  }
+
+  .title {
+    flex: 1;
+    margin: var(--spacing-sm) 0;
+  }
+
+  h2.title {
+    background: none;
+    text-shadow: none;
+  }
+
+  .tagline {
+    flex: 1;
+    color: var(--color-fg);
+    font-weight: 400;
+  }
+
+  .list {
+    margin-bottom: var(--spacing-md);
+  }
+
+  .items {
+    flex-flow: row wrap;
+    place-content: center;
+    gap: var(--spacing-2xs);
+  }
+
+  .term {
+    flex: 0 0 100%;
+  }
+
+  .description {
+    padding: fun.convert-px(2) var(--spacing-xs);
+    border: fun.convert-px(1) solid var(--color-primary-darker);
+    color: var(--color-fg);
+    font-weight: 400;
+
+    &::before {
+      display: none;
+    }
+  }
+}
diff --git a/src/components/molecules/layout/card.stories.tsx b/src/components/molecules/layout/card.stories.tsx
new file mode 100644
index 0000000..a07f8dc
--- /dev/null
+++ b/src/components/molecules/layout/card.stories.tsx
@@ -0,0 +1,102 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import CardComponent from './card';
+
+export default {
+  title: 'Molecules/Layout',
+  component: CardComponent,
+  argTypes: {
+    cover: {
+      description: 'The card cover data (src, dimensions, alternative text).',
+      table: {
+        category: 'Options',
+      },
+      type: {
+        name: 'object',
+        required: false,
+        value: {},
+      },
+    },
+    meta: {
+      description: 'The card metadata (a publication date for example).',
+      table: {
+        category: 'Options',
+      },
+      type: {
+        name: 'object',
+        required: false,
+        value: {},
+      },
+    },
+    tagline: {
+      control: {
+        type: 'text',
+      },
+      description: 'A few words about the card.',
+      table: {
+        category: 'Options',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
+    title: {
+      control: {
+        type: 'text',
+      },
+      description: 'The card title.',
+      type: {
+        name: 'string',
+        required: true,
+      },
+    },
+    titleLevel: {
+      control: {
+        type: 'number',
+      },
+      description: 'The title level.',
+      type: {
+        name: 'number',
+        required: true,
+      },
+    },
+    url: {
+      control: {
+        type: 'text',
+      },
+      description: 'The card target.',
+      type: {
+        name: 'string',
+        required: true,
+      },
+    },
+  },
+} as ComponentMeta<typeof CardComponent>;
+
+const Template: ComponentStory<typeof CardComponent> = (args) => (
+  <CardComponent {...args} />
+);
+
+const cover = {
+  alt: 'A picture',
+  height: 480,
+  src: 'http://placeimg.com/640/480',
+  width: 640,
+};
+
+const meta = [
+  {
+    id: 'an-id',
+    term: 'Voluptates',
+    value: ['Autem', 'Eos'],
+  },
+];
+
+export const Card = Template.bind({});
+Card.args = {
+  cover,
+  meta,
+  title: 'Veritatis dicta quod',
+  titleLevel: 2,
+  url: '#',
+};
diff --git a/src/components/molecules/layout/card.test.tsx b/src/components/molecules/layout/card.test.tsx
new file mode 100644
index 0000000..404bc7a
--- /dev/null
+++ b/src/components/molecules/layout/card.test.tsx
@@ -0,0 +1,52 @@
+import { render, screen } from '@test-utils';
+import Card from './card';
+
+const cover = {
+  alt: 'A picture',
+  height: 480,
+  src: 'http://placeimg.com/640/480',
+  width: 640,
+};
+
+const meta = [
+  {
+    id: 'an-id',
+    term: 'Voluptates',
+    value: ['Autem', 'Eos'],
+  },
+];
+
+const tagline = 'Ut rerum incidunt';
+
+const title = 'Alias qui porro';
+
+const url = '/an-existing-url';
+
+describe('Card', () => {
+  it('renders a title wrapped in h2 element', () => {
+    render(<Card title={title} titleLevel={2} url={url} />);
+    expect(
+      screen.getByRole('heading', { level: 2, name: title })
+    ).toBeInTheDocument();
+  });
+
+  it('renders a link to another page', () => {
+    render(<Card title={title} titleLevel={2} url={url} />);
+    expect(screen.getByRole('link')).toHaveAttribute('href', url);
+  });
+
+  it('renders a cover', () => {
+    render(<Card title={title} titleLevel={2} url={url} cover={cover} />);
+    expect(screen.getByRole('img', { name: cover.alt })).toBeInTheDocument();
+  });
+
+  it('renders a tagline', () => {
+    render(<Card title={title} titleLevel={2} url={url} tagline={tagline} />);
+    expect(screen.getByText(tagline)).toBeInTheDocument();
+  });
+
+  it('renders some meta', () => {
+    render(<Card title={title} titleLevel={2} url={url} meta={meta} />);
+    expect(screen.getByText(meta[0].term)).toBeInTheDocument();
+  });
+});
diff --git a/src/components/molecules/layout/card.tsx b/src/components/molecules/layout/card.tsx
new file mode 100644
index 0000000..23a0e54
--- /dev/null
+++ b/src/components/molecules/layout/card.tsx
@@ -0,0 +1,114 @@
+import ButtonLink from '@components/atoms/buttons/button-link';
+import Heading, { type HeadingLevel } from '@components/atoms/headings/heading';
+import DescriptionList, {
+  DescriptionListItem,
+} from '@components/atoms/lists/description-list';
+import { VFC } from 'react';
+import ResponsiveImage, {
+  ResponsiveImageProps,
+} from '../images/responsive-image';
+import styles from './card.module.scss';
+
+export type Cover = {
+  /**
+   * The cover alternative text.
+   */
+  alt: string;
+  /**
+   * The cover height.
+   */
+  height: number;
+  /**
+   * The cover source.
+   */
+  src: string;
+  /**
+   * The cover width.
+   */
+  width: number;
+};
+
+export type CardProps = {
+  /**
+   * Set additional classnames to the card wrapper.
+   */
+  className?: string;
+  /**
+   * The card cover.
+   */
+  cover?: Cover;
+  /**
+   * The cover fit. Default: cover.
+   */
+  coverFit?: ResponsiveImageProps['objectFit'];
+  /**
+   * The card meta.
+   */
+  meta?: DescriptionListItem[];
+  /**
+   * The card tagline.
+   */
+  tagline?: string;
+  /**
+   * The card title.
+   */
+  title: string;
+  /**
+   * The title level (hn).
+   */
+  titleLevel: HeadingLevel;
+  /**
+   * The card target.
+   */
+  url: string;
+};
+
+/**
+ * Card component
+ *
+ * Render a link with minimal information about its content.
+ */
+const Card: VFC<CardProps> = ({
+  className = '',
+  cover,
+  coverFit = 'cover',
+  meta,
+  tagline,
+  title,
+  titleLevel,
+  url,
+}) => {
+  return (
+    <ButtonLink target={url} className={`${styles.wrapper} ${className}`}>
+      <article className={styles.article}>
+        <header className={styles.header}>
+          {cover && (
+            <ResponsiveImage
+              {...cover}
+              objectFit={coverFit}
+              className={styles.cover}
+            />
+          )}
+          <Heading level={titleLevel} className={styles.title}>
+            {title}
+          </Heading>
+        </header>
+        <div className={styles.tagline}>{tagline}</div>
+        {meta && (
+          <footer className={styles.footer}>
+            <DescriptionList
+              items={meta}
+              layout="inline"
+              className={styles.list}
+              groupClassName={styles.items}
+              termClassName={styles.term}
+              descriptionClassName={styles.description}
+            />
+          </footer>
+        )}
+      </article>
+    </ButtonLink>
+  );
+};
+
+export default Card;
-- 
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/lists/description-list.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 0d59a6d2995b4119865271ed1908ede0bb96497c Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Mon, 9 May 2022 18:19:38 +0200
Subject: refactor: rewrite DescriptionList and Meta components

The meta can have different layout. The previous implementation was not
enough to easily change the layout. Also, I prefer to restrict the meta
types and it prevents me to repeat myself for the labels.
---
 .storybook/preview.js                              |   9 +
 .../atoms/lists/description-list-item.module.scss  |  38 +++
 .../atoms/lists/description-list-item.stories.tsx  | 132 +++++++++
 .../atoms/lists/description-list-item.test.tsx     |  17 ++
 .../atoms/lists/description-list-item.tsx          |  73 +++++
 .../atoms/lists/description-list.module.scss       |  49 +---
 .../atoms/lists/description-list.stories.tsx       |  80 +++++-
 .../atoms/lists/description-list.test.tsx          |   8 +-
 src/components/atoms/lists/description-list.tsx    |  71 ++---
 src/components/molecules/layout/card.module.scss   |  39 ++-
 src/components/molecules/layout/card.stories.tsx   |  38 ++-
 src/components/molecules/layout/card.test.tsx      |  13 +-
 src/components/molecules/layout/card.tsx           |  16 +-
 src/components/molecules/layout/meta.module.scss   |  18 --
 src/components/molecules/layout/meta.stories.tsx   |  57 ++--
 src/components/molecules/layout/meta.test.tsx      |  22 +-
 src/components/molecules/layout/meta.tsx           | 304 +++++++++++++++++++--
 .../molecules/layout/page-footer.stories.tsx       |  12 +-
 src/components/molecules/layout/page-footer.tsx    |   4 +-
 .../molecules/layout/page-header.stories.tsx       |  34 ++-
 src/components/molecules/layout/page-header.tsx    |  25 +-
 .../organisms/layout/cards-list.stories.tsx        |  16 +-
 .../organisms/layout/cards-list.test.tsx           |  26 +-
 .../organisms/layout/comment.module.scss           |  12 +-
 src/components/organisms/layout/comment.test.tsx   |   2 +-
 src/components/organisms/layout/comment.tsx        |  83 +++---
 .../organisms/layout/overview.module.scss          |  34 ++-
 .../organisms/layout/overview.stories.tsx          |  29 +-
 src/components/organisms/layout/overview.test.tsx  |  19 +-
 src/components/organisms/layout/overview.tsx       |  41 ++-
 .../organisms/layout/posts-list.stories.tsx        |  80 ++----
 .../organisms/layout/posts-list.test.tsx           |  76 ++----
 src/components/organisms/layout/posts-list.tsx     |   2 +-
 .../organisms/layout/summary.module.scss           |  13 +
 .../organisms/layout/summary.stories.tsx           |  33 +--
 src/components/organisms/layout/summary.test.tsx   |  27 +-
 src/components/organisms/layout/summary.tsx        |  33 +--
 .../templates/page/page-layout.stories.tsx         | 107 +++-----
 src/components/templates/page/page-layout.tsx      |  14 +-
 src/pages/contact.tsx                              |   4 +-
 src/pages/cv.tsx                                   |  26 +-
 src/pages/index.tsx                                |  12 +-
 src/pages/mentions-legales.tsx                     |  26 +-
 src/utils/helpers/dates.ts                         |  15 -
 44 files changed, 1182 insertions(+), 607 deletions(-)
 create mode 100644 src/components/atoms/lists/description-list-item.module.scss
 create mode 100644 src/components/atoms/lists/description-list-item.stories.tsx
 create mode 100644 src/components/atoms/lists/description-list-item.test.tsx
 create mode 100644 src/components/atoms/lists/description-list-item.tsx

(limited to 'src/components/atoms/lists/description-list.tsx')

diff --git a/.storybook/preview.js b/.storybook/preview.js
index 4bec022..344df2a 100644
--- a/.storybook/preview.js
+++ b/.storybook/preview.js
@@ -1,4 +1,5 @@
 import '@styles/globals.scss';
+import { IntlProvider } from 'react-intl';
 
 export const parameters = {
   actions: { argTypesRegex: '^on[A-Z].*' },
@@ -9,3 +10,11 @@ export const parameters = {
     },
   },
 };
+
+export const decorators = [
+  (Story) => (
+    <IntlProvider locale="en">
+      <Story />
+    </IntlProvider>
+  ),
+];
diff --git a/src/components/atoms/lists/description-list-item.module.scss b/src/components/atoms/lists/description-list-item.module.scss
new file mode 100644
index 0000000..60cad57
--- /dev/null
+++ b/src/components/atoms/lists/description-list-item.module.scss
@@ -0,0 +1,38 @@
+.term {
+  color: var(--color-fg-light);
+  font-weight: 600;
+}
+
+.description {
+  margin: 0;
+  word-break: break-all;
+}
+
+.wrapper {
+  display: flex;
+  width: fit-content;
+
+  &--has-separator {
+    .description:not(:first-of-type) {
+      &::before {
+        content: "/\0000a0";
+      }
+    }
+  }
+
+  &--inline,
+  &--inline-values {
+    flex-flow: row wrap;
+    column-gap: var(--spacing-2xs);
+  }
+
+  &--inline-values {
+    .term {
+      flex: 1 1 100%;
+    }
+  }
+
+  &--stacked {
+    flex-flow: column wrap;
+  }
+}
diff --git a/src/components/atoms/lists/description-list-item.stories.tsx b/src/components/atoms/lists/description-list-item.stories.tsx
new file mode 100644
index 0000000..e05493c
--- /dev/null
+++ b/src/components/atoms/lists/description-list-item.stories.tsx
@@ -0,0 +1,132 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import DescriptionListItemComponent from './description-list-item';
+
+export default {
+  title: 'Atoms/Typography/Lists/DescriptionList/Item',
+  component: DescriptionListItemComponent,
+  args: {
+    layout: 'stacked',
+    withSeparator: false,
+  },
+  argTypes: {
+    className: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the list item wrapper.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
+    descriptionClassName: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the list item description.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
+    label: {
+      control: {
+        type: 'text',
+      },
+      description: 'The item label.',
+      type: {
+        name: 'string',
+        required: true,
+      },
+    },
+    layout: {
+      control: {
+        type: 'select',
+      },
+      description: 'The item layout.',
+      options: ['inline', 'inline-values', 'stacked'],
+      table: {
+        category: 'Styles',
+        defaultValue: { summary: 'stacked' },
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
+    termClassName: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the list item term.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
+    value: {
+      description: 'The item value.',
+      type: {
+        name: 'object',
+        required: true,
+        value: {},
+      },
+    },
+    withSeparator: {
+      control: {
+        type: 'boolean',
+      },
+      description: 'Add a slash as separator between multiple values.',
+      table: {
+        category: 'Options',
+        defaultValue: { summary: false },
+      },
+      type: {
+        name: 'boolean',
+        required: false,
+      },
+    },
+  },
+} as ComponentMeta<typeof DescriptionListItemComponent>;
+
+const Template: ComponentStory<typeof DescriptionListItemComponent> = (
+  args
+) => <DescriptionListItemComponent {...args} />;
+
+export const SingleValueStacked = Template.bind({});
+SingleValueStacked.args = {
+  label: 'Recusandae vitae tenetur',
+  value: ['praesentium'],
+  layout: 'stacked',
+};
+
+export const SingleValueInlined = Template.bind({});
+SingleValueInlined.args = {
+  label: 'Recusandae vitae tenetur',
+  value: ['praesentium'],
+  layout: 'inline',
+};
+
+export const MultipleValuesStacked = Template.bind({});
+MultipleValuesStacked.args = {
+  label: 'Recusandae vitae tenetur',
+  value: ['praesentium', 'voluptate', 'tempore'],
+  layout: 'stacked',
+};
+
+export const MultipleValuesInlined = Template.bind({});
+MultipleValuesInlined.args = {
+  label: 'Recusandae vitae tenetur',
+  value: ['praesentium', 'voluptate', 'tempore'],
+  layout: 'inline-values',
+  withSeparator: true,
+};
diff --git a/src/components/atoms/lists/description-list-item.test.tsx b/src/components/atoms/lists/description-list-item.test.tsx
new file mode 100644
index 0000000..730a52f
--- /dev/null
+++ b/src/components/atoms/lists/description-list-item.test.tsx
@@ -0,0 +1,17 @@
+import { render, screen } from '@test-utils';
+import DescriptionListItem from './description-list-item';
+
+const itemLabel = 'Repellendus corporis facilis';
+const itemValue = ['quos', 'eum'];
+
+describe('DescriptionListItem', () => {
+  it('renders a couple of label', () => {
+    render(<DescriptionListItem label={itemLabel} value={itemValue} />);
+    expect(screen.getByRole('term')).toHaveTextContent(itemLabel);
+  });
+
+  it('renders the right number of values', () => {
+    render(<DescriptionListItem label={itemLabel} value={itemValue} />);
+    expect(screen.getAllByRole('definition')).toHaveLength(itemValue.length);
+  });
+});
diff --git a/src/components/atoms/lists/description-list-item.tsx b/src/components/atoms/lists/description-list-item.tsx
new file mode 100644
index 0000000..9505d01
--- /dev/null
+++ b/src/components/atoms/lists/description-list-item.tsx
@@ -0,0 +1,73 @@
+import { FC, ReactNode, useId } from 'react';
+import styles from './description-list-item.module.scss';
+
+export type ItemLayout = 'inline' | 'inline-values' | 'stacked';
+
+export type DescriptionListItemProps = {
+  /**
+   * Set additional classnames to the list item wrapper.
+   */
+  className?: string;
+  /**
+   * Set additional classnames to the list item description.
+   */
+  descriptionClassName?: string;
+  /**
+   * The item label.
+   */
+  label: string;
+  /**
+   * The item layout.
+   */
+  layout?: ItemLayout;
+  /**
+   * Set additional classnames to the list item term.
+   */
+  termClassName?: string;
+  /**
+   * The item value.
+   */
+  value: ReactNode | ReactNode[];
+  /**
+   * If true, use a slash to delimitate multiple values.
+   */
+  withSeparator?: boolean;
+};
+
+/**
+ * DescriptionListItem component
+ *
+ * Render a couple of dt/dd wrapped in a div.
+ */
+const DescriptionListItem: FC<DescriptionListItemProps> = ({
+  className = '',
+  descriptionClassName = '',
+  label,
+  termClassName = '',
+  value,
+  layout = 'stacked',
+  withSeparator = false,
+}) => {
+  const id = useId();
+  const layoutStyles = styles[`wrapper--${layout}`];
+  const separatorStyles = withSeparator ? styles['wrapper--has-separator'] : '';
+  const itemValues = Array.isArray(value) ? value : [value];
+
+  return (
+    <div
+      className={`${styles.wrapper} ${layoutStyles} ${separatorStyles} ${className}`}
+    >
+      <dt className={`${styles.term} ${termClassName}`}>{label}</dt>
+      {itemValues.map((currentValue, index) => (
+        <dd
+          key={`${id}-${index}`}
+          className={`${styles.description} ${descriptionClassName}`}
+        >
+          {currentValue}
+        </dd>
+      ))}
+    </div>
+  );
+};
+
+export default DescriptionListItem;
diff --git a/src/components/atoms/lists/description-list.module.scss b/src/components/atoms/lists/description-list.module.scss
index caa2711..9e913d4 100644
--- a/src/components/atoms/lists/description-list.module.scss
+++ b/src/components/atoms/lists/description-list.module.scss
@@ -2,53 +2,16 @@
 
 .list {
   display: flex;
-  flex-flow: column wrap;
-  gap: var(--spacing-2xs);
+  column-gap: var(--spacing-md);
+  row-gap: var(--spacing-2xs);
   margin: 0;
 
-  &__term {
-    flex: 0 0 max-content;
-    color: var(--color-fg-light);
-    font-weight: 600;
+  &--inline {
+    flex-flow: row wrap;
+    align-items: baseline;
   }
 
-  &__description {
-    flex: 0 0 auto;
-    margin: 0;
-  }
-
-  &__item {
-    display: flex;
-  }
-
-  &--inline &__item {
-    flex-flow: column wrap;
-
-    @include mix.media("screen") {
-      @include mix.dimensions("xs") {
-        flex-flow: row wrap;
-        gap: var(--spacing-2xs);
-
-        .list__description:not(:first-of-type) {
-          &::before {
-            content: "/";
-            margin-right: var(--spacing-2xs);
-          }
-        }
-      }
-    }
-  }
-
-  &--column#{&}--responsive {
-    @include mix.media("screen") {
-      @include mix.dimensions("xs") {
-        flex-flow: row wrap;
-        gap: var(--spacing-lg);
-      }
-    }
-  }
-
-  &--column &__item {
+  &--column {
     flex-flow: column wrap;
   }
 }
diff --git a/src/components/atoms/lists/description-list.stories.tsx b/src/components/atoms/lists/description-list.stories.tsx
index 43ee66e..347fd78 100644
--- a/src/components/atoms/lists/description-list.stories.tsx
+++ b/src/components/atoms/lists/description-list.stories.tsx
@@ -1,16 +1,15 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import DescriptionListComponent, {
-  DescriptionListItem,
-} from './description-list';
+import DescriptionList, { DescriptionListItem } from './description-list';
 
 /**
  * DescriptionList - Storybook Meta
  */
 export default {
-  title: 'Atoms/Typography/Lists',
-  component: DescriptionListComponent,
+  title: 'Atoms/Typography/Lists/DescriptionList',
+  component: DescriptionList,
   args: {
     layout: 'column',
+    withSeparator: false,
   },
   argTypes: {
     className: {
@@ -26,6 +25,19 @@ export default {
         required: false,
       },
     },
+    groupClassName: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the item wrapper.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     items: {
       control: {
         type: null,
@@ -37,6 +49,19 @@ export default {
         value: {},
       },
     },
+    labelClassName: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the label wrapper.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     layout: {
       control: {
         type: 'select',
@@ -52,28 +77,55 @@ export default {
         required: false,
       },
     },
+    valueClassName: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the value wrapper.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
+    withSeparator: {
+      control: {
+        type: 'boolean',
+      },
+      description: 'Add a slash as separator between multiple values.',
+      table: {
+        category: 'Options',
+        defaultValue: { summary: false },
+      },
+      type: {
+        name: 'boolean',
+        required: false,
+      },
+    },
   },
-} as ComponentMeta<typeof DescriptionListComponent>;
+} as ComponentMeta<typeof DescriptionList>;
 
-const Template: ComponentStory<typeof DescriptionListComponent> = (args) => (
-  <DescriptionListComponent {...args} />
+const Template: ComponentStory<typeof DescriptionList> = (args) => (
+  <DescriptionList {...args} />
 );
 
 const items: DescriptionListItem[] = [
-  { id: 'term-1', term: 'Term 1:', value: ['Value for term 1'] },
-  { id: 'term-2', term: 'Term 2:', value: ['Value for term 2'] },
+  { id: 'term-1', label: 'Term 1:', value: ['Value for term 1'] },
+  { id: 'term-2', label: 'Term 2:', value: ['Value for term 2'] },
   {
     id: 'term-3',
-    term: 'Term 3:',
+    label: 'Term 3:',
     value: ['Value 1 for term 3', 'Value 2 for term 3', 'Value 3 for term 3'],
   },
-  { id: 'term-4', term: 'Term 4:', value: ['Value for term 4'] },
+  { id: 'term-4', label: 'Term 4:', value: ['Value for term 4'] },
 ];
 
 /**
  * List Stories - Description list
  */
-export const DescriptionList = Template.bind({});
-DescriptionList.args = {
+export const List = Template.bind({});
+List.args = {
   items,
 };
diff --git a/src/components/atoms/lists/description-list.test.tsx b/src/components/atoms/lists/description-list.test.tsx
index d3f7045..83e405f 100644
--- a/src/components/atoms/lists/description-list.test.tsx
+++ b/src/components/atoms/lists/description-list.test.tsx
@@ -2,14 +2,14 @@ import { render } from '@test-utils';
 import DescriptionList, { DescriptionListItem } from './description-list';
 
 const items: DescriptionListItem[] = [
-  { id: 'term-1', term: 'Term 1:', value: ['Value for term 1'] },
-  { id: 'term-2', term: 'Term 2:', value: ['Value for term 2'] },
+  { id: 'term-1', label: 'Term 1:', value: ['Value for term 1'] },
+  { id: 'term-2', label: 'Term 2:', value: ['Value for term 2'] },
   {
     id: 'term-3',
-    term: 'Term 3:',
+    label: 'Term 3:',
     value: ['Value 1 for term 3', 'Value 2 for term 3', 'Value 3 for term 3'],
   },
-  { id: 'term-4', term: 'Term 4:', value: ['Value for term 4'] },
+  { id: 'term-4', label: 'Term 4:', value: ['Value for term 4'] },
 ];
 
 describe('DescriptionList', () => {
diff --git a/src/components/atoms/lists/description-list.tsx b/src/components/atoms/lists/description-list.tsx
index a60a6a1..a8e2d53 100644
--- a/src/components/atoms/lists/description-list.tsx
+++ b/src/components/atoms/lists/description-list.tsx
@@ -1,4 +1,7 @@
 import { FC } from 'react';
+import DescriptionListItem, {
+  type DescriptionListItemProps,
+} from './description-list-item';
 import styles from './description-list.module.scss';
 
 export type DescriptionListItem = {
@@ -7,13 +10,17 @@ export type DescriptionListItem = {
    */
   id: string;
   /**
-   * A list term.
+   * The list item layout.
    */
-  term: string;
+  layout?: DescriptionListItemProps['layout'];
   /**
-   * An array of values for the list term.
+   * A list label.
    */
-  value: any[];
+  label: DescriptionListItemProps['label'];
+  /**
+   * An array of values for the list item.
+   */
+  value: DescriptionListItemProps['value'];
 };
 
 export type DescriptionListProps = {
@@ -21,10 +28,6 @@ export type DescriptionListProps = {
    * Set additional classnames to the list wrapper.
    */
   className?: string;
-  /**
-   * Set additional classnames to the `dd` element.
-   */
-  descriptionClassName?: string;
   /**
    * Set additional classnames to the `dt`/`dd` couple wrapper.
    */
@@ -34,17 +37,21 @@ export type DescriptionListProps = {
    */
   items: DescriptionListItem[];
   /**
-   * The list items layout. Default: column.
+   * Set additional classnames to the `dt` element.
+   */
+  labelClassName?: string;
+  /**
+   * The list layout. Default: column.
    */
   layout?: 'inline' | 'column';
   /**
-   * Define if the layout should automatically create rows/columns.
+   * Set additional classnames to the `dd` element.
    */
-  responsiveLayout?: boolean;
+  valueClassName?: string;
   /**
-   * Set additional classnames to the `dt` element.
+   * If true, use a slash to delimitate multiple values.
    */
-  termClassName?: string;
+  withSeparator?: DescriptionListItemProps['withSeparator'];
 };
 
 /**
@@ -54,44 +61,40 @@ export type DescriptionListProps = {
  */
 const DescriptionList: FC<DescriptionListProps> = ({
   className = '',
-  descriptionClassName = '',
   groupClassName = '',
   items,
+  labelClassName = '',
   layout = 'column',
-  responsiveLayout = false,
-  termClassName = '',
+  valueClassName = '',
+  withSeparator,
 }) => {
   const layoutModifier = `list--${layout}`;
-  const responsiveModifier = responsiveLayout ? 'list--responsive' : '';
 
   /**
-   * Retrieve the description list items wrapped in a div element.
+   * Retrieve the description list items.
    *
-   * @param {DescriptionListItem[]} listItems - An array of term and description couples.
+   * @param {DescriptionListItem[]} listItems - An array of items.
    * @returns {JSX.Element[]} The description list items.
    */
   const getItems = (listItems: DescriptionListItem[]): JSX.Element[] => {
-    return listItems.map(({ id, term, value }) => {
+    return listItems.map(({ id, layout: itemLayout, label, value }) => {
       return (
-        <div key={id} className={`${styles.list__item} ${groupClassName}`}>
-          <dt className={`${styles.list__term} ${termClassName}`}>{term}</dt>
-          {value.map((currentValue, index) => (
-            <dd
-              key={`${id}-${index}`}
-              className={`${styles.list__description} ${descriptionClassName}`}
-            >
-              {currentValue}
-            </dd>
-          ))}
-        </div>
+        <DescriptionListItem
+          key={id}
+          label={label}
+          value={value}
+          layout={itemLayout}
+          className={groupClassName}
+          descriptionClassName={valueClassName}
+          termClassName={labelClassName}
+          withSeparator={withSeparator}
+        />
       );
     });
   };
 
   return (
-    <dl
-      className={`${styles.list} ${styles[layoutModifier]} ${styles[responsiveModifier]} ${className}`}
-    >
+    <dl className={`${styles.list} ${styles[layoutModifier]} ${className}`}>
       {getItems(items)}
     </dl>
   );
diff --git a/src/components/molecules/layout/card.module.scss b/src/components/molecules/layout/card.module.scss
index d5b9836..3af8f24 100644
--- a/src/components/molecules/layout/card.module.scss
+++ b/src/components/molecules/layout/card.module.scss
@@ -52,24 +52,35 @@
     margin-bottom: var(--spacing-md);
   }
 
-  .items {
-    flex-flow: row wrap;
-    place-content: center;
-    gap: var(--spacing-2xs);
-  }
+  .meta {
+    &__item {
+      flex-flow: row wrap;
+      place-content: center;
+      gap: var(--spacing-2xs);
+      margin: auto;
+    }
+
+    &__label {
+      flex: 0 0 100%;
+    }
 
-  .term {
-    flex: 0 0 100%;
+    &__value {
+      padding: fun.convert-px(2) var(--spacing-xs);
+      border: fun.convert-px(1) solid var(--color-primary-darker);
+      color: var(--color-fg);
+      font-weight: 400;
+
+      &::before {
+        display: none;
+      }
+    }
   }
 
-  .description {
-    padding: fun.convert-px(2) var(--spacing-xs);
-    border: fun.convert-px(1) solid var(--color-primary-darker);
-    color: var(--color-fg);
-    font-weight: 400;
+  &:not(:disabled):focus {
+    text-decoration: none;
 
-    &::before {
-      display: none;
+    .title {
+      text-decoration: underline solid var(--color-primary) 0.3ex;
     }
   }
 }
diff --git a/src/components/molecules/layout/card.stories.tsx b/src/components/molecules/layout/card.stories.tsx
index ed78d00..2e99bbb 100644
--- a/src/components/molecules/layout/card.stories.tsx
+++ b/src/components/molecules/layout/card.stories.tsx
@@ -8,6 +8,19 @@ export default {
   title: 'Molecules/Layout/Card',
   component: Card,
   argTypes: {
+    className: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the card wrapper.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     cover: {
       description: 'The card cover data (src, dimensions, alternative text).',
       table: {
@@ -19,6 +32,21 @@ export default {
         value: {},
       },
     },
+    coverFit: {
+      control: {
+        type: 'select',
+      },
+      description: 'The cover fit.',
+      options: ['contain', 'cover', 'fill', 'scale-down'],
+      table: {
+        category: 'Options',
+        defaultValue: { summary: 'cover' },
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     meta: {
       description: 'The card metadata (a publication date for example).',
       table: {
@@ -88,13 +116,9 @@ const cover = {
   unoptimized: true,
 };
 
-const meta = [
-  {
-    id: 'an-id',
-    term: 'Voluptates',
-    value: ['Autem', 'Eos'],
-  },
-];
+const meta = {
+  thematics: ['Autem', 'Eos'],
+};
 
 /**
  * Card Stories - Default
diff --git a/src/components/molecules/layout/card.test.tsx b/src/components/molecules/layout/card.test.tsx
index 404bc7a..07c01e9 100644
--- a/src/components/molecules/layout/card.test.tsx
+++ b/src/components/molecules/layout/card.test.tsx
@@ -8,13 +8,10 @@ const cover = {
   width: 640,
 };
 
-const meta = [
-  {
-    id: 'an-id',
-    term: 'Voluptates',
-    value: ['Autem', 'Eos'],
-  },
-];
+const meta = {
+  author: 'Possimus',
+  thematics: ['Autem', 'Eos'],
+};
 
 const tagline = 'Ut rerum incidunt';
 
@@ -47,6 +44,6 @@ describe('Card', () => {
 
   it('renders some meta', () => {
     render(<Card title={title} titleLevel={2} url={url} meta={meta} />);
-    expect(screen.getByText(meta[0].term)).toBeInTheDocument();
+    expect(screen.getByText(meta.author)).toBeInTheDocument();
   });
 });
diff --git a/src/components/molecules/layout/card.tsx b/src/components/molecules/layout/card.tsx
index 89f100e..e416bd5 100644
--- a/src/components/molecules/layout/card.tsx
+++ b/src/components/molecules/layout/card.tsx
@@ -1,13 +1,11 @@
 import ButtonLink from '@components/atoms/buttons/button-link';
 import Heading, { type HeadingLevel } from '@components/atoms/headings/heading';
-import DescriptionList, {
-  type DescriptionListItem,
-} from '@components/atoms/lists/description-list';
 import { FC } from 'react';
 import ResponsiveImage, {
   type ResponsiveImageProps,
 } from '../images/responsive-image';
 import styles from './card.module.scss';
+import Meta, { type MetaData } from './meta';
 
 export type Cover = {
   /**
@@ -44,7 +42,7 @@ export type CardProps = {
   /**
    * The card meta.
    */
-  meta?: DescriptionListItem[];
+  meta?: MetaData;
   /**
    * The card tagline.
    */
@@ -96,13 +94,13 @@ const Card: FC<CardProps> = ({
         <div className={styles.tagline}>{tagline}</div>
         {meta && (
           <footer className={styles.footer}>
-            <DescriptionList
-              items={meta}
+            <Meta
+              data={meta}
               layout="inline"
               className={styles.list}
-              groupClassName={styles.items}
-              termClassName={styles.term}
-              descriptionClassName={styles.description}
+              groupClassName={styles.meta__item}
+              labelClassName={styles.meta__label}
+              valueClassName={styles.meta__value}
             />
           </footer>
         )}
diff --git a/src/components/molecules/layout/meta.module.scss b/src/components/molecules/layout/meta.module.scss
index 0485545..4194a6e 100644
--- a/src/components/molecules/layout/meta.module.scss
+++ b/src/components/molecules/layout/meta.module.scss
@@ -1,23 +1,5 @@
 @use "@styles/abstracts/mixins" as mix;
 
-.list {
-  display: grid;
-  grid-template-columns: repeat(1, minmax(0, 1fr));
-  gap: var(--spacing-sm);
-
-  @include mix.media("screen") {
-    @include mix.dimensions("2xs") {
-      grid-template-columns: repeat(2, minmax(0, 1fr));
-    }
-
-    @include mix.dimensions("sm") {
-      display: flex;
-      flex-flow: column nowrap;
-      gap: var(--spacing-2xs);
-    }
-  }
-}
-
 .value {
   word-break: break-all;
 }
diff --git a/src/components/molecules/layout/meta.stories.tsx b/src/components/molecules/layout/meta.stories.tsx
index 0323f90..a1755a0 100644
--- a/src/components/molecules/layout/meta.stories.tsx
+++ b/src/components/molecules/layout/meta.stories.tsx
@@ -1,5 +1,5 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import MetaComponent from './meta';
+import MetaComponent, { MetaData } from './meta';
 
 /**
  * Meta - Storybook Meta
@@ -8,25 +8,41 @@ export default {
   title: 'Molecules/Layout',
   component: MetaComponent,
   argTypes: {
-    className: {
+    data: {
+      description: 'The page metadata.',
+      type: {
+        name: 'object',
+        required: true,
+        value: {},
+      },
+    },
+    itemsLayout: {
       control: {
-        type: 'text',
+        type: 'select',
       },
-      description: 'Set additional classnames to the meta wrapper.',
+      description: 'The items layout.',
+      options: ['inline', 'inline-values', 'stacked'],
       table: {
-        category: 'Styles',
+        category: 'Options',
+        defaultValue: { summary: 'inline-values' },
       },
       type: {
         name: 'string',
         required: false,
       },
     },
-    data: {
-      description: 'The page metadata.',
+    withSeparator: {
+      control: {
+        type: 'boolean',
+      },
+      description: 'Add a slash as separator between multiple values.',
+      table: {
+        category: 'Options',
+        defaultValue: { summary: true },
+      },
       type: {
-        name: 'object',
+        name: 'boolean',
         required: true,
-        value: {},
       },
     },
   },
@@ -36,19 +52,16 @@ const Template: ComponentStory<typeof MetaComponent> = (args) => (
   <MetaComponent {...args} />
 );
 
-const data = {
-  publication: { name: 'Published on:', value: 'April 9th 2022' },
-  categories: {
-    name: 'Categories:',
-    value: [
-      <a key="category1" href="#">
-        Category 1
-      </a>,
-      <a key="category2" href="#">
-        Category 2
-      </a>,
-    ],
-  },
+const data: MetaData = {
+  publication: { date: '2022-04-09', time: '01:04:00' },
+  thematics: [
+    <a key="category1" href="#">
+      Category 1
+    </a>,
+    <a key="category2" href="#">
+      Category 2
+    </a>,
+  ],
 };
 
 /**
diff --git a/src/components/molecules/layout/meta.test.tsx b/src/components/molecules/layout/meta.test.tsx
index a738bdb..fe66d97 100644
--- a/src/components/molecules/layout/meta.test.tsx
+++ b/src/components/molecules/layout/meta.test.tsx
@@ -1,8 +1,24 @@
-import { render } from '@test-utils';
+import { render, screen } from '@test-utils';
+import { getFormattedDate } from '@utils/helpers/dates';
 import Meta from './meta';
 
+const data = {
+  publication: { date: '2022-04-09' },
+  thematics: [
+    <a key="category1" href="#">
+      Category 1
+    </a>,
+    <a key="category2" href="#">
+      Category 2
+    </a>,
+  ],
+};
+
 describe('Meta', () => {
-  it('renders a Meta component', () => {
-    render(<Meta data={{}} />);
+  it('format a date string', () => {
+    render(<Meta data={data} />);
+    expect(
+      screen.getByText(getFormattedDate(data.publication.date))
+    ).toBeInTheDocument();
   });
 });
diff --git a/src/components/molecules/layout/meta.tsx b/src/components/molecules/layout/meta.tsx
index d05396e..1401ac4 100644
--- a/src/components/molecules/layout/meta.tsx
+++ b/src/components/molecules/layout/meta.tsx
@@ -1,67 +1,312 @@
+import Link from '@components/atoms/links/link';
 import DescriptionList, {
   type DescriptionListProps,
   type DescriptionListItem,
 } from '@components/atoms/lists/description-list';
+import { getFormattedDate, getFormattedTime } from '@utils/helpers/dates';
 import { FC, ReactNode } from 'react';
-import styles from './meta.module.scss';
+import { useIntl } from 'react-intl';
 
-export type MetaItem = {
+export type CustomMeta = {
+  label: string;
+  value: ReactNode | ReactNode[];
+};
+
+export type MetaDate = {
   /**
-   * The meta name.
+   * A date string. Ex: `2022-04-30`.
    */
-  name: string;
+  date: string;
   /**
-   * The meta value.
+   * A time string. Ex: `10:25:59`.
    */
-  value: ReactNode | ReactNode[];
-};
-
-export type MetaMap = {
-  [key: string]: MetaItem | undefined;
+  time?: string;
+  /**
+   * Wrap the date with a link to the given target.
+   */
+  target?: string;
 };
 
-export type MetaProps = {
+export type MetaData = {
+  /**
+   * The author name.
+   */
+  author?: string;
+  /**
+   * The comments count.
+   */
+  commentsCount?: string | JSX.Element;
+  /**
+   * The creation date.
+   */
+  creation?: MetaDate;
+  /**
+   * A custom label/value metadata.
+   */
+  custom?: CustomMeta;
+  /**
+   * The license name.
+   */
+  license?: string;
+  /**
+   * The popularity.
+   */
+  popularity?: string | JSX.Element;
+  /**
+   * The publication date.
+   */
+  publication?: MetaDate;
+  /**
+   * The estimated reading time.
+   */
+  readingTime?: string | JSX.Element;
+  /**
+   * An array of repositories.
+   */
+  repositories?: string[] | JSX.Element[];
+  /**
+   * An array of technologies.
+   */
+  technologies?: string[];
   /**
-   * Set additional classnames to the meta wrapper.
+   * An array of thematics.
    */
-  className?: DescriptionListProps['className'];
+  thematics?: string[] | JSX.Element[];
+  /**
+   * An array of thematics.
+   */
+  topics?: string[] | JSX.Element[];
+  /**
+   * A total.
+   */
+  total?: string;
+  /**
+   * The update date.
+   */
+  update?: MetaDate;
+};
+
+export type MetaProps = Omit<
+  DescriptionListProps,
+  'items' | 'withSeparator'
+> & {
   /**
    * The meta data.
    */
-  data: MetaMap;
+  data: MetaData;
   /**
-   * The meta layout.
+   * The items layout.
    */
-  layout?: DescriptionListProps['layout'];
+  itemsLayout?: DescriptionListItem['layout'];
   /**
-   * Determine if the layout should be responsive.
+   * If true, use a slash to delimitate multiple values. Default: true.
    */
-  responsiveLayout?: DescriptionListProps['responsiveLayout'];
+  withSeparator?: DescriptionListProps['withSeparator'];
 };
 
 /**
  * Meta component
  *
- * Renders the page metadata.
+ * Renders the given metadata.
  */
-const Meta: FC<MetaProps> = ({ className, data, ...props }) => {
+const Meta: FC<MetaProps> = ({
+  data,
+  itemsLayout = 'inline-values',
+  withSeparator = true,
+  ...props
+}) => {
+  const intl = useIntl();
+
+  /**
+   * Retrieve the item label based on its key.
+   *
+   * @param {keyof MetaData} key - The meta key.
+   * @returns {string} The item label.
+   */
+  const getLabel = (key: keyof MetaData): string => {
+    switch (key) {
+      case 'author':
+        return intl.formatMessage({
+          defaultMessage: 'Written by:',
+          id: 'OI0N37',
+          description: 'Meta: author label',
+        });
+      case 'commentsCount':
+        return intl.formatMessage({
+          defaultMessage: 'Comments:',
+          id: 'jTVIh8',
+          description: 'Meta: comments label',
+        });
+      case 'creation':
+        return intl.formatMessage({
+          defaultMessage: 'Created on:',
+          id: 'b4fdYE',
+          description: 'Meta: creation date label',
+        });
+      case 'license':
+        return intl.formatMessage({
+          defaultMessage: 'License:',
+          id: 'AuGklx',
+          description: 'Meta: license label',
+        });
+      case 'popularity':
+        return intl.formatMessage({
+          defaultMessage: 'Popularity:',
+          id: 'pWTj2W',
+          description: 'Meta: popularity label',
+        });
+      case 'publication':
+        return intl.formatMessage({
+          defaultMessage: 'Published on:',
+          id: 'QGi5uD',
+          description: 'Meta: publication date label',
+        });
+      case 'readingTime':
+        return intl.formatMessage({
+          defaultMessage: 'Reading time:',
+          id: 'EbFvsM',
+          description: 'Meta: reading time label',
+        });
+      case 'repositories':
+        return intl.formatMessage({
+          defaultMessage: 'Repositories:',
+          id: 'DssFG1',
+          description: 'Meta: repositories label',
+        });
+      case 'technologies':
+        return intl.formatMessage({
+          defaultMessage: 'Technologies:',
+          id: 'ADQmDF',
+          description: 'Meta: technologies label',
+        });
+      case 'thematics':
+        return intl.formatMessage({
+          defaultMessage: 'Thematics:',
+          id: 'bz53Us',
+          description: 'Meta: thematics label',
+        });
+      case 'topics':
+        return intl.formatMessage({
+          defaultMessage: 'Topics:',
+          id: 'gJNaBD',
+          description: 'Meta: topics label',
+        });
+      case 'total':
+        return intl.formatMessage({
+          defaultMessage: 'Total:',
+          id: '92zgdp',
+          description: 'Meta: total label',
+        });
+      case 'update':
+        return intl.formatMessage({
+          defaultMessage: 'Updated on:',
+          id: 'tLC7bh',
+          description: 'Meta: update date label',
+        });
+      default:
+        return '';
+    }
+  };
+
+  /**
+   * Retrieve a formatted date (and time).
+   *
+   * @param {MetaDate} dateTime - A date object.
+   * @returns {JSX.Element} The formatted date wrapped in a time element.
+   */
+  const getDate = (dateTime: MetaDate): JSX.Element => {
+    const { date, time, target } = dateTime;
+
+    if (!dateTime.time) {
+      const isoDate = new Date(`${date}`).toISOString();
+      return target ? (
+        <Link href={target}>
+          <time dateTime={isoDate}>{getFormattedDate(dateTime.date)}</time>
+        </Link>
+      ) : (
+        <time dateTime={isoDate}>{getFormattedDate(dateTime.date)}</time>
+      );
+    }
+
+    const isoDateTime = new Date(`${date}T${time}`).toISOString();
+
+    return target ? (
+      <Link href={target}>
+        <time dateTime={isoDateTime}>
+          {intl.formatMessage(
+            {
+              defaultMessage: '{date} at {time}',
+              description: 'Meta: publication date and time',
+              id: 'fcHeyC',
+            },
+            {
+              date: getFormattedDate(dateTime.date),
+              time: getFormattedTime(`${dateTime.date}T${dateTime.time}`),
+            }
+          )}
+        </time>
+      </Link>
+    ) : (
+      <time dateTime={isoDateTime}>
+        {intl.formatMessage(
+          {
+            defaultMessage: '{date} at {time}',
+            description: 'Meta: publication date and time',
+            id: 'fcHeyC',
+          },
+          {
+            date: getFormattedDate(dateTime.date),
+            time: getFormattedTime(`${dateTime.date}T${dateTime.time}`),
+          }
+        )}
+      </time>
+    );
+  };
+
+  /**
+   * Retrieve the formatted item value.
+   *
+   * @param {keyof MetaData} key - The meta key.
+   * @param {ValueOf<MetaData>} value - The meta value.
+   * @returns {string|ReactNode|ReactNode[]} - The formatted value.
+   */
+  const getValue = <T extends keyof MetaData>(
+    key: T,
+    value: MetaData[T]
+  ): string | ReactNode | ReactNode[] => {
+    if (key === 'creation' || key === 'publication' || key === 'update') {
+      return getDate(value as MetaDate);
+    }
+    return value as string | ReactNode | ReactNode[];
+  };
+
   /**
    * Transform the metadata to description list item format.
    *
-   * @param {MetaMap} items - The meta.
+   * @param {MetaData} items - The meta.
    * @returns {DescriptionListItem[]} The formatted description list items.
    */
-  const getItems = (items: MetaMap): DescriptionListItem[] => {
+  const getItems = (items: MetaData): DescriptionListItem[] => {
     const listItems: DescriptionListItem[] = Object.entries(items)
-      .map(([key, item]) => {
-        if (!item) return;
+      .map(([key, value]) => {
+        if (!key || !value) return;
 
-        const { name, value } = item;
+        const metaKey = key as keyof MetaData;
 
         return {
-          id: key,
-          term: name,
-          value: Array.isArray(value) ? value : [value],
+          id: metaKey,
+          label:
+            metaKey === 'custom'
+              ? (value as CustomMeta).label
+              : getLabel(metaKey),
+          layout: itemsLayout,
+          value:
+            metaKey === 'custom'
+              ? (value as CustomMeta).value
+              : getValue(
+                  metaKey,
+                  value as string | string[] | JSX.Element | JSX.Element[]
+                ),
         } as DescriptionListItem;
       })
       .filter((item): item is DescriptionListItem => !!item);
@@ -72,8 +317,7 @@ const Meta: FC<MetaProps> = ({ className, data, ...props }) => {
   return (
     <DescriptionList
       items={getItems(data)}
-      className={`${styles.list} ${className}`}
-      descriptionClassName={styles.value}
+      withSeparator={withSeparator}
       {...props}
     />
   );
diff --git a/src/components/molecules/layout/page-footer.stories.tsx b/src/components/molecules/layout/page-footer.stories.tsx
index da0a3fa..31b7a49 100644
--- a/src/components/molecules/layout/page-footer.stories.tsx
+++ b/src/components/molecules/layout/page-footer.stories.tsx
@@ -1,4 +1,5 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { MetaData } from './meta';
 import PageFooterComponent from './page-footer';
 
 /**
@@ -39,8 +40,15 @@ const Template: ComponentStory<typeof PageFooterComponent> = (args) => (
   <PageFooterComponent {...args} />
 );
 
-const meta = {
-  topics: { name: 'More posts about:', value: <a href="#">Topic name</a> },
+const meta: MetaData = {
+  custom: {
+    label: 'More posts about:',
+    value: [
+      <a key="topic-1" href="#">
+        Topic name
+      </a>,
+    ],
+  },
 };
 
 /**
diff --git a/src/components/molecules/layout/page-footer.tsx b/src/components/molecules/layout/page-footer.tsx
index f522482..e998b1e 100644
--- a/src/components/molecules/layout/page-footer.tsx
+++ b/src/components/molecules/layout/page-footer.tsx
@@ -1,5 +1,5 @@
 import { FC } from 'react';
-import Meta, { type MetaMap } from './meta';
+import Meta, { MetaData } from './meta';
 
 export type PageFooterProps = {
   /**
@@ -9,7 +9,7 @@ export type PageFooterProps = {
   /**
    * The footer metadata.
    */
-  meta?: MetaMap;
+  meta?: MetaData;
 };
 
 /**
diff --git a/src/components/molecules/layout/page-header.stories.tsx b/src/components/molecules/layout/page-header.stories.tsx
index 6054845..d58f8b5 100644
--- a/src/components/molecules/layout/page-header.stories.tsx
+++ b/src/components/molecules/layout/page-header.stories.tsx
@@ -8,6 +8,19 @@ export default {
   title: 'Molecules/Layout/PageHeader',
   component: PageHeader,
   argTypes: {
+    className: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the header element.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     intro: {
       control: {
         type: 'text',
@@ -50,18 +63,15 @@ const Template: ComponentStory<typeof PageHeader> = (args) => (
 );
 
 const meta = {
-  publication: { name: 'Published on:', value: 'April 9th 2022' },
-  categories: {
-    name: 'Categories:',
-    value: [
-      <a key="category1" href="#">
-        Category 1
-      </a>,
-      <a key="category2" href="#">
-        Category 2
-      </a>,
-    ],
-  },
+  publication: { date: '2022-04-09' },
+  thematics: [
+    <a key="category1" href="#">
+      Category 1
+    </a>,
+    <a key="category2" href="#">
+      Category 2
+    </a>,
+  ],
 };
 
 /**
diff --git a/src/components/molecules/layout/page-header.tsx b/src/components/molecules/layout/page-header.tsx
index 1663085..9abe9af 100644
--- a/src/components/molecules/layout/page-header.tsx
+++ b/src/components/molecules/layout/page-header.tsx
@@ -1,7 +1,7 @@
 import Heading from '@components/atoms/headings/heading';
-import styles from './page-header.module.scss';
-import Meta, { type MetaMap } from './meta';
 import { FC } from 'react';
+import Meta, { type MetaData } from './meta';
+import styles from './page-header.module.scss';
 
 export type PageHeaderProps = {
   /**
@@ -15,7 +15,7 @@ export type PageHeaderProps = {
   /**
    * The page metadata.
    */
-  meta?: MetaMap;
+  meta?: MetaData;
   /**
    * The page title.
    */
@@ -33,14 +33,29 @@ const PageHeader: FC<PageHeaderProps> = ({
   meta,
   title,
 }) => {
+  const getIntro = () => {
+    return typeof intro === 'string' ? (
+      <div dangerouslySetInnerHTML={{ __html: intro }} />
+    ) : (
+      <div>{intro}</div>
+    );
+  };
+
   return (
     <header className={`${styles.wrapper} ${className}`}>
       <div className={styles.body}>
         <Heading level={1} className={styles.title} withMargin={false}>
           {title}
         </Heading>
-        {meta && <Meta data={meta} className={styles.meta} layout="inline" />}
-        {intro && <div>{intro}</div>}
+        {meta && (
+          <Meta
+            data={meta}
+            className={styles.meta}
+            layout="column"
+            itemsLayout="inline"
+          />
+        )}
+        {intro && getIntro()}
       </div>
     </header>
   );
diff --git a/src/components/organisms/layout/cards-list.stories.tsx b/src/components/organisms/layout/cards-list.stories.tsx
index fe0ebfd..522d068 100644
--- a/src/components/organisms/layout/cards-list.stories.tsx
+++ b/src/components/organisms/layout/cards-list.stories.tsx
@@ -93,9 +93,7 @@ const items: CardsListItem[] = [
       // @ts-ignore - Needed because of the placeholder image.
       unoptimized: true,
     },
-    meta: [
-      { id: 'meta-1', term: 'Quibusdam', value: ['Velit', 'Ex', 'Alias'] },
-    ],
+    meta: { thematics: ['Velit', 'Ex', 'Alias'] },
     tagline: 'Molestias ut error',
     title: 'Et alias omnis',
     url: '#',
@@ -110,7 +108,7 @@ const items: CardsListItem[] = [
       // @ts-ignore - Needed because of the placeholder image.
       unoptimized: true,
     },
-    meta: [{ id: 'meta-1', term: 'Est', value: ['Voluptas'] }],
+    meta: { thematics: ['Voluptas'] },
     tagline: 'Quod vel accusamus',
     title: 'Laboriosam doloremque mollitia',
     url: '#',
@@ -125,13 +123,9 @@ const items: CardsListItem[] = [
       // @ts-ignore - Needed because of the placeholder image.
       unoptimized: true,
     },
-    meta: [
-      {
-        id: 'meta-1',
-        term: 'Omnis',
-        value: ['Quisquam', 'Quia', 'Sapiente', 'Perspiciatis'],
-      },
-    ],
+    meta: {
+      thematics: ['Quisquam', 'Quia', 'Sapiente', 'Perspiciatis'],
+    },
     tagline: 'Quo error eum',
     title: 'Magni rem nulla',
     url: '#',
diff --git a/src/components/organisms/layout/cards-list.test.tsx b/src/components/organisms/layout/cards-list.test.tsx
index 2df3f59..7d98844 100644
--- a/src/components/organisms/layout/cards-list.test.tsx
+++ b/src/components/organisms/layout/cards-list.test.tsx
@@ -1,7 +1,7 @@
 import { render, screen } from '@test-utils';
-import CardsList from './cards-list';
+import CardsList, { type CardsListItem } from './cards-list';
 
-const items = [
+const items: CardsListItem[] = [
   {
     id: 'card-1',
     cover: {
@@ -9,10 +9,10 @@ const items = [
       src: 'http://placeimg.com/640/480',
       width: 640,
       height: 480,
+      // @ts-ignore - Needed because of the placeholder image.
+      unoptimized: true,
     },
-    meta: [
-      { id: 'meta-1', term: 'Quibusdam', value: ['Velit', 'Ex', 'Alias'] },
-    ],
+    meta: { thematics: ['Velit', 'Ex', 'Alias'] },
     tagline: 'Molestias ut error',
     title: 'Et alias omnis',
     url: '#',
@@ -24,8 +24,10 @@ const items = [
       src: 'http://placeimg.com/640/480',
       width: 640,
       height: 480,
+      // @ts-ignore - Needed because of the placeholder image.
+      unoptimized: true,
     },
-    meta: [{ id: 'meta-1', term: 'Est', value: ['Voluptas'] }],
+    meta: { thematics: ['Voluptas'] },
     tagline: 'Quod vel accusamus',
     title: 'Laboriosam doloremque mollitia',
     url: '#',
@@ -37,14 +39,12 @@ const items = [
       src: 'http://placeimg.com/640/480',
       width: 640,
       height: 480,
+      // @ts-ignore - Needed because of the placeholder image.
+      unoptimized: true,
+    },
+    meta: {
+      thematics: ['Quisquam', 'Quia', 'Sapiente', 'Perspiciatis'],
     },
-    meta: [
-      {
-        id: 'meta-1',
-        term: 'Omnis',
-        value: ['Quisquam', 'Quia', 'Sapiente', 'Perspiciatis'],
-      },
-    ],
     tagline: 'Quo error eum',
     title: 'Magni rem nulla',
     url: '#',
diff --git a/src/components/organisms/layout/comment.module.scss b/src/components/organisms/layout/comment.module.scss
index 54201de..d2b68e1 100644
--- a/src/components/organisms/layout/comment.module.scss
+++ b/src/components/organisms/layout/comment.module.scss
@@ -60,16 +60,20 @@
   }
 
   .date {
-    flex-flow: row wrap;
-    justify-content: center;
     margin: var(--spacing-sm) 0;
     font-size: var(--font-size-sm);
-    text-align: center;
+
+    &__item {
+      justify-content: center;
+    }
 
     @include mix.media("screen") {
       @include mix.dimensions("sm") {
-        justify-content: left;
         margin: 0 0 var(--spacing-sm);
+
+        &__item {
+          justify-content: left;
+        }
       }
     }
   }
diff --git a/src/components/organisms/layout/comment.test.tsx b/src/components/organisms/layout/comment.test.tsx
index 9e537e5..4961722 100644
--- a/src/components/organisms/layout/comment.test.tsx
+++ b/src/components/organisms/layout/comment.test.tsx
@@ -1,5 +1,5 @@
 import { render, screen } from '@test-utils';
-import { getFormattedDate, getFormattedTime } from '@utils/helpers/format';
+import { getFormattedDate, getFormattedTime } from '@utils/helpers/dates';
 import Comment from './comment';
 
 const author = {
diff --git a/src/components/organisms/layout/comment.tsx b/src/components/organisms/layout/comment.tsx
index 6d41c00..248efc2 100644
--- a/src/components/organisms/layout/comment.tsx
+++ b/src/components/organisms/layout/comment.tsx
@@ -1,10 +1,12 @@
 import Button from '@components/atoms/buttons/button';
 import Link from '@components/atoms/links/link';
-import DescriptionList from '@components/atoms/lists/description-list';
-import { getFormattedDate, getFormattedTime } from '@utils/helpers/format';
+import Meta from '@components/molecules/layout/meta';
+import useSettings from '@utils/hooks/use-settings';
 import Image from 'next/image';
+import Script from 'next/script';
 import { FC, useState } from 'react';
 import { useIntl } from 'react-intl';
+import { type Comment as CommentSchema, type WithContext } from 'schema-dts';
 import CommentForm, { type CommentFormProps } from '../forms/comment-form';
 import styles from './comment.module.scss';
 
@@ -41,7 +43,11 @@ export type CommentProps = {
    */
   id: number | string;
   /**
-   * The comment date.
+   * The comment parent id.
+   */
+  parentId?: number | string;
+  /**
+   * The comment date and time separated with a space.
    */
   publication: string;
   /**
@@ -60,15 +66,14 @@ const Comment: FC<CommentProps> = ({
   canReply = true,
   content,
   id,
+  parentId,
   publication,
   saveComment,
   ...props
 }) => {
   const intl = useIntl();
   const [isReplying, setIsReplying] = useState<boolean>(false);
-  const commentDate = getFormattedDate(publication);
-  const commentTime = getFormattedTime(publication);
-  const commentDateTime = new Date(publication).toISOString();
+  const [publicationDate, publicationTime] = publication.split(' ');
 
   const avatarAltText = intl.formatMessage(
     {
@@ -89,37 +94,46 @@ const Comment: FC<CommentProps> = ({
         description: 'Comment: reply button',
         id: 'hzHuCc',
       });
-  const dateLabel = intl.formatMessage({
-    defaultMessage: 'Published on:',
-    description: 'Comment: publication date label',
-    id: 'soj7do',
-  });
-  const dateValue = intl.formatMessage(
-    {
-      defaultMessage: '{date} at {time}',
-      description: 'Comment: publication date and time',
-      id: 'Ld6yMP',
-    },
-    {
-      date: commentDate,
-      time: commentTime,
-    }
-  );
   const formTitle = intl.formatMessage({
     defaultMessage: 'Leave a reply',
     description: 'Comment: comment form title',
     id: '2fD5CI',
   });
 
-  const dateLink = (
-    <Link href={`#comment-${id}`}>
-      <time dateTime={commentDateTime}></time>
-      {dateValue}
-    </Link>
-  );
+  const { website } = useSettings();
+
+  const commentSchema: WithContext<CommentSchema> = {
+    '@context': 'https://schema.org',
+    '@id': `${website.url}/#comment-${id}`,
+    '@type': 'Comment',
+    parentItem: parentId
+      ? { '@id': `${website.url}/#comment-${parentId}` }
+      : undefined,
+    about: { '@type': 'Article', '@id': `${website.url}/#article` },
+    author: {
+      '@type': 'Person',
+      name: author.name,
+      image: author.avatar,
+      url: author.url,
+    },
+    creator: {
+      '@type': 'Person',
+      name: author.name,
+      image: author.avatar,
+      url: author.url,
+    },
+    dateCreated: publication,
+    datePublished: publication,
+    text: content,
+  };
 
   return (
     <>
+      <Script
+        id="schema-comments"
+        type="application/ld+json"
+        dangerouslySetInnerHTML={{ __html: JSON.stringify(commentSchema) }}
+      />
       <article
         id={`comment-${id}`}
         className={`${styles.wrapper} ${styles['wrapper--comment']}`}
@@ -142,11 +156,18 @@ const Comment: FC<CommentProps> = ({
             <span className={styles.author}>{author.name}</span>
           )}
         </header>
-        <DescriptionList
-          items={[{ id: 'comment-date', term: dateLabel, value: [dateLink] }]}
+        <Meta
+          data={{
+            publication: {
+              date: publicationDate,
+              time: publicationTime,
+              target: `#comment-${id}`,
+            },
+          }}
           layout="inline"
+          itemsLayout="inline"
           className={styles.date}
-          groupClassName={styles.meta}
+          groupClassName={styles.date__item}
         />
         <div className={styles.body}>{content}</div>
         <footer className={styles.footer}>
diff --git a/src/components/organisms/layout/overview.module.scss b/src/components/organisms/layout/overview.module.scss
index 4d50ad1..e813625 100644
--- a/src/components/organisms/layout/overview.module.scss
+++ b/src/components/organisms/layout/overview.module.scss
@@ -1,12 +1,44 @@
 @use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
 
 .wrapper {
   padding: var(--spacing-sm) var(--spacing-md);
   border: fun.convert-px(1) solid var(--color-border);
+
+  .meta {
+    display: grid;
+    grid-template-columns: repeat(
+      auto-fit,
+      min(calc(100vw - (var(--spacing-md) * 2)), 23ch)
+    );
+    row-gap: var(--spacing-2xs);
+
+    @include mix.media("screen") {
+      @include mix.dimensions("md") {
+        grid-template-columns: repeat(
+          auto-fit,
+          min(calc(100vw - (var(--spacing-md) * 2)), 20ch)
+        );
+      }
+    }
+
+    &--has-techno {
+      div:last-child {
+        gap: var(--spacing-2xs);
+
+        dd {
+          padding: 0 var(--spacing-2xs);
+          border: fun.convert-px(1) solid var(--color-border-dark);
+        }
+      }
+    }
+  }
 }
 
 .cover {
-  max-height: fun.convert-px(150);
+  width: 100%;
+  height: fun.convert-px(175);
   margin: 0 auto var(--spacing-md);
+  padding: var(--spacing-2xs);
   border: fun.convert-px(1) solid var(--color-border);
 }
diff --git a/src/components/organisms/layout/overview.stories.tsx b/src/components/organisms/layout/overview.stories.tsx
index b18a6b6..1139350 100644
--- a/src/components/organisms/layout/overview.stories.tsx
+++ b/src/components/organisms/layout/overview.stories.tsx
@@ -1,5 +1,5 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import Overview from './overview';
+import Overview, { OverviewMeta } from './overview';
 
 /**
  * Overview - Storybook Meta
@@ -8,8 +8,21 @@ export default {
   title: 'Organisms/Layout/Overview',
   component: Overview,
   argTypes: {
+    className: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the overview wrapper.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     cover: {
-      description: 'The overview cover.',
+      description: 'The overview cover',
       table: {
         category: 'Options',
       },
@@ -20,7 +33,7 @@ export default {
       },
     },
     meta: {
-      description: 'The overview metadata.',
+      description: 'The overview meta.',
       type: {
         name: 'object',
         required: true,
@@ -42,12 +55,9 @@ const cover = {
   unoptimized: true,
 };
 
-const meta = {
-  publication: { name: 'Illo ut odio:', value: 'Sequi et excepturi' },
-  update: {
-    name: 'Perspiciatis vel laudantium:',
-    value: 'Dignissimos ratione veritatis',
-  },
+const meta: OverviewMeta = {
+  creation: { date: '2022-05-09' },
+  license: 'Dignissimos ratione veritatis',
 };
 
 /**
@@ -55,7 +65,6 @@ const meta = {
  */
 export const Default = Template.bind({});
 Default.args = {
-  cover,
   meta,
 };
 
diff --git a/src/components/organisms/layout/overview.test.tsx b/src/components/organisms/layout/overview.test.tsx
index 0738d3f..b40a785 100644
--- a/src/components/organisms/layout/overview.test.tsx
+++ b/src/components/organisms/layout/overview.test.tsx
@@ -1,5 +1,5 @@
 import { render, screen } from '@test-utils';
-import Overview from './overview';
+import Overview, { type OverviewMeta } from './overview';
 
 const cover = {
   alt: 'Incidunt unde quam',
@@ -8,22 +8,19 @@ const cover = {
   width: 640,
 };
 
-const meta = {
-  publication: { name: 'Illo ut odio:', value: 'Sequi et excepturi' },
-  update: {
-    name: 'Perspiciatis vel laudantium:',
-    value: 'Dignissimos ratione veritatis',
-  },
+const data: OverviewMeta = {
+  creation: { date: '2022-05-09' },
+  license: 'Dignissimos ratione veritatis',
 };
 
 describe('Overview', () => {
-  it('renders some meta', () => {
-    render(<Overview meta={meta} />);
-    expect(screen.getByText(meta['publication'].name)).toBeInTheDocument();
+  it('renders some data', () => {
+    render(<Overview meta={data} />);
+    expect(screen.getByText(data.license!)).toBeInTheDocument();
   });
 
   it('renders a cover', () => {
-    render(<Overview meta={meta} cover={cover} />);
+    render(<Overview cover={cover} meta={data} />);
     expect(screen.getByRole('img', { name: cover.alt })).toBeInTheDocument();
   });
 });
diff --git a/src/components/organisms/layout/overview.tsx b/src/components/organisms/layout/overview.tsx
index 42d0431..55ad0b5 100644
--- a/src/components/organisms/layout/overview.tsx
+++ b/src/components/organisms/layout/overview.tsx
@@ -1,13 +1,33 @@
 import ResponsiveImage, {
   type ResponsiveImageProps,
 } from '@components/molecules/images/responsive-image';
-import Meta, { type MetaMap } from '@components/molecules/layout/meta';
+import Meta, { type MetaData } from '@components/molecules/layout/meta';
 import { FC } from 'react';
 import styles from './overview.module.scss';
 
+export type OverviewMeta = Pick<
+  MetaData,
+  | 'creation'
+  | 'license'
+  | 'popularity'
+  | 'repositories'
+  | 'technologies'
+  | 'update'
+>;
+
 export type OverviewProps = {
+  /**
+   * Set additional classnames to the overview wrapper.
+   */
+  className?: string;
+  /**
+   * The overview cover.
+   */
   cover?: Pick<ResponsiveImageProps, 'alt' | 'src' | 'width' | 'height'>;
-  meta: MetaMap;
+  /**
+   * The overview meta.
+   */
+  meta: OverviewMeta;
 };
 
 /**
@@ -15,17 +35,26 @@ export type OverviewProps = {
  *
  * Render an overview.
  */
-const Overview: FC<OverviewProps> = ({ cover, meta }) => {
+const Overview: FC<OverviewProps> = ({ className = '', cover, meta }) => {
+  const { technologies, ...remainingMeta } = meta;
+  const metaModifier = technologies ? styles['meta--has-techno'] : '';
+
   return (
-    <div className={styles.wrapper}>
+    <div className={`${styles.wrapper} ${className}`}>
       {cover && (
         <ResponsiveImage
-          objectFit="cover"
+          layout="responsive"
+          objectFit="contain"
           className={styles.cover}
           {...cover}
         />
       )}
-      <Meta data={meta} layout="column" responsiveLayout={true} />
+      <Meta
+        data={{ ...remainingMeta, technologies }}
+        layout="inline"
+        className={`${styles.meta} ${metaModifier}`}
+        withSeparator={false}
+      />
     </div>
   );
 };
diff --git a/src/components/organisms/layout/posts-list.stories.tsx b/src/components/organisms/layout/posts-list.stories.tsx
index f80e1ca..d97ad03 100644
--- a/src/components/organisms/layout/posts-list.stories.tsx
+++ b/src/components/organisms/layout/posts-list.stories.tsx
@@ -1,5 +1,4 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
 import PostsList, { type Post } from './posts-list';
 
 /**
@@ -51,13 +50,6 @@ export default {
       },
     },
   },
-  decorators: [
-    (Story) => (
-      <IntlProvider locale="en">
-        <Story />
-      </IntlProvider>
-    ),
-  ],
 } as ComponentMeta<typeof PostsList>;
 
 const Template: ComponentStory<typeof PostsList> = (args) => (
@@ -70,23 +62,17 @@ const posts: Post[] = [
       'Esse et voluptas sapiente modi impedit unde et. Ducimus nulla ea impedit sit placeat nihil assumenda. Rem est fugiat amet quo hic. Corrupti fuga quod animi autem dolorem ullam corrupti vel aut.',
     id: 'post-1',
     meta: {
-      publication: {
-        name: 'Published on:',
-        value: '2022-02-26T00:42:02',
-      },
-      readingTime: { name: 'Reading time:', value: '5 minutes' },
-      categories: {
-        name: 'Categories:',
-        value: [
-          <a key="cat-1" href="#">
-            Cat 1
-          </a>,
-          <a key="cat-2" href="#">
-            Cat 2
-          </a>,
-        ],
-      },
-      comments: { name: 'Comments:', value: '1 comment' },
+      publication: { date: '2022-02-26' },
+      readingTime: '5 minutes',
+      thematics: [
+        <a key="cat-1" href="#">
+          Cat 1
+        </a>,
+        <a key="cat-2" href="#">
+          Cat 2
+        </a>,
+      ],
+      commentsCount: '1 comment',
     },
     title: 'Ratione velit fuga',
     url: '#',
@@ -104,20 +90,14 @@ const posts: Post[] = [
       'Illum quae asperiores quod aut necessitatibus itaque excepturi voluptas. Incidunt exercitationem ullam saepe alias consequatur sed. Quam veniam quaerat voluptatum earum quia quisquam fugiat sed perspiciatis. Et velit saepe est recusandae facilis eos eum ipsum.',
     id: 'post-2',
     meta: {
-      publication: {
-        name: 'Published on:',
-        value: '2022-02-20T10:40:00',
-      },
-      readingTime: { name: 'Reading time:', value: '8 minutes' },
-      categories: {
-        name: 'Categories:',
-        value: [
-          <a key="cat-2" href="#">
-            Cat 2
-          </a>,
-        ],
-      },
-      comments: { name: 'Comments:', value: '0 comments' },
+      publication: { date: '2022-02-20' },
+      readingTime: '8 minutes',
+      thematics: [
+        <a key="cat-2" href="#">
+          Cat 2
+        </a>,
+      ],
+      commentsCount: '0 comments',
     },
     title: 'Debitis laudantium laudantium',
     url: '#',
@@ -127,20 +107,14 @@ const posts: Post[] = [
       'Sunt aperiam ut rem impedit dolor id sit. Reprehenderit ipsum iusto fugiat. Quaerat laboriosam magnam facilis. Totam sint aliquam voluptatem in quis laborum sunt eum. Enim aut debitis officiis porro iure quia nihil voluptas ipsum. Praesentium quis necessitatibus cumque quia qui velit quos dolorem.',
     id: 'post-3',
     meta: {
-      publication: {
-        name: 'Published on:',
-        value: '2021-12-20T15:12:02',
-      },
-      readingTime: { name: 'Reading time:', value: '3 minutes' },
-      categories: {
-        name: 'Categories:',
-        value: [
-          <a key="cat-1" href="#">
-            Cat 1
-          </a>,
-        ],
-      },
-      comments: { name: 'Comments:', value: '3 comments' },
+      publication: { date: '2021-12-20' },
+      readingTime: '3 minutes',
+      thematics: [
+        <a key="cat-1" href="#">
+          Cat 1
+        </a>,
+      ],
+      commentsCount: '3 comments',
     },
     title: 'Quaerat ut corporis',
     url: '#',
diff --git a/src/components/organisms/layout/posts-list.test.tsx b/src/components/organisms/layout/posts-list.test.tsx
index aa6dffa..98af1c3 100644
--- a/src/components/organisms/layout/posts-list.test.tsx
+++ b/src/components/organisms/layout/posts-list.test.tsx
@@ -1,29 +1,23 @@
 import { render, screen } from '@test-utils';
-import PostsList from './posts-list';
+import PostsList, { Post } from './posts-list';
 
-const posts = [
+const posts: Post[] = [
   {
     excerpt:
       'Esse et voluptas sapiente modi impedit unde et. Ducimus nulla ea impedit sit placeat nihil assumenda. Rem est fugiat amet quo hic. Corrupti fuga quod animi autem dolorem ullam corrupti vel aut.',
     id: 'post-1',
     meta: {
-      publication: {
-        name: 'Published on:',
-        value: '2022-02-26T00:42:02',
-      },
-      readingTime: { name: 'Reading time:', value: '5 minutes' },
-      categories: {
-        name: 'Categories:',
-        value: [
-          <a key="cat-1" href="#">
-            Cat 1
-          </a>,
-          <a key="cat-2" href="#">
-            Cat 2
-          </a>,
-        ],
-      },
-      comments: { name: 'Comments:', value: '1 comment' },
+      publication: { date: '2022-02-26' },
+      readingTime: '5 minutes',
+      thematics: [
+        <a key="cat-1" href="#">
+          Cat 1
+        </a>,
+        <a key="cat-2" href="#">
+          Cat 2
+        </a>,
+      ],
+      commentsCount: '1 comment',
     },
     title: 'Ratione velit fuga',
     url: '#',
@@ -39,20 +33,14 @@ const posts = [
       'Illum quae asperiores quod aut necessitatibus itaque excepturi voluptas. Incidunt exercitationem ullam saepe alias consequatur sed. Quam veniam quaerat voluptatum earum quia quisquam fugiat sed perspiciatis. Et velit saepe est recusandae facilis eos eum ipsum.',
     id: 'post-2',
     meta: {
-      publication: {
-        name: 'Published on:',
-        value: '2022-02-20T10:40:00',
-      },
-      readingTime: { name: 'Reading time:', value: '8 minutes' },
-      categories: {
-        name: 'Categories:',
-        value: [
-          <a key="cat-2" href="#">
-            Cat 2
-          </a>,
-        ],
-      },
-      comments: { name: 'Comments:', value: '0 comments' },
+      publication: { date: '2022-02-20' },
+      readingTime: '8 minutes',
+      thematics: [
+        <a key="cat-2" href="#">
+          Cat 2
+        </a>,
+      ],
+      commentsCount: '0 comments',
     },
     title: 'Debitis laudantium laudantium',
     url: '#',
@@ -62,20 +50,14 @@ const posts = [
       'Sunt aperiam ut rem impedit dolor id sit. Reprehenderit ipsum iusto fugiat. Quaerat laboriosam magnam facilis. Totam sint aliquam voluptatem in quis laborum sunt eum. Enim aut debitis officiis porro iure quia nihil voluptas ipsum. Praesentium quis necessitatibus cumque quia qui velit quos dolorem.',
     id: 'post-3',
     meta: {
-      publication: {
-        name: 'Published on:',
-        value: '2021-12-20T15:12:02',
-      },
-      readingTime: { name: 'Reading time:', value: '3 minutes' },
-      categories: {
-        name: 'Categories:',
-        value: [
-          <a key="cat-1" href="#">
-            Cat 1
-          </a>,
-        ],
-      },
-      comments: { name: 'Comments:', value: '3 comments' },
+      publication: { date: '2021-12-20' },
+      readingTime: '3 minutes',
+      thematics: [
+        <a key="cat-1" href="#">
+          Cat 1
+        </a>,
+      ],
+      commentsCount: '3 comments',
     },
     title: 'Quaerat ut corporis',
     url: '#',
diff --git a/src/components/organisms/layout/posts-list.tsx b/src/components/organisms/layout/posts-list.tsx
index d67b03a..4855205 100644
--- a/src/components/organisms/layout/posts-list.tsx
+++ b/src/components/organisms/layout/posts-list.tsx
@@ -40,7 +40,7 @@ const sortPostsByYear = (data: Post[]): YearCollection => {
   const yearCollection: YearCollection = {};
 
   data.forEach((post) => {
-    const postYear = new Date(post.meta.publication.value as string)
+    const postYear = new Date(post.meta.publication!.date)
       .getFullYear()
       .toString();
     yearCollection[postYear] = [...(yearCollection[postYear] || []), post];
diff --git a/src/components/organisms/layout/summary.module.scss b/src/components/organisms/layout/summary.module.scss
index 1cdda98..6d19853 100644
--- a/src/components/organisms/layout/summary.module.scss
+++ b/src/components/organisms/layout/summary.module.scss
@@ -83,5 +83,18 @@
 }
 
 .meta {
+  display: grid;
+  grid-template-columns: repeat(
+    auto-fit,
+    min(100vw, calc(50% - var(--spacing-lg)))
+  );
+  margin-top: var(--spacing-lg);
   font-size: var(--font-size-sm);
+
+  @include mix.media("screen") {
+    @include mix.dimensions("sm") {
+      display: flex;
+      margin-top: 0;
+    }
+  }
 }
diff --git a/src/components/organisms/layout/summary.stories.tsx b/src/components/organisms/layout/summary.stories.tsx
index 05be7da..42f1d44 100644
--- a/src/components/organisms/layout/summary.stories.tsx
+++ b/src/components/organisms/layout/summary.stories.tsx
@@ -1,5 +1,4 @@
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
 import Summary from './summary';
 
 /**
@@ -78,13 +77,6 @@ export default {
       },
     },
   },
-  decorators: [
-    (Story) => (
-      <IntlProvider locale="en">
-        <Story />
-      </IntlProvider>
-    ),
-  ],
 } as ComponentMeta<typeof Summary>;
 
 const Template: ComponentStory<typeof Summary> = (args) => (
@@ -100,20 +92,17 @@ const cover = {
 };
 
 const meta = {
-  publication: { name: 'Published on:', value: 'April 11th 2022' },
-  readingTime: { name: 'Reading time:', value: '5 minutes' },
-  categories: {
-    name: 'Categories:',
-    value: [
-      <a key="cat-1" href="#">
-        Cat 1
-      </a>,
-      <a key="cat-2" href="#">
-        Cat 2
-      </a>,
-    ],
-  },
-  comments: { name: 'Comments:', value: '1 comment' },
+  publication: { date: '2022-04-11' },
+  readingTime: '5 minutes',
+  thematics: [
+    <a key="cat-1" href="#">
+      Cat 1
+    </a>,
+    <a key="cat-2" href="#">
+      Cat 2
+    </a>,
+  ],
+  commentsCount: '1 comment',
 };
 
 /**
diff --git a/src/components/organisms/layout/summary.test.tsx b/src/components/organisms/layout/summary.test.tsx
index 4944805..09b797c 100644
--- a/src/components/organisms/layout/summary.test.tsx
+++ b/src/components/organisms/layout/summary.test.tsx
@@ -12,20 +12,17 @@ const excerpt =
   'Perspiciatis quasi libero nemo non eligendi nam minima. Deleniti expedita tempore. Praesentium explicabo molestiae eaque consectetur vero. Quae nostrum quisquam similique. Ut hic est quas ut esse quisquam nobis.';
 
 const meta = {
-  publication: { name: 'Published on:', value: 'April 11th 2022' },
-  readingTime: { name: 'Reading time:', value: '5 minutes' },
-  categories: {
-    name: 'Categories:',
-    value: [
-      <a key="cat-1" href="#">
-        Cat 1
-      </a>,
-      <a key="cat-2" href="#">
-        Cat 2
-      </a>,
-    ],
-  },
-  comments: { name: 'Comments:', value: '1 comment' },
+  publication: { date: '2022-04-11' },
+  readingTime: '5 minutes',
+  thematics: [
+    <a key="cat-1" href="#">
+      Cat 1
+    </a>,
+    <a key="cat-2" href="#">
+      Cat 2
+    </a>,
+  ],
+  commentsCount: '1 comment',
 };
 
 const title = 'Odio odit necessitatibus';
@@ -80,6 +77,6 @@ describe('Summary', () => {
 
   it('renders some meta', () => {
     render(<Summary excerpt={excerpt} meta={meta} title={title} url={url} />);
-    expect(screen.getByText(meta.publication.name)).toBeInTheDocument();
+    expect(screen.getByText(meta.readingTime)).toBeInTheDocument();
   });
 });
diff --git a/src/components/organisms/layout/summary.tsx b/src/components/organisms/layout/summary.tsx
index 28aac68..8b47833 100644
--- a/src/components/organisms/layout/summary.tsx
+++ b/src/components/organisms/layout/summary.tsx
@@ -5,7 +5,7 @@ import Link from '@components/atoms/links/link';
 import ResponsiveImage, {
   type ResponsiveImageProps,
 } from '@components/molecules/images/responsive-image';
-import Meta, { type MetaItem } from '@components/molecules/layout/meta';
+import Meta, { MetaData } from '@components/molecules/layout/meta';
 import { FC, ReactNode } from 'react';
 import { useIntl } from 'react-intl';
 import styles from './summary.module.scss';
@@ -15,24 +15,15 @@ export type Cover = Pick<
   'alt' | 'src' | 'width' | 'height'
 >;
 
-export type RequiredMetaKey = 'publication';
-
-export type RequiredMeta = {
-  [key in RequiredMetaKey]: MetaItem;
-};
-
-export type OptionalMetaKey =
+export type SummaryMeta = Pick<
+  MetaData,
   | 'author'
-  | 'categories'
-  | 'comments'
+  | 'commentsCount'
+  | 'publication'
   | 'readingTime'
-  | 'update';
-
-export type OptionalMeta = {
-  [key in OptionalMetaKey]?: MetaItem;
-};
-
-export type Meta = RequiredMeta & OptionalMeta;
+  | 'thematics'
+  | 'update'
+>;
 
 export type SummaryProps = {
   /**
@@ -46,7 +37,7 @@ export type SummaryProps = {
   /**
    * The post meta.
    */
-  meta: Meta;
+  meta: SummaryMeta;
   /**
    * The post title.
    */
@@ -100,7 +91,11 @@ const Summary: FC<SummaryProps> = ({
         </Link>
       </header>
       <div className={styles.body}>
-        {excerpt}
+        {typeof excerpt === 'string' ? (
+          <div dangerouslySetInnerHTML={{ __html: excerpt }} />
+        ) : (
+          excerpt
+        )}
         <ButtonLink target={url} className={styles['read-more']}>
           <>
             {readMore}
diff --git a/src/components/templates/page/page-layout.stories.tsx b/src/components/templates/page/page-layout.stories.tsx
index 1f72cb0..b4b0d68 100644
--- a/src/components/templates/page/page-layout.stories.tsx
+++ b/src/components/templates/page/page-layout.stories.tsx
@@ -6,7 +6,6 @@ import PostsList from '@components/organisms/layout/posts-list';
 import LinksListWidget from '@components/organisms/widgets/links-list-widget';
 import Sharing from '@components/organisms/widgets/sharing';
 import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
 import PageLayoutComponent from './page-layout';
 
 /**
@@ -157,15 +156,6 @@ export default {
       },
     },
   },
-  decorators: [
-    (Story) => (
-      <IntlProvider locale="en">
-        <div id="__next">
-          <Story />
-        </div>
-      </IntlProvider>
-    ),
-  ],
   parameters: {
     layout: 'fullscreen',
   },
@@ -231,7 +221,6 @@ SinglePage.args = {
         'linkedin',
         'twitter',
       ]}
-      title="Share"
       level={2}
       expanded={true}
     />,
@@ -320,22 +309,19 @@ Post.args = {
   title: pageTitle,
   intro: pageIntro,
   headerMeta: {
-    publication: { name: 'Published on:', value: 'March 14th 2020' },
-    categories: {
-      name: 'Categories:',
-      value: [
-        <Link key="cat1" href="#">
-          Cat 1
-        </Link>,
-        <Link key="cat2" href="#">
-          Cat 2
-        </Link>,
-      ],
-    },
+    publication: { date: '2020-03-14' },
+    thematics: [
+      <Link key="cat1" href="#">
+        Cat 1
+      </Link>,
+      <Link key="cat2" href="#">
+        Cat 2
+      </Link>,
+    ],
   },
   footerMeta: {
-    tags: {
-      name: 'Read more about:',
+    custom: {
+      label: 'Read more about:',
       value: <ButtonLink target="#">Topic 1</ButtonLink>,
     },
   },
@@ -379,7 +365,6 @@ Post.args = {
         'linkedin',
         'twitter',
       ]}
-      title="Share"
       level={2}
       expanded={true}
     />,
@@ -400,23 +385,17 @@ const posts = [
       'Esse et voluptas sapiente modi impedit unde et. Ducimus nulla ea impedit sit placeat nihil assumenda. Rem est fugiat amet quo hic. Corrupti fuga quod animi autem dolorem ullam corrupti vel aut.',
     id: 'post-1',
     meta: {
-      publication: {
-        name: 'Published on:',
-        value: '2022-02-26T00:42:02',
-      },
-      readingTime: { name: 'Reading time:', value: '5 minutes' },
-      categories: {
-        name: 'Categories:',
-        value: [
-          <a key="cat-1" href="#">
-            Cat 1
-          </a>,
-          <a key="cat-2" href="#">
-            Cat 2
-          </a>,
-        ],
-      },
-      comments: { name: 'Comments:', value: '1 comment' },
+      publication: { date: '2022-02-26' },
+      readingTime: '5 minutes',
+      categories: [
+        <a key="cat-1" href="#">
+          Cat 1
+        </a>,
+        <a key="cat-2" href="#">
+          Cat 2
+        </a>,
+      ],
+      commentsCount: '1 comment',
     },
     title: 'Ratione velit fuga',
     url: '#',
@@ -435,19 +414,15 @@ const posts = [
     id: 'post-2',
     meta: {
       publication: {
-        name: 'Published on:',
-        value: '2022-02-20T10:40:00',
+        date: '2022-02-20',
       },
-      readingTime: { name: 'Reading time:', value: '8 minutes' },
-      categories: {
-        name: 'Categories:',
-        value: [
-          <a key="cat-2" href="#">
-            Cat 2
-          </a>,
-        ],
-      },
-      comments: { name: 'Comments:', value: '0 comments' },
+      readingTime: '8 minutes',
+      categories: [
+        <a key="cat-2" href="#">
+          Cat 2
+        </a>,
+      ],
+      comments: '0 comments',
     },
     title: 'Debitis laudantium laudantium',
     url: '#',
@@ -458,19 +433,15 @@ const posts = [
     id: 'post-3',
     meta: {
       publication: {
-        name: 'Published on:',
-        value: '2021-12-20T15:12:02',
-      },
-      readingTime: { name: 'Reading time:', value: '3 minutes' },
-      categories: {
-        name: 'Categories:',
-        value: [
-          <a key="cat-1" href="#">
-            Cat 1
-          </a>,
-        ],
+        date: '2021-12-20',
       },
-      comments: { name: 'Comments:', value: '3 comments' },
+      readingTime: '3 minutes',
+      categories: [
+        <a key="cat-1" href="#">
+          Cat 1
+        </a>,
+      ],
+      comments: '3 comments',
     },
     title: 'Quaerat ut corporis',
     url: '#',
@@ -502,7 +473,7 @@ export const Blog = Template.bind({});
 Blog.args = {
   breadcrumb: postsListBreadcrumb,
   title: 'Blog',
-  headerMeta: { total: { name: 'Total:', value: `${posts.length} posts` } },
+  headerMeta: { total: `${posts.length} posts` },
   children: (
     <>
       <PostsList posts={posts} byYear={true} />
diff --git a/src/components/templates/page/page-layout.tsx b/src/components/templates/page/page-layout.tsx
index 24c4e50..ac021ba 100644
--- a/src/components/templates/page/page-layout.tsx
+++ b/src/components/templates/page/page-layout.tsx
@@ -133,9 +133,17 @@ const PageLayout: FC<PageLayoutProps> = ({
           )}
         </Sidebar>
       )}
-      <div ref={bodyRef} className={styles.body}>
-        {children}
-      </div>
+      {typeof children === 'string' ? (
+        <div
+          ref={bodyRef}
+          className={styles.body}
+          dangerouslySetInnerHTML={{ __html: children }}
+        />
+      ) : (
+        <div ref={bodyRef} className={styles.body}>
+          {children}
+        </div>
+      )}
       <PageFooter meta={footerMeta} className={styles.footer} />
       <Sidebar className={`${styles.sidebar} ${styles['sidebar--last']}`}>
         {widgets}
diff --git a/src/pages/contact.tsx b/src/pages/contact.tsx
index d88a8a1..fcfbe1d 100644
--- a/src/pages/contact.tsx
+++ b/src/pages/contact.tsx
@@ -41,7 +41,7 @@ const ContactPage: NextPage = () => {
   const { asPath } = useRouter();
   const pageUrl = `${website.url}${asPath}`;
   const pagePublicationDate = new Date(dates.publication);
-  const pageUpdateDate = new Date(dates.update);
+  const pageUpdateDate = dates.update ? new Date(dates.update) : undefined;
 
   const webpageSchema: WebPage = {
     '@id': `${pageUrl}`,
@@ -64,7 +64,7 @@ const ContactPage: NextPage = () => {
     author: { '@id': `${website.url}/#branding` },
     creator: { '@id': `${website.url}/#branding` },
     dateCreated: pagePublicationDate.toISOString(),
-    dateModified: pageUpdateDate.toISOString(),
+    dateModified: pageUpdateDate && pageUpdateDate.toISOString(),
     datePublished: pagePublicationDate.toISOString(),
     editor: { '@id': `${website.url}/#branding` },
     inLanguage: website.locales.default,
diff --git a/src/pages/cv.tsx b/src/pages/cv.tsx
index d47edc6..0e4765e 100644
--- a/src/pages/cv.tsx
+++ b/src/pages/cv.tsx
@@ -7,7 +7,6 @@ import PageLayout, {
 } from '@components/templates/page/page-layout';
 import CVContent, { data, meta } from '@content/pages/cv.mdx';
 import styles from '@styles/pages/cv.module.scss';
-import { getFormattedDate } from '@utils/helpers/dates';
 import { loadTranslation } from '@utils/helpers/i18n';
 import useSettings from '@utils/hooks/use-settings';
 import { GetStaticProps, NextPage } from 'next';
@@ -46,24 +45,15 @@ const CVPage: NextPage = () => {
     id: '+Dre5J',
   });
 
-  const publicationLabel = intl.formatMessage({
-    defaultMessage: 'Published on:',
-    description: 'Meta: publication date label',
-    id: 'QGi5uD',
-  });
-
-  const updateLabel = intl.formatMessage({
-    defaultMessage: 'Updated on:',
-    description: 'Meta: update date label',
-    id: 'tLC7bh',
-  });
-
   const headerMeta: PageLayoutProps['headerMeta'] = {
     publication: {
-      name: publicationLabel,
-      value: getFormattedDate(dates.publication),
+      date: dates.publication,
     },
-    update: { name: updateLabel, value: getFormattedDate(dates.update) },
+    update: dates.update
+      ? {
+          date: dates.update,
+        }
+      : undefined,
   };
 
   const { website } = useSettings();
@@ -118,7 +108,7 @@ const CVPage: NextPage = () => {
   const { asPath } = useRouter();
   const pageUrl = `${website.url}${asPath}`;
   const pagePublicationDate = new Date(dates.publication);
-  const pageUpdateDate = new Date(dates.update);
+  const pageUpdateDate = dates.update ? new Date(dates.update) : undefined;
 
   const webpageSchema: WebPage = {
     '@id': `${pageUrl}`,
@@ -141,7 +131,7 @@ const CVPage: NextPage = () => {
     author: { '@id': `${website.url}/#branding` },
     creator: { '@id': `${website.url}/#branding` },
     dateCreated: pagePublicationDate.toISOString(),
-    dateModified: pageUpdateDate.toISOString(),
+    dateModified: pageUpdateDate && pageUpdateDate.toISOString(),
     datePublished: pagePublicationDate.toISOString(),
     editor: { '@id': `${website.url}/#branding` },
     image: image.src,
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index c965320..6108d2f 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -225,17 +225,7 @@ const HomePage: NextPage<HomeProps> = ({ recentPosts }) => {
       return {
         cover: post.cover,
         id: post.slug,
-        meta: [
-          {
-            id: 'publication',
-            term: intl.formatMessage({
-              defaultMessage: 'Published on:',
-              description: 'HomePage: publication date label',
-              id: 'pT5nHk',
-            }),
-            value: [post.dates.publication],
-          },
-        ],
+        meta: { publication: { date: post.dates.publication } },
         title: post.title,
         url: `/article/${post.slug}`,
       };
diff --git a/src/pages/mentions-legales.tsx b/src/pages/mentions-legales.tsx
index 8dd0a1d..9235e69 100644
--- a/src/pages/mentions-legales.tsx
+++ b/src/pages/mentions-legales.tsx
@@ -5,7 +5,6 @@ import PageLayout, {
   type PageLayoutProps,
 } from '@components/templates/page/page-layout';
 import LegalNoticeContent, { meta } from '@content/pages/legal-notice.mdx';
-import { getFormattedDate } from '@utils/helpers/dates';
 import { loadTranslation } from '@utils/helpers/i18n';
 import useSettings from '@utils/hooks/use-settings';
 import { NestedMDXComponents } from 'mdx/types';
@@ -32,24 +31,15 @@ const LegalNoticePage: NextPage = () => {
     { id: 'legal-notice', name: title, url: '/mentions-legales' },
   ];
 
-  const publicationLabel = intl.formatMessage({
-    defaultMessage: 'Published on:',
-    description: 'Meta: publication date label',
-    id: 'QGi5uD',
-  });
-
-  const updateLabel = intl.formatMessage({
-    defaultMessage: 'Updated on:',
-    description: 'Meta: update date label',
-    id: 'tLC7bh',
-  });
-
   const headerMeta: PageLayoutProps['headerMeta'] = {
     publication: {
-      name: publicationLabel,
-      value: getFormattedDate(dates.publication),
+      date: dates.publication,
     },
-    update: { name: updateLabel, value: getFormattedDate(dates.update) },
+    update: dates.update
+      ? {
+          date: dates.update,
+        }
+      : undefined,
   };
 
   const components: NestedMDXComponents = {
@@ -61,7 +51,7 @@ const LegalNoticePage: NextPage = () => {
   const { asPath } = useRouter();
   const pageUrl = `${website.url}${asPath}`;
   const pagePublicationDate = new Date(dates.publication);
-  const pageUpdateDate = new Date(dates.update);
+  const pageUpdateDate = dates.update ? new Date(dates.update) : undefined;
 
   const webpageSchema: WebPage = {
     '@id': `${pageUrl}`,
@@ -87,7 +77,7 @@ const LegalNoticePage: NextPage = () => {
     copyrightYear: pagePublicationDate.getFullYear(),
     creator: { '@id': `${website.url}/#branding` },
     dateCreated: pagePublicationDate.toISOString(),
-    dateModified: pageUpdateDate.toISOString(),
+    dateModified: pageUpdateDate && pageUpdateDate.toISOString(),
     datePublished: pagePublicationDate.toISOString(),
     editor: { '@id': `${website.url}/#branding` },
     headline: title,
diff --git a/src/utils/helpers/dates.ts b/src/utils/helpers/dates.ts
index fa167a7..cb56ad2 100644
--- a/src/utils/helpers/dates.ts
+++ b/src/utils/helpers/dates.ts
@@ -1,4 +1,3 @@
-import { Dates } from '@ts/types/app';
 import { settings } from '@utils/config';
 
 /**
@@ -39,17 +38,3 @@ export const getFormattedTime = (
 
   return locale === 'fr' ? formattedTime.replace(':', 'h') : formattedTime;
 };
-
-/**
- * Retrieve a Dates object.
- *
- * @param publication - The publication date.
- * @param update - The update date.
- * @returns {Dates} A Dates object.
- */
-export const getDates = (publication: string, update: string): Dates => {
-  return {
-    publication: getFormattedDate(publication),
-    update: getFormattedDate(update),
-  };
-};
-- 
cgit v1.2.3

