From a08291b1586858fc894a27d56f55f87a88f8dbd3 Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Wed, 20 Apr 2022 19:24:21 +0200 Subject: refactor(storybook): reorganize design system Add more stories for each components and change some components categories for better organization. --- src/components/molecules/images/flipping-logo.tsx | 55 +++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/components/molecules/images/flipping-logo.tsx (limited to 'src/components/molecules/images/flipping-logo.tsx') diff --git a/src/components/molecules/images/flipping-logo.tsx b/src/components/molecules/images/flipping-logo.tsx new file mode 100644 index 0000000..0d31fa3 --- /dev/null +++ b/src/components/molecules/images/flipping-logo.tsx @@ -0,0 +1,55 @@ +import Logo, { type LogoProps } from '@components/atoms/images/logo'; +import Image from 'next/image'; +import { FC } from 'react'; +import styles from './flipping-logo.module.scss'; + +export type FlippingLogoProps = { + /** + * Set additional classnames to the logo wrapper. + */ + className?: string; + /** + * Photo alternative text. + */ + altText: string; + /** + * Logo image title. + */ + logoTitle?: LogoProps['title']; + /** + * Photo url. + */ + photo: string; +}; + +/** + * FlippingLogo component + * + * Render a logo and a photo with a flipping effect. + */ +const FlippingLogo: FC = ({ + className = '', + altText, + logoTitle, + photo, + ...props +}) => { + return ( +
+
+ {altText} +
+
+ +
+
+ ); +}; + +export default FlippingLogo; -- cgit v1.2.3 From 52c185d0f23504fc6410cf36285968eff9e7b21f Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Fri, 22 Apr 2022 18:07:26 +0200 Subject: chore: add a Header component --- src/components/molecules/images/flipping-logo.tsx | 4 +- src/components/organisms/layout/header.module.scss | 50 ++++++++ src/components/organisms/layout/header.stories.tsx | 131 +++++++++++++++++++++ src/components/organisms/layout/header.test.tsx | 27 +++++ src/components/organisms/layout/header.tsx | 35 ++++++ 5 files changed, 245 insertions(+), 2 deletions(-) create mode 100644 src/components/organisms/layout/header.module.scss create mode 100644 src/components/organisms/layout/header.stories.tsx create mode 100644 src/components/organisms/layout/header.test.tsx create mode 100644 src/components/organisms/layout/header.tsx (limited to 'src/components/molecules/images/flipping-logo.tsx') diff --git a/src/components/molecules/images/flipping-logo.tsx b/src/components/molecules/images/flipping-logo.tsx index 0d31fa3..47e54ab 100644 --- a/src/components/molecules/images/flipping-logo.tsx +++ b/src/components/molecules/images/flipping-logo.tsx @@ -1,5 +1,5 @@ import Logo, { type LogoProps } from '@components/atoms/images/logo'; -import Image from 'next/image'; +import Image, { type ImageProps } from 'next/image'; import { FC } from 'react'; import styles from './flipping-logo.module.scss'; @@ -19,7 +19,7 @@ export type FlippingLogoProps = { /** * Photo url. */ - photo: string; + photo: ImageProps['src']; }; /** diff --git a/src/components/organisms/layout/header.module.scss b/src/components/organisms/layout/header.module.scss new file mode 100644 index 0000000..7ae683f --- /dev/null +++ b/src/components/organisms/layout/header.module.scss @@ -0,0 +1,50 @@ +@use "@styles/abstracts/functions" as fun; +@use "@styles/abstracts/mixins" as mix; + +.wrapper { + display: grid; + grid-template-columns: + minmax(0, 1fr) min(calc(100vw - calc(var(--spacing-md) * 2)), 100ch) + minmax(0, 1fr); + align-items: center; + padding: var(--spacing-sm) 0 var(--spacing-md); + + .toolbar { + justify-content: space-around; + position: fixed; + bottom: 0; + left: 0; + z-index: 5; + background: var(--color-bg); + border-top: fun.convert-px(4) solid; + border-image: radial-gradient( + ellipse at top, + var(--color-primary-lighter) 20%, + var(--color-primary) 100% + ) + 1; + box-shadow: 0 fun.convert-px(-2) fun.convert-px(3) fun.convert-px(-1) + var(--color-shadow-dark); + + @include mix.media("screen") { + @include mix.dimensions("sm") { + justify-content: flex-end; + width: auto; + position: relative; + left: unset; + background: inherit; + border: none; + box-shadow: none; + } + } + } +} + +.body { + grid-column: 2; + display: flex; + flex-flow: row wrap; + align-items: center; + justify-content: space-between; + gap: var(--spacing-md); +} diff --git a/src/components/organisms/layout/header.stories.tsx b/src/components/organisms/layout/header.stories.tsx new file mode 100644 index 0000000..c58c344 --- /dev/null +++ b/src/components/organisms/layout/header.stories.tsx @@ -0,0 +1,131 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { IntlProvider } from 'react-intl'; +import HeaderComponent from './header'; + +/** + * Header - Storybook Meta + */ +export default { + title: 'Organisms/Layout', + component: HeaderComponent, + args: { + isHome: false, + withLink: false, + }, + argTypes: { + baseline: { + control: { + type: 'text', + }, + description: 'The branding baseline.', + table: { + category: 'Options', + }, + type: { + name: 'string', + required: false, + }, + }, + className: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the header wrapper.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + isHome: { + control: { + type: 'boolean', + }, + description: 'Determine if the current page is homepage or not.', + table: { + category: 'Options', + }, + type: { + name: 'boolean', + required: false, + }, + }, + nav: { + description: 'The main navigation items.', + type: { + name: 'object', + required: true, + value: {}, + }, + }, + photo: { + control: { + type: 'text', + }, + description: 'The branding photo.', + type: { + name: 'string', + required: true, + }, + }, + title: { + control: { + type: 'text', + }, + description: 'The website title.', + type: { + name: 'string', + required: true, + }, + }, + unoptimized: { table: { disable: true } }, + withLink: { + control: { + type: 'boolean', + }, + description: 'Wrap the website title with a link to homepage.', + table: { + category: 'Options', + }, + type: { + name: 'boolean', + required: false, + }, + }, + }, + decorators: [ + (Story) => ( + + + + ), + ], + parameters: { + layout: 'fullscreen', + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + +); + +const nav = [ + { id: 'home-link', href: '#', label: 'Home' }, + { id: 'blog-link', href: '#', label: 'Blog' }, + { id: 'cv-link', href: '#', label: 'CV' }, + { id: 'contact-link', href: '#', label: 'Contact' }, +]; + +/** + * Layout Stories - Header + */ +export const Header = Template.bind({}); +Header.args = { + nav, + photo: 'http://placeimg.com/640/480/people', + title: 'Website title', + // @ts-ignore - Needed because of the placeholder image. + unoptimized: true, +}; diff --git a/src/components/organisms/layout/header.test.tsx b/src/components/organisms/layout/header.test.tsx new file mode 100644 index 0000000..05baaec --- /dev/null +++ b/src/components/organisms/layout/header.test.tsx @@ -0,0 +1,27 @@ +import { render, screen } from '@test-utils'; +import Header from './header'; + +const nav = [ + { id: 'home-link', href: '#', label: 'Home' }, + { id: 'blog-link', href: '#', label: 'Blog' }, + { id: 'cv-link', href: '#', label: 'CV' }, + { id: 'contact-link', href: '#', label: 'Contact' }, +]; + +const photo = 'http://placeimg.com/640/480/nightlife'; + +const title = 'Assumenda quis quod'; + +describe('Header', () => { + it('renders the website title', () => { + render(
); + expect( + screen.getByRole('heading', { level: 1, name: title }) + ).toBeInTheDocument(); + }); + + it('renders the main nav', () => { + render(
); + expect(screen.getByRole('navigation')).toBeInTheDocument(); + }); +}); diff --git a/src/components/organisms/layout/header.tsx b/src/components/organisms/layout/header.tsx new file mode 100644 index 0000000..f6ebc9c --- /dev/null +++ b/src/components/organisms/layout/header.tsx @@ -0,0 +1,35 @@ +import Branding, { + type BrandingProps, +} from '@components/molecules/layout/branding'; +import { FC } from 'react'; +import Toolbar, { type ToolbarProps } from '../toolbar/toolbar'; +import styles from './header.module.scss'; + +export type HeaderProps = BrandingProps & { + /** + * Set additional classnames to the header element. + */ + className?: string; + /** + * The main nav items. + */ + nav: ToolbarProps['nav']; +}; + +/** + * Header component + * + * Render the website header. + */ +const Header: FC = ({ className, nav, ...props }) => { + return ( +
+
+ + +
+
+ ); +}; + +export default Header; -- cgit v1.2.3 From 54883bb5c36cf21462a421605a709fdd6f04b150 Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Wed, 18 May 2022 11:44:37 +0200 Subject: chore: add branding animation --- src/components/molecules/images/flipping-logo.tsx | 20 ++-- .../molecules/layout/branding.module.scss | 125 +++++++++++++++------ src/components/molecules/layout/branding.tsx | 26 ++++- .../organisms/toolbar/toolbar-items.module.scss | 1 + .../organisms/toolbar/toolbar.module.scss | 16 ++- src/utils/hooks/use-styles.tsx | 29 +++++ 6 files changed, 171 insertions(+), 46 deletions(-) create mode 100644 src/utils/hooks/use-styles.tsx (limited to 'src/components/molecules/images/flipping-logo.tsx') diff --git a/src/components/molecules/images/flipping-logo.tsx b/src/components/molecules/images/flipping-logo.tsx index 47e54ab..1099d53 100644 --- a/src/components/molecules/images/flipping-logo.tsx +++ b/src/components/molecules/images/flipping-logo.tsx @@ -1,6 +1,6 @@ import Logo, { type LogoProps } from '@components/atoms/images/logo'; import Image, { type ImageProps } from 'next/image'; -import { FC } from 'react'; +import { ForwardedRef, forwardRef, ForwardRefRenderFunction } from 'react'; import styles from './flipping-logo.module.scss'; export type FlippingLogoProps = { @@ -27,15 +27,15 @@ export type FlippingLogoProps = { * * Render a logo and a photo with a flipping effect. */ -const FlippingLogo: FC = ({ - className = '', - altText, - logoTitle, - photo, - ...props -}) => { +const FlippingLogo: ForwardRefRenderFunction< + HTMLDivElement, + FlippingLogoProps +> = ( + { className = '', altText, logoTitle, photo, ...props }, + ref: ForwardedRef +) => { return ( -
+
= ({ ); }; -export default FlippingLogo; +export default forwardRef(FlippingLogo); diff --git a/src/components/molecules/layout/branding.module.scss b/src/components/molecules/layout/branding.module.scss index aa18002..6121fa1 100644 --- a/src/components/molecules/layout/branding.module.scss +++ b/src/components/molecules/layout/branding.module.scss @@ -1,48 +1,105 @@ @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: - var(--logo-size, fun.convert-px(100)) - minmax(0, 1fr); - grid-template-rows: 1fr min-content; - align-items: center; - column-gap: var(--spacing-sm); -} + grid-template-columns: minmax(0, 1fr); + justify-items: center; + width: 100%; -.logo { - grid-row: span 2; -} + @include mix.media("screen") { + @include mix.dimensions("2xs") { + grid-template-columns: + var(--logo-size, fun.convert-px(100)) + minmax(0, 1fr); + grid-template-rows: 1fr min-content; + align-items: center; + justify-items: left; + column-gap: var(--spacing-sm); + width: unset; + } + } -.title { - font-size: var(--font-size-2xl); -} + .logo { + grid-row: span 2; + margin-bottom: var(--spacing-sm); -.baseline { - color: var(--color-fg-light); -} + @include mix.media("screen") { + @include mix.dimensions("2xs") { + margin-bottom: 0; + } + } + } + + .title { + font-size: clamp(var(--font-size-xl), 8vw, var(--font-size-2xl)); + text-align: center; -.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%; + @include typing-animation; } - &:focus { - color: var(--color-primary-light); + .baseline { + color: var(--color-fg-light); + font-size: var(--font-size-lg); + text-align: center; + + @include typing-animation; } - &:active { - background-size: 0 100%; - color: var(--color-primary-dark); + .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.tsx b/src/components/molecules/layout/branding.tsx index 423c54f..9a82a74 100644 --- a/src/components/molecules/layout/branding.tsx +++ b/src/components/molecules/layout/branding.tsx @@ -1,6 +1,7 @@ import Heading from '@components/atoms/headings/heading'; +import useStyles from '@utils/hooks/use-styles'; import Link from 'next/link'; -import { FC } from 'react'; +import { FC, useRef } from 'react'; import { useIntl } from 'react-intl'; import FlippingLogo, { type FlippingLogoProps } from '../images/flipping-logo'; import styles from './branding.module.scss'; @@ -37,6 +38,9 @@ const Branding: FC = ({ withLink = false, ...props }) => { + const baselineRef = useRef(null); + const logoRef = useRef(null); + const titleRef = useRef(null); const intl = useIntl(); const altText = intl.formatMessage( { @@ -55,6 +59,23 @@ const Branding: FC = ({ { website: title } ); + 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, + }); + useStyles({ + property: 'animation', + styles: 'flip-logo 9s ease-in 0s 1', + target: logoRef, + }); + return (
= ({ altText={altText} logoTitle={logoTitle} photo={photo} + ref={logoRef} {...props} /> = ({ level={1} withMargin={false} className={styles.title} + ref={titleRef} > {withLink ? ( @@ -84,6 +107,7 @@ const Branding: FC = ({ level={4} withMargin={false} className={styles.baseline} + ref={baselineRef} > {baseline} diff --git a/src/components/organisms/toolbar/toolbar-items.module.scss b/src/components/organisms/toolbar/toolbar-items.module.scss index fd526d6..c970b71 100644 --- a/src/components/organisms/toolbar/toolbar-items.module.scss +++ b/src/components/organisms/toolbar/toolbar-items.module.scss @@ -26,6 +26,7 @@ @include mix.media("screen") { @include mix.dimensions(null, "sm") { + position: fixed; left: 0; right: 0; } diff --git a/src/components/organisms/toolbar/toolbar.module.scss b/src/components/organisms/toolbar/toolbar.module.scss index cda9b37..4bcabcb 100644 --- a/src/components/organisms/toolbar/toolbar.module.scss +++ b/src/components/organisms/toolbar/toolbar.module.scss @@ -23,6 +23,18 @@ box-shadow: 0 fun.convert-px(-2) fun.convert-px(3) fun.convert-px(-1) var(--color-shadow-dark); + :global { + animation: slide-in-from-bottom 0.8s ease-in-out 0s 1; + } + + @include mix.media("screen") { + @include mix.dimensions("sm") { + :global { + animation: slide-in-from-top 1s ease-in-out 0s 1; + } + } + } + .modal { &--search, &--settings { @@ -52,8 +64,10 @@ } .tooltip { + padding: calc(var(--title-height) / 2 + var(--spacing-2xs)) + var(--spacing-2xs) var(--spacing-2xs); top: unset; - bottom: calc(100% + var(--spacing-xs)); + bottom: calc(100% + var(--spacing-2xs)); transform-origin: bottom right; } } diff --git a/src/utils/hooks/use-styles.tsx b/src/utils/hooks/use-styles.tsx new file mode 100644 index 0000000..d47e9fb --- /dev/null +++ b/src/utils/hooks/use-styles.tsx @@ -0,0 +1,29 @@ +import { RefObject, useEffect } from 'react'; + +export type UseStylesProps = { + /** + * A property name or a CSS variable. + */ + property: string; + /** + * The styles. + */ + styles: string; + /** + * A targeted element reference. + */ + target: RefObject; +}; + +/** + * Add styles to an element using a React reference. + * + * @param {UseStylesProps} props - An object with property, styles and target. + */ +const useStyles = ({ property, styles, target }: UseStylesProps) => { + useEffect(() => { + if (target.current) target.current.style.setProperty(property, styles); + }, [property, styles, target]); +}; + +export default useStyles; -- cgit v1.2.3