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

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

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

diff --git a/src/components/atoms/headings/heading.test.tsx b/src/components/atoms/headings/heading.test.tsx
new file mode 100644
index 0000000..b83f7cd
--- /dev/null
+++ b/src/components/atoms/headings/heading.test.tsx
@@ -0,0 +1,46 @@
+import { render, screen } from '@test-utils';
+import Heading from './heading';
+
+describe('Heading', () => {
+  it('renders a h1', () => {
+    render(<Heading level={1}>Level 1</Heading>);
+    expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent(
+      'Level 1'
+    );
+  });
+
+  it('renders a h2', () => {
+    render(<Heading level={2}>Level 2</Heading>);
+    expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(
+      'Level 2'
+    );
+  });
+
+  it('renders a h3', () => {
+    render(<Heading level={3}>Level 3</Heading>);
+    expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent(
+      'Level 3'
+    );
+  });
+
+  it('renders a h4', () => {
+    render(<Heading level={4}>Level 4</Heading>);
+    expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent(
+      'Level 4'
+    );
+  });
+
+  it('renders a h5', () => {
+    render(<Heading level={5}>Level 5</Heading>);
+    expect(screen.getByRole('heading', { level: 5 })).toHaveTextContent(
+      'Level 5'
+    );
+  });
+
+  it('renders a h6', () => {
+    render(<Heading level={6}>Level 6</Heading>);
+    expect(screen.getByRole('heading', { level: 6 })).toHaveTextContent(
+      'Level 6'
+    );
+  });
+});
-- 
cgit v1.2.3


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

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

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

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

