diff options
Diffstat (limited to 'src/components/molecules/branding')
| -rw-r--r-- | src/components/molecules/branding/branding.module.scss | 80 | ||||
| -rw-r--r-- | src/components/molecules/branding/branding.stories.tsx | 92 | ||||
| -rw-r--r-- | src/components/molecules/branding/branding.test.tsx | 77 | ||||
| -rw-r--r-- | src/components/molecules/branding/branding.tsx | 57 | ||||
| -rw-r--r-- | src/components/molecules/branding/index.ts | 1 |
5 files changed, 307 insertions, 0 deletions
diff --git a/src/components/molecules/branding/branding.module.scss b/src/components/molecules/branding/branding.module.scss new file mode 100644 index 0000000..2f35fd7 --- /dev/null +++ b/src/components/molecules/branding/branding.module.scss @@ -0,0 +1,80 @@ +@use "../../../styles/abstracts/functions" as fun; +@use "../../../styles/abstracts/mixins" as mix; + +.wrapper { + display: grid; + grid-template-columns: minmax(0, 1fr); + justify-items: center; + width: 100%; + text-align: center; + + @include mix.media("screen") { + @include mix.dimensions("2xs") { + grid-template-columns: + auto + minmax(0, 1fr); + align-items: center; + justify-items: left; + column-gap: var(--spacing-sm); + width: unset; + } + } + + > *:first-child { + max-width: fun.convert-px(200); + max-height: fun.convert-px(200); + margin-bottom: var(--spacing-2xs); + + @include mix.media("screen") { + @include mix.dimensions("2xs") { + margin-bottom: 0; + } + } + } + + > *:nth-child(2) { + margin-block: var(--spacing-2xs); + } + + > *:nth-child(3) { + margin-block: 0 var(--spacing-xs); + } + + > *:first-child, + > *:first-child:nth-last-child(2) + * { + grid-row: span 2; + } + + > *:first-child:nth-last-child(3) + * { + align-self: self-end; + } + + > *:first-child:nth-last-child(3) ~ *:last-child { + align-self: self-start; + } +} + +.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/branding/branding.stories.tsx b/src/components/molecules/branding/branding.stories.tsx new file mode 100644 index 0000000..c2f216a --- /dev/null +++ b/src/components/molecules/branding/branding.stories.tsx @@ -0,0 +1,92 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import NextImage from 'next/image'; +import { Heading } from '../../atoms'; +import { Branding } from './branding'; + +/** + * Branding - Storybook Meta + */ +export default { + title: 'Molecules/Branding', + component: Branding, + args: {}, + argTypes: { + baseline: { + control: { + type: 'object', + }, + description: 'The brand baseline.', + type: { + name: 'function', + required: false, + }, + }, + logo: { + control: { + type: 'object', + }, + description: 'The brand logo.', + type: { + name: 'function', + required: true, + }, + }, + name: { + control: { + type: 'object', + }, + description: 'The brand name.', + type: { + name: 'function', + required: true, + }, + }, + url: { + control: { + type: 'string', + }, + description: 'The homepage url.', + type: { + name: 'string', + required: false, + }, + }, + }, +} as ComponentMeta<typeof Branding>; + +const Template: ComponentStory<typeof Branding> = (args) => ( + <Branding {...args} /> +); + +/** + * Branding Stories - Logo and title + */ +export const LogoAndTitle = Template.bind({}); +LogoAndTitle.args = { + logo: ( + <NextImage + alt="Your brand logo" + height={150} + src="https://picsum.photos/150" + width={150} + /> + ), + name: <Heading level={1}>Your brand name</Heading>, +}; + +/** + * Branding Stories - Logo, title and baseline + */ +export const LogoTitleAndBaseline = Template.bind({}); +LogoTitleAndBaseline.args = { + baseline: <div>Your brand baseline if any</div>, + logo: ( + <NextImage + alt="Your brand logo" + height={150} + src="https://picsum.photos/150" + width={150} + /> + ), + name: <Heading level={1}>Your brand name</Heading>, +}; diff --git a/src/components/molecules/branding/branding.test.tsx b/src/components/molecules/branding/branding.test.tsx new file mode 100644 index 0000000..7f41098 --- /dev/null +++ b/src/components/molecules/branding/branding.test.tsx @@ -0,0 +1,77 @@ +import { describe, expect, it } from '@jest/globals'; +import { render, screen as rtlScreen } from '@testing-library/react'; +import NextImage from 'next/image'; +import { Branding } from './branding'; + +describe('Branding', () => { + it('renders the brand logo and name', () => { + const altText = 'dolorem aut ullam'; + const name = 'ducimus quo enim'; + + render( + <Branding + logo={ + <NextImage + alt={altText} + height={100} + src="https://picsum.photos/100" + width={100} + /> + } + name={<div>{name}</div>} + /> + ); + + expect(rtlScreen.getByRole('img', { name: altText })).toBeInTheDocument(); + expect(rtlScreen.getByText(name)).toBeInTheDocument(); + }); + + it('can render the brand logo, name and baseline', () => { + const altText = 'dolorem aut ullam'; + const name = 'ducimus quo enim'; + const baseline = 'ab consequatur est'; + + render( + <Branding + baseline={<div>{baseline}</div>} + logo={ + <NextImage + alt={altText} + height={100} + src="https://picsum.photos/100" + width={100} + /> + } + name={<div>{name}</div>} + /> + ); + + expect(rtlScreen.getByRole('img', { name: altText })).toBeInTheDocument(); + expect(rtlScreen.getByText(name)).toBeInTheDocument(); + expect(rtlScreen.getByText(baseline)).toBeInTheDocument(); + }); + + it('can render the brand name wrapped in a link', () => { + const altText = 'dolorem aut ullam'; + const name = 'ducimus quo enim'; + const url = '/velit'; + + render( + <Branding + logo={ + <NextImage + alt={altText} + height={100} + src="https://picsum.photos/100" + width={100} + /> + } + name={<div>{name}</div>} + url={url} + /> + ); + + expect(rtlScreen.getByRole('img', { name: altText })).toBeInTheDocument(); + expect(rtlScreen.getByRole('link', { name })).toHaveAttribute('href', url); + }); +}); diff --git a/src/components/molecules/branding/branding.tsx b/src/components/molecules/branding/branding.tsx new file mode 100644 index 0000000..bb88a04 --- /dev/null +++ b/src/components/molecules/branding/branding.tsx @@ -0,0 +1,57 @@ +import { + type HTMLAttributes, + type ForwardRefRenderFunction, + forwardRef, + type ReactElement, +} from 'react'; +import styles from './branding.module.scss'; +import { Link } from 'src/components/atoms'; + +export type BrandingProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & { + /** + * The brand baseline. + */ + baseline?: ReactElement | null; + /** + * The brand logo. + * + * The logo size should not exceed ~200px. + */ + logo: ReactElement; + /** + * The brand name. + */ + name: ReactElement; + /** + * The homepage url if you want to wrap the name with a link. + */ + url?: string; +}; + +const BrandingWithRef: ForwardRefRenderFunction< + HTMLDivElement, + BrandingProps +> = ({ className = '', baseline, logo, name, url, ...props }, ref) => { + const wrapperClass = `${styles.wrapper} ${className}`; + + return ( + <div {...props} className={wrapperClass} ref={ref}> + {logo} + {url ? ( + <Link className={styles.link} href={url}> + {name} + </Link> + ) : ( + name + )} + {baseline} + </div> + ); +}; + +/** + * Branding component + * + * Render the branding logo, title and optional baseline. + */ +export const Branding = forwardRef(BrandingWithRef); diff --git a/src/components/molecules/branding/index.ts b/src/components/molecules/branding/index.ts new file mode 100644 index 0000000..5cf12ed --- /dev/null +++ b/src/components/molecules/branding/index.ts @@ -0,0 +1 @@ +export * from './branding'; |
