Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add table of contents for docs pages #308

Merged
merged 6 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 77 additions & 24 deletions app/components/Doc.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,93 @@
import * as React from 'react'
import { FaEdit } from 'react-icons/fa'
import { marked } from 'marked'
import markedAlert from 'marked-alert'
import { gfmHeadingId, getHeadingList } from 'marked-gfm-heading-id'
import { DocTitle } from '~/components/DocTitle'
import { Markdown } from '~/components/Markdown'
import { Toc } from './Toc'
import { twMerge } from 'tailwind-merge'

type DocProps = {
title: string
content: string
repo: string
branch: string
filePath: string
shouldRenderToc?: boolean
colorFrom?: string
colorTo?: string
}

export function Doc({
title,
content,
repo,
branch,
filePath,
}: {
title: string
content: string
repo: string
branch: string
filePath: string
}) {
shouldRenderToc = false,
colorFrom,
colorTo,
}: DocProps) {
const { markup, headings } = React.useMemo(() => {
const markup = marked.use(
{ gfm: true },
gfmHeadingId(),
markedAlert()
)(content) as string

const headings = getHeadingList()

return { markup, headings }
}, [content])

const isTocVisible = shouldRenderToc && headings && headings.length > 1

return (
<div className="p-4 lg:p-6 overflow-auto w-full bg-white/70 dark:bg-black/30 m-2 md:m-4 xl:m-8 rounded-xl">
{title ? <DocTitle>{title}</DocTitle> : null}
<div className="h-4" />
<div className="h-px bg-gray-500 opacity-20" />
<div className="h-4" />
<div className="prose prose-gray prose-sm prose-p:leading-7 dark:prose-invert max-w-none">
<Markdown code={content} />
</div>
<div className="h-12" />
<div className="w-full h-px bg-gray-500 opacity-30" />
<div className="py-4 opacity-70">
<a
href={`https://github.com/${repo}/tree/${branch}/${filePath}`}
className="flex items-center gap-2"
<div className="w-full p-2 md:p-4 xl:p-8">
<div
className={twMerge(
'flex bg-white/70 dark:bg-black/30 mx-auto rounded-xl max-w-[936px]',
isTocVisible && 'max-w-full'
)}
>
<div
className={twMerge(
'flex overflow-auto flex-col w-full p-4 lg:p-6',
isTocVisible && 'border-r border-gray-500/20 !pr-0'
)}
>
<FaEdit /> Edit on GitHub
</a>
{title ? <DocTitle>{title}</DocTitle> : null}
<div className="h-4" />
<div className="h-px bg-gray-500 opacity-20" />
<div className="h-4" />
<div
className={twMerge(
'prose prose-gray prose-sm prose-p:leading-7 dark:prose-invert max-w-none',
isTocVisible && 'pr-4 lg:pr-6'
)}
>
<Markdown htmlMarkup={markup} />
</div>
<div className="h-12" />
<div className="w-full h-px bg-gray-500 opacity-30" />
<div className="py-4 opacity-70">
<a
href={`https://github.com/${repo}/tree/${branch}/${filePath}`}
className="flex items-center gap-2"
>
<FaEdit /> Edit on GitHub
</a>
</div>
<div className="h-24" />
</div>

{isTocVisible && (
<div className="max-w-52 w-full hidden 2xl:block transition-all">
<Toc headings={headings} colorFrom={colorFrom} colorTo={colorTo} />
</div>
)}
</div>
<div className="h-24" />
</div>
)
}
4 changes: 2 additions & 2 deletions app/components/DocsLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ export function DocsLayout({
<div
className={twMerge(
`max-w-full min-w-0 flex relative justify-center w-full min-h-[88dvh] lg:min-h-0`,
!isExample && 'mx-auto w-[1000px]'
!isExample && 'mx-auto w-[1208px]'
)}
>
{children}
Expand Down Expand Up @@ -601,7 +601,7 @@ export function DocsLayout({
</div>
</div>
</div>
<div className="-ml-2 pl-2 w-64 hidden md:block sticky top-0 max-h-screen overflow-y-auto">
<div className="-ml-2 pl-2 w-64 shrink-0 hidden md:block sticky top-0 max-h-screen overflow-y-auto">
<div className="ml-auto flex flex-col space-y-4">
<div className="bg-white dark:bg-gray-900/30 border-gray-500/20 shadow-xl divide-y divide-gray-500/20 flex flex-col border border-r-0 border-t-0 rounded-bl-lg">
<div className="uppercase font-black text-center p-3 opacity-50">
Expand Down
58 changes: 33 additions & 25 deletions app/components/Markdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,33 +206,41 @@ const getHighlighter = cache(async (language: string, themes: string[]) => {
return highlighter
})

export function Markdown({ code }: { code: string }) {
const jsx = React.useMemo(() => {
const markup = marked.use(
{ gfm: true },
gfmHeadingId(),
markedAlert()
)(code) as string

const options: HTMLReactParserOptions = {
replace: (domNode) => {
if (domNode instanceof Element && domNode.attribs) {
const replacer = markdownComponents[domNode.name]
if (replacer) {
return React.createElement(
replacer,
attributesToProps(domNode.attribs),
domToReact(domNode.children, options)
)
}
}
const options: HTMLReactParserOptions = {
replace: (domNode) => {
if (domNode instanceof Element && domNode.attribs) {
const replacer = markdownComponents[domNode.name]
if (replacer) {
return React.createElement(
replacer,
attributesToProps(domNode.attribs),
domToReact(domNode.children, options)
)
}
}

return
},
}

type MarkdownProps = { rawContent?: string; htmlMarkup?: string }

return
},
export function Markdown({ rawContent, htmlMarkup }: MarkdownProps) {
return React.useMemo(() => {
if (rawContent) {
const markup = marked.use(
{ gfm: true },
gfmHeadingId(),
markedAlert()
)(rawContent) as string

return parse(markup, options)
}

return parse(markup, options)
}, [code])
if (htmlMarkup) {
return parse(htmlMarkup, options)
}

return jsx
return null
}, [rawContent, htmlMarkup])
}
59 changes: 59 additions & 0 deletions app/components/Toc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as React from 'react'
import { twMerge } from 'tailwind-merge'
import { HeadingData } from 'marked-gfm-heading-id'
import { useLocation } from '@tanstack/react-router'

const headingLevels: Record<number, string> = {
1: 'pl-2',
2: 'pl-2',
3: 'pl-6',
4: 'pl-10',
5: 'pl-14',
6: 'pl-16',
}

type TocProps = {
headings: HeadingData[]
colorFrom?: string
colorTo?: string
}

export function Toc({ headings, colorFrom, colorTo }: TocProps) {
const location = useLocation()

const [hash, setHash] = React.useState('')

React.useEffect(() => {
setHash(location.hash)
}, [location])

return (
<nav className="flex flex-col p-2 gap-1 sticky top-2 max-h-screen">
<h3 className="text-[.9em] font-medium px-2">On this page</h3>

<ul
className={twMerge('flex flex-col overflow-y-auto gap-0.5 text-[.8em]')}
>
{headings?.map((heading) => (
<li
key={heading.id}
className={twMerge(
'cursor-pointer py-[.1rem] w-full rounded-lg hover:bg-gray-500 hover:bg-opacity-10',
headingLevels[heading.level]
)}
>
<a
title={heading.id}
href={`#${heading.id}`}
aria-current={hash === heading.id && 'location'}
className={`truncate block aria-current:bg-gradient-to-r ${colorFrom} ${colorTo} aria-current:bg-clip-text aria-current:text-transparent`}
dangerouslySetInnerHTML={{
__html: heading.text,
}}
/>
</li>
))}
</ul>
</nav>
)
}
3 changes: 3 additions & 0 deletions app/routes/$libraryId/$version.docs.$.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ function Docs() {
repo={library.repo}
branch={branch}
filePath={filePath}
colorFrom={library.colorFrom}
colorTo={library.colorTo}
shouldRenderToc
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ function Docs() {
repo={library.repo}
branch={branch}
filePath={filePath}
colorFrom={library.colorFrom}
colorTo={library.colorTo}
shouldRenderToc
/>
)
}
2 changes: 1 addition & 1 deletion app/routes/_libraries/blog.$.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export default function BlogPost() {

const blogContent = `_by ${formatAuthors(authors)} on ${format(
new Date(published || 0),
'MMM dd, yyyy',
'MMM dd, yyyy'
)}._
${content}`

Expand Down
2 changes: 1 addition & 1 deletion app/routes/_libraries/blog.index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ function BlogIndex() {
<div
className={`text-sm mt-4 text-black dark:text-white leading-7`}
>
<Markdown code={excerpt || ''} />
<Markdown rawContent={excerpt || ''} />
</div>
</div>
<div>
Expand Down
3 changes: 3 additions & 0 deletions tailwind.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ module.exports = {
inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.04)',
none: 'none',
},
aria: {
current: 'current="location"',
},
colors: {
// red: {
// default: '#FF4255',
Expand Down
Loading