From a08291b1586858fc894a27d56f55f87a88f8dbd3 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Wed, 20 Apr 2022 19:24:21 +0200
Subject: refactor(storybook): reorganize design system

Add more stories for each components and change some components
categories for better organization.
---
 src/components/molecules/images/flipping-logo.tsx | 55 +++++++++++++++++++++++
 1 file changed, 55 insertions(+)
 create mode 100644 src/components/molecules/images/flipping-logo.tsx

(limited to 'src/components/molecules/images/flipping-logo.tsx')

diff --git a/src/components/molecules/images/flipping-logo.tsx b/src/components/molecules/images/flipping-logo.tsx
new file mode 100644
index 0000000..0d31fa3
--- /dev/null
+++ b/src/components/molecules/images/flipping-logo.tsx
@@ -0,0 +1,55 @@
+import Logo, { type LogoProps } from '@components/atoms/images/logo';
+import Image from 'next/image';
+import { FC } from 'react';
+import styles from './flipping-logo.module.scss';
+
+export type FlippingLogoProps = {
+  /**
+   * Set additional classnames to the logo wrapper.
+   */
+  className?: string;
+  /**
+   * Photo alternative text.
+   */
+  altText: string;
+  /**
+   * Logo image title.
+   */
+  logoTitle?: LogoProps['title'];
+  /**
+   * Photo url.
+   */
+  photo: string;
+};
+
+/**
+ * FlippingLogo component
+ *
+ * Render a logo and a photo with a flipping effect.
+ */
+const FlippingLogo: FC<FlippingLogoProps> = ({
+  className = '',
+  altText,
+  logoTitle,
+  photo,
+  ...props
+}) => {
+  return (
+    <div className={`${styles.logo} ${className}`}>
+      <div className={styles.logo__front}>
+        <Image
+          src={photo}
+          alt={altText}
+          layout="fill"
+          objectFit="cover"
+          {...props}
+        />
+      </div>
+      <div className={styles.logo__back}>
+        <Logo title={logoTitle} />
+      </div>
+    </div>
+  );
+};
+
+export default FlippingLogo;
-- 
cgit v1.2.3


From 52c185d0f23504fc6410cf36285968eff9e7b21f Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Fri, 22 Apr 2022 18:07:26 +0200
Subject: chore: add a Header component

---
 src/components/molecules/images/flipping-logo.tsx  |   4 +-
 src/components/organisms/layout/header.module.scss |  50 ++++++++
 src/components/organisms/layout/header.stories.tsx | 131 +++++++++++++++++++++
 src/components/organisms/layout/header.test.tsx    |  27 +++++
 src/components/organisms/layout/header.tsx         |  35 ++++++
 5 files changed, 245 insertions(+), 2 deletions(-)
 create mode 100644 src/components/organisms/layout/header.module.scss
 create mode 100644 src/components/organisms/layout/header.stories.tsx
 create mode 100644 src/components/organisms/layout/header.test.tsx
 create mode 100644 src/components/organisms/layout/header.tsx

(limited to 'src/components/molecules/images/flipping-logo.tsx')

diff --git a/src/components/molecules/images/flipping-logo.tsx b/src/components/molecules/images/flipping-logo.tsx
index 0d31fa3..47e54ab 100644
--- a/src/components/molecules/images/flipping-logo.tsx
+++ b/src/components/molecules/images/flipping-logo.tsx
@@ -1,5 +1,5 @@
 import Logo, { type LogoProps } from '@components/atoms/images/logo';
-import Image from 'next/image';
+import Image, { type ImageProps } from 'next/image';
 import { FC } from 'react';
 import styles from './flipping-logo.module.scss';
 
@@ -19,7 +19,7 @@ export type FlippingLogoProps = {
   /**
    * Photo url.
    */
-  photo: string;
+  photo: ImageProps['src'];
 };
 
 /**
diff --git a/src/components/organisms/layout/header.module.scss b/src/components/organisms/layout/header.module.scss
new file mode 100644
index 0000000..7ae683f
--- /dev/null
+++ b/src/components/organisms/layout/header.module.scss
@@ -0,0 +1,50 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+.wrapper {
+  display: grid;
+  grid-template-columns:
+    minmax(0, 1fr) min(calc(100vw - calc(var(--spacing-md) * 2)), 100ch)
+    minmax(0, 1fr);
+  align-items: center;
+  padding: var(--spacing-sm) 0 var(--spacing-md);
+
+  .toolbar {
+    justify-content: space-around;
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    z-index: 5;
+    background: var(--color-bg);
+    border-top: fun.convert-px(4) solid;
+    border-image: radial-gradient(
+        ellipse at top,
+        var(--color-primary-lighter) 20%,
+        var(--color-primary) 100%
+      )
+      1;
+    box-shadow: 0 fun.convert-px(-2) fun.convert-px(3) fun.convert-px(-1)
+      var(--color-shadow-dark);
+
+    @include mix.media("screen") {
+      @include mix.dimensions("sm") {
+        justify-content: flex-end;
+        width: auto;
+        position: relative;
+        left: unset;
+        background: inherit;
+        border: none;
+        box-shadow: none;
+      }
+    }
+  }
+}
+
+.body {
+  grid-column: 2;
+  display: flex;
+  flex-flow: row wrap;
+  align-items: center;
+  justify-content: space-between;
+  gap: var(--spacing-md);
+}
diff --git a/src/components/organisms/layout/header.stories.tsx b/src/components/organisms/layout/header.stories.tsx
new file mode 100644
index 0000000..c58c344
--- /dev/null
+++ b/src/components/organisms/layout/header.stories.tsx
@@ -0,0 +1,131 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { IntlProvider } from 'react-intl';
+import HeaderComponent from './header';
+
+/**
+ * Header - Storybook Meta
+ */
+export default {
+  title: 'Organisms/Layout',
+  component: HeaderComponent,
+  args: {
+    isHome: false,
+    withLink: false,
+  },
+  argTypes: {
+    baseline: {
+      control: {
+        type: 'text',
+      },
+      description: 'The branding baseline.',
+      table: {
+        category: 'Options',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
+    className: {
+      control: {
+        type: 'text',
+      },
+      description: 'Set additional classnames to the header wrapper.',
+      table: {
+        category: 'Styles',
+      },
+      type: {
+        name: 'string',
+        required: false,
+      },
+    },
+    isHome: {
+      control: {
+        type: 'boolean',
+      },
+      description: 'Determine if the current page is homepage or not.',
+      table: {
+        category: 'Options',
+      },
+      type: {
+        name: 'boolean',
+        required: false,
+      },
+    },
+    nav: {
+      description: 'The main navigation items.',
+      type: {
+        name: 'object',
+        required: true,
+        value: {},
+      },
+    },
+    photo: {
+      control: {
+        type: 'text',
+      },
+      description: 'The branding photo.',
+      type: {
+        name: 'string',
+        required: true,
+      },
+    },
+    title: {
+      control: {
+        type: 'text',
+      },
+      description: 'The website title.',
+      type: {
+        name: 'string',
+        required: true,
+      },
+    },
+    unoptimized: { table: { disable: true } },
+    withLink: {
+      control: {
+        type: 'boolean',
+      },
+      description: 'Wrap the website title with a link to homepage.',
+      table: {
+        category: 'Options',
+      },
+      type: {
+        name: 'boolean',
+        required: false,
+      },
+    },
+  },
+  decorators: [
+    (Story) => (
+      <IntlProvider locale="en">
+        <Story />
+      </IntlProvider>
+    ),
+  ],
+  parameters: {
+    layout: 'fullscreen',
+  },
+} as ComponentMeta<typeof HeaderComponent>;
+
+const Template: ComponentStory<typeof HeaderComponent> = (args) => (
+  <HeaderComponent {...args} />
+);
+
+const nav = [
+  { id: 'home-link', href: '#', label: 'Home' },
+  { id: 'blog-link', href: '#', label: 'Blog' },
+  { id: 'cv-link', href: '#', label: 'CV' },
+  { id: 'contact-link', href: '#', label: 'Contact' },
+];
+
+/**
+ * Layout Stories - Header
+ */
+export const Header = Template.bind({});
+Header.args = {
+  nav,
+  photo: 'http://placeimg.com/640/480/people',
+  title: 'Website title',
+  // @ts-ignore - Needed because of the placeholder image.
+  unoptimized: true,
+};
diff --git a/src/components/organisms/layout/header.test.tsx b/src/components/organisms/layout/header.test.tsx
new file mode 100644
index 0000000..05baaec
--- /dev/null
+++ b/src/components/organisms/layout/header.test.tsx
@@ -0,0 +1,27 @@
+import { render, screen } from '@test-utils';
+import Header from './header';
+
+const nav = [
+  { id: 'home-link', href: '#', label: 'Home' },
+  { id: 'blog-link', href: '#', label: 'Blog' },
+  { id: 'cv-link', href: '#', label: 'CV' },
+  { id: 'contact-link', href: '#', label: 'Contact' },
+];
+
+const photo = 'http://placeimg.com/640/480/nightlife';
+
+const title = 'Assumenda quis quod';
+
+describe('Header', () => {
+  it('renders the website title', () => {
+    render(<Header title={title} photo={photo} nav={nav} isHome={true} />);
+    expect(
+      screen.getByRole('heading', { level: 1, name: title })
+    ).toBeInTheDocument();
+  });
+
+  it('renders the main nav', () => {
+    render(<Header title={title} photo={photo} nav={nav} />);
+    expect(screen.getByRole('navigation')).toBeInTheDocument();
+  });
+});
diff --git a/src/components/organisms/layout/header.tsx b/src/components/organisms/layout/header.tsx
new file mode 100644
index 0000000..f6ebc9c
--- /dev/null
+++ b/src/components/organisms/layout/header.tsx
@@ -0,0 +1,35 @@
+import Branding, {
+  type BrandingProps,
+} from '@components/molecules/layout/branding';
+import { FC } from 'react';
+import Toolbar, { type ToolbarProps } from '../toolbar/toolbar';
+import styles from './header.module.scss';
+
+export type HeaderProps = BrandingProps & {
+  /**
+   * Set additional classnames to the header element.
+   */
+  className?: string;
+  /**
+   * The main nav items.
+   */
+  nav: ToolbarProps['nav'];
+};
+
+/**
+ * Header component
+ *
+ * Render the website header.
+ */
+const Header: FC<HeaderProps> = ({ className, nav, ...props }) => {
+  return (
+    <header className={`${styles.wrapper} ${className}`}>
+      <div className={styles.body}>
+        <Branding {...props} />
+        <Toolbar nav={nav} className={styles.toolbar} />
+      </div>
+    </header>
+  );
+};
+
+export default Header;
-- 
cgit v1.2.3


From 54883bb5c36cf21462a421605a709fdd6f04b150 Mon Sep 17 00:00:00 2001
From: Armand Philippot <git@armandphilippot.com>
Date: Wed, 18 May 2022 11:44:37 +0200
Subject: chore: add branding animation

---
 src/components/molecules/images/flipping-logo.tsx  |  20 ++--
 .../molecules/layout/branding.module.scss          | 125 +++++++++++++++------
 src/components/molecules/layout/branding.tsx       |  26 ++++-
 .../organisms/toolbar/toolbar-items.module.scss    |   1 +
 .../organisms/toolbar/toolbar.module.scss          |  16 ++-
 src/utils/hooks/use-styles.tsx                     |  29 +++++
 6 files changed, 171 insertions(+), 46 deletions(-)
 create mode 100644 src/utils/hooks/use-styles.tsx

(limited to 'src/components/molecules/images/flipping-logo.tsx')

diff --git a/src/components/molecules/images/flipping-logo.tsx b/src/components/molecules/images/flipping-logo.tsx
index 47e54ab..1099d53 100644
--- a/src/components/molecules/images/flipping-logo.tsx
+++ b/src/components/molecules/images/flipping-logo.tsx
@@ -1,6 +1,6 @@
 import Logo, { type LogoProps } from '@components/atoms/images/logo';
 import Image, { type ImageProps } from 'next/image';
-import { FC } from 'react';
+import { ForwardedRef, forwardRef, ForwardRefRenderFunction } from 'react';
 import styles from './flipping-logo.module.scss';
 
 export type FlippingLogoProps = {
@@ -27,15 +27,15 @@ export type FlippingLogoProps = {
  *
  * Render a logo and a photo with a flipping effect.
  */
-const FlippingLogo: FC<FlippingLogoProps> = ({
-  className = '',
-  altText,
-  logoTitle,
-  photo,
-  ...props
-}) => {
+const FlippingLogo: ForwardRefRenderFunction<
+  HTMLDivElement,
+  FlippingLogoProps
+> = (
+  { className = '', altText, logoTitle, photo, ...props },
+  ref: ForwardedRef<HTMLDivElement>
+) => {
   return (
-    <div className={`${styles.logo} ${className}`}>
+    <div className={`${styles.logo} ${className}`} ref={ref}>
       <div className={styles.logo__front}>
         <Image
           src={photo}
@@ -52,4 +52,4 @@ const FlippingLogo: FC<FlippingLogoProps> = ({
   );
 };
 
-export default FlippingLogo;
+export default forwardRef(FlippingLogo);
diff --git a/src/components/molecules/layout/branding.module.scss b/src/components/molecules/layout/branding.module.scss
index aa18002..6121fa1 100644
--- a/src/components/molecules/layout/branding.module.scss
+++ b/src/components/molecules/layout/branding.module.scss
@@ -1,48 +1,105 @@
 @use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+@mixin typing-animation {
+  --typing-animation: none;
+
+  width: fit-content;
+  position: relative;
+  overflow: hidden;
+
+  &::after {
+    content: "|";
+    display: block;
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    top: 0;
+    right: 0;
+    background: var(--color-bg);
+    color: var(--color-primary-darker);
+    font-weight: 400;
+    text-align: left;
+    visibility: hidden;
+    transform: translateX(100%);
+    transform-origin: right;
+    animation: var(--typing-animation);
+
+    :global {
+      animation: var(--typing-animation);
+    }
+  }
+}
 
 .wrapper {
+  --logo-size: #{clamp(fun.convert-px(90), 12vw, fun.convert-px(100))};
+
   display: grid;
-  grid-template-columns:
-    var(--logo-size, fun.convert-px(100))
-    minmax(0, 1fr);
-  grid-template-rows: 1fr min-content;
-  align-items: center;
-  column-gap: var(--spacing-sm);
-}
+  grid-template-columns: minmax(0, 1fr);
+  justify-items: center;
+  width: 100%;
 
-.logo {
-  grid-row: span 2;
-}
+  @include mix.media("screen") {
+    @include mix.dimensions("2xs") {
+      grid-template-columns:
+        var(--logo-size, fun.convert-px(100))
+        minmax(0, 1fr);
+      grid-template-rows: 1fr min-content;
+      align-items: center;
+      justify-items: left;
+      column-gap: var(--spacing-sm);
+      width: unset;
+    }
+  }
 
-.title {
-  font-size: var(--font-size-2xl);
-}
+  .logo {
+    grid-row: span 2;
+    margin-bottom: var(--spacing-sm);
 
-.baseline {
-  color: var(--color-fg-light);
-}
+    @include mix.media("screen") {
+      @include mix.dimensions("2xs") {
+        margin-bottom: 0;
+      }
+    }
+  }
+
+  .title {
+    font-size: clamp(var(--font-size-xl), 8vw, var(--font-size-2xl));
+    text-align: center;
 
-.link {
-  background: linear-gradient(
-      to top,
-      var(--color-primary-light) fun.convert-px(5),
-      transparent fun.convert-px(5)
-    )
-    left / 0 100% no-repeat;
-  text-decoration: none;
-  transition: all 0.6s ease-out 0s;
-
-  &:hover,
-  &:focus {
-    background-size: 100% 100%;
+    @include typing-animation;
   }
 
-  &:focus {
-    color: var(--color-primary-light);
+  .baseline {
+    color: var(--color-fg-light);
+    font-size: var(--font-size-lg);
+    text-align: center;
+
+    @include typing-animation;
   }
 
-  &:active {
-    background-size: 0 100%;
-    color: var(--color-primary-dark);
+  .link {
+    background: linear-gradient(
+        to top,
+        var(--color-primary-light) fun.convert-px(5),
+        transparent fun.convert-px(5)
+      )
+      left / 0 100% no-repeat;
+    text-decoration: none;
+    transition: all 0.6s ease-out 0s;
+
+    &:hover,
+    &:focus {
+      background-size: 100% 100%;
+    }
+
+    &:focus {
+      color: var(--color-primary-light);
+    }
+
+    &:active {
+      background-size: 0 100%;
+      color: var(--color-primary-dark);
+    }
   }
 }
diff --git a/src/components/molecules/layout/branding.tsx b/src/components/molecules/layout/branding.tsx
index 423c54f..9a82a74 100644
--- a/src/components/molecules/layout/branding.tsx
+++ b/src/components/molecules/layout/branding.tsx
@@ -1,6 +1,7 @@
 import Heading from '@components/atoms/headings/heading';
+import useStyles from '@utils/hooks/use-styles';
 import Link from 'next/link';
-import { FC } from 'react';
+import { FC, useRef } from 'react';
 import { useIntl } from 'react-intl';
 import FlippingLogo, { type FlippingLogoProps } from '../images/flipping-logo';
 import styles from './branding.module.scss';
@@ -37,6 +38,9 @@ const Branding: FC<BrandingProps> = ({
   withLink = false,
   ...props
 }) => {
+  const baselineRef = useRef<HTMLParagraphElement>(null);
+  const logoRef = useRef<HTMLDivElement>(null);
+  const titleRef = useRef<HTMLHeadingElement | HTMLParagraphElement>(null);
   const intl = useIntl();
   const altText = intl.formatMessage(
     {
@@ -55,6 +59,23 @@ const Branding: FC<BrandingProps> = ({
     { website: title }
   );
 
+  useStyles({
+    property: '--typing-animation',
+    styles: 'blink 0.7s ease-in-out 0s 2, typing 4.3s linear 0s 1',
+    target: titleRef,
+  });
+  useStyles({
+    property: '--typing-animation',
+    styles:
+      'hide-text 4.25s linear 0s 1, blink 0.8s ease-in-out 4.25s 2, typing 3.8s linear 4.25s 1',
+    target: baselineRef,
+  });
+  useStyles({
+    property: 'animation',
+    styles: 'flip-logo 9s ease-in 0s 1',
+    target: logoRef,
+  });
+
   return (
     <div className={styles.wrapper}>
       <FlippingLogo
@@ -62,6 +83,7 @@ const Branding: FC<BrandingProps> = ({
         altText={altText}
         logoTitle={logoTitle}
         photo={photo}
+        ref={logoRef}
         {...props}
       />
       <Heading
@@ -69,6 +91,7 @@ const Branding: FC<BrandingProps> = ({
         level={1}
         withMargin={false}
         className={styles.title}
+        ref={titleRef}
       >
         {withLink ? (
           <Link href="/">
@@ -84,6 +107,7 @@ const Branding: FC<BrandingProps> = ({
           level={4}
           withMargin={false}
           className={styles.baseline}
+          ref={baselineRef}
         >
           {baseline}
         </Heading>
diff --git a/src/components/organisms/toolbar/toolbar-items.module.scss b/src/components/organisms/toolbar/toolbar-items.module.scss
index fd526d6..c970b71 100644
--- a/src/components/organisms/toolbar/toolbar-items.module.scss
+++ b/src/components/organisms/toolbar/toolbar-items.module.scss
@@ -26,6 +26,7 @@
 
   @include mix.media("screen") {
     @include mix.dimensions(null, "sm") {
+      position: fixed;
       left: 0;
       right: 0;
     }
diff --git a/src/components/organisms/toolbar/toolbar.module.scss b/src/components/organisms/toolbar/toolbar.module.scss
index cda9b37..4bcabcb 100644
--- a/src/components/organisms/toolbar/toolbar.module.scss
+++ b/src/components/organisms/toolbar/toolbar.module.scss
@@ -23,6 +23,18 @@
   box-shadow: 0 fun.convert-px(-2) fun.convert-px(3) fun.convert-px(-1)
     var(--color-shadow-dark);
 
+  :global {
+    animation: slide-in-from-bottom 0.8s ease-in-out 0s 1;
+  }
+
+  @include mix.media("screen") {
+    @include mix.dimensions("sm") {
+      :global {
+        animation: slide-in-from-top 1s ease-in-out 0s 1;
+      }
+    }
+  }
+
   .modal {
     &--search,
     &--settings {
@@ -52,8 +64,10 @@
       }
 
       .tooltip {
+        padding: calc(var(--title-height) / 2 + var(--spacing-2xs))
+          var(--spacing-2xs) var(--spacing-2xs);
         top: unset;
-        bottom: calc(100% + var(--spacing-xs));
+        bottom: calc(100% + var(--spacing-2xs));
         transform-origin: bottom right;
       }
     }
diff --git a/src/utils/hooks/use-styles.tsx b/src/utils/hooks/use-styles.tsx
new file mode 100644
index 0000000..d47e9fb
--- /dev/null
+++ b/src/utils/hooks/use-styles.tsx
@@ -0,0 +1,29 @@
+import { RefObject, useEffect } from 'react';
+
+export type UseStylesProps = {
+  /**
+   * A property name or a CSS variable.
+   */
+  property: string;
+  /**
+   * The styles.
+   */
+  styles: string;
+  /**
+   * A targeted element reference.
+   */
+  target: RefObject<HTMLElement>;
+};
+
+/**
+ * Add styles to an element using a React reference.
+ *
+ * @param {UseStylesProps} props - An object with property, styles and target.
+ */
+const useStyles = ({ property, styles, target }: UseStylesProps) => {
+  useEffect(() => {
+    if (target.current) target.current.style.setProperty(property, styles);
+  }, [property, styles, target]);
+};
+
+export default useStyles;
-- 
cgit v1.2.3

