aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/molecules
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-10-09 16:31:00 +0200
committerArmand Philippot <git@armandphilippot.com>2023-11-11 18:14:41 +0100
commit891441a76173c708c6604fa203b175aefa222333 (patch)
tree27295311bb01a4e44dcc4f68422975cd705a24b8 /src/components/molecules
parentf11a906420975e833f278a08470d8f9783c76f73 (diff)
refactor(components): rewrite Branding component
The component should only be responsible of the layout for the logo, the name and the optional baseline. Also, the homepage url could be different from `/` so the consumer should give the right url.
Diffstat (limited to 'src/components/molecules')
-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
-rw-r--r--src/components/molecules/index.ts1
-rw-r--r--src/components/molecules/layout/branding.module.scss110
-rw-r--r--src/components/molecules/layout/branding.stories.tsx115
-rw-r--r--src/components/molecules/layout/branding.test.tsx109
-rw-r--r--src/components/molecules/layout/branding.tsx86
-rw-r--r--src/components/molecules/layout/index.ts1
11 files changed, 308 insertions, 421 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';
diff --git a/src/components/molecules/index.ts b/src/components/molecules/index.ts
index a62f3bf..70ac3c9 100644
--- a/src/components/molecules/index.ts
+++ b/src/components/molecules/index.ts
@@ -1,3 +1,4 @@
+export * from './branding';
export * from './buttons';
export * from './collapsible';
export * from './forms';
diff --git a/src/components/molecules/layout/branding.module.scss b/src/components/molecules/layout/branding.module.scss
deleted file mode 100644
index 6f67c8b..0000000
--- a/src/components/molecules/layout/branding.module.scss
+++ /dev/null
@@ -1,110 +0,0 @@
-@use "../../../styles/abstracts/functions" as fun;
-@use "../../../styles/abstracts/mixins" as mix;
-
-@mixin typing-animation {
- --typing-animation: none;
-
- width: fit-content;
- position: relative;
- overflow: hidden;
-
- &::after {
- content: "|";
- display: block;
- width: 100%;
- height: 100%;
- position: absolute;
- top: 0;
- right: 0;
- background: var(--color-bg);
- color: var(--color-primary-darker);
- font-weight: 400;
- text-align: left;
- visibility: hidden;
- transform: translateX(100%);
- transform-origin: right;
- animation: var(--typing-animation);
-
- :global {
- animation: var(--typing-animation);
- }
- }
-}
-
-.wrapper {
- --logo-size: #{clamp(fun.convert-px(90), 12vw, fun.convert-px(100))};
-
- display: grid;
- grid-template-columns: minmax(0, 1fr);
- justify-items: center;
- width: 100%;
-
- @include mix.media("screen") {
- @include mix.dimensions("2xs") {
- grid-template-columns:
- var(--logo-size)
- minmax(0, 1fr);
- grid-template-rows: 1fr min-content;
- align-items: center;
- justify-items: left;
- column-gap: var(--spacing-sm);
- width: unset;
- }
- }
-
- .logo {
- grid-row: span 2;
- animation: flip-logo 9s ease-in 0s 1;
- }
-
- .title {
- font-size: clamp(var(--font-size-xl), 8vw, var(--font-size-2xl));
- text-align: center;
-
- @include typing-animation;
- }
-
- .baseline {
- color: var(--color-fg-light);
- font-size: var(--font-size-lg);
- text-align: center;
-
- @include typing-animation;
- }
-
- .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);
- }
- }
-}
-
-@keyframes flip-logo {
- 0%,
- 90% {
- transform: rotateY(180deg);
- }
-
- 100% {
- transform: rotateY(0deg);
- }
-}
diff --git a/src/components/molecules/layout/branding.stories.tsx b/src/components/molecules/layout/branding.stories.tsx
deleted file mode 100644
index 7ff88c9..0000000
--- a/src/components/molecules/layout/branding.stories.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import type { ComponentMeta, ComponentStory } from '@storybook/react';
-import NextImage from 'next/image';
-import { Logo } from '../../atoms';
-import { Branding } from './branding';
-
-/**
- * Branding - Storybook Meta
- */
-export default {
- title: 'Molecules/Layout/Branding',
- component: Branding,
- args: {
- isHome: false,
- withLink: 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 Branding>;
-
-const Template: ComponentStory<typeof Branding> = (args) => (
- <Branding {...args} />
-);
-
-/**
- * Branding Stories - Default
- */
-export const Default = Template.bind({});
-Default.args = {
- logo: <Logo heading="A logo example" />,
- photo: (
- <NextImage
- alt="A photo example"
- height={200}
- src="https://picsum.photos/200"
- width={200}
- />
- ),
- title: 'Website title',
-};
-
-/**
- * Branding Stories - With baseline
- */
-export const WithBaseline = Template.bind({});
-WithBaseline.args = {
- baseline: 'Maiores corporis qui',
- logo: <Logo heading="A logo example" />,
- photo: (
- <NextImage
- alt="A photo example"
- height={200}
- src="https://picsum.photos/200"
- width={200}
- />
- ),
- title: 'Website title',
-};
diff --git a/src/components/molecules/layout/branding.test.tsx b/src/components/molecules/layout/branding.test.tsx
deleted file mode 100644
index cfb55c5..0000000
--- a/src/components/molecules/layout/branding.test.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import { describe, expect, it } from '@jest/globals';
-import NextImage from 'next/image';
-import { render, screen as rtlScreen } from '../../../../tests/utils';
-import { Logo } from '../../atoms';
-import { Branding } from './branding';
-
-describe('Branding', () => {
- it('renders a photo', () => {
- const altText = 'A photo example';
-
- render(
- <Branding
- logo={<Logo />}
- photo={
- <NextImage
- alt="A photo example"
- height={200}
- src="https://picsum.photos/200"
- width={200}
- />
- }
- title="Website title"
- />
- );
- expect(rtlScreen.getByRole('img', { name: altText })).toBeInTheDocument();
- });
-
- it('renders a logo', () => {
- const logoHeading = 'sed enim voluptatem';
-
- render(
- <Branding
- logo={<Logo heading={logoHeading} />}
- photo={
- <NextImage
- alt="A photo example"
- height={200}
- src="https://picsum.photos/200"
- width={200}
- />
- }
- title="Website name"
- />
- );
- expect(rtlScreen.getByTitle(logoHeading)).toBeInTheDocument();
- });
-
- it('renders a baseline', () => {
- render(
- <Branding
- logo={<Logo />}
- photo={
- <NextImage
- alt="A photo example"
- height={200}
- src="https://picsum.photos/200"
- width={200}
- />
- }
- title="Website title"
- baseline="Website baseline"
- />
- );
- expect(rtlScreen.getByText('Website baseline')).toBeInTheDocument();
- });
-
- it('renders a title wrapped with h1 element', () => {
- render(
- <Branding
- logo={<Logo />}
- photo={
- <NextImage
- alt="A photo example"
- height={200}
- src="https://picsum.photos/200"
- width={200}
- />
- }
- title="Website title"
- isHome={true}
- />
- );
- expect(
- rtlScreen.getByRole('heading', { level: 1, name: 'Website title' })
- ).toBeInTheDocument();
- });
-
- it('renders a title with h1 styles', () => {
- render(
- <Branding
- logo={<Logo />}
- photo={
- <NextImage
- alt="A photo example"
- height={200}
- src="https://picsum.photos/200"
- width={200}
- />
- }
- title="Website title"
- isHome={false}
- />
- );
- expect(
- rtlScreen.queryByRole('heading', { level: 1, name: 'Website title' })
- ).not.toBeInTheDocument();
- expect(rtlScreen.getByText('Website title')).toHaveClass('heading--1');
- });
-});
diff --git a/src/components/molecules/layout/branding.tsx b/src/components/molecules/layout/branding.tsx
deleted file mode 100644
index 9f8e6ce..0000000
--- a/src/components/molecules/layout/branding.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import { type FC, useRef, type ReactNode } from 'react';
-import { useStyles } from '../../../utils/hooks';
-import { Heading, Link } from '../../atoms';
-import { FlippingLogo } from '../images';
-import styles from './branding.module.scss';
-
-export type BrandingProps = {
- /**
- * The Branding baseline.
- */
- baseline?: string;
- /**
- * Use H1 if the current page is homepage. Default: false.
- */
- isHome?: boolean;
- /**
- * The website logo.
- */
- logo: ReactNode;
- /**
- * Your photo.
- */
- photo: ReactNode;
- /**
- * 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.
- */
-export const Branding: FC<BrandingProps> = ({
- baseline,
- isHome = false,
- logo,
- photo,
- title,
- withLink = false,
-}) => {
- const baselineRef = useRef<HTMLParagraphElement>(null);
- const titleRef = useRef<HTMLHeadingElement | HTMLParagraphElement>(null);
-
- useStyles({
- property: '--typing-animation',
- styles: 'blink 0.7s ease-in-out 0s 2, typing 4.3s linear 0s 1',
- target: titleRef,
- });
- useStyles({
- property: '--typing-animation',
- styles:
- 'hide-text 4.25s linear 0s 1, blink 0.8s ease-in-out 4.25s 2, typing 3.8s linear 4.25s 1',
- target: baselineRef,
- });
-
- return (
- <div className={styles.wrapper}>
- <FlippingLogo back={logo} className={styles.logo} front={photo} />
- <Heading
- className={styles.title}
- isFake={!isHome}
- level={1}
- ref={titleRef}
- >
- {withLink ? (
- <Link className={styles.link} href="/">
- {title}
- </Link>
- ) : (
- title
- )}
- </Heading>
- {baseline ? (
- <Heading className={styles.baseline} isFake level={4} ref={baselineRef}>
- {baseline}
- </Heading>
- ) : null}
- </div>
- );
-};
diff --git a/src/components/molecules/layout/index.ts b/src/components/molecules/layout/index.ts
index 1580baa..e43e664 100644
--- a/src/components/molecules/layout/index.ts
+++ b/src/components/molecules/layout/index.ts
@@ -1,4 +1,3 @@
-export * from './branding';
export * from './card';
export * from './code';
export * from './columns';