diff options
Diffstat (limited to 'src/components')
| -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; |
