Added Sidenote Component
This commit is contained in:
3
src/components/Article/Article.module.css
Normal file
3
src/components/Article/Article.module.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.article {
|
||||
@mixin layout-wrapper;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
.wrapper {
|
||||
@mixin mx auto;
|
||||
@mixin px var(--spacing-comfortable);
|
||||
|
||||
max-width: 90ch;
|
||||
font-size: clamp(1rem, 2.5vw, 1.5rem);
|
||||
}
|
||||
|
||||
.hasMargin {
|
||||
@media screen and (--bp-margin) {
|
||||
@mixin px 0;
|
||||
|
||||
display: flex;
|
||||
gap: var(--spacing-generous);
|
||||
max-width: none;
|
||||
|
||||
& .content {
|
||||
flex: 1 1 auto;
|
||||
min-width: 50ch;
|
||||
max-width: 75ch;
|
||||
}
|
||||
}
|
||||
}
|
||||
56
src/components/Article/Content/MarkdocRenderer/index.tsx
Normal file
56
src/components/Article/Content/MarkdocRenderer/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import Markdoc from '@markdoc/markdoc';
|
||||
import type { Node } from '@markdoc/markdoc';
|
||||
|
||||
import styles from './MarkdocRenderer.module.css';
|
||||
|
||||
import { nodes } from '@/lib/markdoc/nodes';
|
||||
import { tags } from '@/lib/markdoc/tags';
|
||||
|
||||
import { collectSideNotes, hasComponents } from '@/lib/markdoc/utils';
|
||||
|
||||
import { Column } from '@/components/Content/Grid/Column';
|
||||
import { Row } from '@/components/Content/Grid/Row';
|
||||
import Sidenote from '@/components/Content/Sidenote/Item';
|
||||
import SidenoteContainer from '@/components/Content/Sidenote/Container';
|
||||
|
||||
interface MarkdocRendererProps {
|
||||
content: () => Promise<{ node: Node }>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const MARGIN_COMPONENTS = ['Sidenote'];
|
||||
|
||||
export default async function MarkdocRenderer({
|
||||
content,
|
||||
className,
|
||||
}: MarkdocRendererProps) {
|
||||
const components = {
|
||||
Column,
|
||||
Row,
|
||||
Sidenote,
|
||||
};
|
||||
|
||||
const { node } = await content();
|
||||
const errors = Markdoc.validate(node, { tags, nodes });
|
||||
|
||||
if (errors.length) {
|
||||
console.error('Markdoc validation errors:', errors);
|
||||
throw new Error('Invalid Content');
|
||||
}
|
||||
|
||||
const renderable = Markdoc.transform(node, { tags, nodes });
|
||||
const sidenotes = collectSideNotes(renderable);
|
||||
const showMargin = hasComponents(renderable, MARGIN_COMPONENTS);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.wrapper} ${className} ${showMargin ? styles.hasMargin : ''}`}
|
||||
>
|
||||
<div className={`${styles.content} content`}>
|
||||
{Markdoc.renderers.react(renderable, React, { components })}
|
||||
</div>
|
||||
<SidenoteContainer items={sidenotes} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
src/components/Article/Content/index.tsx
Normal file
10
src/components/Article/Content/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import MarkdocRenderer from '@/components/Article/Content/MarkdocRenderer';
|
||||
import { ArticleContent } from '@/lib/types/content';
|
||||
|
||||
interface ContentProps {
|
||||
article: ArticleContent;
|
||||
}
|
||||
|
||||
export default function Content({ article }: ContentProps) {
|
||||
return <MarkdocRenderer content={article.content} />;
|
||||
}
|
||||
0
src/components/Article/Footer/Footer.module.tsx
Normal file
0
src/components/Article/Footer/Footer.module.tsx
Normal file
0
src/components/Article/Footer/index.tsx
Normal file
0
src/components/Article/Footer/index.tsx
Normal file
36
src/components/Article/Header/Cover/index.tsx
Normal file
36
src/components/Article/Header/Cover/index.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import Image from 'next/image';
|
||||
|
||||
import styles from './Cover.module.css';
|
||||
|
||||
interface CoverProps {
|
||||
cover: {
|
||||
readonly src: string;
|
||||
readonly alt: string;
|
||||
readonly caption: string;
|
||||
readonly showInHeader: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export default function Cover({ cover }: CoverProps) {
|
||||
return (
|
||||
<figure className={styles.cover}>
|
||||
<Image
|
||||
src={cover.src}
|
||||
alt={cover.alt || 'Standard Alt'}
|
||||
width={0}
|
||||
height={0}
|
||||
sizes={'100vw'}
|
||||
className={styles.image}
|
||||
/>
|
||||
<figcaption className={styles.caption}>
|
||||
<div className={styles.captionwrapper}>
|
||||
{cover.caption ? (
|
||||
<span className={styles.captiontext}>{cover.caption}</span>
|
||||
) : (
|
||||
<span className={styles.covermeta}>{cover.src}</span>
|
||||
)}
|
||||
</div>
|
||||
</figcaption>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
0
src/components/Article/Header/Header.module.css
Normal file
0
src/components/Article/Header/Header.module.css
Normal file
0
src/components/Article/Header/Meta/Meta.module.css
Normal file
0
src/components/Article/Header/Meta/Meta.module.css
Normal file
60
src/components/Article/Header/Meta/index.tsx
Normal file
60
src/components/Article/Header/Meta/index.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Author, Tag } from '@/lib/types/taxonomy';
|
||||
import { toMilitaryDTG } from '@/lib/utils/date';
|
||||
|
||||
import styles from './Meta.module.css';
|
||||
|
||||
interface MetaProps {
|
||||
author: Author | null;
|
||||
tags: Tag[];
|
||||
updateDate?: string;
|
||||
publicationDate: string;
|
||||
}
|
||||
|
||||
export default function Meta({
|
||||
author,
|
||||
tags,
|
||||
updateDate,
|
||||
publicationDate,
|
||||
}: MetaProps) {
|
||||
return (
|
||||
<div className={styles.meta}>
|
||||
{author && (
|
||||
<div className={styles.section}>
|
||||
<span className={styles.label}>Author</span>
|
||||
<span className={styles.author}>
|
||||
<Link href={author.slug}>{author.name}</Link>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{tags && tags.length > 0 && (
|
||||
<div className={styles.section}>
|
||||
<span className={styles.label}>Tags</span>
|
||||
<span className={styles.author}>
|
||||
<ul className={styles.taglist}>
|
||||
{tags.map((tag) => (
|
||||
<li key={tag.slug} className={styles.tag}>
|
||||
<Link className={styles.link} href={`/tags/${tag.slug}`}>
|
||||
{tag.name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{publicationDate && (
|
||||
<div className={styles.metasection}>
|
||||
<span className={styles.metalabel}>Last Update</span>
|
||||
<time className={styles.updatedate}>
|
||||
⟫
|
||||
{updateDate
|
||||
? toMilitaryDTG(updateDate)
|
||||
: toMilitaryDTG(publicationDate)}
|
||||
</time>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
46
src/components/Article/Header/Overline/index.tsx
Normal file
46
src/components/Article/Header/Overline/index.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
import styles from './Overline.module.css';
|
||||
import { toMilitaryDTG } from '@/lib/utils/date';
|
||||
|
||||
interface OverlineProps {
|
||||
breadcrumbs?: string[];
|
||||
publicationDate: string;
|
||||
}
|
||||
|
||||
export default function Overline({
|
||||
breadcrumbs = [],
|
||||
publicationDate,
|
||||
}: OverlineProps) {
|
||||
const breadcrumber = breadcrumbs.map((crumb, index) => {
|
||||
const isLast = index === breadcrumbs.length - 1;
|
||||
const href = `/${breadcrumbs.slice(0, index + 1).join('/')}`;
|
||||
return (
|
||||
<li key={index} className={styles.crumb}>
|
||||
<span className={styles.separator}>/</span>
|
||||
{isLast ? (
|
||||
<span className={styles.current}>{crumb}</span>
|
||||
) : (
|
||||
<Link href={href} className={styles.link}>
|
||||
{crumb}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div className={styles.overline}>
|
||||
<ul className={styles.breadcrumbs}>
|
||||
<li className={styles.crumb}>
|
||||
<Link href="/" className={styles.link}>
|
||||
dave-dmg.de
|
||||
</Link>
|
||||
</li>
|
||||
{breadcrumber}
|
||||
</ul>
|
||||
<time className={styles.publicationDate}>
|
||||
⟫ {toMilitaryDTG(publicationDate)}
|
||||
</time>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
14
src/components/Article/Header/Title/index.tsx
Normal file
14
src/components/Article/Header/Title/index.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import styles from './Title.module.css';
|
||||
import { JSX } from 'react';
|
||||
|
||||
interface TitleProps {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export default function Title({ title }: TitleProps) {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<h1 className={styles.title}>{title}</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
41
src/components/Article/Header/index.tsx
Normal file
41
src/components/Article/Header/index.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { ArticleContent } from '@/lib/types/content';
|
||||
import { Author, Tag } from '@/lib/types/taxonomy';
|
||||
import { isValidCover } from '@/lib/utils/guards';
|
||||
|
||||
import styles from './Header.module.css';
|
||||
import Overline from '@/components/Article/Header/Overline';
|
||||
import Title from '@/components/Article/Header/Title';
|
||||
import Meta from '@/components/Article/Header/Meta';
|
||||
import Cover from '@/components/Article/Header/Cover';
|
||||
|
||||
interface HeaderProps {
|
||||
article: ArticleContent;
|
||||
breadcrumbs?: string[];
|
||||
author: Author | null;
|
||||
tags: Tag[];
|
||||
}
|
||||
|
||||
export default function Header({
|
||||
article,
|
||||
breadcrumbs,
|
||||
author,
|
||||
tags,
|
||||
}: HeaderProps) {
|
||||
const { title, meta, cover } = article;
|
||||
return (
|
||||
<header className={styles.container}>
|
||||
<Overline
|
||||
publicationDate={article.meta.publicationDate}
|
||||
breadcrumbs={breadcrumbs}
|
||||
/>
|
||||
<Title title={article.title} />
|
||||
{cover && isValidCover(cover) && <Cover cover={cover} />}
|
||||
<Meta
|
||||
tags={tags}
|
||||
author={author}
|
||||
updateDate={meta.updateDate}
|
||||
publicationDate={meta.publicationDate}
|
||||
/>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
32
src/components/Article/index.tsx
Normal file
32
src/components/Article/index.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { ArticleContent } from '@/lib/types/content';
|
||||
import { Author, Tag } from '@/lib/types/taxonomy';
|
||||
import Header from './Header';
|
||||
import Content from '@/components/Article/Content';
|
||||
|
||||
import styles from './Article.module.css';
|
||||
|
||||
interface ArticleProps {
|
||||
article: ArticleContent;
|
||||
author: Author | null;
|
||||
tags: Tag[];
|
||||
breadcrumbs?: string[];
|
||||
}
|
||||
|
||||
export default function Article({
|
||||
article,
|
||||
author,
|
||||
tags,
|
||||
breadcrumbs,
|
||||
}: ArticleProps) {
|
||||
return (
|
||||
<article className={styles.article}>
|
||||
<Header
|
||||
article={article}
|
||||
author={author}
|
||||
tags={tags}
|
||||
breadcrumbs={breadcrumbs}
|
||||
/>
|
||||
<Content article={article} />
|
||||
</article>
|
||||
);
|
||||
}
|
||||
3
src/components/Content/Grid/Column/Column.module.css
Normal file
3
src/components/Content/Grid/Column/Column.module.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.column {
|
||||
position: relative;
|
||||
}
|
||||
23
src/components/Content/Grid/Column/index.tsx
Normal file
23
src/components/Content/Grid/Column/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
|
||||
import styles from './Column.module.css';
|
||||
|
||||
interface ColumnProps {
|
||||
style: {
|
||||
colspan: number;
|
||||
};
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Column({ style, children }: ColumnProps) {
|
||||
return (
|
||||
<div
|
||||
className={styles.column}
|
||||
style={{
|
||||
gridColumn: `span ${style.colspan}`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
src/components/Content/Grid/Row/Row.module.css
Normal file
3
src/components/Content/Grid/Row/Row.module.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.row {
|
||||
display: grid;
|
||||
}
|
||||
29
src/components/Content/Grid/Row/index.tsx
Normal file
29
src/components/Content/Grid/Row/index.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
|
||||
import styles from './Row.module.css';
|
||||
|
||||
interface RowProps {
|
||||
style: {
|
||||
cols: number;
|
||||
gap: string;
|
||||
align: string;
|
||||
justify: string;
|
||||
};
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Row({ style, children }: RowProps) {
|
||||
return (
|
||||
<div
|
||||
className={`${styles.row}`}
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${style.cols}, 1 fr)`,
|
||||
gap: `${style.gap}`,
|
||||
alignItems: `${style.align}`,
|
||||
justifyContent: `${style.justify}`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
.container {
|
||||
flex: 1 0 auto;
|
||||
min-width: 16rem;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
@media screen and (--bp-margin) {
|
||||
position: absolute;
|
||||
top: anchor(top);
|
||||
}
|
||||
}
|
||||
|
||||
.note {
|
||||
position: relative;
|
||||
min-width: 16rem;
|
||||
font-family: var(--font-mono);
|
||||
|
||||
@media screen and (--bp-margin) {
|
||||
@mixin text-xs;
|
||||
|
||||
margin-right: var(--spacing-generous);
|
||||
padding: var(--spacing-cozy) var(--spacing-cozy) var(--spacing-cozy) var(--size-12) ;
|
||||
border: var(--size-1) solid var(--color-surface-inverse);
|
||||
|
||||
background: var(--color-surface-elevated-1);
|
||||
box-shadow: 2px 2px 0 var(--color-palette-woodsmoke), 4px 4px 0 var(--color-palette-charcoal-gray);
|
||||
}
|
||||
}
|
||||
|
||||
.marker {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: var(--size-8);
|
||||
|
||||
font-size: var(--typo-size-3xl);
|
||||
font-weight: var(--typo-weight-black);
|
||||
color: var(--color-text-inverse);
|
||||
|
||||
background-color: var(--color-surface-inverse);
|
||||
}
|
||||
|
||||
.content {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ref {
|
||||
@mixin ml var(--spacing-snug);
|
||||
@mixin anim-txt-characterglitch;
|
||||
|
||||
font-weight: var(--typo-weight-black);
|
||||
color: var(--color-text-quarternary);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
33
src/components/Content/Sidenote/Container/index.tsx
Normal file
33
src/components/Content/Sidenote/Container/index.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { Sidenote } from '@/lib/types/components';
|
||||
import styles from './Container.module.css';
|
||||
|
||||
interface ContainerProps {
|
||||
items: Sidenote[];
|
||||
}
|
||||
|
||||
export default function Container({ items }: ContainerProps) {
|
||||
return (
|
||||
<aside className={styles.container}>
|
||||
{items.map((sidenote: Sidenote) => (
|
||||
<div
|
||||
key={sidenote.id}
|
||||
id={sidenote.id}
|
||||
className={styles.wrapper}
|
||||
style={{ positionAnchor: `--note-${sidenote.id}` }}
|
||||
>
|
||||
<div className={`${styles.note} ${styles[sidenote.type]}`}>
|
||||
<span className={styles.marker}>
|
||||
<span className={styles.symbol}>{sidenote.marker}</span>
|
||||
</span>
|
||||
<div className={styles.content}>
|
||||
{sidenote.content}
|
||||
<a href={`#ref-${sidenote.id}`} className={styles.ref}>
|
||||
↩
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
11
src/components/Content/Sidenote/Item/Item.module.css
Normal file
11
src/components/Content/Sidenote/Item/Item.module.css
Normal file
@@ -0,0 +1,11 @@
|
||||
.ref {
|
||||
@mixin px var(--spacing-tight);
|
||||
|
||||
display: inline-block;
|
||||
font-weight: var(--typo-weight-black);
|
||||
transition: color 0.5s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
17
src/components/Content/Sidenote/Item/index.tsx
Normal file
17
src/components/Content/Sidenote/Item/index.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Sidenote } from '@/lib/types/components';
|
||||
import styles from './Item.module.css';
|
||||
|
||||
export default function SidenoteItem({ id, marker, content, type }: Sidenote) {
|
||||
return (
|
||||
<sup>
|
||||
<a
|
||||
href={`#${id}`}
|
||||
className={styles.ref}
|
||||
id={`#ref-${id}`}
|
||||
style={{ anchorName: `--note-${id}` }}
|
||||
>
|
||||
[{marker}]
|
||||
</a>
|
||||
</sup>
|
||||
);
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
}
|
||||
|
||||
.inner {
|
||||
@mixin responsive-wrapper;
|
||||
@mixin layout-wrapper;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -60,7 +60,8 @@
|
||||
--area-bg-animation-keyframe: none;
|
||||
--area-bg--animation-duration: 0s;
|
||||
--area-bg--animation-timing: linear;
|
||||
--area-bg-filter: grayscale(100%) contrast(150) brightness(150);
|
||||
--area-bg-filter: grayscale(100%) contrast(10) brightness(250);
|
||||
--area-bg-blendmode: color-dodge;
|
||||
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
@@ -70,6 +71,7 @@
|
||||
|
||||
object-fit: cover;
|
||||
filter: var(--area-bg-filter);
|
||||
mix-blend-mode: var(--area-bg-blendmode);
|
||||
|
||||
@media screen and (--bp-tablet-down) {
|
||||
display: none;
|
||||
@@ -93,27 +95,10 @@
|
||||
--subtitle-transition: all 0.3s ease-in-out;
|
||||
--title-transition: color 0.3s ease-in-out;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
opacity: 0;
|
||||
background: alpharize(var(--grid-bg), 0.66);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
--divider-color: var(--color-tertiary);
|
||||
--title-color: var(--color-tertiary);
|
||||
--subtitle-color: var(--color-tertiary);
|
||||
|
||||
&::before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
.menu {
|
||||
--grid-bg: var(--color-palette-charcoal-gray);
|
||||
--grid-fg: var(--color-palette-light-silver);
|
||||
--area-blendmode: normal;
|
||||
|
||||
/* === MenuTitle Vars === */
|
||||
--title-color: var(--grid-fg);
|
||||
@@ -40,13 +41,14 @@
|
||||
--divider-width: var(--size-12);
|
||||
--divider-height: var(--size-2);
|
||||
--divider-font: var(--font-mono);
|
||||
--divider-font-size: var(--typo-size-2xl);
|
||||
--divider-line-height: 1;
|
||||
--divider-font-size: var(--typo-size-3xl);
|
||||
--divider-padding: 0 var(--typo-spacing-cozy);
|
||||
--subtitle-font: var(--font-mono);
|
||||
--subtitle-color: var(--grid-fg);
|
||||
--subtitle-font-size: var(--typo-size-xl);
|
||||
--subtitle-text-transform: uppercase;
|
||||
--subtitle-letter-spacing: var(--typo-spacing-cozy);
|
||||
--subtitle-letter-spacing: var(--typo-spacing-snug);
|
||||
--subtitle-transition: none;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
Reference in New Issue
Block a user