feat: blog theme

This commit is contained in:
Hossein Mehrabi
2023-06-13 17:16:46 +03:30
parent c876b52258
commit c3edece3f6
19 changed files with 841 additions and 1 deletions
@@ -2,4 +2,5 @@
font-size: var(--lsd-#{$variant}-fontSize) !important;
line-height: var(--lsd-#{$variant}-lineHeight) !important;
font-family: var(--lsd-typography-generic-font-family) !important;
font-weight: 400 !important;
}
@@ -0,0 +1,315 @@
@use './utils';
@use './lsd';
.blog-wrapper {
}
.blog-wrapper > .container {
display: flex;
width: 100%;
margin: 0;
padding-inline: 0;
max-width: unset;
}
.blog-wrapper > .container > .row {
width: 100%;
}
.blog-wrapper main > header h1 {
@include lsd.typography('h3');
margin-bottom: 0.5rem;
}
.blog-wrapper > .container > .row > aside {
max-width: unset;
> nav {
top: var(--ifm-navbar-height);
padding-top: 16px;
> div:first-child {
@include lsd.typography('subtitle2');
border-left: 1px solid transparent;
padding: 4px 0 4px 15px;
margin-bottom: 0 !important;
}
& > ul {
li {
margin: 0;
a {
@include lsd.typography('subtitle2');
display: block;
box-sizing: border-box;
padding: 4px 0 4px 2rem;
border-left: 1px solid transparent;
opacity: 0.6;
}
a:hover {
opacity: 1;
}
a[class*='sidebarItemLinkActive_'] {
border-color: rgb(var(--lsd-border-primary));
}
}
}
}
}
.blog-wrapper > .container > .row > main {
margin-top: 16px;
max-width: unset;
}
.blog-wrapper article {
header {
h1,
h2 {
@include lsd.typography('h3');
}
}
footer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-top: 2rem !important;
}
}
.blog-wrapper--index > .container > .row > aside {
> nav {
> div:first-child {
border-color: rgb(var(--lsd-border-primary));
}
}
}
.blog-wrapper {
main {
article {
margin-bottom: 40px !important;
padding-bottom: 39px;
border-bottom: 1px solid rgb(var(--lsd-border-primary));
&:last-of-type {
border-bottom: none !important;
}
}
}
}
// grid
@include utils.responsive('lg', 'up') {
.blog-wrapper > .container > .row {
display: grid;
grid-template-columns: repeat(24, 1fr);
gap: 16px;
}
.blog-wrapper > .container > .row > aside {
grid-column: 1 / 5;
}
.blog-wrapper > .container > .row > main {
grid-column: 6 / 20;
}
.blog-wrapper:not(.blog-wrapper--index) > .container > .row {
> div:last-child {
grid-column: 21 / 25;
max-width: unset;
}
}
}
@include utils.responsive('lg', 'down') {
.blog-wrapper {
main {
padding-inline: var(--content-padding);
}
}
.blog-wrapper article footer {
flex-direction: column;
align-items: flex-start;
.read-more-link {
margin-top: 2rem;
&,
button {
width: 100%;
flex-basis: 100%;
}
}
}
}
.blog-archive-page {
.main-wrapper {
padding: 2rem var(--content-padding);
header {
margin: 0;
padding: 0;
padding-bottom: 1rem;
border-bottom: 1px solid var(--ifm-hero-text-color);
.container {
margin: 0;
padding: 0 !important;
h1 {
@include lsd.typography('h3');
}
p {
display: none;
}
}
}
main > section {
margin-top: 0 !important;
.container {
margin: 0;
padding: 0;
max-width: unset;
}
.container > .row {
margin-top: 64px;
> .col {
max-width: unset;
padding: 0;
margin: 0 !important;
h3 {
@include lsd.typography('h6');
}
a {
@include lsd.typography('body1');
}
ul {
margin: 0;
padding: 0;
padding-left: 1.5rem;
margin-top: 1rem;
}
}
}
}
}
@include utils.responsive('md', 'down') {
.main-wrapper {
main > section {
.container > .row {
margin-top: 0px;
.col {
margin-top: 40px !important;
}
}
}
}
}
@include utils.responsive('md', 'up') {
.main-wrapper {
main > section {
.container > .row {
display: grid;
grid-template-columns: repeat(24, 1fr);
gap: 64px 24px;
> .col {
max-width: unset;
padding: 0;
margin: 0 !important;
grid-column: span 12;
padding-right: calc(100% / 12);
}
}
}
}
}
@include utils.responsive('xl', 'up') {
.main-wrapper {
main > section {
.container > .row {
display: grid;
grid-template-columns: repeat(24, 1fr);
gap: 64px 24px;
> .col {
max-width: unset;
padding: 0;
margin: 0 !important;
grid-column: span 8;
padding-right: calc(100% / 8);
}
}
}
}
}
}
// tags page
.blog-wrapper main > section {
article {
h2 {
@include lsd.typography('h3');
}
border-bottom: none;
padding: 0;
ul {
margin-top: 1rem;
li {
a::before,
a::after {
display: none;
}
a {
padding: 0;
}
margin: 0 !important;
padding: 0 !important;
margin-right: 0.5rem !important;
.lsd-tag {
padding: 3px 11px;
height: 28px;
> span {
color: rgb(var(--lsd-text-primary));
@include lsd.typography('label1');
> span {
@include lsd.typography('label1');
color: rgb(var(--lsd-text-primary));
}
}
}
}
}
}
}
@@ -1166,3 +1166,10 @@ table {
height: 28px !important;
}
}
.hero--primary {
--ifm-hero-background-color: rgb(var(--lsd-surface-primary));
--ifm-hero-text-color: rgb(var(--lsd-text-primary));
}
@import './blog.scss';
@@ -7,7 +7,7 @@ import styles from './AuthorPage.module.scss'
export type AuthorPageProps = {
data: {
author: Author
authors: Author
docs: {
id: string
title: string
@@ -0,0 +1,14 @@
import BlogArchivePage from '@docusaurus/theme-classic/lib/theme/BlogArchivePage'
import type { WrapperProps } from '@docusaurus/types'
import type BlogArchivePageType from '@theme/BlogArchivePage'
import React from 'react'
type Props = WrapperProps<typeof BlogArchivePageType>
export default function BlogArchivePageWrapper(props: Props): JSX.Element {
return (
<div className="blog-archive-page">
<BlogArchivePage {...props} />
</div>
)
}
@@ -0,0 +1,40 @@
import clsx from 'clsx'
import React from 'react'
import BlogLayout from '@docusaurus/theme-classic/lib/theme/BlogLayout'
import { Props } from '@theme/BlogLayout'
import { ensureTrailingSlash } from '../../lib/string.utils'
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'
const useIsIndexPage = (props: Props) => {
if (typeof window === 'undefined') return false
const ctx = useDocusaurusContext()
const activePlugin = ctx.siteConfig.plugins.find(
(plugin) =>
plugin === '"@docusaurus/plugin-content-blog"' ||
plugin?.[0] === '@docusaurus/plugin-content-blog',
)
const routeBasePath = activePlugin?.[1]?.routeBasePath ?? '/blog'
return (
ensureTrailingSlash(routeBasePath) ===
ensureTrailingSlash(window.location.pathname)
)
}
export default function BlogLayoutWrapper(props: Props): JSX.Element {
const { sidebar, toc, children, ...layoutProps } = props
const isIndexPage = useIsIndexPage(props)
return (
<BlogLayout
{...props}
wrapperClassName={clsx(
'blog-wrapper',
isIndexPage && 'blog-wrapper--index',
)}
/>
)
}
@@ -0,0 +1,42 @@
import React from 'react'
import Translate, { translate } from '@docusaurus/Translate'
import Link from '@docusaurus/Link'
import type { Props } from '@theme/BlogPostItem/Footer/ReadMoreLink'
import { Button, Typography } from '@acid-info/lsd-react'
function ReadMoreLabel() {
return (
<Translate
id="theme.blog.post.readMore"
description="The label used in blog post item excerpts to link to full blog posts"
>
Read More
</Translate>
)
}
export default function BlogPostItemFooterReadMoreLink(
props: Props,
): JSX.Element {
const { blogPostTitle, ...linkProps } = props
return (
<Link
aria-label={translate(
{
message: 'Read more about {title}',
id: 'theme.blog.post.readMoreLabel',
description:
'The ARIA label for the link to full blog posts from excerpts',
},
{ title: blogPostTitle },
)}
{...linkProps}
>
<Button size="medium" variant="filled">
<Typography variant="label1" component="span">
<ReadMoreLabel />
</Typography>
</Button>
</Link>
)
}
@@ -0,0 +1,51 @@
import React from 'react'
import clsx from 'clsx'
import { useBlogPost } from '@docusaurus/theme-common/internal'
import EditThisPage from '@theme/EditThisPage'
import TagsListInline from '@theme/TagsListInline'
import ReadMoreLink from '@theme/BlogPostItem/Footer/ReadMoreLink'
import styles from './styles.module.css'
export default function BlogPostItemFooter(): JSX.Element | null {
const { metadata, isBlogPostPage } = useBlogPost()
const { tags, title, editUrl, hasTruncateMarker } = metadata
// A post is truncated if it's in the "list view" and it has a truncate marker
const truncatedPost = !isBlogPostPage && hasTruncateMarker
const tagsExists = tags.length > 0
const renderFooter = tagsExists || truncatedPost || editUrl
if (!renderFooter) {
return null
}
return (
<footer
className={clsx(
'row docusaurus-mt-lg',
isBlogPostPage && styles.blogPostFooterDetailsFull,
)}
>
<div>{tagsExists && <TagsListInline tags={tags} />}</div>
{isBlogPostPage && editUrl && (
<div>
<EditThisPage editUrl={editUrl} />
</div>
)}
{truncatedPost && (
<div className="read-more-link">
<ReadMoreLink
className={styles.readMoreLink}
blogPostTitle={title}
to={metadata.permalink}
/>
</div>
)}
</footer>
)
}
@@ -0,0 +1,7 @@
.blogPostFooterDetailsFull {
flex-direction: column;
}
.readMoreLink span {
color: rgb(var(--lsd-text-secondary)) !important;
}
@@ -0,0 +1,42 @@
import React from 'react'
import clsx from 'clsx'
import Link, { type Props as LinkProps } from '@docusaurus/Link'
import type { Props } from '@theme/BlogPostItem/Header/Author'
import { Typography } from '@acid-info/lsd-react'
function MaybeLink(props: LinkProps): JSX.Element {
if (props.href) {
return <Link {...props} />
}
return <>{props.children}</>
}
export default function BlogPostItemHeaderAuthor({
author,
className,
}: Props): JSX.Element {
const { name, title, url, imageURL, email } = author
console.log(author)
const link = url || (email && `mailto:${email}`) || undefined
return (
<div className={clsx('avatar margin-bottom--sm', className)}>
{name && (
<div
className="avatar__intro"
itemProp="author"
itemScope
itemType="https://schema.org/Person"
>
<div className="avatar__name">
<MaybeLink href={link} itemProp="url">
<Typography variant="body2" itemProp="name">
{name}
</Typography>
</MaybeLink>
</div>
</div>
)}
</div>
)
}
@@ -0,0 +1,44 @@
import React from 'react'
import clsx from 'clsx'
import { useBlogPost } from '@docusaurus/theme-common/internal'
import BlogPostItemHeaderAuthor from '@theme/BlogPostItem/Header/Author'
import type { Props } from '@theme/BlogPostItem/Header/Authors'
import styles from './styles.module.css'
import { Typography } from '@acid-info/lsd-react'
// Component responsible for the authors layout
export default function BlogPostItemHeaderAuthors({
className,
}: Props): JSX.Element | null {
const {
metadata: { authors },
assets,
} = useBlogPost()
const authorsCount = authors.length
if (authorsCount === 0) {
return null
}
const imageOnly = authors.every(({ name }) => !name)
return (
<div className={clsx(className)}>
<Typography variant="body2">{'by '}</Typography>
{authors.map((author, idx) => (
<div
className={clsx(
!imageOnly && 'col col--6',
imageOnly ? styles.imageOnlyAuthorCol : styles.authorCol,
)}
key={idx}
>
<BlogPostItemHeaderAuthor
author={{
...author,
// Handle author images using relative paths
imageURL: assets.authorsImageUrls[idx] ?? author.imageURL,
}}
/>
</div>
))}
</div>
)
}
@@ -0,0 +1,14 @@
.authorCol {
max-width: inherit !important;
flex-grow: 1 !important;
}
.imageOnlyAuthorRow {
display: flex;
flex-flow: row wrap;
}
.imageOnlyAuthorCol {
margin-left: 0.3rem;
margin-right: 0.3rem;
}
@@ -0,0 +1,67 @@
import { usePluralForm } from '@docusaurus/theme-common'
import { useBlogPost } from '@docusaurus/theme-common/internal'
import { translate } from '@docusaurus/Translate'
import type { Props } from '@theme/BlogPostItem/Header/Info'
import clsx from 'clsx'
import React from 'react'
import BlogPostItemHeaderAuthors from '@theme/BlogPostItem/Header/Authors'
import styles from './styles.module.scss'
// Very simple pluralization: probably good enough for now
function useReadingTimePlural() {
const { selectMessage } = usePluralForm()
return (readingTimeFloat: number) => {
const readingTime = Math.ceil(readingTimeFloat)
return selectMessage(
readingTime,
translate(
{
id: 'theme.blog.post.readingTime.plurals',
description:
'Pluralized label for "{readingTime} min read". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)',
message: 'One min read|{readingTime} min read',
},
{ readingTime },
),
)
}
}
function ReadingTime({ readingTime }: { readingTime: number }) {
const readingTimePlural = useReadingTimePlural()
return <div>{readingTimePlural(readingTime)}</div>
}
function Date({
date,
formattedDate,
}: {
date: string
formattedDate: string
}) {
return (
<time dateTime={date} itemProp="datePublished">
{formattedDate}
</time>
)
}
export default function BlogPostItemHeaderInfo({
className,
}: Props): JSX.Element {
const { metadata } = useBlogPost()
const { date, formattedDate, readingTime } = metadata
return (
<div className={clsx(styles.container, 'margin-vert--md', className)}>
<Date date={date} formattedDate={formattedDate} />
<BlogPostItemHeaderAuthors className={styles.authors} />
{typeof readingTime !== 'undefined' && (
<>
<ReadingTime readingTime={readingTime} />
</>
)}
</div>
)
}
@@ -0,0 +1,45 @@
@use '../../../../css/lsd';
.container {
margin-bottom: 24px !important;
@include lsd.typography('body2');
}
.container > div {
display: inline-block;
}
.container > *:not(:last-child)::after {
content: '';
display: inline-block;
margin-inline: 0.75rem;
font-size: 10px;
}
.authors {
display: inline-block;
}
.authors > div {
display: inline-flex;
max-width: unset;
width: auto;
padding: 0;
flex-direction: row;
align-items: flex-end;
}
.authors > div :global(.avatar) {
margin: 0 !important;
}
.authors > div:not(:last-child)::after {
content: ',';
display: inline-block;
margin-right: 4px;
}
.authors :global(.avatar__name) {
line-height: 0;
}
@@ -0,0 +1,12 @@
import BlogPostItemHeaderInfo from '@theme/BlogPostItem/Header/Info'
import BlogPostItemHeaderTitle from '@theme/BlogPostItem/Header/Title'
import React from 'react'
export default function BlogPostItemHeader(): JSX.Element {
return (
<header>
<BlogPostItemHeaderTitle />
<BlogPostItemHeaderInfo />
</header>
)
}
@@ -0,0 +1,21 @@
import React from 'react'
import clsx from 'clsx'
import Link from '@docusaurus/Link'
import type { Props } from '@theme/Tag'
import styles from './styles.module.css'
import { Tag as LsdTag } from '@acid-info/lsd-react'
export default function Tag({ permalink, label, count }: Props): JSX.Element {
return (
<Link
href={permalink}
className={clsx(count ? styles.tagWithCount : styles.tagRegular)}
>
<LsdTag disabled={false} size="small">
{label}
{count && <span>{count}</span>}
</LsdTag>
</Link>
)
}
@@ -0,0 +1,63 @@
:root {
--docusaurus-tag-list-border: var(--ifm-color-emphasis-300);
}
.tag {
border: 1px solid var(--docusaurus-tag-list-border);
transition: border var(--ifm-transition-fast);
}
.tag:hover {
--docusaurus-tag-list-border: var(--ifm-link-color);
text-decoration: none;
}
.tagRegular {
border-radius: var(--ifm-global-radius);
padding: 0.2rem 0.5rem 0.3rem;
font-size: 90%;
}
.tagWithCount {
display: flex;
align-items: center;
position: relative;
padding: 0 0.5rem 0 1rem;
border-left: 0;
}
.tagWithCount::before,
.tagWithCount::after {
content: '';
position: absolute;
top: 50%;
border: 1px solid var(--docusaurus-tag-list-border);
transition: inherit;
}
.tagWithCount::before {
right: 100%;
transform: translate(50%, -50%) rotate(-45deg);
width: 1.18rem;
height: 1.18rem;
border-right: 0;
border-bottom: 0;
}
.tagWithCount::after {
left: 0;
transform: translateY(-50%);
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
}
.tagWithCount span {
background: var(--ifm-color-secondary);
color: var(--ifm-color-black);
font-size: 0.7rem;
line-height: 1.2;
border-radius: var(--ifm-global-radius);
padding: 0.1rem 0.4rem;
margin-left: 0.3rem;
}
@@ -0,0 +1,30 @@
import React from 'react'
import clsx from 'clsx'
import Translate from '@docusaurus/Translate'
import Tag from '@theme/Tag'
import type { Props } from '@theme/TagsListInline'
import styles from './styles.module.css'
import { Typography } from '@acid-info/lsd-react'
export default function TagsListInline({ tags }: Props): JSX.Element {
return (
<div className={styles.root}>
<Typography variant="body2" component="div">
<Translate
id="theme.tags.tagsListLabel"
description="The label alongside a tag list"
>
Tags:
</Translate>
</Typography>
<ul className={clsx(styles.tags, 'padding--none', 'margin-left--sm')}>
{tags.map(({ label, permalink: tagPermalink }) => (
<li key={tagPermalink} className={styles.tag}>
<Tag label={label} permalink={tagPermalink} />
</li>
))}
</ul>
</div>
)
}
@@ -0,0 +1,25 @@
.root {
display: flex;
flex-direction: row;
align-items: center;
}
.tags {
display: inline;
padding: 0 !important;
margin: 0 !important;
margin-left: 1rem !important;
}
.tag {
display: inline-block;
}
.tag:not(:first-child) {
margin-left: 0.5rem;
}
.tag a {
margin: 0 !important;
padding: 0 !important;
}