diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/atoms/headings/heading.module.scss | 57 | ||||
| -rw-r--r-- | src/components/atoms/headings/heading.stories.tsx | 32 | ||||
| -rw-r--r-- | src/components/atoms/headings/heading.test.tsx | 10 | ||||
| -rw-r--r-- | src/components/atoms/headings/heading.tsx | 35 | ||||
| -rw-r--r-- | src/components/molecules/layout/branding.module.scss | 48 | ||||
| -rw-r--r-- | src/components/molecules/layout/branding.stories.tsx | 83 | ||||
| -rw-r--r-- | src/components/molecules/layout/branding.test.tsx | 61 | ||||
| -rw-r--r-- | src/components/molecules/layout/branding.tsx | 97 | 
8 files changed, 419 insertions, 4 deletions
| 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; | 
