aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/molecules/branding
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/molecules/branding')
-rw-r--r--src/components/molecules/branding/branding.module.scss80
-rw-r--r--src/components/molecules/branding/branding.stories.tsx92
-rw-r--r--src/components/molecules/branding/branding.test.tsx77
-rw-r--r--src/components/molecules/branding/branding.tsx57
-rw-r--r--src/components/molecules/branding/index.ts1
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';