From 0b3146f7278929c4d1b33dd8f94f34e351e5e5a9 Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Fri, 8 Apr 2022 22:36:24 +0200 Subject: chore: add a Settings modal component --- .../organisms/modals/settings-modal.module.scss | 14 ++++++ .../organisms/modals/settings-modal.stories.tsx | 31 +++++++++++++ .../organisms/modals/settings-modal.test.tsx | 34 +++++++++++++++ src/components/organisms/modals/settings-modal.tsx | 51 ++++++++++++++++++++++ 4 files changed, 130 insertions(+) create mode 100644 src/components/organisms/modals/settings-modal.module.scss create mode 100644 src/components/organisms/modals/settings-modal.stories.tsx create mode 100644 src/components/organisms/modals/settings-modal.test.tsx create mode 100644 src/components/organisms/modals/settings-modal.tsx (limited to 'src/components/organisms') diff --git a/src/components/organisms/modals/settings-modal.module.scss b/src/components/organisms/modals/settings-modal.module.scss new file mode 100644 index 0000000..f17c9b3 --- /dev/null +++ b/src/components/organisms/modals/settings-modal.module.scss @@ -0,0 +1,14 @@ +.wrapper { + max-width: 30ch; + + .label { + margin-right: auto; + } +} + +.tooltip { + width: 120%; + top: calc(100% + var(--spacing-sm)); + right: -10%; + transform-origin: top right; +} diff --git a/src/components/organisms/modals/settings-modal.stories.tsx b/src/components/organisms/modals/settings-modal.stories.tsx new file mode 100644 index 0000000..c19a6d7 --- /dev/null +++ b/src/components/organisms/modals/settings-modal.stories.tsx @@ -0,0 +1,31 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { IntlProvider } from 'react-intl'; +import SettingsModal from './settings-modal'; + +export default { + title: 'Organisms/Modals', + component: SettingsModal, + argTypes: { + className: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the modal wrapper.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + + + +); + +export const Settings = Template.bind({}); diff --git a/src/components/organisms/modals/settings-modal.test.tsx b/src/components/organisms/modals/settings-modal.test.tsx new file mode 100644 index 0000000..44695d7 --- /dev/null +++ b/src/components/organisms/modals/settings-modal.test.tsx @@ -0,0 +1,34 @@ +import { render, screen } from '@test-utils'; +import SettingsModal from './settings-modal'; + +jest.mock('next/dynamic', () => () => 'dynamic-import'); + +describe('SettingsModal', () => { + it('renders a theme toggle setting', () => { + render(); + expect( + screen.getByRole('checkbox', { name: /^Theme:/i }) + ).toBeInTheDocument(); + }); + + it('renders a code blocks toggle setting', () => { + render(); + expect( + screen.getByRole('checkbox', { name: /^Code blocks:/i }) + ).toBeInTheDocument(); + }); + + it('renders a motion setting', () => { + render(); + expect( + screen.getByRole('checkbox', { name: /^Animations:/i }) + ).toBeInTheDocument(); + }); + + it('renders a Ackee setting', () => { + render(); + expect( + screen.getByRole('combobox', { name: /^Tracking:/i }) + ).toBeInTheDocument(); + }); +}); diff --git a/src/components/organisms/modals/settings-modal.tsx b/src/components/organisms/modals/settings-modal.tsx new file mode 100644 index 0000000..0fac332 --- /dev/null +++ b/src/components/organisms/modals/settings-modal.tsx @@ -0,0 +1,51 @@ +import Form from '@components/atoms/forms/form'; +import AckeeSelect from '@components/molecules/forms/ackee-select'; +import MotionToggle from '@components/molecules/forms/motion-toggle'; +import PrismThemeToggle from '@components/molecules/forms/prism-theme-toggle'; +import ThemeToggle from '@components/molecules/forms/theme-toggle'; +import Modal from '@components/molecules/modals/modal'; +import { VFC } from 'react'; +import { useIntl } from 'react-intl'; +import styles from './settings-modal.module.scss'; + +export type SettingsModalProps = { + /** + * Set additional classnames to modal wrapper. + */ + className?: string; +}; + +/** + * SettingsModal component + * + * Render a modal with settings options. + */ +const SettingsModal: VFC = ({ className }) => { + const intl = useIntl(); + const title = intl.formatMessage({ + defaultMessage: 'Settings', + description: 'SettingsModal: title', + id: 'gPfT/K', + }); + + return ( + +
null}> + + + + + +
+ ); +}; + +export default SettingsModal; -- cgit v1.2.3 From 27ff3104aabed240470d351bda02095d8169501f Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Tue, 12 Apr 2022 16:09:21 +0200 Subject: chore: add a Summary component --- .../atoms/buttons/button-link.stories.tsx | 13 +++ src/components/atoms/buttons/button-link.tsx | 14 ++- .../molecules/images/responsive-image.tsx | 14 ++- .../organisms/layout/summary.module.scss | 84 +++++++++++++++ .../organisms/layout/summary.stories.tsx | 114 +++++++++++++++++++++ src/components/organisms/layout/summary.test.tsx | 85 +++++++++++++++ src/components/organisms/layout/summary.tsx | 105 +++++++++++++++++++ 7 files changed, 424 insertions(+), 5 deletions(-) create mode 100644 src/components/organisms/layout/summary.module.scss create mode 100644 src/components/organisms/layout/summary.stories.tsx create mode 100644 src/components/organisms/layout/summary.test.tsx create mode 100644 src/components/organisms/layout/summary.tsx (limited to 'src/components/organisms') diff --git a/src/components/atoms/buttons/button-link.stories.tsx b/src/components/atoms/buttons/button-link.stories.tsx index 6fe786b..92b7521 100644 --- a/src/components/atoms/buttons/button-link.stories.tsx +++ b/src/components/atoms/buttons/button-link.stories.tsx @@ -28,6 +28,19 @@ export default { required: true, }, }, + className: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the button link.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, kind: { control: { type: 'select', diff --git a/src/components/atoms/buttons/button-link.tsx b/src/components/atoms/buttons/button-link.tsx index 81229c8..77a7f7b 100644 --- a/src/components/atoms/buttons/button-link.tsx +++ b/src/components/atoms/buttons/button-link.tsx @@ -7,6 +7,10 @@ export type ButtonLinkProps = { * ButtonLink accessible label. */ 'aria-label'?: string; + /** + * Set additional classnames to the button link. + */ + className?: string; /** * True if it is an external link. Default: false. */ @@ -18,7 +22,7 @@ export type ButtonLinkProps = { /** * ButtonLink shape. Default: rectangle. */ - shape?: 'rectangle' | 'square'; + shape?: 'circle' | 'rectangle' | 'square'; /** * Define an URL as target. */ @@ -32,6 +36,7 @@ export type ButtonLinkProps = { */ const ButtonLink: FC = ({ children, + className, target, kind = 'secondary', shape = 'rectangle', @@ -44,14 +49,17 @@ const ButtonLink: FC = ({ return external ? ( {children} ) : ( - + {children} diff --git a/src/components/molecules/images/responsive-image.tsx b/src/components/molecules/images/responsive-image.tsx index 9f96f18..3d54e95 100644 --- a/src/components/molecules/images/responsive-image.tsx +++ b/src/components/molecules/images/responsive-image.tsx @@ -12,6 +12,10 @@ type ResponsiveImageProps = Omit & { * A figure caption. */ caption?: string; + /** + * Set additional classnames to the figure wrapper. + */ + className?: string; /** * The image height. */ @@ -34,16 +38,22 @@ type ResponsiveImageProps = Omit & { const ResponsiveImage: VFC = ({ alt, caption, + className = '', layout, objectFit, target, ...props }) => { return ( -
+
{target ? ( - {alt} + {alt} {caption && (
{caption}
)} diff --git a/src/components/organisms/layout/summary.module.scss b/src/components/organisms/layout/summary.module.scss new file mode 100644 index 0000000..5da0a18 --- /dev/null +++ b/src/components/organisms/layout/summary.module.scss @@ -0,0 +1,84 @@ +@use "@styles/abstracts/functions" as fun; +@use "@styles/abstracts/mixins" as mix; + +.wrapper { + @include mix.media("screen") { + @include mix.dimensions("xs") { + padding: var(--spacing-sm) var(--spacing-sm) var(--spacing-md); + border: fun.convert-px(1) solid var(--color-primary-dark); + border-radius: fun.convert-px(3); + box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1) 0 + var(--color-shadow), + fun.convert-px(3) fun.convert-px(3) fun.convert-px(3) fun.convert-px(-1) + var(--color-shadow-light), + fun.convert-px(5) fun.convert-px(5) fun.convert-px(7) fun.convert-px(-1) + var(--color-shadow-light); + } + + @include mix.dimensions("sm") { + display: grid; + grid-template-columns: minmax(0, 3fr) minmax(0, 1fr); + grid-template-rows: repeat(3, max-content); + column-gap: var(--spacing-md); + } + } +} + +.cover { + width: auto; + max-height: fun.convert-px(100); + max-width: 100%; + border: fun.convert-px(1) solid var(--color-border); + + @include mix.media("screen") { + @include mix.dimensions("sm") { + grid-column: 2; + grid-row: 1; + } + } +} + +.header { + @include mix.media("screen") { + @include mix.dimensions("sm") { + grid-column: 1; + grid-row: 1; + align-self: center; + } + } +} + +.body { + @include mix.media("screen") { + @include mix.dimensions("sm") { + grid-column: 1; + grid-row: 2; + } + } +} + +.footer { + @include mix.media("screen") { + @include mix.dimensions("sm") { + grid-column: 2; + grid-row: 2 / 4; + } + } +} + +.title { + background: none; + text-shadow: none; +} + +.read-more { + display: flex; + flex-flow: row nowrap; + column-gap: var(--spacing-xs); + width: max-content; + margin: var(--spacing-sm) 0; +} + +.meta { + font-size: var(--font-size-sm); +} diff --git a/src/components/organisms/layout/summary.stories.tsx b/src/components/organisms/layout/summary.stories.tsx new file mode 100644 index 0000000..5214d70 --- /dev/null +++ b/src/components/organisms/layout/summary.stories.tsx @@ -0,0 +1,114 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { IntlProvider } from 'react-intl'; +import SummaryComponent from './summary'; + +export default { + title: 'Organisms/Layout', + component: SummaryComponent, + args: { + titleLevel: 2, + }, + argTypes: { + cover: { + description: 'The cover data.', + table: { + category: 'Options', + }, + type: { + name: 'object', + required: false, + value: {}, + }, + }, + excerpt: { + control: { + type: 'text', + }, + description: 'The page excerpt.', + type: { + name: 'string', + required: true, + }, + }, + meta: { + description: 'The page metadata.', + type: { + name: 'object', + required: true, + value: {}, + }, + }, + title: { + control: { + type: 'text', + }, + description: 'The page title', + type: { + name: 'string', + required: true, + }, + }, + titleLevel: { + control: { + type: 'number', + }, + description: 'The page title level (hn)', + table: { + category: 'Options', + defaultValue: { summary: 2 }, + }, + type: { + name: 'number', + required: false, + }, + }, + url: { + control: { + type: 'text', + }, + description: 'The page url.', + type: { + name: 'string', + required: true, + }, + }, + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + + + +); + +const meta = { + publication: { name: 'Published on:', value: 'April 11th 2022' }, + readingTime: { name: 'Reading time:', value: '5 minutes' }, + categories: { + name: 'Categories:', + value: [ + + Cat 1 + , + + Cat 2 + , + ], + }, + comments: { name: 'Comments:', value: '1 comment' }, +}; + +export const Summary = Template.bind({}); +Summary.args = { + cover: { + alt: 'A cover', + height: 480, + url: 'http://placeimg.com/640/480', + width: 640, + }, + excerpt: + 'Perspiciatis quasi libero nemo non eligendi nam minima. Deleniti expedita tempore. Praesentium explicabo molestiae eaque consectetur vero. Quae nostrum quisquam similique. Ut hic est quas ut esse quisquam nobis.', + meta, + title: 'Odio odit necessitatibus', + url: '#', +}; diff --git a/src/components/organisms/layout/summary.test.tsx b/src/components/organisms/layout/summary.test.tsx new file mode 100644 index 0000000..ce87c0c --- /dev/null +++ b/src/components/organisms/layout/summary.test.tsx @@ -0,0 +1,85 @@ +import { render, screen } from '@test-utils'; +import Summary from './summary'; + +const cover = { + alt: 'A cover', + height: 480, + url: 'http://placeimg.com/640/480', + width: 640, +}; + +const excerpt = + 'Perspiciatis quasi libero nemo non eligendi nam minima. Deleniti expedita tempore. Praesentium explicabo molestiae eaque consectetur vero. Quae nostrum quisquam similique. Ut hic est quas ut esse quisquam nobis.'; + +const meta = { + publication: { name: 'Published on:', value: 'April 11th 2022' }, + readingTime: { name: 'Reading time:', value: '5 minutes' }, + categories: { + name: 'Categories:', + value: [ + + Cat 1 + , + + Cat 2 + , + ], + }, + comments: { name: 'Comments:', value: '1 comment' }, +}; + +const title = 'Odio odit necessitatibus'; + +const url = '#'; + +describe('Summary', () => { + it('renders a title wrapped in a h2 element', () => { + render( + + ); + expect( + screen.getByRole('heading', { level: 2, name: title }) + ).toBeInTheDocument(); + }); + + it('renders an excerpt', () => { + render(); + expect(screen.getByText(excerpt)).toBeInTheDocument(); + }); + + it('renders a cover', () => { + render( + + ); + expect(screen.getByRole('img', { name: cover.alt })).toBeInTheDocument(); + }); + + it('renders a link to the full post', () => { + render(); + expect(screen.getByRole('link', { name: title })).toBeInTheDocument(); + }); + + it('renders a read more link', () => { + render(); + expect( + screen.getByRole('link', { name: `Read more about ${title}` }) + ).toBeInTheDocument(); + }); + + it('renders some meta', () => { + render(); + expect(screen.getByText(meta.publication.name)).toBeInTheDocument(); + }); +}); diff --git a/src/components/organisms/layout/summary.tsx b/src/components/organisms/layout/summary.tsx new file mode 100644 index 0000000..3624e5d --- /dev/null +++ b/src/components/organisms/layout/summary.tsx @@ -0,0 +1,105 @@ +import ButtonLink from '@components/atoms/buttons/button-link'; +import Heading, { type HeadingLevel } from '@components/atoms/headings/heading'; +import Arrow from '@components/atoms/icons/arrow'; +import Link from '@components/atoms/links/link'; +import ResponsiveImage from '@components/molecules/images/responsive-image'; +import Meta, { type MetaItem } from '@components/molecules/layout/meta'; +import { VFC } from 'react'; +import { useIntl } from 'react-intl'; +import styles from './summary.module.scss'; + +export type Cover = { + alt: string; + height: number; + url: string; + width: number; +}; + +export type RequiredMetaKey = 'publication'; + +export type RequiredMeta = { + [key in RequiredMetaKey]: MetaItem; +}; + +export type OptionalMetaKey = + | 'author' + | 'categories' + | 'comments' + | 'readingTime' + | 'update'; + +export type OptionalMeta = { + [key in OptionalMetaKey]?: MetaItem; +}; + +export type Meta = RequiredMeta & OptionalMeta; + +export type SummaryProps = { + cover?: Cover; + excerpt: string; + meta: Meta; + title: string; + titleLevel?: HeadingLevel; + url: string; +}; + +/** + * Summary component + * + * Render a page summary. + */ +const Summary: VFC = ({ + cover, + excerpt, + meta, + title, + titleLevel = 2, + url, +}) => { + const intl = useIntl(); + + return ( +
+ {cover && ( + + )} +
+ + + {title} + + +
+
+ {excerpt} + + {intl.formatMessage( + { + defaultMessage: 'Read more about {title}', + description: 'Summary: read more link', + id: 'Zpgv+f', + }, + { + title, + a11y: (chunks: string) => ( + {chunks} + ), + } + )} + + +
+
+ +
+
+ ); +}; + +export default Summary; -- cgit v1.2.3 From 017d01680a933897df6ddd11d2e081730756250b Mon Sep 17 00:00:00 2001 From: Armand Philippot Date: Tue, 12 Apr 2022 16:55:59 +0200 Subject: chore: add a Footer component --- .../molecules/buttons/back-to-top.module.scss | 10 +-- src/components/molecules/buttons/back-to-top.tsx | 7 +- src/components/organisms/layout/footer.module.scss | 39 ++++++++++++ src/components/organisms/layout/footer.stories.tsx | 74 ++++++++++++++++++++++ src/components/organisms/layout/footer.test.tsx | 33 ++++++++++ src/components/organisms/layout/footer.tsx | 52 +++++++++++++++ 6 files changed, 210 insertions(+), 5 deletions(-) create mode 100644 src/components/organisms/layout/footer.module.scss create mode 100644 src/components/organisms/layout/footer.stories.tsx create mode 100644 src/components/organisms/layout/footer.test.tsx create mode 100644 src/components/organisms/layout/footer.tsx (limited to 'src/components/organisms') diff --git a/src/components/molecules/buttons/back-to-top.module.scss b/src/components/molecules/buttons/back-to-top.module.scss index 1abf1f6..9721bff 100644 --- a/src/components/molecules/buttons/back-to-top.module.scss +++ b/src/components/molecules/buttons/back-to-top.module.scss @@ -1,21 +1,24 @@ @use "@styles/abstracts/functions" as fun; .wrapper { - a { + .link { + width: clamp(#{fun.convert-px(44)}, 6vw, #{fun.convert-px(55)}); + height: clamp(#{fun.convert-px(44)}, 6vw, #{fun.convert-px(55)}); + svg { width: 100%; } :global { .arrow-head { - transform: translateY(30%) scale(1.1); + transform: translateY(30%) scale(1.2); transition: all 0.45s ease-in-out 0s; } .arrow-bar { opacity: 0; transform: translateY(30%) scaleY(0); - transition: transform 0.4s ease-in-out 0s, opacity 0.1s linear 0.4s; + transition: transform 0.45s ease-in-out 0s, opacity 0.1s linear 0.2s; } } @@ -29,7 +32,6 @@ .arrow-bar { opacity: 1; transform: translateY(0) scaleY(1); - transition: transform 0.45s ease-in-out 0s; } } diff --git a/src/components/molecules/buttons/back-to-top.tsx b/src/components/molecules/buttons/back-to-top.tsx index 56c5247..8a52231 100644 --- a/src/components/molecules/buttons/back-to-top.tsx +++ b/src/components/molecules/buttons/back-to-top.tsx @@ -30,7 +30,12 @@ const BackToTop: VFC = ({ className = '', target }) => { return (
- +
diff --git a/src/components/organisms/layout/footer.module.scss b/src/components/organisms/layout/footer.module.scss new file mode 100644 index 0000000..a809d3c --- /dev/null +++ b/src/components/organisms/layout/footer.module.scss @@ -0,0 +1,39 @@ +@use "@styles/abstracts/functions" as fun; +@use "@styles/abstracts/mixins" as mix; + +.wrapper { + display: flex; + flex-flow: column wrap; + gap: var(--spacing-xs); + place-items: center; + place-content: center; + padding: var(--spacing-md) 0 calc(var(--toolbar-size) + var(--spacing-md)); + + @include mix.media("screen") { + @include mix.dimensions("sm") { + flex-flow: row wrap; + font-size: var(--font-size-sm); + } + } +} + +.nav { + display: flex; + flex-flow: row wrap; + + @include mix.media("screen") { + @include mix.dimensions("sm") { + &::before { + content: "\2022"; + margin-right: var(--spacing-2xs); + } + } + } +} + +.back-to-top { + position: fixed; + bottom: calc(var(--toolbar-size, 0px) + var(--spacing-md)); + right: var(--spacing-md); + transition: all 0.4s ease-in 0s; +} diff --git a/src/components/organisms/layout/footer.stories.tsx b/src/components/organisms/layout/footer.stories.tsx new file mode 100644 index 0000000..2ce7ee1 --- /dev/null +++ b/src/components/organisms/layout/footer.stories.tsx @@ -0,0 +1,74 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { IntlProvider } from 'react-intl'; +import FooterComponent from './footer'; + +export default { + title: 'Organisms/Layout', + component: FooterComponent, + argTypes: { + className: { + control: { + type: 'text', + }, + description: 'Set additional classnames to the footer element.', + table: { + category: 'Styles', + }, + type: { + name: 'string', + required: false, + }, + }, + copyright: { + description: 'The copyright information.', + type: { + name: 'object', + required: true, + value: {}, + }, + }, + navItems: { + description: 'The footer nav items.', + table: { + category: 'Options', + }, + type: { + name: 'object', + required: false, + value: {}, + }, + }, + topId: { + control: { + type: 'text', + }, + description: + 'An element id (without hashtag) used as target by back to top button.', + type: { + name: 'string', + required: true, + }, + }, + }, +} as ComponentMeta; + +const Template: ComponentStory = (args) => ( + + + +); + +const copyright = { + dates: { start: '2017', end: '2022' }, + owner: 'Lorem ipsum', + icon: 'CC', +}; + +const navItems = [{ id: 'legal-notice', href: '#', label: 'Legal notice' }]; + +export const Footer = Template.bind({}); +Footer.args = { + copyright, + navItems, + topId: 'top', +}; diff --git a/src/components/organisms/layout/footer.test.tsx b/src/components/organisms/layout/footer.test.tsx new file mode 100644 index 0000000..bc23732 --- /dev/null +++ b/src/components/organisms/layout/footer.test.tsx @@ -0,0 +1,33 @@ +import { render, screen } from '@test-utils'; +import Footer, { type FooterProps } from './footer'; + +const copyright: FooterProps['copyright'] = { + dates: { start: '2017', end: '2022' }, + owner: 'Lorem ipsum', + icon: 'CC', +}; + +const navItems: FooterProps['navItems'] = [ + { id: 'legal-notice', href: '#', label: 'Legal notice' }, +]; + +describe('Footer', () => { + it('renders the website copyright', () => { + render(