aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-09-27 17:38:23 +0200
committerArmand Philippot <git@armandphilippot.com>2023-10-24 12:25:00 +0200
commit7255d25f6834a208c0ed44636356cc260f6ab6ba (patch)
tree88016a958190f766a3ac0ab4b77f4732e17502e8
parentba793e043e4d8515b1a9ea490ee2c5f92b1fd6c2 (diff)
refactor(components): rewrite Heading component
* remove `alignment` and `withMargin` props (consumer should handle that) * move styles to Sass placeholders to avoid repeats with headings coming from WordPress * refactor some other components that depend on Heading to avoid ESlint errors
-rw-r--r--src/components/atoms/heading/heading.module.scss27
-rw-r--r--src/components/atoms/heading/heading.stories.tsx (renamed from src/components/atoms/headings/heading.stories.tsx)58
-rw-r--r--src/components/atoms/heading/heading.test.tsx80
-rw-r--r--src/components/atoms/heading/heading.tsx57
-rw-r--r--src/components/atoms/heading/index.ts (renamed from src/components/atoms/headings/index.ts)0
-rw-r--r--src/components/atoms/headings/heading.module.scss69
-rw-r--r--src/components/atoms/headings/heading.test.tsx57
-rw-r--r--src/components/atoms/headings/heading.tsx93
-rw-r--r--src/components/atoms/index.ts2
-rw-r--r--src/components/atoms/layout/section/section.module.scss1
-rw-r--r--src/components/atoms/layout/section/section.stories.tsx2
-rw-r--r--src/components/atoms/modal/modal.stories.tsx4
-rw-r--r--src/components/atoms/modal/modal.test.tsx8
-rw-r--r--src/components/atoms/modal/modal.tsx10
-rw-r--r--src/components/molecules/buttons/heading-button.tsx17
-rw-r--r--src/components/molecules/layout/branding.tsx15
-rw-r--r--src/components/molecules/layout/card.module.scss5
-rw-r--r--src/components/molecules/layout/card.tsx8
-rw-r--r--src/components/molecules/layout/page-header.tsx41
-rw-r--r--src/components/organisms/forms/comment-form/comment-form.module.scss6
-rw-r--r--src/components/organisms/forms/comment-form/comment-form.tsx122
-rw-r--r--src/components/templates/page/page-layout.module.scss10
-rw-r--r--src/components/templates/page/page-layout.tsx179
-rw-r--r--src/pages/index.tsx63
-rw-r--r--src/pages/projets/[slug].tsx63
-rw-r--r--src/styles/abstracts/_placeholders.scss1
-rw-r--r--src/styles/abstracts/placeholders/_headings.scss54
-rw-r--r--src/styles/base/_typography.scss60
-rw-r--r--src/styles/pages/article.module.scss2
-rw-r--r--src/styles/pages/partials/_article-headings.scss37
30 files changed, 601 insertions, 550 deletions
diff --git a/src/components/atoms/heading/heading.module.scss b/src/components/atoms/heading/heading.module.scss
new file mode 100644
index 0000000..a2e339a
--- /dev/null
+++ b/src/components/atoms/heading/heading.module.scss
@@ -0,0 +1,27 @@
+@use "../../../styles/abstracts/placeholders";
+
+.heading {
+ &--1 {
+ @extend %h1;
+ }
+
+ &--2 {
+ @extend %h2;
+ }
+
+ &--3 {
+ @extend %h3;
+ }
+
+ &--4 {
+ @extend %h4;
+ }
+
+ &--5 {
+ @extend %h5;
+ }
+
+ &--6 {
+ @extend %h6;
+ }
+}
diff --git a/src/components/atoms/headings/heading.stories.tsx b/src/components/atoms/heading/heading.stories.tsx
index 4aa79c2..c5ac4a0 100644
--- a/src/components/atoms/headings/heading.stories.tsx
+++ b/src/components/atoms/heading/heading.stories.tsx
@@ -1,46 +1,16 @@
-import { ComponentMeta, ComponentStory } from '@storybook/react';
+import type { ComponentMeta, ComponentStory } from '@storybook/react';
import { Heading } from './heading';
/**
* Heading - Storybook Meta
*/
export default {
- title: 'Atoms/Typography/Headings',
+ title: 'Atoms/Headings',
component: Heading,
args: {
- alignment: 'left',
isFake: false,
- withMargin: true,
},
argTypes: {
- alignment: {
- control: {
- type: 'select',
- },
- description: 'The title alignment.',
- options: ['center', 'left'],
- table: {
- category: 'Options',
- defaultValue: { summary: 'left' },
- },
- type: {
- name: 'string',
- required: false,
- },
- },
- className: {
- control: {
- type: 'text',
- },
- description: 'Set additional classnames.',
- table: {
- category: 'Styles',
- },
- type: {
- name: 'string',
- required: false,
- },
- },
children: {
description: 'Heading body.',
type: {
@@ -48,16 +18,6 @@ export default {
required: true,
},
},
- id: {
- control: {
- type: 'text',
- },
- description: 'An unique id.',
- type: {
- name: 'string',
- required: false,
- },
- },
isFake: {
control: {
type: 'boolean',
@@ -84,20 +44,6 @@ export default {
required: true,
},
},
- withMargin: {
- control: {
- type: 'boolean',
- },
- description: 'Adds margin.',
- table: {
- category: 'Options',
- defaultValue: { summary: true },
- },
- type: {
- name: 'boolean',
- required: false,
- },
- },
},
} as ComponentMeta<typeof Heading>;
diff --git a/src/components/atoms/heading/heading.test.tsx b/src/components/atoms/heading/heading.test.tsx
new file mode 100644
index 0000000..39b23ad
--- /dev/null
+++ b/src/components/atoms/heading/heading.test.tsx
@@ -0,0 +1,80 @@
+import { describe, expect, it } from '@jest/globals';
+import { render, screen as rtlScreen } from '@testing-library/react';
+import { Heading } from './heading';
+
+describe('Heading', () => {
+ it('renders a h1', () => {
+ const body = 'provident';
+
+ render(<Heading level={1}>{body}</Heading>);
+
+ expect(rtlScreen.getByRole('heading', { level: 1 })).toHaveTextContent(
+ body
+ );
+ });
+
+ it('renders a h2', () => {
+ const body = 'iure';
+
+ render(<Heading level={2}>{body}</Heading>);
+
+ expect(rtlScreen.getByRole('heading', { level: 2 })).toHaveTextContent(
+ body
+ );
+ });
+
+ it('renders a h3', () => {
+ const body = 'ut';
+
+ render(<Heading level={3}>{body}</Heading>);
+
+ expect(rtlScreen.getByRole('heading', { level: 3 })).toHaveTextContent(
+ body
+ );
+ });
+
+ it('renders a h4', () => {
+ const body = 'dolor';
+
+ render(<Heading level={4}>{body}</Heading>);
+
+ expect(rtlScreen.getByRole('heading', { level: 4 })).toHaveTextContent(
+ body
+ );
+ });
+
+ it('renders a h5', () => {
+ const body = 'temporibus';
+
+ render(<Heading level={5}>{body}</Heading>);
+
+ expect(rtlScreen.getByRole('heading', { level: 5 })).toHaveTextContent(
+ body
+ );
+ });
+
+ it('renders a h6', () => {
+ const body = 'at';
+
+ render(<Heading level={6}>{body}</Heading>);
+
+ expect(rtlScreen.getByRole('heading', { level: 6 })).toHaveTextContent(
+ body
+ );
+ });
+
+ it('renders a fake heading', () => {
+ const body = 'dignissimos';
+
+ render(
+ <Heading isFake level={2}>
+ {body}
+ </Heading>
+ );
+
+ expect(
+ rtlScreen.queryByRole('heading', { level: 2 })
+ ).not.toBeInTheDocument();
+ expect(rtlScreen.getByText(body)).toHaveClass('heading--2');
+ });
+});
diff --git a/src/components/atoms/heading/heading.tsx b/src/components/atoms/heading/heading.tsx
new file mode 100644
index 0000000..6cdb578
--- /dev/null
+++ b/src/components/atoms/heading/heading.tsx
@@ -0,0 +1,57 @@
+import {
+ type ForwardedRef,
+ forwardRef,
+ type ForwardRefRenderFunction,
+ type HTMLAttributes,
+ type ReactNode,
+} from 'react';
+import styles from './heading.module.scss';
+
+// eslint-disable-next-line @typescript-eslint/no-magic-numbers
+export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
+
+export type HeadingProps = HTMLAttributes<HTMLHeadingElement> & {
+ /**
+ * The heading body.
+ */
+ children: ReactNode;
+ /**
+ * Use an heading element or only its styles.
+ *
+ * @default false
+ */
+ isFake?: boolean;
+ /**
+ * HTML heading level.
+ */
+ level: HeadingLevel;
+};
+
+const HeadingWithRef: ForwardRefRenderFunction<
+ HTMLHeadingElement | HTMLParagraphElement,
+ HeadingProps
+> = (
+ { children, className = '', isFake = false, level, ...props },
+ ref: ForwardedRef<HTMLHeadingElement | HTMLParagraphElement>
+) => {
+ const HeadingTag = `h${level}` as const;
+ const levelClass = styles[`heading--${level}`];
+ const headingClass = `${levelClass} ${className}`;
+
+ return isFake ? (
+ <p {...props} className={headingClass} ref={ref}>
+ {children}
+ </p>
+ ) : (
+ <HeadingTag {...props} className={headingClass} ref={ref}>
+ {children}
+ </HeadingTag>
+ );
+};
+
+/**
+ * Heading component.
+ *
+ * Render an HTML heading element or a paragraph with heading styles.
+ */
+export const Heading = forwardRef(HeadingWithRef);
diff --git a/src/components/atoms/headings/index.ts b/src/components/atoms/heading/index.ts
index 3de265c..3de265c 100644
--- a/src/components/atoms/headings/index.ts
+++ b/src/components/atoms/heading/index.ts
diff --git a/src/components/atoms/headings/heading.module.scss b/src/components/atoms/headings/heading.module.scss
deleted file mode 100644
index 1c898e6..0000000
--- a/src/components/atoms/headings/heading.module.scss
+++ /dev/null
@@ -1,69 +0,0 @@
-@use "../../../styles/abstracts/functions" as fun;
-
-.heading {
- color: var(--color-primary-dark);
- font-family: var(--font-family-secondary);
- letter-spacing: 0.01ex;
-
- &--regular {
- margin-bottom: 0;
- margin-top: 0;
- }
-
- &--left {
- text-align: left;
- }
-
- &--center {
- width: fit-content;
- margin-left: auto;
- margin-right: auto;
- }
-
- &--margin {
- margin-top: 0;
- margin-bottom: var(--spacing-sm);
-
- & + & {
- margin-top: var(--spacing-md);
- }
- }
-
- &--1 {
- font-size: var(--font-size-3xl);
- font-weight: 500;
- }
-
- &--2 {
- padding-bottom: fun.convert-px(3);
- background: linear-gradient(
- to top,
- var(--color-primary-dark) 0.3rem,
- transparent 0.3rem
- )
- 0 0 / 3rem 100% no-repeat;
- font-size: var(--font-size-2xl);
- font-weight: 500;
- text-shadow: fun.convert-px(1) fun.convert-px(1) 0 var(--color-shadow-light);
- }
-
- &--3 {
- font-size: var(--font-size-xl);
- font-weight: 500;
- }
-
- &--4 {
- font-size: var(--font-size-lg);
- font-weight: 500;
- }
-
- &--5 {
- font-size: var(--font-size-md);
- font-weight: 600;
- }
-
- &--6 {
- font-size: var(--font-size-md);
- font-weight: 500;
- }
-}
diff --git a/src/components/atoms/headings/heading.test.tsx b/src/components/atoms/headings/heading.test.tsx
deleted file mode 100644
index 61d7f8e..0000000
--- a/src/components/atoms/headings/heading.test.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { describe, expect, it } from '@jest/globals';
-import { render, screen } from '../../../../tests/utils';
-import { Heading } from './heading';
-
-describe('Heading', () => {
- it('renders a h1', () => {
- render(<Heading level={1}>Level 1</Heading>);
- expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent(
- 'Level 1'
- );
- });
-
- it('renders a h2', () => {
- render(<Heading level={2}>Level 2</Heading>);
- expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(
- 'Level 2'
- );
- });
-
- it('renders a h3', () => {
- render(<Heading level={3}>Level 3</Heading>);
- expect(screen.getByRole('heading', { level: 3 })).toHaveTextContent(
- 'Level 3'
- );
- });
-
- it('renders a h4', () => {
- render(<Heading level={4}>Level 4</Heading>);
- expect(screen.getByRole('heading', { level: 4 })).toHaveTextContent(
- 'Level 4'
- );
- });
-
- it('renders a h5', () => {
- render(<Heading level={5}>Level 5</Heading>);
- expect(screen.getByRole('heading', { level: 5 })).toHaveTextContent(
- 'Level 5'
- );
- });
-
- it('renders a h6', () => {
- render(<Heading level={6}>Level 6</Heading>);
- expect(screen.getByRole('heading', { level: 6 })).toHaveTextContent(
- 'Level 6'
- );
- });
-
- it('renders a text with heading styles', () => {
- render(
- <Heading isFake={true} level={2}>
- Fake heading
- </Heading>
- );
- expect(screen.queryByRole('heading', { level: 2 })).not.toBeInTheDocument();
- expect(screen.getByText('Fake heading')).toHaveClass('heading');
- });
-});
diff --git a/src/components/atoms/headings/heading.tsx b/src/components/atoms/headings/heading.tsx
deleted file mode 100644
index b1b6164..0000000
--- a/src/components/atoms/headings/heading.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import {
- createElement,
- ForwardedRef,
- forwardRef,
- ForwardRefRenderFunction,
- HTMLAttributes,
- ReactNode,
-} from 'react';
-import styles from './heading.module.scss';
-
-export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
-
-export type HeadingProps = HTMLAttributes<HTMLHeadingElement> & {
- /**
- * The title alignment. Default: left;
- */
- alignment?: 'center' | 'left';
- /**
- * The heading body.
- */
- children: ReactNode;
- /**
- * Use an heading element or only its styles. Default: false.
- */
- isFake?: boolean;
- /**
- * HTML heading level.
- */
- level: HeadingLevel;
- /**
- * Adds margin. Default: true.
- */
- withMargin?: boolean;
-};
-
-type TitleTagProps = Pick<HeadingProps, 'children' | 'className' | 'id'> & {
- tagName: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p';
-};
-
-const TitleTag = forwardRef<
- HTMLHeadingElement | HTMLParagraphElement,
- TitleTagProps
->(
- (
- { children, tagName, ...props },
- ref: ForwardedRef<HTMLHeadingElement | HTMLParagraphElement>
- ) => {
- return createElement(tagName, { ...props, ref }, children);
- }
-);
-TitleTag.displayName = 'TitleTag';
-
-const HeadingWithRef: ForwardRefRenderFunction<
- HTMLHeadingElement | HTMLParagraphElement,
- HeadingProps
-> = (
- {
- alignment = 'left',
- children,
- className = '',
- id,
- isFake = false,
- level,
- withMargin = true,
- ...props
- },
- ref: ForwardedRef<HTMLHeadingElement | HTMLParagraphElement>
-) => {
- const tagName = isFake ? 'p' : (`h${level}` as TitleTagProps['tagName']);
- const levelClass = `heading--${level}`;
- const alignmentClass = `heading--${alignment}`;
- const marginClass = withMargin ? 'heading--margin' : 'heading--regular';
- const headingClass = `${styles.heading} ${styles[levelClass]} ${styles[alignmentClass]} ${styles[marginClass]} ${className}`;
-
- return (
- <TitleTag
- {...props}
- className={headingClass}
- id={id}
- ref={ref}
- tagName={tagName}
- >
- {children}
- </TitleTag>
- );
-};
-
-/**
- * Heading component.
- *
- * Render an HTML heading element or a paragraph with heading styles.
- */
-export const Heading = forwardRef(HeadingWithRef);
diff --git a/src/components/atoms/index.ts b/src/components/atoms/index.ts
index d9cf865..672440c 100644
--- a/src/components/atoms/index.ts
+++ b/src/components/atoms/index.ts
@@ -1,6 +1,6 @@
export * from './buttons';
export * from './forms';
-export * from './headings';
+export * from './heading';
export * from './icons';
export * from './images';
export * from './layout';
diff --git a/src/components/atoms/layout/section/section.module.scss b/src/components/atoms/layout/section/section.module.scss
index 771b8e3..3da74a2 100644
--- a/src/components/atoms/layout/section/section.module.scss
+++ b/src/components/atoms/layout/section/section.module.scss
@@ -21,5 +21,6 @@
> * {
grid-column: 2;
+ margin-block: 0;
}
}
diff --git a/src/components/atoms/layout/section/section.stories.tsx b/src/components/atoms/layout/section/section.stories.tsx
index 0a3388b..e21209b 100644
--- a/src/components/atoms/layout/section/section.stories.tsx
+++ b/src/components/atoms/layout/section/section.stories.tsx
@@ -1,5 +1,5 @@
import type { ComponentMeta, ComponentStory } from '@storybook/react';
-import { Heading } from '../../headings';
+import { Heading } from '../../heading';
import { Section } from './section';
/**
diff --git a/src/components/atoms/modal/modal.stories.tsx b/src/components/atoms/modal/modal.stories.tsx
index d0c2f0b..0490a8f 100644
--- a/src/components/atoms/modal/modal.stories.tsx
+++ b/src/components/atoms/modal/modal.stories.tsx
@@ -1,6 +1,6 @@
-import { ComponentMeta, ComponentStory } from '@storybook/react';
+import type { ComponentMeta, ComponentStory } from '@storybook/react';
+import { Heading } from '../heading';
import { Modal } from './modal';
-import { Heading } from '../headings';
/**
* Switch - Storybook Meta
diff --git a/src/components/atoms/modal/modal.test.tsx b/src/components/atoms/modal/modal.test.tsx
index 6e7d29e..dfa4a88 100644
--- a/src/components/atoms/modal/modal.test.tsx
+++ b/src/components/atoms/modal/modal.test.tsx
@@ -1,6 +1,6 @@
import { describe, expect, it } from '@jest/globals';
-import { render, screen } from '../../../../tests/utils';
-import { Heading } from '../headings';
+import { render, screen as rtlScreen } from '../../../../tests/utils';
+import { Heading } from '../heading';
import { Modal } from './modal';
const title = 'A custom title';
@@ -16,11 +16,11 @@ describe('Modal', () => {
{children}
</Modal>
);
- expect(screen.getByRole('heading', { level })).toHaveTextContent(title);
+ expect(rtlScreen.getByRole('heading', { level })).toHaveTextContent(title);
});
it('renders the modal body', () => {
render(<Modal>{children}</Modal>);
- expect(screen.getByText(children)).toBeInTheDocument();
+ expect(rtlScreen.getByText(children)).toBeInTheDocument();
});
});
diff --git a/src/components/atoms/modal/modal.tsx b/src/components/atoms/modal/modal.tsx
index 78b4f6e..6f5506f 100644
--- a/src/components/atoms/modal/modal.tsx
+++ b/src/components/atoms/modal/modal.tsx
@@ -1,11 +1,11 @@
import {
- ForwardRefRenderFunction,
- HTMLAttributes,
- ReactElement,
- ReactNode,
+ type ForwardRefRenderFunction,
+ type HTMLAttributes,
+ type ReactElement,
+ type ReactNode,
forwardRef,
} from 'react';
-import { HeadingProps } from '../headings';
+import type { HeadingProps } from '../heading';
import styles from './modal.module.scss';
export type ModalProps = HTMLAttributes<HTMLDivElement> & {
diff --git a/src/components/molecules/buttons/heading-button.tsx b/src/components/molecules/buttons/heading-button.tsx
index 93ccdbe..97e2c84 100644
--- a/src/components/molecules/buttons/heading-button.tsx
+++ b/src/components/molecules/buttons/heading-button.tsx
@@ -1,4 +1,4 @@
-import { FC, SetStateAction } from 'react';
+import { useCallback, type FC, type SetStateAction } from 'react';
import { useIntl } from 'react-intl';
import { Heading, type HeadingProps, PlusMinus } from '../../atoms';
import styles from './heading-button.module.scss';
@@ -35,6 +35,7 @@ export const HeadingButton: FC<HeadingButtonProps> = ({
title,
}) => {
const intl = useIntl();
+ const btnClass = `${styles.wrapper} ${className}`;
const iconState = expanded ? 'minus' : 'plus';
const titlePrefix = expanded
? intl.formatMessage({
@@ -48,13 +49,15 @@ export const HeadingButton: FC<HeadingButtonProps> = ({
id: 'bcyOgC',
});
+ const toggleExpand = useCallback(
+ () => setExpanded((prev) => !prev),
+ [setExpanded]
+ );
+
return (
- <button
- className={`${styles.wrapper} ${className}`}
- onClick={() => setExpanded(!expanded)}
- type="button"
- >
- <Heading level={level} withMargin={false} className={styles.heading}>
+ <button className={btnClass} onClick={toggleExpand} type="button">
+ <Heading className={styles.heading} level={level}>
+ {/* eslint-disable-next-line react/jsx-no-literals -- SR class allowed */}
<span className="screen-reader-text">{titlePrefix} </span>
{title}
</Heading>
diff --git a/src/components/molecules/layout/branding.tsx b/src/components/molecules/layout/branding.tsx
index b105796..981da74 100644
--- a/src/components/molecules/layout/branding.tsx
+++ b/src/components/molecules/layout/branding.tsx
@@ -1,5 +1,5 @@
import Link from 'next/link';
-import { FC, useRef } from 'react';
+import { type FC, useRef } from 'react';
import { useIntl } from 'react-intl';
import { useStyles } from '../../../utils/hooks';
import { Heading } from '../../atoms';
@@ -90,7 +90,6 @@ export const Branding: FC<BrandingProps> = ({
className={styles.title}
isFake={!isHome}
level={1}
- withMargin={false}
ref={titleRef}
>
{withLink ? (
@@ -101,17 +100,11 @@ export const Branding: FC<BrandingProps> = ({
title
)}
</Heading>
- {baseline && (
- <Heading
- className={styles.baseline}
- isFake={true}
- level={4}
- withMargin={false}
- ref={baselineRef}
- >
+ {baseline ? (
+ <Heading className={styles.baseline} isFake level={4} ref={baselineRef}>
{baseline}
</Heading>
- )}
+ ) : null}
</div>
);
};
diff --git a/src/components/molecules/layout/card.module.scss b/src/components/molecules/layout/card.module.scss
index 8f9f4a5..31f6a4b 100644
--- a/src/components/molecules/layout/card.module.scss
+++ b/src/components/molecules/layout/card.module.scss
@@ -31,9 +31,8 @@
}
.title {
- flex: 1;
- margin-top: var(--spacing-sm);
- margin-bottom: var(--spacing-sm);
+ width: fit-content;
+ margin: var(--spacing-sm) auto;
}
h2.title {
diff --git a/src/components/molecules/layout/card.tsx b/src/components/molecules/layout/card.tsx
index f39a430..722e5a5 100644
--- a/src/components/molecules/layout/card.tsx
+++ b/src/components/molecules/layout/card.tsx
@@ -65,13 +65,7 @@ export const Card: FC<CardProps> = ({
{cover ? (
<ResponsiveImage {...cover} className={styles.cover} />
) : null}
- <Heading
- // eslint-disable-next-line react/jsx-no-literals -- Hardcoded config
- alignment="center"
- className={styles.title}
- id={headingId}
- level={titleLevel}
- >
+ <Heading className={styles.title} id={headingId} level={titleLevel}>
{title}
</Heading>
</header>
diff --git a/src/components/molecules/layout/page-header.tsx b/src/components/molecules/layout/page-header.tsx
index 9c11feb..04f2966 100644
--- a/src/components/molecules/layout/page-header.tsx
+++ b/src/components/molecules/layout/page-header.tsx
@@ -1,4 +1,4 @@
-import { FC, ReactNode } from 'react';
+import type { FC, ReactNode } from 'react';
import { Heading } from '../../atoms';
import { Meta, type MetaData } from './meta';
import styles from './page-header.module.scss';
@@ -11,7 +11,7 @@ export type PageHeaderProps = {
/**
* The page introduction.
*/
- intro?: string | JSX.Element;
+ intro?: string | ReactNode;
/**
* The page metadata.
*/
@@ -33,32 +33,39 @@ export const PageHeader: FC<PageHeaderProps> = ({
meta,
title,
}) => {
+ const headerClass = `${styles.wrapper} ${className}`;
+
const getIntro = () => {
- return typeof intro === 'string' ? (
- <div
- className={styles.intro}
- dangerouslySetInnerHTML={{ __html: intro }}
- />
- ) : (
- <div className={styles.intro}>{intro}</div>
- );
+ if (typeof intro === 'string')
+ return (
+ <div
+ className={styles.intro}
+ /* eslint-disable-next-line react/no-danger -- Not safe but intro can
+ * contains links or formatting so we need it. */
+ dangerouslySetInnerHTML={{ __html: intro }}
+ />
+ );
+
+ return <div className={styles.intro}>{intro}</div>;
};
return (
- <header className={`${styles.wrapper} ${className}`}>
+ <header className={headerClass}>
<div className={styles.body}>
- <Heading level={1} className={styles.title} withMargin={false}>
+ <Heading className={styles.title} level={1}>
{title}
</Heading>
- {meta && (
+ {meta ? (
<Meta
- data={meta}
className={styles.meta}
- layout="column"
+ data={meta}
+ // eslint-disable-next-line react/jsx-no-literals -- Layout allowed
itemsLayout="inline"
+ // eslint-disable-next-line react/jsx-no-literals -- Layout allowed
+ layout="column"
/>
- )}
- {intro && getIntro()}
+ ) : null}
+ {intro ? getIntro() : null}
</div>
</header>
);
diff --git a/src/components/organisms/forms/comment-form/comment-form.module.scss b/src/components/organisms/forms/comment-form/comment-form.module.scss
index fbf8c96..a4de51e 100644
--- a/src/components/organisms/forms/comment-form/comment-form.module.scss
+++ b/src/components/organisms/forms/comment-form/comment-form.module.scss
@@ -8,6 +8,12 @@
}
}
+.title {
+ width: fit-content;
+ margin-inline: auto;
+ margin-bottom: var(--spacing-sm);
+}
+
.field {
width: 100%;
}
diff --git a/src/components/organisms/forms/comment-form/comment-form.tsx b/src/components/organisms/forms/comment-form/comment-form.tsx
index be5d58f..e645ede 100644
--- a/src/components/organisms/forms/comment-form/comment-form.tsx
+++ b/src/components/organisms/forms/comment-form/comment-form.tsx
@@ -1,4 +1,14 @@
-import { ChangeEvent, FC, FormEvent, ReactNode, useState } from 'react';
+/* eslint-disable max-statements */
+import {
+ type ChangeEvent,
+ type FC,
+ type FormEvent,
+ type ReactNode,
+ useCallback,
+ useMemo,
+ useState,
+ useId,
+} from 'react';
import { useIntl } from 'react-intl';
import {
Button,
@@ -6,7 +16,6 @@ import {
type FormProps,
Heading,
type HeadingLevel,
- type HeadingProps,
Spinner,
Input,
TextArea,
@@ -42,10 +51,6 @@ export type CommentFormProps = Pick<FormProps, 'className'> & {
*/
title?: string;
/**
- * The form title alignment. Default: left.
- */
- titleAlignment?: HeadingProps['alignment'];
- /**
* The title level. Default: 2.
*/
titleLevel?: HeadingLevel;
@@ -57,29 +62,30 @@ export const CommentForm: FC<CommentFormProps> = ({
parentId,
saveComment,
title,
- titleAlignment,
titleLevel = 2,
...props
}) => {
const formClass = `${styles.form} ${className}`;
const intl = useIntl();
- const emptyForm: CommentFormData = {
- author: '',
- comment: '',
- email: '',
- parentId,
- website: '',
- };
+ const emptyForm: CommentFormData = useMemo(() => {
+ return {
+ author: '',
+ comment: '',
+ email: '',
+ parentId,
+ website: '',
+ };
+ }, [parentId]);
const [data, setData] = useState(emptyForm);
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
/**
* Reset all the form fields.
*/
- const resetForm = () => {
+ const resetForm = useCallback(() => {
setData(emptyForm);
setIsSubmitting(false);
- };
+ }, [emptyForm]);
const nameLabel = intl.formatMessage({
defaultMessage: 'Name:',
@@ -112,43 +118,47 @@ export const CommentForm: FC<CommentFormProps> = ({
});
const formAriaLabel = title ? undefined : formTitle;
- const formId = 'comment-form-title';
+ const formId = useId();
const formLabelledBy = title ? formId : undefined;
- const updateForm = (
- e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
- ) => {
- switch (e.target.name) {
- case 'author':
- setData((prevData) => {
- return { ...prevData, author: e.target.value };
- });
- break;
- case 'comment':
- setData((prevData) => {
- return { ...prevData, comment: e.target.value };
- });
- break;
- case 'email':
- setData((prevData) => {
- return { ...prevData, email: e.target.value };
- });
- break;
- case 'website':
- setData((prevData) => {
- return { ...prevData, website: e.target.value };
- });
- break;
- default:
- break;
- }
- };
+ const updateForm = useCallback(
+ (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
+ switch (e.target.name) {
+ case 'author':
+ setData((prevData) => {
+ return { ...prevData, author: e.target.value };
+ });
+ break;
+ case 'comment':
+ setData((prevData) => {
+ return { ...prevData, comment: e.target.value };
+ });
+ break;
+ case 'email':
+ setData((prevData) => {
+ return { ...prevData, email: e.target.value };
+ });
+ break;
+ case 'website':
+ setData((prevData) => {
+ return { ...prevData, website: e.target.value };
+ });
+ break;
+ default:
+ break;
+ }
+ },
+ []
+ );
- const submitHandler = (e: FormEvent) => {
- e.preventDefault();
- setIsSubmitting(true);
- saveComment(data, resetForm).then(() => setIsSubmitting(false));
- };
+ const sendForm = useCallback(
+ (e: FormEvent) => {
+ e.preventDefault();
+ setIsSubmitting(true);
+ saveComment(data, resetForm).then(() => setIsSubmitting(false));
+ },
+ [data, resetForm, saveComment]
+ );
return (
<Form
@@ -156,13 +166,13 @@ export const CommentForm: FC<CommentFormProps> = ({
aria-label={formAriaLabel}
aria-labelledby={formLabelledBy}
className={formClass}
- onSubmit={submitHandler}
+ onSubmit={sendForm}
>
- {title && (
- <Heading alignment={titleAlignment} id={formId} level={titleLevel}>
+ {title ? (
+ <Heading className={styles.title} id={formId} level={titleLevel}>
{title}
</Heading>
- )}
+ ) : null}
<LabelledField
className={styles.field}
field={
@@ -236,7 +246,7 @@ export const CommentForm: FC<CommentFormProps> = ({
id: 'OL0Yzx',
})}
</Button>
- {isSubmitting && (
+ {isSubmitting ? (
<Spinner
message={intl.formatMessage({
defaultMessage: 'Submitting...',
@@ -244,7 +254,7 @@ export const CommentForm: FC<CommentFormProps> = ({
id: 'IY5ew6',
})}
/>
- )}
+ ) : null}
{Notice}
</Form>
);
diff --git a/src/components/templates/page/page-layout.module.scss b/src/components/templates/page/page-layout.module.scss
index d29df2c..09bb957 100644
--- a/src/components/templates/page/page-layout.module.scss
+++ b/src/components/templates/page/page-layout.module.scss
@@ -81,6 +81,12 @@
}
}
+ &__title {
+ width: fit-content;
+ margin-bottom: var(--spacing-md);
+ margin-inline: auto;
+ }
+
&__no-comments {
text-align: center;
}
@@ -90,3 +96,7 @@
margin: auto;
}
}
+
+.notice {
+ margin-top: var(--spacing-sm);
+}
diff --git a/src/components/templates/page/page-layout.tsx b/src/components/templates/page/page-layout.tsx
index 72bfd3f..dfd9353 100644
--- a/src/components/templates/page/page-layout.tsx
+++ b/src/components/templates/page/page-layout.tsx
@@ -1,15 +1,22 @@
import Script from 'next/script';
-import { FC, HTMLAttributes, ReactNode, useRef, useState } from 'react';
+import {
+ type FC,
+ type HTMLAttributes,
+ type ReactNode,
+ useRef,
+ useState,
+ useCallback,
+} from 'react';
import { useIntl } from 'react-intl';
-import { BreadcrumbList } from 'schema-dts';
+import type { BreadcrumbList } from 'schema-dts';
import { sendComment } from '../../../services/graphql';
-import { SendCommentInput } from '../../../types';
+import type { Approved, SendCommentInput, SingleComment } from '../../../types';
import { useIsMounted } from '../../../utils/hooks';
import { Heading, Notice, type NoticeKind, Sidebar } from '../../atoms';
import {
Breadcrumb,
type BreadcrumbItem,
- MetaData,
+ type MetaData,
PageFooter,
type PageFooterProps,
PageHeader,
@@ -24,6 +31,29 @@ import {
} from '../../organisms';
import styles from './page-layout.module.scss';
+/**
+ * Check if there is at least one comment.
+ *
+ * @param {SingleComment[] | undefined} comments - The comments.
+ */
+const hasComments = (
+ comments: SingleComment[] | undefined
+): comments is SingleComment[] =>
+ Array.isArray(comments) && comments.length > 0;
+
+/**
+ * Check if meta properties are defined.
+ *
+ * @param {MetaData} meta - The metadata.
+ */
+const hasMeta = (meta: MetaData) => Object.values(meta).every((value) => value);
+
+type CommentStatus = {
+ isReply: boolean;
+ kind: NoticeKind;
+ message: string;
+};
+
export type PageLayoutProps = {
/**
* True if the page accepts new comments. Default: false.
@@ -118,67 +148,69 @@ export const PageLayout: FC<PageLayoutProps> = ({
const bodyRef = useRef<HTMLDivElement>(null);
const isMounted = useIsMounted(bodyRef);
- const hasComments = Array.isArray(comments) && comments.length > 0;
- const [status, setStatus] = useState<NoticeKind>('info');
- const [statusMessage, setStatusMessage] = useState<string>('');
- const isReplyRef = useRef<boolean>(false);
-
- const saveComment: CommentFormProps['saveComment'] = async (data, reset) => {
- if (!id) throw new Error('Page id missing. Cannot save comment.');
-
- const { author, comment: commentBody, email, parentId, website } = data;
- const commentData: SendCommentInput = {
- author,
- authorEmail: email,
- authorUrl: website ?? '',
- clientMutationId: 'contact',
- commentOn: id,
- content: commentBody,
- parent: parentId,
- };
- const { comment, success } = await sendComment(commentData);
+ const [commentStatus, setCommentStatus] = useState<CommentStatus | undefined>(
+ undefined
+ );
- isReplyRef.current = !!parentId;
+ const isSuccessStatus = useCallback(
+ (comment: Approved | null, isReply: boolean, isSuccess: boolean) => {
+ if (isSuccess) {
+ const successPrefix = intl.formatMessage({
+ defaultMessage: 'Thanks, your comment was successfully sent.',
+ description: 'PageLayout: comment form success message',
+ id: 'B290Ph',
+ });
+ const successMessage = comment?.approved
+ ? intl.formatMessage({
+ defaultMessage: 'It has been approved.',
+ id: 'g3+Ahv',
+ description: 'PageLayout: comment approved.',
+ })
+ : intl.formatMessage({
+ defaultMessage: 'It is now awaiting moderation.',
+ id: 'Vmj5cw',
+ description: 'PageLayout: comment awaiting moderation',
+ });
+ setCommentStatus({
+ isReply,
+ kind: 'success',
+ message: `${successPrefix} ${successMessage}`,
+ });
+ return true;
+ }
- if (success) {
- setStatus('success');
- const successPrefix = intl.formatMessage({
- defaultMessage: 'Thanks, your comment was successfully sent.',
- description: 'PageLayout: comment form success message',
- id: 'B290Ph',
- });
- const successMessage = comment?.approved
- ? intl.formatMessage({
- defaultMessage: 'It has been approved.',
- id: 'g3+Ahv',
- description: 'PageLayout: comment approved.',
- })
- : intl.formatMessage({
- defaultMessage: 'It is now awaiting moderation.',
- id: 'Vmj5cw',
- description: 'PageLayout: comment awaiting moderation',
- });
- setStatusMessage(`${successPrefix} ${successMessage}`);
- reset();
- } else {
const error = intl.formatMessage({
defaultMessage: 'An error occurred:',
description: 'PageLayout: comment form error message',
id: 'fkcTGp',
});
- setStatus('error');
- setStatusMessage(error);
- }
- };
+ setCommentStatus({ isReply, kind: 'error', message: error });
+ return false;
+ },
+ [intl]
+ );
- /**
- * Check if meta properties are defined.
- *
- * @param {MetaData} meta - The metadata.
- */
- const hasMeta = (meta: MetaData) => {
- return Object.values(meta).every((value) => value);
- };
+ const saveComment: CommentFormProps['saveComment'] = useCallback(
+ async (data, reset) => {
+ if (!id) throw new Error('Page id missing. Cannot save comment.');
+
+ const { author, comment: commentBody, email, parentId, website } = data;
+ const isReply = !!parentId;
+ const commentData: SendCommentInput = {
+ author,
+ authorEmail: email,
+ authorUrl: website ?? '',
+ clientMutationId: 'comment',
+ commentOn: id,
+ content: commentBody,
+ parent: parentId,
+ };
+ const { comment, success } = await sendComment(commentData);
+
+ if (isSuccessStatus(comment, isReply, success)) reset();
+ },
+ [id, isSuccessStatus]
+ );
return (
<>
@@ -198,7 +230,7 @@ export const PageLayout: FC<PageLayoutProps> = ({
meta={headerMeta}
title={title}
/>
- {withToC && (
+ {withToC ? (
<Sidebar
aria-label={intl.formatMessage({
defaultMessage: 'Table of contents sidebar',
@@ -207,11 +239,11 @@ export const PageLayout: FC<PageLayoutProps> = ({
})}
className={`${styles.sidebar} ${styles['sidebar--first']}`}
>
- {isMounted && bodyRef.current && (
+ {isMounted && bodyRef.current ? (
<TableOfContents wrapper={bodyRef.current} />
- )}
+ ) : null}
</Sidebar>
- )}
+ ) : null}
{typeof children === 'string' ? (
<div
{...bodyAttributes}
@@ -224,9 +256,9 @@ export const PageLayout: FC<PageLayoutProps> = ({
{children}
</div>
)}
- {footerMeta && hasMeta(footerMeta) && (
+ {footerMeta && hasMeta(footerMeta) ? (
<PageFooter meta={footerMeta} className={styles.footer} />
- )}
+ ) : null}
<Sidebar
aria-label={intl.formatMessage({
defaultMessage: 'Sidebar',
@@ -237,22 +269,22 @@ export const PageLayout: FC<PageLayoutProps> = ({
>
{widgets}
</Sidebar>
- {allowComments && (
+ {allowComments ? (
<div className={styles.comments} id="comments">
<section className={styles.comments__section}>
- <Heading level={2} alignment="center">
+ <Heading className={styles.comments__title} level={2}>
{commentsTitle}
</Heading>
- {hasComments ? (
+ {hasComments(comments) ? (
<CommentsList
comments={comments}
depth={2}
Notice={
- isReplyRef.current === true && statusMessage ? (
+ commentStatus?.isReply ? (
<Notice
className={styles.notice}
- kind={status}
- message={statusMessage}
+ kind={commentStatus.kind}
+ message={commentStatus.message}
/>
) : null
}
@@ -273,20 +305,19 @@ export const PageLayout: FC<PageLayoutProps> = ({
className={styles.comments__form}
saveComment={saveComment}
title={commentFormTitle}
- titleAlignment="center"
Notice={
- isReplyRef.current === false && statusMessage ? (
+ commentStatus && !commentStatus.isReply ? (
<Notice
className={styles.notice}
- kind={status}
- message={statusMessage}
+ kind={commentStatus.kind}
+ message={commentStatus.message}
/>
) : null
}
/>
</section>
</div>
- )}
+ ) : null}
</>
);
};
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index c06fb7e..d99462f 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -2,7 +2,7 @@ import type { MDXComponents } from 'mdx/types';
import type { GetStaticProps } from 'next';
import Head from 'next/head';
import Script from 'next/script';
-import type { FC } from 'react';
+import type { FC, HTMLAttributes } from 'react';
import { useIntl } from 'react-intl';
import FeedIcon from '../assets/images/icon-feed.svg';
import {
@@ -19,6 +19,7 @@ import {
ResponsiveImage,
Section,
type SectionProps,
+ Heading,
} from '../components';
import HomePageContent from '../content/pages/homepage.mdx';
import { getArticlesCard } from '../services/graphql';
@@ -29,6 +30,60 @@ import { getSchemaJson, getWebPageSchema } from '../utils/helpers';
import { loadTranslation, type Messages } from '../utils/helpers/server';
import { useBreadcrumb, useSettings } from '../utils/hooks';
+const H1 = ({
+ children = '',
+ ...props
+}: HTMLAttributes<HTMLHeadingElement>) => (
+ <Heading {...props} level={1}>
+ {children}
+ </Heading>
+);
+
+const H2 = ({
+ children = '',
+ ...props
+}: HTMLAttributes<HTMLHeadingElement>) => (
+ <Heading {...props} level={2}>
+ {children}
+ </Heading>
+);
+
+const H3 = ({
+ children = '',
+ ...props
+}: HTMLAttributes<HTMLHeadingElement>) => (
+ <Heading {...props} level={3}>
+ {children}
+ </Heading>
+);
+
+const H4 = ({
+ children = '',
+ ...props
+}: HTMLAttributes<HTMLHeadingElement>) => (
+ <Heading {...props} level={4}>
+ {children}
+ </Heading>
+);
+
+const H5 = ({
+ children = '',
+ ...props
+}: HTMLAttributes<HTMLHeadingElement>) => (
+ <Heading {...props} level={5}>
+ {children}
+ </Heading>
+);
+
+const H6 = ({
+ children = '',
+ ...props
+}: HTMLAttributes<HTMLHeadingElement>) => (
+ <Heading {...props} level={6}>
+ {children}
+ </Heading>
+);
+
/**
* Retrieve a list of coding links.
*
@@ -276,6 +331,12 @@ const HomePage: NextPageWithLayout<HomeProps> = ({ recentPosts }) => {
ColdarkRepos,
Column,
Columns: StyledColumns,
+ h1: H1,
+ h2: H2,
+ h3: H3,
+ h4: H4,
+ h5: H5,
+ h6: H6,
Image: ResponsiveImage,
LibreLinks,
MoreLinks,
diff --git a/src/pages/projets/[slug].tsx b/src/pages/projets/[slug].tsx
index 9981868..afcf060 100644
--- a/src/pages/projets/[slug].tsx
+++ b/src/pages/projets/[slug].tsx
@@ -5,7 +5,7 @@ import dynamic from 'next/dynamic';
import Head from 'next/head';
import { useRouter } from 'next/router';
import Script from 'next/script';
-import type { ComponentType } from 'react';
+import type { ComponentType, HTMLAttributes } from 'react';
import { useIntl } from 'react-intl';
import {
Code,
@@ -22,6 +22,7 @@ import {
type SocialWebsite,
Spinner,
type MetaData,
+ Heading,
} from '../../components';
import styles from '../../styles/pages/project.module.scss';
import type { NextPageWithLayout, ProjectPreview, Repos } from '../../types';
@@ -44,9 +45,69 @@ const BorderedImage = (props: ResponsiveImageProps) => (
<ResponsiveImage withBorders={true} {...props} />
);
+const H1 = ({
+ children = '',
+ ...props
+}: HTMLAttributes<HTMLHeadingElement>) => (
+ <Heading {...props} level={1}>
+ {children}
+ </Heading>
+);
+
+const H2 = ({
+ children = '',
+ ...props
+}: HTMLAttributes<HTMLHeadingElement>) => (
+ <Heading {...props} level={2}>
+ {children}
+ </Heading>
+);
+
+const H3 = ({
+ children = '',
+ ...props
+}: HTMLAttributes<HTMLHeadingElement>) => (
+ <Heading {...props} level={3}>
+ {children}
+ </Heading>
+);
+
+const H4 = ({
+ children = '',
+ ...props
+}: HTMLAttributes<HTMLHeadingElement>) => (
+ <Heading {...props} level={4}>
+ {children}
+ </Heading>
+);
+
+const H5 = ({
+ children = '',
+ ...props
+}: HTMLAttributes<HTMLHeadingElement>) => (
+ <Heading {...props} level={5}>
+ {children}
+ </Heading>
+);
+
+const H6 = ({
+ children = '',
+ ...props
+}: HTMLAttributes<HTMLHeadingElement>) => (
+ <Heading {...props} level={6}>
+ {children}
+ </Heading>
+);
+
const components: MDXComponents = {
Code,
Gallery,
+ h1: H1,
+ h2: H2,
+ h3: H3,
+ h4: H4,
+ h5: H5,
+ h6: H6,
Image: BorderedImage,
Link,
};
diff --git a/src/styles/abstracts/_placeholders.scss b/src/styles/abstracts/_placeholders.scss
index 7729e84..76fdbd6 100644
--- a/src/styles/abstracts/_placeholders.scss
+++ b/src/styles/abstracts/_placeholders.scss
@@ -1,5 +1,6 @@
@forward "./placeholders/animations";
@forward "./placeholders/buttons";
@forward "./placeholders/clearfix";
+@forward "./placeholders/headings";
@forward "./placeholders/layout";
@forward "./placeholders/list";
diff --git a/src/styles/abstracts/placeholders/_headings.scss b/src/styles/abstracts/placeholders/_headings.scss
new file mode 100644
index 0000000..a836c74
--- /dev/null
+++ b/src/styles/abstracts/placeholders/_headings.scss
@@ -0,0 +1,54 @@
+@use "../functions" as fun;
+
+%heading {
+ margin: 0;
+ color: var(--color-primary-dark);
+ font-family: var(--font-family-secondary);
+ font-weight: 500;
+ letter-spacing: 0.01ex;
+}
+
+%h1 {
+ @extend %heading;
+
+ font-size: var(--font-size-3xl);
+}
+
+%h2 {
+ @extend %heading;
+
+ padding-bottom: fun.convert-px(3);
+ background: linear-gradient(
+ to top,
+ var(--color-primary-dark) 0.3rem,
+ transparent 0.3rem
+ )
+ 0 0 / 3rem 100% no-repeat;
+ font-size: var(--font-size-2xl);
+ text-shadow: fun.convert-px(1) fun.convert-px(1) 0 var(--color-shadow-light);
+}
+
+%h3 {
+ @extend %heading;
+
+ font-size: var(--font-size-xl);
+}
+
+%h4 {
+ @extend %heading;
+
+ font-size: var(--font-size-lg);
+}
+
+%h5 {
+ @extend %heading;
+
+ font-size: var(--font-size-md);
+ font-weight: 600;
+}
+
+%h6 {
+ @extend %heading;
+
+ font-size: var(--font-size-md);
+}
diff --git a/src/styles/base/_typography.scss b/src/styles/base/_typography.scss
index dc958c4..11b506f 100644
--- a/src/styles/base/_typography.scss
+++ b/src/styles/base/_typography.scss
@@ -1,61 +1,5 @@
@use "../abstracts/functions" as fun;
-h1 {
- font-size: var(--font-size-3xl);
-}
-
-h2 {
- padding-bottom: fun.convert-px(3);
- background: linear-gradient(
- to top,
- var(--color-primary-dark) 0.3rem,
- transparent 0.3rem
- )
- 0 0 / 3rem 100% no-repeat;
- font-size: var(--font-size-2xl);
- text-shadow: fun.convert-px(1) fun.convert-px(1) 0 var(--color-shadow-light);
-}
-
-h3 {
- font-size: var(--font-size-xl);
-}
-
-h4 {
- font-size: var(--font-size-lg);
-}
-
-h5 {
- font-size: var(--font-size-md);
- font-weight: 600;
-}
-
-h6 {
- font-size: var(--font-size-md);
-}
-
-h1,
-h2,
-h3,
-h4,
-h5,
-h6 {
- color: var(--color-primary-dark);
- font-family: var(--font-family-secondary);
- font-weight: 500;
- letter-spacing: 0.01ex;
- margin: 0 0 var(--spacing-sm);
-
- * + {
- h2,
- h3,
- h4,
- h5,
- h6 {
- margin-top: var(--spacing-md);
- }
- }
-}
-
p {
font-size: var(--font-size-md);
margin: 0 0 var(--spacing-sm);
@@ -132,7 +76,9 @@ a {
color: var(--color-primary);
text-decoration-thickness: 0.15em;
text-underline-offset: 20%;
- transition: all 0.3s linear 0s, text-decoration 0.18s ease-in-out 0s;
+ transition:
+ all 0.3s linear 0s,
+ text-decoration 0.18s ease-in-out 0s;
&:hover {
color: var(--color-primary-light);
diff --git a/src/styles/pages/article.module.scss b/src/styles/pages/article.module.scss
index 48aed79..088718f 100644
--- a/src/styles/pages/article.module.scss
+++ b/src/styles/pages/article.module.scss
@@ -1,5 +1,6 @@
@use "../abstracts/functions" as fun;
@use "../abstracts/mixins" as mix;
+@use "partials/article-headings";
@use "partials/article-links";
@use "partials/article-lists";
@use "partials/article-media";
@@ -18,6 +19,7 @@
.body {
:global {
+ @include article-headings.styles;
@include article-links.styles;
@include article-lists.styles;
@include article-media.styles;
diff --git a/src/styles/pages/partials/_article-headings.scss b/src/styles/pages/partials/_article-headings.scss
index dfeceb7..7a273e4 100644
--- a/src/styles/pages/partials/_article-headings.scss
+++ b/src/styles/pages/partials/_article-headings.scss
@@ -1,42 +1,28 @@
-@use "../../abstracts/functions" as fun;
+@use "../../abstracts/placeholders";
@mixin styles {
h1 {
- font-size: var(--font-size-3xl);
- font-weight: 500;
+ @extend %h1;
}
h2 {
- padding-bottom: fun.convert-px(3);
- background: linear-gradient(
- to top,
- var(--color-primary-dark) 0.3rem,
- transparent 0.3rem
- )
- 0 0 / 3rem 100% no-repeat;
- font-size: var(--font-size-2xl);
- font-weight: 500;
- text-shadow: fun.convert-px(1) fun.convert-px(1) 0 var(--color-shadow-light);
+ @extend %h2;
}
h3 {
- font-size: var(--font-size-xl);
- font-weight: 500;
+ @extend %h3;
}
h4 {
- font-size: var(--font-size-lg);
- font-weight: 500;
+ @extend %h4;
}
h5 {
- font-size: var(--font-size-md);
- font-weight: 600;
+ @extend %h5;
}
h6 {
- font-size: var(--font-size-md);
- font-weight: 500;
+ @extend %h6;
}
h1,
@@ -45,13 +31,8 @@
h4,
h5,
h6 {
- color: var(--color-primary-dark);
- font-family: var(--font-family-secondary);
- letter-spacing: 0.01ex;
- margin: 0 0 var(--spacing-sm);
-
- & + & {
- margin-top: var(--spacing-md);
+ &:not(:first-child) {
+ margin-block: var(--spacing-sm);
}
}
}