From dfd816d1891545aa8ead982b57891858f1c82bb4 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Wed, 6 Apr 2022 16:24:05 +0200
Subject: chore: add a ResponsiveImage component

---
 .../molecules/images/responsive-image.tsx          | 68 ++++++++++++++++++++++
 1 file changed, 68 insertions(+)
 create mode 100644 src/components/molecules/images/responsive-image.tsx

(limited to 'src/components/molecules/images/responsive-image.tsx')

diff --git a/src/components/molecules/images/responsive-image.tsx b/src/components/molecules/images/responsive-image.tsx
new file mode 100644
index 0000000..db2f5ab
--- /dev/null
+++ b/src/components/molecules/images/responsive-image.tsx
@@ -0,0 +1,68 @@
+import Link from '@components/atoms/links/link';
+import Image, { ImageProps } from 'next/image';
+import { FC } from 'react';
+import styles from './responsive-image.module.scss';
+
+type ResponsiveImageProps = Omit<ImageProps, 'alt' | 'width' | 'height'> & {
+  /**
+   * An alternative text.
+   */
+  alt: string;
+  /**
+   * A figure caption.
+   */
+  caption?: string;
+  /**
+   * The image height.
+   */
+  height: number | string;
+  /**
+   * A link target.
+   */
+  target?: string;
+  /**
+   * The image width.
+   */
+  width: number | string;
+};
+
+/**
+ * ResponsiveImage component
+ *
+ * Render a responsive image wrapped in a figure element.
+ */
+const ResponsiveImage: FC<ResponsiveImageProps> = ({
+  alt,
+  caption,
+  layout,
+  objectFit,
+  target,
+  ...props
+}) => {
+  return (
+    <figure className={styles.wrapper}>
+      {target ? (
+        <Link href={target} classes={styles.link}>
+          <Image alt={alt} layout={layout || 'intrinsic'} {...props} />
+          {caption && (
+            <figcaption className={styles.caption}>{caption}</figcaption>
+          )}
+        </Link>
+      ) : (
+        <>
+          <Image
+            alt={alt}
+            layout={layout || 'intrinsic'}
+            objectFit={objectFit || 'contain'}
+            {...props}
+          />
+          {caption && (
+            <figcaption className={styles.caption}>{caption}</figcaption>
+          )}
+        </>
+      )}
+    </figure>
+  );
+};
+
+export default ResponsiveImage;
-- 
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/molecules/images/responsive-image.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 27ff3104aabed240470d351bda02095d8169501f Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Tue, 12 Apr 2022 16:09:21 +0200
Subject: chore: add a Summary component

---
 .../atoms/buttons/button-link.stories.tsx          |  13 +++
 src/components/atoms/buttons/button-link.tsx       |  14 ++-
 .../molecules/images/responsive-image.tsx          |  14 ++-
 .../organisms/layout/summary.module.scss           |  84 +++++++++++++++
 .../organisms/layout/summary.stories.tsx           | 114 +++++++++++++++++++++
 src/components/organisms/layout/summary.test.tsx   |  85 +++++++++++++++
 src/components/organisms/layout/summary.tsx        | 105 +++++++++++++++++++
 7 files changed, 424 insertions(+), 5 deletions(-)
 create mode 100644 src/components/organisms/layout/summary.module.scss
 create mode 100644 src/components/organisms/layout/summary.stories.tsx
 create mode 100644 src/components/organisms/layout/summary.test.tsx
 create mode 100644 src/components/organisms/layout/summary.tsx

(limited to 'src/components/molecules/images/responsive-image.tsx')

diff --git a/src/components/atoms/buttons/button-link.stories.tsx b/src/components/atoms/buttons/button-link.stories.tsx
index 6fe786b..92b7521 100644
--- a/src/components/atoms/buttons/button-link.stories.tsx
+++ b/src/components/atoms/buttons/button-link.stories.tsx
@@ -28,6 +28,19 @@ export default {
         required: true,
       },
     },
+    className: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the button link.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     kind: {
       control: {
         type: 'select',
diff --git a/src/components/atoms/buttons/button-link.tsx b/src/components/atoms/buttons/button-link.tsx
index 81229c8..77a7f7b 100644
--- a/src/components/atoms/buttons/button-link.tsx
+++ b/src/components/atoms/buttons/button-link.tsx
@@ -7,6 +7,10 @@ export type ButtonLinkProps = {
    * ButtonLink accessible label.
    */
   'aria-label'?: string;
+  /**
+   * Set additional classnames to the button link.
+   */
+  className?: string;
   /**
    * True if it is an external link. Default: false.
    */
@@ -18,7 +22,7 @@ export type ButtonLinkProps = {
   /**
    * ButtonLink shape. Default: rectangle.
    */
-  shape?: 'rectangle' | 'square';
+  shape?: 'circle' | 'rectangle' | 'square';
   /**
    * Define an URL as target.
    */
@@ -32,6 +36,7 @@ export type ButtonLinkProps = {
  */
 const ButtonLink: FC<ButtonLinkProps> = ({
   children,
+  className,
   target,
   kind = 'secondary',
   shape = 'rectangle',
@@ -44,14 +49,17 @@ const ButtonLink: FC<ButtonLinkProps> = ({
   return external ? (
     <a
       href={target}
-      className={`${styles.btn} ${kindClass} ${shapeClass}`}
+      className={`${styles.btn} ${kindClass} ${shapeClass} ${className}`}
       {...props}
     >
       {children}
     </a>
   ) : (
     <Link href={target}>
-      <a className={`${styles.btn} ${kindClass} ${shapeClass}`} {...props}>
+      <a
+        className={`${styles.btn} ${kindClass} ${shapeClass} ${className}`}
+        {...props}
+      >
         {children}
       </a>
     </Link>
diff --git a/src/components/molecules/images/responsive-image.tsx b/src/components/molecules/images/responsive-image.tsx
index 9f96f18..3d54e95 100644
--- a/src/components/molecules/images/responsive-image.tsx
+++ b/src/components/molecules/images/responsive-image.tsx
@@ -12,6 +12,10 @@ type ResponsiveImageProps = Omit<ImageProps, 'alt' | 'width' | 'height'> & {
    * A figure caption.
    */
   caption?: string;
+  /**
+   * Set additional classnames to the figure wrapper.
+   */
+  className?: string;
   /**
    * The image height.
    */
@@ -34,16 +38,22 @@ type ResponsiveImageProps = Omit<ImageProps, 'alt' | 'width' | 'height'> & {
 const ResponsiveImage: VFC<ResponsiveImageProps> = ({
   alt,
   caption,
+  className = '',
   layout,
   objectFit,
   target,
   ...props
 }) => {
   return (
-    <figure className={styles.wrapper}>
+    <figure className={`${styles.wrapper} ${className}`}>
       {target ? (
         <Link href={target} className={styles.link}>
-          <Image alt={alt} layout={layout || 'intrinsic'} {...props} />
+          <Image
+            alt={alt}
+            layout={layout || 'intrinsic'}
+            objectFit={objectFit || 'contain'}
+            {...props}
+          />
           {caption && (
             <figcaption className={styles.caption}>{caption}</figcaption>
           )}
diff --git a/src/components/organisms/layout/summary.module.scss b/src/components/organisms/layout/summary.module.scss
new file mode 100644
index 0000000..5da0a18
--- /dev/null
+++ b/src/components/organisms/layout/summary.module.scss
@@ -0,0 +1,84 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+.wrapper {
+  @include mix.media("screen") {
+    @include mix.dimensions("xs") {
+      padding: var(--spacing-sm) var(--spacing-sm) var(--spacing-md);
+      border: fun.convert-px(1) solid var(--color-primary-dark);
+      border-radius: fun.convert-px(3);
+      box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) 0
+          var(--color-shadow),
+        fun.convert-px(3) fun.convert-px(3) fun.convert-px(3) fun.convert-px(-1)
+          var(--color-shadow-light),
+        fun.convert-px(5) fun.convert-px(5) fun.convert-px(7) fun.convert-px(-1)
+          var(--color-shadow-light);
+    }
+
+    @include mix.dimensions("sm") {
+      display: grid;
+      grid-template-columns: minmax(0, 3fr) minmax(0, 1fr);
+      grid-template-rows: repeat(3, max-content);
+      column-gap: var(--spacing-md);
+    }
+  }
+}
+
+.cover {
+  width: auto;
+  max-height: fun.convert-px(100);
+  max-width: 100%;
+  border: fun.convert-px(1) solid var(--color-border);
+
+  @include mix.media("screen") {
+    @include mix.dimensions("sm") {
+      grid-column: 2;
+      grid-row: 1;
+    }
+  }
+}
+
+.header {
+  @include mix.media("screen") {
+    @include mix.dimensions("sm") {
+      grid-column: 1;
+      grid-row: 1;
+      align-self: center;
+    }
+  }
+}
+
+.body {
+  @include mix.media("screen") {
+    @include mix.dimensions("sm") {
+      grid-column: 1;
+      grid-row: 2;
+    }
+  }
+}
+
+.footer {
+  @include mix.media("screen") {
+    @include mix.dimensions("sm") {
+      grid-column: 2;
+      grid-row: 2 / 4;
+    }
+  }
+}
+
+.title {
+  background: none;
+  text-shadow: none;
+}
+
+.read-more {
+  display: flex;
+  flex-flow: row nowrap;
+  column-gap: var(--spacing-xs);
+  width: max-content;
+  margin: var(--spacing-sm) 0;
+}
+
+.meta {
+  font-size: var(--font-size-sm);
+}
diff --git a/src/components/organisms/layout/summary.stories.tsx b/src/components/organisms/layout/summary.stories.tsx
new file mode 100644
index 0000000..5214d70
--- /dev/null
+++ b/src/components/organisms/layout/summary.stories.tsx
@@ -0,0 +1,114 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { IntlProvider } from 'react-intl';
+import SummaryComponent from './summary';
+
+export default {
+  title: 'Organisms/Layout',
+  component: SummaryComponent,
+  args: {
+    titleLevel: 2,
+  },
+  argTypes: {
+    cover: {
+      description: 'The cover data.',
+      table: {
+        category: 'Options',
+      },
+      type: {
+        name: 'object',
+        required: false,
+        value: {},
+      },
+    },
+    excerpt: {
+      control: {
+        type: 'text',
+      },
+      description: 'The page excerpt.',
+      type: {
+        name: 'string',
+        required: true,
+      },
+    },
+    meta: {
+      description: 'The page metadata.',
+      type: {
+        name: 'object',
+        required: true,
+        value: {},
+      },
+    },
+    title: {
+      control: {
+        type: 'text',
+      },
+      description: 'The page title',
+      type: {
+        name: 'string',
+        required: true,
+      },
+    },
+    titleLevel: {
+      control: {
+        type: 'number',
+      },
+      description: 'The page title level (hn)',
+      table: {
+        category: 'Options',
+        defaultValue: { summary: 2 },
+      },
+      type: {
+        name: 'number',
+        required: false,
+      },
+    },
+    url: {
+      control: {
+        type: 'text',
+      },
+      description: 'The page url.',
+      type: {
+        name: 'string',
+        required: true,
+      },
+    },
+  },
+} as ComponentMeta<typeof SummaryComponent>;
+
+const Template: ComponentStory<typeof SummaryComponent> = (args) => (
+  <IntlProvider locale="en">
+    <SummaryComponent {...args} />
+  </IntlProvider>
+);
+
+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' },
+};
+
+export const Summary = Template.bind({});
+Summary.args = {
+  cover: {
+    alt: 'A cover',
+    height: 480,
+    url: 'http://placeimg.com/640/480',
+    width: 640,
+  },
+  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.',
+  meta,
+  title: 'Odio odit necessitatibus',
+  url: '#',
+};
diff --git a/src/components/organisms/layout/summary.test.tsx b/src/components/organisms/layout/summary.test.tsx
new file mode 100644
index 0000000..ce87c0c
--- /dev/null
+++ b/src/components/organisms/layout/summary.test.tsx
@@ -0,0 +1,85 @@
+import { render, screen } from '@test-utils';
+import Summary from './summary';
+
+const cover = {
+  alt: 'A cover',
+  height: 480,
+  url: 'http://placeimg.com/640/480',
+  width: 640,
+};
+
+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' },
+};
+
+const title = 'Odio odit necessitatibus';
+
+const url = '#';
+
+describe('Summary', () => {
+  it('renders a title wrapped in a h2 element', () => {
+    render(
+      <Summary
+        excerpt={excerpt}
+        meta={meta}
+        title={title}
+        titleLevel={2}
+        url={url}
+      />
+    );
+    expect(
+      screen.getByRole('heading', { level: 2, name: title })
+    ).toBeInTheDocument();
+  });
+
+  it('renders an excerpt', () => {
+    render(<Summary excerpt={excerpt} meta={meta} title={title} url={url} />);
+    expect(screen.getByText(excerpt)).toBeInTheDocument();
+  });
+
+  it('renders a cover', () => {
+    render(
+      <Summary
+        cover={cover}
+        excerpt={excerpt}
+        meta={meta}
+        title={title}
+        url={url}
+      />
+    );
+    expect(screen.getByRole('img', { name: cover.alt })).toBeInTheDocument();
+  });
+
+  it('renders a link to the full post', () => {
+    render(<Summary excerpt={excerpt} meta={meta} title={title} url={url} />);
+    expect(screen.getByRole('link', { name: title })).toBeInTheDocument();
+  });
+
+  it('renders a read more link', () => {
+    render(<Summary excerpt={excerpt} meta={meta} title={title} url={url} />);
+    expect(
+      screen.getByRole('link', { name: `Read more about ${title}` })
+    ).toBeInTheDocument();
+  });
+
+  it('renders some meta', () => {
+    render(<Summary excerpt={excerpt} meta={meta} title={title} url={url} />);
+    expect(screen.getByText(meta.publication.name)).toBeInTheDocument();
+  });
+});
diff --git a/src/components/organisms/layout/summary.tsx b/src/components/organisms/layout/summary.tsx
new file mode 100644
index 0000000..3624e5d
--- /dev/null
+++ b/src/components/organisms/layout/summary.tsx
@@ -0,0 +1,105 @@
+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 Meta, { type MetaItem } from '@components/molecules/layout/meta';
+import { VFC } 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 RequiredMetaKey = 'publication';
+
+export type RequiredMeta = {
+  [key in RequiredMetaKey]: MetaItem;
+};
+
+export type OptionalMetaKey =
+  | 'author'
+  | 'categories'
+  | 'comments'
+  | 'readingTime'
+  | 'update';
+
+export type OptionalMeta = {
+  [key in OptionalMetaKey]?: MetaItem;
+};
+
+export type Meta = RequiredMeta & OptionalMeta;
+
+export type SummaryProps = {
+  cover?: Cover;
+  excerpt: string;
+  meta: Meta;
+  title: string;
+  titleLevel?: HeadingLevel;
+  url: string;
+};
+
+/**
+ * Summary component
+ *
+ * Render a page summary.
+ */
+const Summary: VFC<SummaryProps> = ({
+  cover,
+  excerpt,
+  meta,
+  title,
+  titleLevel = 2,
+  url,
+}) => {
+  const intl = useIntl();
+
+  return (
+    <article className={styles.wrapper}>
+      {cover && (
+        <ResponsiveImage
+          alt={cover.alt}
+          src={cover.url}
+          width={cover.width}
+          height={cover.height}
+          className={styles.cover}
+        />
+      )}
+      <header className={styles.header}>
+        <Link href={url}>
+          <Heading level={titleLevel} className={styles.title}>
+            {title}
+          </Heading>
+        </Link>
+      </header>
+      <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" />
+        </ButtonLink>
+      </div>
+      <footer className={styles.footer}>
+        <Meta data={meta} layout="column" className={styles.meta} />
+      </footer>
+    </article>
+  );
+};
+
+export default Summary;
-- 
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/molecules/images/responsive-image.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/molecules/images/responsive-image.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 ee04742d1f0645908baa30e47845126c28848f50 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Tue, 3 May 2022 16:53:55 +0200
Subject: chore: add a CV page

---
 src/components/atoms/links/link.module.scss        |  87 ++++++++-
 src/components/atoms/links/link.stories.tsx        |  20 ++-
 src/components/atoms/links/link.tsx                |  18 +-
 .../molecules/images/responsive-image.module.scss  |  11 +-
 .../molecules/images/responsive-image.stories.tsx  |   1 +
 .../molecules/images/responsive-image.tsx          |   6 +-
 .../organisms/widgets/image-widget.module.scss     |  13 +-
 .../organisms/widgets/image-widget.stories.tsx     |  25 ++-
 .../organisms/widgets/image-widget.test.tsx        |  11 +-
 src/components/organisms/widgets/image-widget.tsx  |  18 +-
 src/pages/cv.tsx                                   | 198 +++++++++++++++++++++
 src/styles/pages/cv.module.scss                    |   3 +
 12 files changed, 375 insertions(+), 36 deletions(-)
 create mode 100644 src/pages/cv.tsx
 create mode 100644 src/styles/pages/cv.module.scss

(limited to 'src/components/molecules/images/responsive-image.tsx')

diff --git a/src/components/atoms/links/link.module.scss b/src/components/atoms/links/link.module.scss
index d23667a..1b89727 100644
--- a/src/components/atoms/links/link.module.scss
+++ b/src/components/atoms/links/link.module.scss
@@ -5,23 +5,64 @@
   &[hreflang] {
     &::after {
       display: inline-block;
+
       /* Prettier is removing spacing between content parts. */
+
       /* prettier-ignore */
       content: "\0000a0[" attr(hreflang) "]";
       font-size: var(--font-size-sm);
     }
   }
 
+  &--download {
+    &::after {
+      display: inline-block;
+
+      /* 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="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'
+));
+    }
+
+    &:focus:not(:active)::after {
+      /* 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="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_white}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'
+));
+    }
+
+    &[hreflang] {
+      &::after {
+        /* 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="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'
+));
+      }
+    }
+  }
+
   &--external {
     &::after {
       display: inline-block;
+
       /* 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 {
       /* 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>'));
     }
@@ -29,18 +70,56 @@
     &[hreflang] {
       &::after {
         /* Prettier is removing spacing between content parts. */
+
         /* prettier-ignore */
-        content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg(
+        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 {
         /* Prettier is removing spacing between content parts. */
+
         /* prettier-ignore */
-        content: "\0000a0[" attr(hreflang) "]\0000a0" url(fun.encode-svg(
+        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>'
-          ));
+));
+      }
+    }
+  }
+
+  &--external#{&}--download {
+    &::after {
+      /* 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="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'
+)) "\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>'));
+    }
+
+    &[hreflang] {
+      &::after {
+        /* 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="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'
+)) "\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 {
+        /* 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="m49 80.048-28.445-30.77 19.32 4.095V5.06h18.252v48.313l21.318-4.095z"/><path fill="#{var.$light-theme_blue}" d="M0 67.57v27.37h100V67.57H87.973v15.344H12.027V67.569z"/></svg>'
+)) "\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.stories.tsx b/src/components/atoms/links/link.stories.tsx
index c3b3465..420eafe 100644
--- a/src/components/atoms/links/link.stories.tsx
+++ b/src/components/atoms/links/link.stories.tsx
@@ -31,14 +31,29 @@ export default {
         required: false,
       },
     },
-    external: {
+    download: {
       control: {
         type: 'boolean',
       },
+      description: 'Determine if the link purpose is to download a file.',
       table: {
         category: 'Options',
+        defaultValue: { summary: false },
+      },
+      type: {
+        name: 'boolean',
+        required: false,
+      },
+    },
+    external: {
+      control: {
+        type: 'boolean',
       },
       description: 'Determine if the link is external of the current website.',
+      table: {
+        category: 'Options',
+        defaultValue: { summary: false },
+      },
       type: {
         name: 'boolean',
         required: false,
@@ -79,6 +94,7 @@ export const Default = Template.bind({});
 Default.args = {
   children: 'A link',
   href: '#',
+  download: false,
   external: false,
 };
 
@@ -89,6 +105,7 @@ export const External = Template.bind({});
 External.args = {
   children: 'A link',
   href: '#',
+  download: false,
   external: true,
 };
 
@@ -99,6 +116,7 @@ export const ExternalWithLang = Template.bind({});
 ExternalWithLang.args = {
   children: 'A link',
   href: '#',
+  download: false,
   external: true,
   lang: 'en',
 };
diff --git a/src/components/atoms/links/link.tsx b/src/components/atoms/links/link.tsx
index 674c07b..c8ba273 100644
--- a/src/components/atoms/links/link.tsx
+++ b/src/components/atoms/links/link.tsx
@@ -11,6 +11,10 @@ export type LinkProps = {
    * Set additional classnames to the link.
    */
   className?: string;
+  /**
+   * True if it is a download link. Default: false.
+   */
+  download?: boolean;
   /**
    * True if it is an external link. Default: false.
    */
@@ -33,21 +37,29 @@ export type LinkProps = {
 const Link: FC<LinkProps> = ({
   children,
   className = '',
+  download = false,
+  external = false,
   href,
   lang,
-  external = false,
 }) => {
+  const downloadClass = download ? styles['link--download'] : '';
+
   return external ? (
     <a
       href={href}
       hrefLang={lang}
-      className={`${styles.link} ${styles['link--external']} ${className}`}
+      className={`${styles.link} ${styles['link--external']} ${downloadClass} ${className}`}
     >
       {children}
     </a>
   ) : (
     <NextLink href={href}>
-      <a className={`${styles.link} ${className}`}>{children}</a>
+      <a
+        hrefLang={lang}
+        className={`${styles.link} ${downloadClass} ${className}`}
+      >
+        {children}
+      </a>
     </NextLink>
   );
 };
diff --git a/src/components/molecules/images/responsive-image.module.scss b/src/components/molecules/images/responsive-image.module.scss
index 3566421..3a4f283 100644
--- a/src/components/molecules/images/responsive-image.module.scss
+++ b/src/components/molecules/images/responsive-image.module.scss
@@ -3,8 +3,7 @@
 .wrapper {
   display: flex;
   flex-flow: column;
-  width: 100%;
-  max-width: max-content;
+  width: fit-content;
   margin: 0;
   position: relative;
   text-align: center;
@@ -14,9 +13,7 @@
   margin: 0;
   padding: fun.convert-px(4) var(--spacing-2xs);
   background: var(--color-bg-secondary);
-  border: fun.convert-px(1) solid var(--color-border);
-  box-shadow: 0 fun.convert-px(-1) fun.convert-px(1) fun.convert-px(1)
-    var(--color-shadow-light);
+  border: fun.convert-px(1) solid var(--color-border-light);
   font-weight: 500;
 }
 
@@ -32,7 +29,7 @@
 
   &:hover,
   &:focus {
-    transform: scale(1.1);
+    transform: scale(var(--scale-up, 1.05));
   }
 
   &:focus {
@@ -43,7 +40,7 @@
   }
 
   &:active {
-    transform: scale(0.9);
+    transform: scale(var(--scale-down, 0.95));
 
     .caption {
       text-decoration: none;
diff --git a/src/components/molecules/images/responsive-image.stories.tsx b/src/components/molecules/images/responsive-image.stories.tsx
index a1f5295..35d9116 100644
--- a/src/components/molecules/images/responsive-image.stories.tsx
+++ b/src/components/molecules/images/responsive-image.stories.tsx
@@ -64,6 +64,7 @@ export default {
         required: false,
       },
     },
+    unoptimized: { table: { disable: true } },
     width: {
       control: {
         type: 'number',
diff --git a/src/components/molecules/images/responsive-image.tsx b/src/components/molecules/images/responsive-image.tsx
index 31cbcd1..d07dd6c 100644
--- a/src/components/molecules/images/responsive-image.tsx
+++ b/src/components/molecules/images/responsive-image.tsx
@@ -1,6 +1,6 @@
 import Link, { type LinkProps } from '@components/atoms/links/link';
 import Image, { type ImageProps } from 'next/image';
-import { FC } from 'react';
+import { FC, ReactNode } from 'react';
 import styles from './responsive-image.module.scss';
 
 export type ResponsiveImageProps = Omit<
@@ -14,7 +14,7 @@ export type ResponsiveImageProps = Omit<
   /**
    * A figure caption.
    */
-  caption?: string;
+  caption?: ReactNode;
   /**
    * Set additional classnames to the figure wrapper.
    */
@@ -55,6 +55,7 @@ const ResponsiveImage: FC<ResponsiveImageProps> = ({
             alt={alt}
             layout={layout || 'intrinsic'}
             objectFit={objectFit || 'contain'}
+            className={styles.img}
             {...props}
           />
           {caption && (
@@ -67,6 +68,7 @@ const ResponsiveImage: FC<ResponsiveImageProps> = ({
             alt={alt}
             layout={layout || 'intrinsic'}
             objectFit={objectFit || 'contain'}
+            className={styles.img}
             {...props}
           />
           {caption && (
diff --git a/src/components/organisms/widgets/image-widget.module.scss b/src/components/organisms/widgets/image-widget.module.scss
index 8e4d0aa..0d69441 100644
--- a/src/components/organisms/widgets/image-widget.module.scss
+++ b/src/components/organisms/widgets/image-widget.module.scss
@@ -1,7 +1,12 @@
 @use "@styles/abstracts/functions" as fun;
 
-.img {
+.figure {
+  --scale-up: 1.02;
+  --scale-down: 0.98;
+
   margin: 0;
+  padding: fun.convert-px(5);
+  border: fun.convert-px(1) solid var(--color-border);
 }
 
 .txt {
@@ -10,7 +15,7 @@
 
 .widget {
   &--left {
-    .img {
+    .figure {
       margin-right: auto;
     }
 
@@ -20,7 +25,7 @@
   }
 
   &--center {
-    .img {
+    .figure {
       margin-left: auto;
       margin-right: auto;
     }
@@ -31,7 +36,7 @@
   }
 
   &--right {
-    .img {
+    .figure {
       margin-left: auto;
     }
 
diff --git a/src/components/organisms/widgets/image-widget.stories.tsx b/src/components/organisms/widgets/image-widget.stories.tsx
index 27871ae..3014b36 100644
--- a/src/components/organisms/widgets/image-widget.stories.tsx
+++ b/src/components/organisms/widgets/image-widget.stories.tsx
@@ -50,7 +50,7 @@ export default {
         required: true,
       },
     },
-    img: {
+    image: {
       description: 'An image object.',
       type: {
         name: 'object',
@@ -58,6 +58,19 @@ export default {
         value: {},
       },
     },
+    imageClassName: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the image wrapper.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
     level: {
       control: {
         type: 'number',
@@ -107,7 +120,7 @@ const Template: ComponentStory<typeof ImageWidget> = (args) => (
   <ImageWidget {...args} />
 );
 
-const img = {
+const image = {
   alt: 'Et perferendis quaerat',
   height: 480,
   src: 'http://placeimg.com/640/480/nature',
@@ -122,7 +135,7 @@ export const AlignLeft = Template.bind({});
 AlignLeft.args = {
   alignment: 'left',
   expanded: true,
-  img,
+  image,
   level: 2,
   title: 'Quo et totam',
 };
@@ -134,7 +147,7 @@ export const AlignCenter = Template.bind({});
 AlignCenter.args = {
   alignment: 'center',
   expanded: true,
-  img,
+  image,
   level: 2,
   title: 'Quo et totam',
 };
@@ -146,7 +159,7 @@ export const AlignRight = Template.bind({});
 AlignRight.args = {
   alignment: 'right',
   expanded: true,
-  img,
+  image,
   level: 2,
   title: 'Quo et totam',
 };
@@ -158,7 +171,7 @@ export const WithDescription = Template.bind({});
 WithDescription.args = {
   description: 'Sint enim harum',
   expanded: true,
-  img,
+  image,
   level: 2,
   title: 'Quo et totam',
 };
diff --git a/src/components/organisms/widgets/image-widget.test.tsx b/src/components/organisms/widgets/image-widget.test.tsx
index 8c24bd9..c6b1a3a 100644
--- a/src/components/organisms/widgets/image-widget.test.tsx
+++ b/src/components/organisms/widgets/image-widget.test.tsx
@@ -18,7 +18,12 @@ const url = '/another-page';
 describe('ImageWidget', () => {
   it('renders an image', () => {
     render(
-      <ImageWidget expanded={true} img={img} title={title} level={titleLevel} />
+      <ImageWidget
+        expanded={true}
+        image={img}
+        title={title}
+        level={titleLevel}
+      />
     );
     expect(screen.getByRole('img', { name: img.alt })).toBeInTheDocument();
   });
@@ -27,7 +32,7 @@ describe('ImageWidget', () => {
     render(
       <ImageWidget
         expanded={true}
-        img={img}
+        image={img}
         title={title}
         level={titleLevel}
         url={url}
@@ -43,7 +48,7 @@ describe('ImageWidget', () => {
     render(
       <ImageWidget
         expanded={true}
-        img={img}
+        image={img}
         description={description}
         title={title}
         level={titleLevel}
diff --git a/src/components/organisms/widgets/image-widget.tsx b/src/components/organisms/widgets/image-widget.tsx
index ba04c6a..873337b 100644
--- a/src/components/organisms/widgets/image-widget.tsx
+++ b/src/components/organisms/widgets/image-widget.tsx
@@ -14,7 +14,7 @@ export type Image = Pick<
 
 export type ImageWidgetProps = Pick<
   WidgetProps,
-  'expanded' | 'level' | 'title'
+  'className' | 'expanded' | 'level' | 'title'
 > & {
   /**
    * The content alignment.
@@ -27,7 +27,11 @@ export type ImageWidgetProps = Pick<
   /**
    * An object describing the image.
    */
-  img: Image;
+  image: Image;
+  /**
+   * Set additional classnames to the image wrapper.
+   */
+  imageClassName?: string;
   /**
    * Add a link to the image.
    */
@@ -41,20 +45,22 @@ export type ImageWidgetProps = Pick<
  */
 const ImageWidget: FC<ImageWidgetProps> = ({
   alignment = 'left',
+  className = '',
   description,
-  img,
+  image,
+  imageClassName = '',
   url,
   ...props
 }) => {
   const alignmentClass = `widget--${alignment}`;
 
   return (
-    <Widget className={styles[alignmentClass]} {...props}>
+    <Widget className={`${styles[alignmentClass]} ${className}`} {...props}>
       <ResponsiveImage
         target={url}
         caption={description}
-        className={styles.img}
-        {...img}
+        className={`${styles.figure} ${imageClassName}`}
+        {...image}
       />
     </Widget>
   );
diff --git a/src/pages/cv.tsx b/src/pages/cv.tsx
new file mode 100644
index 0000000..d47edc6
--- /dev/null
+++ b/src/pages/cv.tsx
@@ -0,0 +1,198 @@
+import Link from '@components/atoms/links/link';
+import { type BreadcrumbItem } from '@components/molecules/nav/breadcrumb';
+import ImageWidget from '@components/organisms/widgets/image-widget';
+import SocialMedia from '@components/organisms/widgets/social-media';
+import PageLayout, {
+  type PageLayoutProps,
+} 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';
+import Head from 'next/head';
+import { useRouter } from 'next/router';
+import Script from 'next/script';
+import { ReactNode } from 'react';
+import { useIntl } from 'react-intl';
+import { AboutPage, Graph, WebPage } from 'schema-dts';
+
+/**
+ * CV page.
+ */
+const CVPage: NextPage = () => {
+  const intl = useIntl();
+  const { file, image } = data;
+  const { dates, intro, seo, title } = meta;
+  const homeLabel = intl.formatMessage({
+    defaultMessage: 'Home',
+    description: 'Breadcrumb: home label',
+    id: 'j5k9Fe',
+  });
+  const breadcrumb: BreadcrumbItem[] = [
+    { id: 'home', name: homeLabel, url: '/' },
+    { id: 'cv', name: title, url: '/cv' },
+  ];
+
+  const imageWidgetTitle = intl.formatMessage({
+    defaultMessage: 'Others formats',
+    description: 'CVPage: cv preview widget title',
+    id: 'B9OCyV',
+  });
+  const socialMediaTitle = intl.formatMessage({
+    defaultMessage: 'Open-source projects',
+    description: 'CVPage: social media widget title',
+    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),
+    },
+    update: { name: updateLabel, value: getFormattedDate(dates.update) },
+  };
+
+  const { website } = useSettings();
+  const cvAlt = intl.formatMessage(
+    {
+      defaultMessage: '{name} CV',
+      description: 'CVPage: CV image alternative text',
+      id: 'KUowUk',
+    },
+    { name: website.name }
+  );
+  const cvCaption = intl.formatMessage(
+    {
+      defaultMessage: '<link>Download the CV in PDF</link>',
+      id: 'fN04AJ',
+      description: 'CVPage: download CV in PDF text',
+    },
+    {
+      link: (chunks: ReactNode) => (
+        <Link download={true} href={file}>
+          {chunks}
+        </Link>
+      ),
+    }
+  );
+
+  const widgets = [
+    <ImageWidget
+      key="image-widget"
+      expanded={true}
+      title={imageWidgetTitle}
+      level={2}
+      image={{ alt: cvAlt, ...image }}
+      description={cvCaption}
+      imageClassName={styles.image}
+    />,
+    <SocialMedia
+      key="social-media"
+      title={socialMediaTitle}
+      level={2}
+      media={[
+        { name: 'Github', url: 'https://github.com/ArmandPhilippot' },
+        { name: 'Gitlab', url: 'https://gitlab.com/ArmandPhilippot' },
+        {
+          name: 'LinkedIn',
+          url: 'https://www.linkedin.com/in/armandphilippot',
+        },
+      ]}
+    />,
+  ];
+
+  const { asPath } = useRouter();
+  const pageUrl = `${website.url}${asPath}`;
+  const pagePublicationDate = new Date(dates.publication);
+  const pageUpdateDate = new Date(dates.update);
+
+  const webpageSchema: WebPage = {
+    '@id': `${pageUrl}`,
+    '@type': 'WebPage',
+    breadcrumb: { '@id': `${website.url}/#breadcrumb` },
+    name: seo.title,
+    description: seo.description,
+    reviewedBy: { '@id': `${website.url}/#branding` },
+    url: `${pageUrl}`,
+    isPartOf: {
+      '@id': `${website.url}`,
+    },
+  };
+
+  const cvSchema: AboutPage = {
+    '@id': `${website.url}/#cv`,
+    '@type': 'AboutPage',
+    name: seo.title,
+    description: intro,
+    author: { '@id': `${website.url}/#branding` },
+    creator: { '@id': `${website.url}/#branding` },
+    dateCreated: pagePublicationDate.toISOString(),
+    dateModified: pageUpdateDate.toISOString(),
+    datePublished: pagePublicationDate.toISOString(),
+    editor: { '@id': `${website.url}/#branding` },
+    image: image.src,
+    inLanguage: website.locales.default,
+    license: 'https://creativecommons.org/licenses/by-sa/4.0/deed.fr',
+    thumbnailUrl: image.src,
+    mainEntityOfPage: { '@id': `${pageUrl}` },
+  };
+
+  const schemaJsonLd: Graph = {
+    '@context': 'https://schema.org',
+    '@graph': [webpageSchema, cvSchema],
+  };
+
+  return (
+    <PageLayout
+      title={title}
+      intro={intro}
+      headerMeta={headerMeta}
+      breadcrumb={breadcrumb}
+      withToC={true}
+      widgets={widgets}
+    >
+      <Head>
+        <title>{`${seo.title} - ${website.name}`}</title>
+        <meta name="description" content={seo.description} />
+        <meta property="og:url" content={`${pageUrl}`} />
+        <meta property="og:type" content="article" />
+        <meta property="og:title" content={title} />
+        <meta property="og:description" content={intro} />
+        <meta property="og:image" content={image.src} />
+        <meta property="og:image:alt" content={title} />
+      </Head>
+      <Script
+        id="schema-cv"
+        type="application/ld+json"
+        dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaJsonLd) }}
+      />
+      <CVContent />
+    </PageLayout>
+  );
+};
+
+export const getStaticProps: GetStaticProps = async ({ locale }) => {
+  const translation = await loadTranslation(locale);
+
+  return {
+    props: {
+      translation,
+    },
+  };
+};
+
+export default CVPage;
diff --git a/src/styles/pages/cv.module.scss b/src/styles/pages/cv.module.scss
new file mode 100644
index 0000000..615c50d
--- /dev/null
+++ b/src/styles/pages/cv.module.scss
@@ -0,0 +1,3 @@
+.image {
+  max-width: min(100%, 25ch);
+}
-- 
cgit v1.2.3


From 7fd11d99f8a547e4acb89b9f9159b92e208dc90f Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Fri, 20 May 2022 22:12:43 +0200
Subject: chore: update images styles

---
 .../molecules/images/responsive-image.module.scss  | 46 ++++++++++++++++++----
 .../molecules/images/responsive-image.stories.tsx  | 17 ++++++++
 .../molecules/images/responsive-image.tsx          | 14 ++++++-
 .../organisms/layout/overview.module.scss          | 14 +++----
 src/components/organisms/layout/overview.tsx       |  2 +-
 src/styles/pages/article.module.scss               |  2 +-
 src/styles/pages/partials/_article-media.scss      | 17 ++++++--
 src/styles/pages/partials/_article-wp-blocks.scss  | 32 +++++++++------
 8 files changed, 111 insertions(+), 33 deletions(-)

(limited to 'src/components/molecules/images/responsive-image.tsx')

diff --git a/src/components/molecules/images/responsive-image.module.scss b/src/components/molecules/images/responsive-image.module.scss
index 3a4f283..8a1d51f 100644
--- a/src/components/molecules/images/responsive-image.module.scss
+++ b/src/components/molecules/images/responsive-image.module.scss
@@ -1,26 +1,49 @@
 @use "@styles/abstracts/functions" as fun;
 
+.caption {
+  margin: 0;
+  padding: fun.convert-px(4) var(--spacing-2xs);
+  background: var(--color-bg-secondary);
+  border: fun.convert-px(1) solid var(--color-border-light);
+  font-size: var(--font-size-sm);
+  font-weight: 500;
+}
+
 .wrapper {
   display: flex;
   flex-flow: column;
   width: fit-content;
-  margin: 0;
+  margin: 0 auto;
   position: relative;
   text-align: center;
-}
 
-.caption {
-  margin: 0;
-  padding: fun.convert-px(4) var(--spacing-2xs);
-  background: var(--color-bg-secondary);
-  border: fun.convert-px(1) solid var(--color-border-light);
-  font-weight: 500;
+  &--has-borders {
+    .caption {
+      margin-top: fun.convert-px(4);
+    }
+  }
+
+  &--has-borders#{&}--has-link {
+    .link {
+      padding: fun.convert-px(4);
+    }
+  }
+
+  &--has-borders#{&}--no-link {
+    padding: fun.convert-px(4);
+    border: fun.convert-px(1) solid var(--color-border);
+    box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) 0
+      var(--color-shadow);
+  }
 }
 
 .link {
   display: flex;
   flex-flow: column;
   background: none;
+  border: fun.convert-px(1) solid var(--color-border);
+  box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) 0
+    var(--color-shadow);
   text-decoration: none;
 
   .caption {
@@ -29,6 +52,11 @@
 
   &:hover,
   &:focus {
+    box-shadow: 0 0 fun.convert-px(2) 0 var(--color-shadow-light),
+      fun.convert-px(2) fun.convert-px(2) fun.convert-px(4) fun.convert-px(1)
+        var(--color-shadow-light),
+      fun.convert-px(4) fun.convert-px(4) fun.convert-px(8) fun.convert-px(2)
+        var(--color-shadow-light);
     transform: scale(var(--scale-up, 1.05));
   }
 
@@ -40,6 +68,8 @@
   }
 
   &:active {
+    box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
+      fun.convert-px(1) var(--color-shadow-light);
     transform: scale(var(--scale-down, 0.95));
 
     .caption {
diff --git a/src/components/molecules/images/responsive-image.stories.tsx b/src/components/molecules/images/responsive-image.stories.tsx
index 35d9116..4294208 100644
--- a/src/components/molecules/images/responsive-image.stories.tsx
+++ b/src/components/molecules/images/responsive-image.stories.tsx
@@ -7,6 +7,9 @@ import ResponsiveImage from './responsive-image';
 export default {
   title: 'Molecules/Images/ResponsiveImage',
   component: ResponsiveImage,
+  args: {
+    withBorders: false,
+  },
   argTypes: {
     alt: {
       control: {
@@ -75,6 +78,20 @@ export default {
         required: true,
       },
     },
+    withBorders: {
+      control: {
+        type: 'boolean',
+      },
+      description: 'Add borders around the image.',
+      table: {
+        category: 'Styles',
+        defaultValue: { summary: false },
+      },
+      type: {
+        name: 'boolean',
+        required: false,
+      },
+    },
   },
 } as ComponentMeta<typeof ResponsiveImage>;
 
diff --git a/src/components/molecules/images/responsive-image.tsx b/src/components/molecules/images/responsive-image.tsx
index d07dd6c..4541df8 100644
--- a/src/components/molecules/images/responsive-image.tsx
+++ b/src/components/molecules/images/responsive-image.tsx
@@ -31,6 +31,10 @@ export type ResponsiveImageProps = Omit<
    * The image width.
    */
   width: number | string;
+  /**
+   * Wrap the image with borders.
+   */
+  withBorders?: boolean;
 };
 
 /**
@@ -45,10 +49,18 @@ const ResponsiveImage: FC<ResponsiveImageProps> = ({
   layout,
   objectFit,
   target,
+  withBorders,
   ...props
 }) => {
+  const bordersModifier = withBorders
+    ? 'wrapper--has-borders'
+    : 'wrapper--no-borders';
+  const linkModifier = target ? 'wrapper--has-link' : 'wrapper--no-link';
+
   return (
-    <figure className={`${styles.wrapper} ${className}`}>
+    <figure
+      className={`${styles.wrapper} ${styles[bordersModifier]} ${styles[linkModifier]} ${className}`}
+    >
       {target ? (
         <Link href={target} className={styles.link}>
           <Image
diff --git a/src/components/organisms/layout/overview.module.scss b/src/components/organisms/layout/overview.module.scss
index e813625..895bae5 100644
--- a/src/components/organisms/layout/overview.module.scss
+++ b/src/components/organisms/layout/overview.module.scss
@@ -33,12 +33,12 @@
       }
     }
   }
-}
 
-.cover {
-  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);
+  .cover {
+    width: fit-content;
+    max-height: fun.convert-px(175);
+    margin-bottom: var(--spacing-sm);
+    padding: var(--spacing-2xs);
+    border: fun.convert-px(1) solid var(--color-border);
+  }
 }
diff --git a/src/components/organisms/layout/overview.tsx b/src/components/organisms/layout/overview.tsx
index d49dee1..b110e68 100644
--- a/src/components/organisms/layout/overview.tsx
+++ b/src/components/organisms/layout/overview.tsx
@@ -43,8 +43,8 @@ const Overview: FC<OverviewProps> = ({ className = '', cover, meta }) => {
     <div className={`${styles.wrapper} ${className}`}>
       {cover && (
         <ResponsiveImage
-          objectFit="contain"
           className={styles.cover}
+          objectFit="contain"
           {...cover}
         />
       )}
diff --git a/src/styles/pages/article.module.scss b/src/styles/pages/article.module.scss
index 04acad9..a5299fe 100644
--- a/src/styles/pages/article.module.scss
+++ b/src/styles/pages/article.module.scss
@@ -11,7 +11,7 @@
   margin-right: var(--spacing-2xs);
   padding: var(--spacing-2xs) var(--spacing-xs);
 
-  &__icon {
+  figure {
     max-width: fun.convert-px(22);
     margin-right: var(--spacing-2xs);
   }
diff --git a/src/styles/pages/partials/_article-media.scss b/src/styles/pages/partials/_article-media.scss
index 8359881..0cd3654 100644
--- a/src/styles/pages/partials/_article-media.scss
+++ b/src/styles/pages/partials/_article-media.scss
@@ -1,11 +1,20 @@
+@use "@styles/abstracts/functions" as fun;
+
 @mixin styles {
   figure {
-    margin: var(--spacing-md) 0;
+    display: flex;
+    flex-flow: column;
+    width: fit-content;
+    margin: 0 auto;
+    position: relative;
+    text-align: center;
   }
 
   figcaption {
-    margin-top: var(--spacing-xs);
-    font-size: var(--font-size-sm);
-    text-align: center;
+    margin: 0;
+    padding: fun.convert-px(4) var(--spacing-2xs);
+    background: var(--color-bg-secondary);
+    border: fun.convert-px(1) solid var(--color-border-light);
+    font-weight: 500;
   }
 }
diff --git a/src/styles/pages/partials/_article-wp-blocks.scss b/src/styles/pages/partials/_article-wp-blocks.scss
index f75a4d5..33e59a1 100644
--- a/src/styles/pages/partials/_article-wp-blocks.scss
+++ b/src/styles/pages/partials/_article-wp-blocks.scss
@@ -62,6 +62,24 @@
     }
   }
 
+  .wp-block-image {
+    display: flex;
+    flex-flow: column;
+    width: fit-content;
+    margin: 0 auto;
+    padding: fun.convert-px(4);
+    position: relative;
+    border: fun.convert-px(1) solid var(--color-border);
+    box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) 0
+      var(--color-shadow);
+    text-align: center;
+
+    figcaption {
+      margin-top: fun.convert-px(4);
+      font-size: var(--font-size-sm);
+    }
+  }
+
   .wp-block-gallery {
     display: grid;
     grid-template-columns: minmax(0, 1fr);
@@ -142,18 +160,10 @@
         }
       }
     }
-  }
 
-  .wp-block-image {
-    img {
-      display: block;
-      margin: auto;
-      box-shadow: 0 0 fun.convert-px(1) 0 var(--color-shadow),
-        fun.convert-px(2) fun.convert-px(2) fun.convert-px(2) 0
-          var(--color-shadow-light),
-        fun.convert-px(3) fun.convert-px(3) fun.convert-px(6) 0
-          var(--color-shadow-light);
-      text-align: center;
+    .wp-block-image img {
+      height: 100%;
+      object-fit: cover;
     }
   }
 
-- 
cgit v1.2.3

