Documentation Updates (#79)
* Docs: Added Shadcn sidebar; added algorithm to automatically infer parents without needing to specify it. * Extracted Markdoc compilation in a separate file * Site Navigation: simplify nav by removing the border * Docs Navigation: added TOC; improved layout on mobile
This commit is contained in:
committed by
GitHub
parent
6490102e9f
commit
9615d1a4bb
26
apps/web/app/(marketing)/docs/_components/docs-nav-link.tsx
Normal file
26
apps/web/app/(marketing)/docs/_components/docs-nav-link.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { SidebarMenuButton, SidebarMenuItem } from '@kit/ui/shadcn-sidebar';
|
||||
import { cn, isRouteActive } from '@kit/ui/utils';
|
||||
|
||||
export function DocsNavLink({ label, url }: { label: string; url: string }) {
|
||||
const currentPath = usePathname();
|
||||
const isCurrent = isRouteActive(url, currentPath, true);
|
||||
|
||||
return (
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={isCurrent}
|
||||
className={cn('border-l-3 transition-background !font-normal')}
|
||||
>
|
||||
<Link href={url}>
|
||||
<span className="block max-w-full truncate">{label}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
}
|
||||
@@ -1,192 +1,68 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { Menu } from 'lucide-react';
|
||||
|
||||
import { Cms } from '@kit/cms';
|
||||
import { isBrowser } from '@kit/shared/utils';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { cn, isRouteActive } from '@kit/ui/utils';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuSub,
|
||||
} from '@kit/ui/shadcn-sidebar';
|
||||
|
||||
function DocsNavLink({
|
||||
label,
|
||||
url,
|
||||
level,
|
||||
activePath,
|
||||
}: {
|
||||
label: string;
|
||||
url: string;
|
||||
level: number;
|
||||
activePath: string;
|
||||
}) {
|
||||
const isCurrent = isRouteActive(url, activePath, true);
|
||||
const isFirstLevel = level === 0;
|
||||
import { DocsNavLink } from '~/(marketing)/docs/_components/docs-nav-link';
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={cn('w-full shadow-none', {
|
||||
['font-normal']: !isFirstLevel,
|
||||
})}
|
||||
variant={isCurrent ? 'secondary' : 'ghost'}
|
||||
>
|
||||
<Link
|
||||
className="flex h-full max-w-full grow items-center space-x-2"
|
||||
href={url}
|
||||
>
|
||||
<span className="block max-w-full truncate">{label}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
import { FloatingDocumentationNavigation } from './floating-docs-navigation';
|
||||
|
||||
function Node({
|
||||
node,
|
||||
level,
|
||||
activePath,
|
||||
}: {
|
||||
node: Cms.ContentItem;
|
||||
level: number;
|
||||
activePath: string;
|
||||
}) {
|
||||
function Node({ node, level }: { node: Cms.ContentItem; level: number }) {
|
||||
const pathPrefix = `/docs`;
|
||||
const url = `${pathPrefix}/${node.slug}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocsNavLink
|
||||
label={node.title}
|
||||
url={url}
|
||||
level={level}
|
||||
activePath={activePath}
|
||||
/>
|
||||
<DocsNavLink label={node.title} url={url} />
|
||||
|
||||
{(node.children ?? []).length > 0 && (
|
||||
<Tree
|
||||
pages={node.children ?? []}
|
||||
level={level + 1}
|
||||
activePath={activePath}
|
||||
/>
|
||||
<Tree pages={node.children ?? []} level={level + 1} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Tree({
|
||||
pages,
|
||||
level,
|
||||
activePath,
|
||||
}: {
|
||||
pages: Cms.ContentItem[];
|
||||
level: number;
|
||||
activePath: string;
|
||||
}) {
|
||||
function Tree({ pages, level }: { pages: Cms.ContentItem[]; level: number }) {
|
||||
if (level === 0) {
|
||||
return pages.map((treeNode, index) => (
|
||||
<SidebarGroup key={index}>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<Node key={index} node={treeNode} level={level} />
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
));
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn('w-full space-y-1', {
|
||||
['pl-3']: level > 0,
|
||||
})}
|
||||
>
|
||||
<SidebarMenuSub>
|
||||
{pages.map((treeNode, index) => (
|
||||
<Node
|
||||
key={index}
|
||||
node={treeNode}
|
||||
level={level}
|
||||
activePath={activePath}
|
||||
/>
|
||||
<Node key={index} node={treeNode} level={level} />
|
||||
))}
|
||||
</div>
|
||||
</SidebarMenuSub>
|
||||
);
|
||||
}
|
||||
|
||||
export function DocsNavigation({ pages }: { pages: Cms.ContentItem[] }) {
|
||||
const activePath = usePathname();
|
||||
|
||||
return (
|
||||
<>
|
||||
<aside
|
||||
style={{
|
||||
height: `calc(100vh - 64px)`,
|
||||
}}
|
||||
className="sticky top-2 hidden w-80 shrink-0 overflow-y-auto border-r p-4 lg:flex"
|
||||
<Sidebar
|
||||
variant={'ghost'}
|
||||
className={'sticky'}
|
||||
>
|
||||
<Tree pages={pages} level={0} activePath={activePath} />
|
||||
</aside>
|
||||
<Tree pages={pages} level={0} />
|
||||
</Sidebar>
|
||||
|
||||
<div className={'lg:hidden'}>
|
||||
<FloatingDocumentationNavigation
|
||||
pages={pages}
|
||||
activePath={activePath}
|
||||
/>
|
||||
<FloatingDocumentationNavigation>
|
||||
<Tree pages={pages} level={0} />
|
||||
</FloatingDocumentationNavigation>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function FloatingDocumentationNavigation({
|
||||
pages,
|
||||
activePath,
|
||||
}: React.PropsWithChildren<{
|
||||
pages: Cms.ContentItem[];
|
||||
activePath: string;
|
||||
}>) {
|
||||
const body = useMemo(() => {
|
||||
return isBrowser() ? document.body : null;
|
||||
}, []);
|
||||
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const enableScrolling = (element: HTMLElement) =>
|
||||
(element.style.overflowY = '');
|
||||
|
||||
const disableScrolling = (element: HTMLElement) =>
|
||||
(element.style.overflowY = 'hidden');
|
||||
|
||||
// enable/disable body scrolling when the docs are toggled
|
||||
useEffect(() => {
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isVisible) {
|
||||
disableScrolling(body);
|
||||
} else {
|
||||
enableScrolling(body);
|
||||
}
|
||||
}, [isVisible, body]);
|
||||
|
||||
// hide docs when navigating to another page
|
||||
useEffect(() => {
|
||||
setIsVisible(false);
|
||||
}, [activePath]);
|
||||
|
||||
const onClick = () => {
|
||||
setIsVisible(!isVisible);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<If condition={isVisible}>
|
||||
<div
|
||||
className={
|
||||
'fixed left-0 top-0 z-10 h-screen w-full p-4' +
|
||||
' flex flex-col space-y-4 overflow-auto bg-white dark:bg-background'
|
||||
}
|
||||
>
|
||||
<Tree pages={pages} level={0} activePath={activePath} />
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<Button
|
||||
className={'fixed bottom-5 right-5 z-10 h-16 w-16 rounded-full'}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Menu className={'h-8'} />
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
interface NavItem {
|
||||
text: string;
|
||||
level: number;
|
||||
href: string;
|
||||
children: NavItem[];
|
||||
}
|
||||
|
||||
export function DocsTableOfContents(props: { data: NavItem[] }) {
|
||||
const navData = props.data;
|
||||
|
||||
return (
|
||||
<div className="lg:block hidden sticky z-10 bg-background min-w-[14em] border-l p-4 inset-y-0 h-svh">
|
||||
<ol
|
||||
role="list"
|
||||
className="relative text-sm text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
{navData.map((item) => (
|
||||
<li key={item.href} className="group/item relative mt-3 first:mt-0">
|
||||
<a
|
||||
href={item.href}
|
||||
className="block transition-colors hover:text-gray-950 dark:hover:text-white [&_*]:[font:inherit]"
|
||||
>
|
||||
{item.text}
|
||||
</a>
|
||||
{item.children && (
|
||||
<ol role="list" className="relative mt-3 pl-4">
|
||||
{item.children.map((child) => (
|
||||
<li
|
||||
key={child.href}
|
||||
className="group/subitem relative mt-3 first:mt-0"
|
||||
>
|
||||
<Link
|
||||
href={child.href}
|
||||
className="block transition-colors hover:text-gray-950 dark:hover:text-white [&_*]:[font:inherit]"
|
||||
>
|
||||
{child.text}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { Menu } from 'lucide-react';
|
||||
|
||||
import { isBrowser } from '@kit/shared/utils';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { If } from '@kit/ui/if';
|
||||
|
||||
export function FloatingDocumentationNavigation(
|
||||
props: React.PropsWithChildren,
|
||||
) {
|
||||
const activePath = usePathname();
|
||||
|
||||
const body = useMemo(() => {
|
||||
return isBrowser() ? document.body : null;
|
||||
}, []);
|
||||
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const enableScrolling = (element: HTMLElement) =>
|
||||
(element.style.overflowY = '');
|
||||
|
||||
const disableScrolling = (element: HTMLElement) =>
|
||||
(element.style.overflowY = 'hidden');
|
||||
|
||||
// enable/disable body scrolling when the docs are toggled
|
||||
useEffect(() => {
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isVisible) {
|
||||
disableScrolling(body);
|
||||
} else {
|
||||
enableScrolling(body);
|
||||
}
|
||||
}, [isVisible, body]);
|
||||
|
||||
// hide docs when navigating to another page
|
||||
useEffect(() => {
|
||||
setIsVisible(false);
|
||||
}, [activePath]);
|
||||
|
||||
const onClick = () => {
|
||||
setIsVisible(!isVisible);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<If condition={isVisible}>
|
||||
<div
|
||||
className={
|
||||
'fixed left-0 top-0 z-10 h-screen w-full p-4' +
|
||||
' flex flex-col space-y-4 overflow-auto bg-white dark:bg-background'
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<Button
|
||||
className={'fixed bottom-5 right-5 z-10 h-16 w-16 rounded-full'}
|
||||
onClick={onClick}
|
||||
>
|
||||
<Menu className={'h-8'} />
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user