From e210294146f123b5af2b11c783d7194e765ada5b Mon Sep 17 00:00:00 2001 From: codecalm Date: Thu, 27 Jul 2023 01:16:23 +0200 Subject: [PATCH] docs improvements --- .../(marketing)/docs/[[...slug]]/layout.tsx | 23 +-- .../app/(marketing)/docs/[[...slug]]/page.tsx | 139 +++++++----------- site/app/(marketing)/page.tsx | 2 + site/app/layout.tsx | 3 +- site/components/TOC.tsx | 101 +++++++++++++ site/components/layout/hero/Ui.tsx | 2 +- site/hooks/use-mounted.ts | 14 +- site/hooks/use-on-click-outside.ts | 26 ++-- site/lib/toc.ts | 76 ++++++++++ site/package.json | 15 +- site/pnpm-lock.yaml | 23 +++ site/styles/_config.scss | 1 + site/styles/_docs.scss | 14 +- site/styles/_icons.scss | 2 +- site/styles/_typo.scss | 1 - 15 files changed, 299 insertions(+), 143 deletions(-) create mode 100644 site/components/TOC.tsx create mode 100644 site/lib/toc.ts diff --git a/site/app/(marketing)/docs/[[...slug]]/layout.tsx b/site/app/(marketing)/docs/[[...slug]]/layout.tsx index 1fbfe105d..c8849a588 100644 --- a/site/app/(marketing)/docs/[[...slug]]/layout.tsx +++ b/site/app/(marketing)/docs/[[...slug]]/layout.tsx @@ -1,11 +1,3 @@ -// import Link from "next/link"; -// import { useRouter } from "next/router"; - -// import { getCategory, getDocsMenu, getPrevNext } from "@/lib/docs"; -import DocsMenu from '@/components/DocsMenu'; - -// import Icon from "@/components/Icon"; - export const metadata = { title: 'Documentation', template: '%s - Documentation', @@ -13,23 +5,10 @@ export const metadata = { }; export default function DocsLayout({ children /*, meta = {}, pageProps*/ }) { - // const docsMenu = getDocsMenu(), - // router = useRouter(), - // category = getCategory(router.pathname), - // [prev, next] = getPrevNext(router.pathname) - return (
-
-
- {/**/} - -
-
-
{children}
-
-
+ {children}
); diff --git a/site/app/(marketing)/docs/[[...slug]]/page.tsx b/site/app/(marketing)/docs/[[...slug]]/page.tsx index f22425a3d..06591439a 100644 --- a/site/app/(marketing)/docs/[[...slug]]/page.tsx +++ b/site/app/(marketing)/docs/[[...slug]]/page.tsx @@ -4,10 +4,13 @@ import { notFound } from 'next/navigation'; import { allDocs } from 'contentlayer/generated'; import { name } from '@/config/site'; +import { getTableOfContents } from '@/lib/toc'; import Mdx from '@/components/MDX'; import TablerSponsorsBanner from '@/components/TablerSponsorsBanner'; import Link from 'next/link'; - +import TOC from '@/components/TOC'; +import DocsMenu from '@/components/DocsMenu'; +import Icon from '@/components/Icon'; interface DocPageProps { params: { @@ -59,100 +62,62 @@ export default async function DocPage({ params }: DocPageProps) { notFound(); } - // const toc = await getTableOfContents(doc.body.raw) + const toc = await getTableOfContents(doc.body.raw); return ( - <> - -
- {/* {category && ( +
+
+ {/**/} + +
+
+
+ +
+ {/* {category && (
{category}
)} */} - {doc.title &&

{doc.title}

} - {doc.description &&

{doc.description}

} + {doc.title &&

{doc.title}

} + {doc.description &&

{doc.description}

} - -
- - - -
-
-
- {/* {prev && ( - -
-
-
-
- -
-
-
-
- {prev.category} -
-
{prev.title}
-
-
-
- - )} */} +
-
- {/* {next && ( - -
-
-
-
- {next.category} -
-
{next.title}
-
-
-
- -
-
-
-
- - )} */} + + +
+
+
+
+
On this page
+
+ +
+
- {/* //
- //
- // {/* - // - //
- // {/* - //
- //
- //
- // {/* - //
- //
- //
*/} - +
); } diff --git a/site/app/(marketing)/page.tsx b/site/app/(marketing)/page.tsx index 47948525f..ceadd1994 100644 --- a/site/app/(marketing)/page.tsx +++ b/site/app/(marketing)/page.tsx @@ -653,3 +653,5 @@ export default async function HomePage() { ); } + +HomePage.bodyClass = 'xxx'; diff --git a/site/app/layout.tsx b/site/app/layout.tsx index 6c03351cd..e97eea93d 100644 --- a/site/app/layout.tsx +++ b/site/app/layout.tsx @@ -62,7 +62,7 @@ export const metadata = { }, }; -export default function RootLayout({ children }: { children: React.ReactNode; }) { +export default function RootLayout({ children, bodyClass }: { children: React.ReactNode; bodyClass?: string }) { return ( @@ -74,6 +74,7 @@ export default function RootLayout({ children }: { children: React.ReactNode; }) )} + {bodyClass} {children} diff --git a/site/components/TOC.tsx b/site/components/TOC.tsx new file mode 100644 index 000000000..d9f2359d9 --- /dev/null +++ b/site/components/TOC.tsx @@ -0,0 +1,101 @@ +'use client'; + +import * as React from 'react'; + +import { TableOfContents } from '@/lib/toc'; +import useMounted from '@/hooks/use-mounted'; +import clsx from 'clsx'; + +interface TocProps { + toc: TableOfContents; +} + +export default function TOC({ toc }: TocProps) { + const itemIds = React.useMemo( + () => + toc.items + ? toc.items + .flatMap((item) => [item.url, item?.items?.map((item) => item.url)]) + .flat() + .filter(Boolean) + .map((id) => id?.split('#')[1]) + : [], + [toc], + ); + const activeHeading = useActiveItem(itemIds); + const mounted = useMounted(); + + if (!toc?.items) { + return null; + } + + return mounted ? ( + + ) : null; +} + +function useActiveItem(itemIds: (string | undefined)[]) { + const [activeId, setActiveId] = React.useState(''); + + React.useEffect(() => { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setActiveId(entry.target.id); + } + }); + }, + { rootMargin: '0% 0% -80% 0%' }, + ); + + itemIds?.forEach((id) => { + if (!id) { + return; + } + + const element = document.getElementById(id); + if (element) { + observer.observe(element); + } + }); + + return () => { + itemIds?.forEach((id) => { + if (!id) { + return; + } + + const element = document.getElementById(id); + if (element) { + observer.unobserve(element); + } + }); + }; + }, [itemIds]); + + return activeId; +} + +interface TreeProps { + tree: TableOfContents; + level?: number; + activeItem?: string | null; +} + +function Tree({ tree, level = 1, activeItem }: TreeProps) { + return tree?.items?.length && level < 3 ? ( +
    + {tree.items.map((item, index) => { + return ( +
  • + + {item.title} + + {item.items?.length ? : null} +
  • + ); + })} +
+ ) : null; +} diff --git a/site/components/layout/hero/Ui.tsx b/site/components/layout/hero/Ui.tsx index 935486944..5d9f6019c 100644 --- a/site/components/layout/hero/Ui.tsx +++ b/site/components/layout/hero/Ui.tsx @@ -5,7 +5,7 @@ import { uiDownloadUrl } from '@/config/site'; export default function LayoutHeroUi() { return ( <> -
+

Develop beautiful web apps with Tabler diff --git a/site/hooks/use-mounted.ts b/site/hooks/use-mounted.ts index 4887de69c..38231bce6 100644 --- a/site/hooks/use-mounted.ts +++ b/site/hooks/use-mounted.ts @@ -1,13 +1,13 @@ -import React from "react"; +import React from 'react'; const useMounted = () => { - const [mounted, setMounted] = React.useState(false) + const [mounted, setMounted] = React.useState(false); React.useEffect(() => { - setMounted(true) - }, []) + setMounted(true); + }, []); - return mounted -} + return mounted; +}; -export default useMounted +export default useMounted; diff --git a/site/hooks/use-on-click-outside.ts b/site/hooks/use-on-click-outside.ts index 04d906703..ac4dd0cfe 100644 --- a/site/hooks/use-on-click-outside.ts +++ b/site/hooks/use-on-click-outside.ts @@ -1,23 +1,19 @@ -import { RefObject } from 'react' -import useEventListener from './use-event-listener' +import { RefObject } from 'react'; +import useEventListener from './use-event-listener'; -type Handler = (event: MouseEvent) => void +type Handler = (event: MouseEvent) => void; -const useOnClickOutside = ( - ref: RefObject, - handler: Handler, - mouseEvent: 'mousedown' | 'mouseup' = 'mousedown', -): void => { - useEventListener(mouseEvent, event => { - const el = ref?.current +const useOnClickOutside = (ref: RefObject, handler: Handler, mouseEvent: 'mousedown' | 'mouseup' = 'mousedown'): void => { + useEventListener(mouseEvent, (event) => { + const el = ref?.current; // Do nothing if clicking ref's element or descendent elements if (!el || el.contains(event.target as Node)) { - return + return; } - handler(event) - }) -} + handler(event); + }); +}; -export default useOnClickOutside \ No newline at end of file +export default useOnClickOutside; diff --git a/site/lib/toc.ts b/site/lib/toc.ts new file mode 100644 index 000000000..ad4d11d85 --- /dev/null +++ b/site/lib/toc.ts @@ -0,0 +1,76 @@ +import { toc } from 'mdast-util-toc'; +import { remark } from 'remark'; +import { visit } from 'unist-util-visit'; + +const textTypes = ['text', 'emphasis', 'strong', 'inlineCode']; + +function flattenNode(node: any) { + const p: any[] = []; + visit(node, (node) => { + if (!textTypes.includes(node.type)) { + return; + } + p.push(node.value); + }); + return p.join(''); +} + +interface Item { + title: string; + url: string; + items?: Item[]; +} + +interface Items { + items?: Item[]; +} + +function getItems(node, current): Items { + if (!node) { + return {}; + } + + if (node.type === 'paragraph') { + visit(node, (item) => { + if (item.type === 'link') { + current.url = item.url; + current.title = flattenNode(node); + } + + if (item.type === 'text') { + current.title = flattenNode(node); + } + }); + + return current; + } + + if (node.type === 'list') { + current.items = node.children.map((i) => getItems(i, {})); + + return current; + } else if (node.type === 'listItem') { + const heading = getItems(node.children[0], {}); + + if (node.children.length > 1) { + getItems(node.children[1], heading); + } + + return heading; + } + + return {}; +} + +const getToc = () => (node, file) => { + const table = toc(node); + file.data = getItems(table.map, {}); +}; + +export type TableOfContents = Items; + +export async function getTableOfContents(content: string): Promise { + const result = await remark().use(getToc).process(content); + + return result.data; +} diff --git a/site/package.json b/site/package.json index 2b9d073e7..0950df377 100644 --- a/site/package.json +++ b/site/package.json @@ -32,6 +32,11 @@ "@tabler/icons": "^2.28.0", "@tabler/icons-png": "^2.25.0", "@tabler/icons-webfont": "^2.25.0", + "@types/debug": "^4.1.8", + "@types/eslint": "^8.44.0", + "@types/eslint-scope": "^3.7.4", + "@types/estree-jsx": "^1.0.0", + "@types/extend": "^3.0.1", "@types/node": "^20.4.2", "@types/react": "18.2.15", "@types/react-dom": "18.2.7", @@ -62,6 +67,8 @@ "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", + "mdast-util-to-string": "^4.0.0", + "mdast-util-toc": "^7.0.0", "mdx": "^0.3.1", "mdx-annotations": "^0.1.3", "minimatch": "^9.0.3", @@ -105,12 +112,6 @@ "typescript": "5.1.6", "unist-util-visit": "^5.0.0", "webpack": "^5.88.2", - "yaml": "^2.3.1", - "@types/debug": "^4.1.8", - "@types/eslint": "^8.44.0", - "@types/eslint-scope": "^3.7.4", - "@types/estree-jsx": "^1.0.0", - "@types/extend": "^3.0.1", - "mdast-util-to-string": "^4.0.0" + "yaml": "^2.3.1" } } diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 8a47fe684..d6f7e137a 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -142,6 +142,9 @@ dependencies: mdast-util-to-string: specifier: ^4.0.0 version: 4.0.0 + mdast-util-toc: + specifier: ^7.0.0 + version: 7.0.0 mdx: specifier: ^0.3.1 version: 0.3.1 @@ -3004,6 +3007,10 @@ packages: resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==} dev: false + /@types/ungap__structured-clone@0.3.0: + resolution: {integrity: sha512-eBWREUhVUGPze+bUW22AgUr05k8u+vETzuYdLYSvWqGTUe0KOf+zVnOB1qER5wMcw8V6D9Ar4DfJmVvD1yu0kQ==} + dev: false + /@types/unist@2.0.6: resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} dev: false @@ -3074,6 +3081,10 @@ packages: eslint-visitor-keys: 3.3.0 dev: false + /@ungap/structured-clone@1.2.0: + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + dev: false + /@vercel/analytics@1.0.1: resolution: {integrity: sha512-Ux0c9qUfkcPqng3vrR0GTrlQdqNJ2JREn/2ydrVuKwM3RtMfF2mWX31Ijqo1opSjNAq6rK76PwtANw6kl6TAow==} dev: false @@ -6100,6 +6111,18 @@ packages: unist-util-visit: 4.1.2 dev: false + /mdast-util-toc@7.0.0: + resolution: {integrity: sha512-C28UcSqjmnWuvgT8d97qpaItHKvySqVPAECUzqQ51xuMyNFFJwcFoKW77KoMjtXrclTidLQFDzLUmTmrshRweA==} + dependencies: + '@types/mdast': 4.0.0 + '@types/ungap__structured-clone': 0.3.0 + '@ungap/structured-clone': 1.2.0 + github-slugger: 2.0.0 + mdast-util-to-string: 4.0.0 + unist-util-is: 6.0.0 + unist-util-visit: 5.0.0 + dev: false + /mdn-data@2.0.28: resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} dev: false diff --git a/site/styles/_config.scss b/site/styles/_config.scss index ffa9794b8..1fabdccf5 100644 --- a/site/styles/_config.scss +++ b/site/styles/_config.scss @@ -222,6 +222,7 @@ $utilities: ( class: text, values: map-merge($colors, ( muted: var(--color-muted, #{$color-muted}), + headers: var(--color-headers, #{$color-headers}), reset: inherit )) ), diff --git a/site/styles/_docs.scss b/site/styles/_docs.scss index b7baa6c4b..f5b745a84 100644 --- a/site/styles/_docs.scss +++ b/site/styles/_docs.scss @@ -8,7 +8,7 @@ height: calc(100vh - #{$header-height}); display: flex; flex-direction: column; - width: 15rem; + width: 13rem; &:after { content: ''; @@ -22,6 +22,18 @@ } } +.docs-side-toc { + width: 15rem; + position: sticky; + top: calc(#{$header-height} + 1px); + height: calc(100vh - #{$header-height}); + display: none; + + @include media-breakpoint-up(xl) { + display: block; + } +} + .docs-menu, .docs-menu-group, .docs-menu-submenu { diff --git a/site/styles/_icons.scss b/site/styles/_icons.scss index 54d810c0d..de0963326 100644 --- a/site/styles/_icons.scss +++ b/site/styles/_icons.scss @@ -34,7 +34,7 @@ .icon-inline { width: 1.2em; height: 1.2em; - stroke-width: 2; + stroke-width: 1.5; stroke: currentColor; vertical-align: sub; } diff --git a/site/styles/_typo.scss b/site/styles/_typo.scss index cf20b0d28..3d9b55d1f 100644 --- a/site/styles/_typo.scss +++ b/site/styles/_typo.scss @@ -88,7 +88,6 @@ h5, h6, .h6 { @extend %headers-common; - font-weight: $font-weight-normal; font-size: $font-size-h6; line-height: $line-height-h6; margin-bottom: $gap-1;