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
@@ -48,12 +48,8 @@ export function SiteNavigation() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={'hidden items-center justify-center md:flex'}>
|
<div className={'hidden items-center justify-center md:flex'}>
|
||||||
<NavigationMenu
|
<NavigationMenu className={'px-4 py-2'}>
|
||||||
className={
|
<NavigationMenuList className={'space-x-5'}>
|
||||||
'rounded-full border border-gray-100 px-4 py-2 dark:border-primary/10'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<NavigationMenuList className={'space-x-4'}>
|
|
||||||
{NavItems}
|
{NavItems}
|
||||||
</NavigationMenuList>
|
</NavigationMenuList>
|
||||||
</NavigationMenu>
|
</NavigationMenu>
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
|
.HTML {
|
||||||
|
@apply text-secondary-foreground;
|
||||||
|
}
|
||||||
|
|
||||||
.HTML h1 {
|
.HTML h1 {
|
||||||
@apply mt-14 text-4xl font-bold font-heading;
|
@apply mt-14 text-4xl font-semibold font-heading tracking-tight ;
|
||||||
}
|
}
|
||||||
|
|
||||||
.HTML h2 {
|
.HTML h2 {
|
||||||
@apply mb-4 mt-12 text-2xl font-semibold lg:text-3xl font-heading;
|
@apply mb-4 mt-12 font-semibold text-2xl font-heading tracking-tight;
|
||||||
}
|
}
|
||||||
|
|
||||||
.HTML h3 {
|
.HTML h3 {
|
||||||
@apply mt-10 text-2xl font-bold font-heading;
|
@apply mt-10 text-xl font-semibold font-heading tracking-tight;
|
||||||
}
|
}
|
||||||
|
|
||||||
.HTML h4 {
|
.HTML h4 {
|
||||||
@apply mt-8 text-xl font-bold;
|
@apply mt-8 text-lg font-medium tracking-tight;
|
||||||
}
|
}
|
||||||
|
|
||||||
.HTML h5 {
|
.HTML h5 {
|
||||||
@apply mt-6 text-lg font-semibold;
|
@apply mt-6 text-base font-medium tracking-tight;
|
||||||
}
|
}
|
||||||
|
|
||||||
.HTML h6 {
|
.HTML h6 {
|
||||||
@apply mt-2 text-base font-medium;
|
@apply mt-2 text-sm font-normal tracking-tight;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,11 +41,11 @@ For more info: https://github.com/tailwindlabs/tailwindcss/issues/3258#issuecomm
|
|||||||
}
|
}
|
||||||
|
|
||||||
.HTML p {
|
.HTML p {
|
||||||
@apply mb-4 mt-2 text-base leading-7 text-muted-foreground;
|
@apply mb-4 mt-2 text-base leading-7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.HTML li {
|
.HTML li {
|
||||||
@apply relative my-1.5 text-base leading-7 text-muted-foreground;
|
@apply relative my-1.5 text-base leading-7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.HTML ul > li:before {
|
.HTML ul > li:before {
|
||||||
@@ -93,5 +97,5 @@ For more info: https://github.com/tailwindlabs/tailwindcss/issues/3258#issuecomm
|
|||||||
}
|
}
|
||||||
|
|
||||||
.HTML pre {
|
.HTML pre {
|
||||||
@apply my-6 text-sm text-current border p-6 rounded-lg;
|
@apply my-6 text-sm text-current border p-6 rounded-lg overflow-x-scroll;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,17 @@ import { notFound } from 'next/navigation';
|
|||||||
import { ContentRenderer, createCmsClient } from '@kit/cms';
|
import { ContentRenderer, createCmsClient } from '@kit/cms';
|
||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
import { Separator } from '@kit/ui/separator';
|
import { Separator } from '@kit/ui/separator';
|
||||||
|
import { cn } from '@kit/ui/utils';
|
||||||
|
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
import { SitePageHeader } from '../../_components/site-page-header';
|
// styles
|
||||||
import styles from '../../blog/_components/html-renderer.module.css';
|
import styles from '../../blog/_components/html-renderer.module.css';
|
||||||
|
|
||||||
|
// local imports
|
||||||
import { DocsCards } from '../_components/docs-cards';
|
import { DocsCards } from '../_components/docs-cards';
|
||||||
|
import { DocsTableOfContents } from '../_components/docs-table-of-contents';
|
||||||
|
import { extractHeadingsFromJSX } from '../_lib/utils';
|
||||||
|
|
||||||
const getPageBySlug = cache(pageLoader);
|
const getPageBySlug = cache(pageLoader);
|
||||||
|
|
||||||
@@ -50,26 +55,35 @@ async function DocumentationPage({ params }: DocumentationPageProps) {
|
|||||||
|
|
||||||
const description = page?.description ?? '';
|
const description = page?.description ?? '';
|
||||||
|
|
||||||
return (
|
const headings = extractHeadingsFromJSX(
|
||||||
<div className={'flex flex-1 flex-col'}>
|
page.content as {
|
||||||
<SitePageHeader
|
props: { children: React.ReactElement[] };
|
||||||
className={'lg:px-8'}
|
},
|
||||||
container={false}
|
);
|
||||||
title={page.title}
|
|
||||||
subtitle={description}
|
return (
|
||||||
/>
|
<div className={'flex flex-col flex-1 space-y-4'}>
|
||||||
|
<div className={'flex'}>
|
||||||
|
<article className={cn(styles.HTML, 'space-y-12 container')}>
|
||||||
|
<section className={'flex flex-col space-y-4 pt-6'}>
|
||||||
|
<h1 className={'!my-0'}>{page.title}</h1>
|
||||||
|
|
||||||
|
<h2 className={'!mb-0 !font-normal text-muted-foreground'}>
|
||||||
|
{description}
|
||||||
|
</h2>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div className={'flex flex-col space-y-4 py-6 lg:px-8'}>
|
|
||||||
<article className={styles.HTML}>
|
|
||||||
<ContentRenderer content={page.content} />
|
<ContentRenderer content={page.content} />
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<If condition={page.children.length > 0}>
|
<DocsTableOfContents data={headings} />
|
||||||
<Separator />
|
|
||||||
|
|
||||||
<DocsCards cards={page.children ?? []} />
|
|
||||||
</If>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<If condition={page.children.length > 0}>
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<DocsCards cards={page.children ?? []} />
|
||||||
|
</If>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
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 { Cms } from '@kit/cms';
|
||||||
import { isBrowser } from '@kit/shared/utils';
|
import {
|
||||||
import { Button } from '@kit/ui/button';
|
Sidebar,
|
||||||
import { If } from '@kit/ui/if';
|
SidebarGroup,
|
||||||
import { cn, isRouteActive } from '@kit/ui/utils';
|
SidebarGroupContent,
|
||||||
|
SidebarMenu,
|
||||||
|
SidebarMenuSub,
|
||||||
|
} from '@kit/ui/shadcn-sidebar';
|
||||||
|
|
||||||
function DocsNavLink({
|
import { DocsNavLink } from '~/(marketing)/docs/_components/docs-nav-link';
|
||||||
label,
|
|
||||||
url,
|
|
||||||
level,
|
|
||||||
activePath,
|
|
||||||
}: {
|
|
||||||
label: string;
|
|
||||||
url: string;
|
|
||||||
level: number;
|
|
||||||
activePath: string;
|
|
||||||
}) {
|
|
||||||
const isCurrent = isRouteActive(url, activePath, true);
|
|
||||||
const isFirstLevel = level === 0;
|
|
||||||
|
|
||||||
return (
|
import { FloatingDocumentationNavigation } from './floating-docs-navigation';
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Node({
|
function Node({ node, level }: { node: Cms.ContentItem; level: number }) {
|
||||||
node,
|
|
||||||
level,
|
|
||||||
activePath,
|
|
||||||
}: {
|
|
||||||
node: Cms.ContentItem;
|
|
||||||
level: number;
|
|
||||||
activePath: string;
|
|
||||||
}) {
|
|
||||||
const pathPrefix = `/docs`;
|
const pathPrefix = `/docs`;
|
||||||
const url = `${pathPrefix}/${node.slug}`;
|
const url = `${pathPrefix}/${node.slug}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DocsNavLink
|
<DocsNavLink label={node.title} url={url} />
|
||||||
label={node.title}
|
|
||||||
url={url}
|
|
||||||
level={level}
|
|
||||||
activePath={activePath}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{(node.children ?? []).length > 0 && (
|
{(node.children ?? []).length > 0 && (
|
||||||
<Tree
|
<Tree pages={node.children ?? []} level={level + 1} />
|
||||||
pages={node.children ?? []}
|
|
||||||
level={level + 1}
|
|
||||||
activePath={activePath}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Tree({
|
function Tree({ pages, level }: { pages: Cms.ContentItem[]; level: number }) {
|
||||||
pages,
|
if (level === 0) {
|
||||||
level,
|
return pages.map((treeNode, index) => (
|
||||||
activePath,
|
<SidebarGroup key={index}>
|
||||||
}: {
|
<SidebarGroupContent>
|
||||||
pages: Cms.ContentItem[];
|
<SidebarMenu>
|
||||||
level: number;
|
<Node key={index} node={treeNode} level={level} />
|
||||||
activePath: string;
|
</SidebarMenu>
|
||||||
}) {
|
</SidebarGroupContent>
|
||||||
|
</SidebarGroup>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<SidebarMenuSub>
|
||||||
className={cn('w-full space-y-1', {
|
|
||||||
['pl-3']: level > 0,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{pages.map((treeNode, index) => (
|
{pages.map((treeNode, index) => (
|
||||||
<Node
|
<Node key={index} node={treeNode} level={level} />
|
||||||
key={index}
|
|
||||||
node={treeNode}
|
|
||||||
level={level}
|
|
||||||
activePath={activePath}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</SidebarMenuSub>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DocsNavigation({ pages }: { pages: Cms.ContentItem[] }) {
|
export function DocsNavigation({ pages }: { pages: Cms.ContentItem[] }) {
|
||||||
const activePath = usePathname();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<aside
|
<Sidebar
|
||||||
style={{
|
variant={'ghost'}
|
||||||
height: `calc(100vh - 64px)`,
|
className={'sticky'}
|
||||||
}}
|
|
||||||
className="sticky top-2 hidden w-80 shrink-0 overflow-y-auto border-r p-4 lg:flex"
|
|
||||||
>
|
>
|
||||||
<Tree pages={pages} level={0} activePath={activePath} />
|
<Tree pages={pages} level={0} />
|
||||||
</aside>
|
</Sidebar>
|
||||||
|
|
||||||
<div className={'lg:hidden'}>
|
<div className={'lg:hidden'}>
|
||||||
<FloatingDocumentationNavigation
|
<FloatingDocumentationNavigation>
|
||||||
pages={pages}
|
<Tree pages={pages} level={0} />
|
||||||
activePath={activePath}
|
</FloatingDocumentationNavigation>
|
||||||
/>
|
|
||||||
</div>
|
</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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
136
apps/web/app/(marketing)/docs/_lib/utils.ts
Normal file
136
apps/web/app/(marketing)/docs/_lib/utils.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { Cms } from '@kit/cms';
|
||||||
|
|
||||||
|
interface HeadingNode {
|
||||||
|
text: string;
|
||||||
|
level: number;
|
||||||
|
href: string;
|
||||||
|
children: HeadingNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name buildDocumentationTree
|
||||||
|
* @description Build a tree structure for the documentation pages.
|
||||||
|
* @param pages
|
||||||
|
*/
|
||||||
|
export function buildDocumentationTree(pages: Cms.ContentItem[]) {
|
||||||
|
const tree: Cms.ContentItem[] = [];
|
||||||
|
|
||||||
|
pages.forEach((page) => {
|
||||||
|
if (page.parentId) {
|
||||||
|
const parent = pages.find((item) => item.slug === page.parentId);
|
||||||
|
|
||||||
|
if (!parent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parent.children) {
|
||||||
|
parent.children = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.children.push(page);
|
||||||
|
|
||||||
|
// sort children by order
|
||||||
|
parent.children.sort((a, b) => a.order - b.order);
|
||||||
|
} else {
|
||||||
|
tree.push(page);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return tree.sort((a, b) => a.order - b.order);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name extractHeadingsFromJSX
|
||||||
|
* @description Extract headings from JSX. This is used to generate the table of contents for the documentation pages.
|
||||||
|
* @param jsx
|
||||||
|
*/
|
||||||
|
export function extractHeadingsFromJSX(jsx: {
|
||||||
|
props: { children: React.ReactElement[] };
|
||||||
|
}) {
|
||||||
|
const headings: HeadingNode[] = [];
|
||||||
|
let currentH2: HeadingNode | null = null;
|
||||||
|
|
||||||
|
function getTextContent(
|
||||||
|
children: React.ReactElement[] | string | React.ReactElement,
|
||||||
|
): string {
|
||||||
|
if (typeof children === 'string') {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(children)) {
|
||||||
|
return children.map((child) => getTextContent(child)).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(
|
||||||
|
children.props as {
|
||||||
|
children: React.ReactElement;
|
||||||
|
}
|
||||||
|
).children
|
||||||
|
) {
|
||||||
|
return getTextContent((children.props as { children: React.ReactElement }).children);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
jsx.props.children.forEach((node) => {
|
||||||
|
if (!node || typeof node !== 'object' || !('type' in node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeType = node.type as string;
|
||||||
|
|
||||||
|
const text = getTextContent(
|
||||||
|
(
|
||||||
|
node.props as {
|
||||||
|
children: React.ReactElement[];
|
||||||
|
}
|
||||||
|
).children,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (nodeType === 'h1') {
|
||||||
|
const slug = generateSlug(text);
|
||||||
|
|
||||||
|
headings.push({
|
||||||
|
text,
|
||||||
|
level: 1,
|
||||||
|
href: `#${slug}`,
|
||||||
|
children: [],
|
||||||
|
});
|
||||||
|
} else if (nodeType === 'h2') {
|
||||||
|
const slug = generateSlug(text);
|
||||||
|
|
||||||
|
currentH2 = {
|
||||||
|
text,
|
||||||
|
level: 2,
|
||||||
|
href: `#${slug}`,
|
||||||
|
children: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (headings.length > 0) {
|
||||||
|
headings[headings.length - 1]!.children.push(currentH2);
|
||||||
|
} else {
|
||||||
|
headings.push(currentH2);
|
||||||
|
}
|
||||||
|
} else if (nodeType === 'h3' && currentH2) {
|
||||||
|
const slug = generateSlug(text);
|
||||||
|
|
||||||
|
currentH2.children.push({
|
||||||
|
text,
|
||||||
|
level: 3,
|
||||||
|
href: `#${slug}`,
|
||||||
|
children: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return headings;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSlug(text: string): string {
|
||||||
|
return text
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
|
.replace(/(^-|-$)/g, '');
|
||||||
|
}
|
||||||
@@ -1,56 +1,24 @@
|
|||||||
import { Cms } from '@kit/cms';
|
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
|
||||||
|
|
||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
|
|
||||||
// local imports
|
// local imports
|
||||||
import { DocsNavigation } from './_components/docs-navigation';
|
import { DocsNavigation } from './_components/docs-navigation';
|
||||||
import { getDocs } from './_lib/server/docs.loader';
|
import { getDocs } from './_lib/server/docs.loader';
|
||||||
|
import { buildDocumentationTree } from './_lib/utils';
|
||||||
|
|
||||||
async function DocsLayout({ children }: React.PropsWithChildren) {
|
async function DocsLayout({ children }: React.PropsWithChildren) {
|
||||||
const { resolvedLanguage } = await createI18nServerInstance();
|
const { resolvedLanguage } = await createI18nServerInstance();
|
||||||
const pages = await getDocs(resolvedLanguage);
|
const docs = await getDocs(resolvedLanguage);
|
||||||
|
const tree = buildDocumentationTree(docs);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'md:container flex'}>
|
<SidebarProvider className={'lg:container'}>
|
||||||
<DocsNavigation pages={buildDocumentationTree(pages)} />
|
<DocsNavigation pages={tree} />
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</SidebarProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DocsLayout;
|
export default DocsLayout;
|
||||||
|
|
||||||
// we want to place all the children under their parent
|
|
||||||
// based on the property parentId
|
|
||||||
function buildDocumentationTree(pages: Cms.ContentItem[]) {
|
|
||||||
const tree: Cms.ContentItem[] = [];
|
|
||||||
const map: Record<string, Cms.ContentItem> = {};
|
|
||||||
|
|
||||||
pages.forEach((page) => {
|
|
||||||
map[page.id] = page;
|
|
||||||
});
|
|
||||||
|
|
||||||
pages.forEach((page) => {
|
|
||||||
if (page.parentId) {
|
|
||||||
const parent = map[page.parentId];
|
|
||||||
|
|
||||||
if (!parent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!parent.children) {
|
|
||||||
parent.children = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
parent.children.push(page);
|
|
||||||
|
|
||||||
// sort children by order
|
|
||||||
parent.children.sort((a, b) => a.order - b.order);
|
|
||||||
} else {
|
|
||||||
tree.push(page);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return tree.sort((a, b) => a.order - b.order);
|
|
||||||
}
|
|
||||||
|
|||||||
3
apps/web/app/(marketing)/docs/loading.tsx
Normal file
3
apps/web/app/(marketing)/docs/loading.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { GlobalLoader } from '@kit/ui/global-loader';
|
||||||
|
|
||||||
|
export default GlobalLoader;
|
||||||
@@ -2,8 +2,7 @@
|
|||||||
title: "Configuration"
|
title: "Configuration"
|
||||||
description: "Learn how authentication works in MakerKit and how to configure it."
|
description: "Learn how authentication works in MakerKit and how to configure it."
|
||||||
publishedAt: 2024-04-11
|
publishedAt: 2024-04-11
|
||||||
order: 0
|
order: 1
|
||||||
parent: "authentication/authentication"
|
|
||||||
status: "published"
|
status: "published"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
title: "Installing Dependencies"
|
title: "Installing Dependencies"
|
||||||
description: "Learn how to install dependencies for your project."
|
description: "Learn how to install dependencies for your project."
|
||||||
publishedAt: 2024-04-11
|
publishedAt: 2024-04-11
|
||||||
parent: "getting-started/getting-started"
|
|
||||||
order: 0
|
order: 0
|
||||||
status: "published"
|
status: "published"
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export function KeystaticContentRenderer(props: { content: unknown }) {
|
export function KeystaticContentRenderer(props: { content: unknown }) {
|
||||||
return <div>{props.content as React.ReactNode}</div>;
|
return props.content as React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { Cms, CmsClient } from '@kit/cms-types';
|
import { Cms, CmsClient } from '@kit/cms-types';
|
||||||
|
|
||||||
import { createKeystaticReader } from './create-reader';
|
import { createKeystaticReader } from './create-reader';
|
||||||
import {
|
|
||||||
CustomMarkdocComponents,
|
|
||||||
CustomMarkdocTags,
|
|
||||||
} from './custom-components';
|
|
||||||
import { PostEntryProps } from './keystatic.config';
|
import { PostEntryProps } from './keystatic.config';
|
||||||
|
import { renderMarkdoc } from './markdoc';
|
||||||
|
|
||||||
export function createKeystaticClient() {
|
export function createKeystaticClient() {
|
||||||
return new KeystaticClient();
|
return new KeystaticClient();
|
||||||
@@ -85,12 +80,78 @@ class KeystaticClient implements CmsClient {
|
|||||||
return right - left;
|
return right - left;
|
||||||
});
|
});
|
||||||
|
|
||||||
const items = await Promise.all(
|
function processItems(items: typeof docs) {
|
||||||
filtered.slice(startOffset, endOffset).map(async (item) => {
|
const result: typeof docs = [...items];
|
||||||
const children = docs.filter((item) => item.entry.parent === item.slug);
|
|
||||||
|
|
||||||
return this.mapPost(item, children);
|
const indexFiles = items.filter((item) => {
|
||||||
}),
|
const parts = item.slug.split('/');
|
||||||
|
|
||||||
|
return (
|
||||||
|
parts.length > 1 &&
|
||||||
|
parts[parts.length - 1] === parts[parts.length - 2]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function findParentIndex(pathParts: string[]): string | null {
|
||||||
|
// Try each level up from the current path until we find an index file
|
||||||
|
for (let i = pathParts.length - 1; i > 0; i--) {
|
||||||
|
const currentPath = pathParts.slice(0, i).join('/');
|
||||||
|
|
||||||
|
const possibleParent = indexFiles.find((indexFile) => {
|
||||||
|
const indexParts = indexFile.slug.split('/');
|
||||||
|
const indexFolderPath = indexParts.slice(0, -1).join('/');
|
||||||
|
|
||||||
|
return indexFolderPath === currentPath;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (possibleParent) {
|
||||||
|
return possibleParent.slug;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.forEach((item) => {
|
||||||
|
const pathParts = item.slug.split('/');
|
||||||
|
|
||||||
|
// Skip if this is a root level index file (e.g., "authentication/authentication")
|
||||||
|
if (pathParts.length === 2 && pathParts[0] === pathParts[1]) {
|
||||||
|
item.entry.parent = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if current item is an index file
|
||||||
|
const isIndexFile =
|
||||||
|
pathParts[pathParts.length - 1] === pathParts[pathParts.length - 2];
|
||||||
|
|
||||||
|
if (isIndexFile) {
|
||||||
|
// For index files, find parent in the level above
|
||||||
|
const parentPath = pathParts.slice(0, -2);
|
||||||
|
if (parentPath.length > 0) {
|
||||||
|
item.entry.parent = findParentIndex(
|
||||||
|
parentPath.concat(parentPath[parentPath.length - 1]!),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
item.entry.parent = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For regular files, find parent in the current folder
|
||||||
|
item.entry.parent = findParentIndex(pathParts);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemsWithParents = processItems(filtered);
|
||||||
|
|
||||||
|
const items = await Promise.all(
|
||||||
|
itemsWithParents
|
||||||
|
.slice(startOffset, endOffset)
|
||||||
|
.sort((a, b) => {
|
||||||
|
return (a.entry.order ?? 0) - (b.entry.order ?? 0);
|
||||||
|
})
|
||||||
|
.map((item) => this.mapPost(item)),
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -157,25 +218,17 @@ class KeystaticClient implements CmsClient {
|
|||||||
slug: string;
|
slug: string;
|
||||||
},
|
},
|
||||||
>(item: Type, children: Type[] = []): Promise<Cms.ContentItem> {
|
>(item: Type, children: Type[] = []): Promise<Cms.ContentItem> {
|
||||||
const { transform, renderers } = await import('@markdoc/markdoc');
|
|
||||||
|
|
||||||
const publishedAt = item.entry.publishedAt
|
const publishedAt = item.entry.publishedAt
|
||||||
? new Date(item.entry.publishedAt)
|
? new Date(item.entry.publishedAt)
|
||||||
: new Date();
|
: new Date();
|
||||||
|
|
||||||
const markdoc = await item.entry.content();
|
const content = await item.entry.content();
|
||||||
|
const html = await renderMarkdoc(content.node);
|
||||||
const content = transform(markdoc.node, {
|
|
||||||
tags: CustomMarkdocTags,
|
|
||||||
});
|
|
||||||
|
|
||||||
const html = renderers.react(content, React, {
|
|
||||||
components: CustomMarkdocComponents,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: item.slug,
|
id: item.slug,
|
||||||
title: item.entry.title,
|
title: item.entry.title,
|
||||||
|
label: item.entry.label,
|
||||||
url: item.slug,
|
url: item.slug,
|
||||||
slug: item.slug,
|
slug: item.slug,
|
||||||
description: item.entry.description,
|
description: item.entry.description,
|
||||||
@@ -201,7 +254,7 @@ class KeystaticClient implements CmsClient {
|
|||||||
parentId: item.entry.parent ?? undefined,
|
parentId: item.entry.parent ?? undefined,
|
||||||
order: item.entry.order ?? 1,
|
order: item.entry.order ?? 1,
|
||||||
children: await Promise.all(
|
children: await Promise.all(
|
||||||
children.map(async (child) => this.mapPost(child, [])),
|
children.map((child) => this.mapPost(child, [])),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ function getKeystaticCollections(path: string) {
|
|||||||
format: { contentField: 'content' },
|
format: { contentField: 'content' },
|
||||||
schema: {
|
schema: {
|
||||||
title: fields.slug({ name: { label: 'Title' } }),
|
title: fields.slug({ name: { label: 'Title' } }),
|
||||||
|
label: fields.text({ label: 'Label', validation: { isRequired: false } }),
|
||||||
image: fields.image({
|
image: fields.image({
|
||||||
label: 'Image',
|
label: 'Image',
|
||||||
directory: 'public/site/images',
|
directory: 'public/site/images',
|
||||||
@@ -101,6 +102,7 @@ function getKeystaticCollections(path: string) {
|
|||||||
format: { contentField: 'content' },
|
format: { contentField: 'content' },
|
||||||
schema: {
|
schema: {
|
||||||
title: fields.slug({ name: { label: 'Title' } }),
|
title: fields.slug({ name: { label: 'Title' } }),
|
||||||
|
label: fields.text({ label: 'Label', validation: { isRequired: false } }),
|
||||||
content: getContentField(),
|
content: getContentField(),
|
||||||
image: fields.image({
|
image: fields.image({
|
||||||
label: 'Image',
|
label: 'Image',
|
||||||
|
|||||||
42
packages/cms/keystatic/src/markdoc-nodes.ts
Normal file
42
packages/cms/keystatic/src/markdoc-nodes.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// Or replace this with your own function
|
||||||
|
import { Config, Node, RenderableTreeNode, Tag } from '@markdoc/markdoc';
|
||||||
|
|
||||||
|
function generateID(
|
||||||
|
children: Array<RenderableTreeNode>,
|
||||||
|
attributes: Record<string, unknown>,
|
||||||
|
) {
|
||||||
|
if (attributes.id && typeof attributes.id === 'string') {
|
||||||
|
return attributes.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children
|
||||||
|
.filter((child) => typeof child === 'string')
|
||||||
|
.join(' ')
|
||||||
|
.replace(/[?]/g, '')
|
||||||
|
.replace(/\s+/g, '-')
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
const heading = {
|
||||||
|
children: ['inline'],
|
||||||
|
attributes: {
|
||||||
|
id: { type: String },
|
||||||
|
level: { type: Number, required: true, default: 1 },
|
||||||
|
},
|
||||||
|
transform(node: Node, config: Config) {
|
||||||
|
const attributes = node.transformAttributes(config);
|
||||||
|
const children = node.transformChildren(config);
|
||||||
|
|
||||||
|
const id = generateID(children, attributes);
|
||||||
|
|
||||||
|
return new Tag(
|
||||||
|
`h${node.attributes.level}`,
|
||||||
|
{ ...attributes, id },
|
||||||
|
children,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MarkdocNodes = {
|
||||||
|
heading,
|
||||||
|
};
|
||||||
30
packages/cms/keystatic/src/markdoc.tsx
Normal file
30
packages/cms/keystatic/src/markdoc.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Node } from '@markdoc/markdoc';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CustomMarkdocComponents,
|
||||||
|
CustomMarkdocTags,
|
||||||
|
} from './custom-components';
|
||||||
|
import { MarkdocNodes } from './markdoc-nodes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @name renderMarkdoc
|
||||||
|
* @description Renders a Markdoc tree to React
|
||||||
|
*/
|
||||||
|
export async function renderMarkdoc(node: Node) {
|
||||||
|
const { transform, renderers } = await import('@markdoc/markdoc');
|
||||||
|
|
||||||
|
const content = transform(node, {
|
||||||
|
tags: {
|
||||||
|
...CustomMarkdocTags,
|
||||||
|
},
|
||||||
|
nodes: {
|
||||||
|
...MarkdocNodes,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return renderers.react(content, React, {
|
||||||
|
components: CustomMarkdocComponents,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ export namespace Cms {
|
|||||||
export interface ContentItem {
|
export interface ContentItem {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
label: string | undefined;
|
||||||
url: string;
|
url: string;
|
||||||
description: string | undefined;
|
description: string | undefined;
|
||||||
content: unknown;
|
content: unknown;
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ class WordpressClient implements CmsClient {
|
|||||||
return {
|
return {
|
||||||
id: item.id.toString(),
|
id: item.id.toString(),
|
||||||
title: item.title.rendered,
|
title: item.title.rendered,
|
||||||
|
label: item.title.rendered,
|
||||||
content: item.content.rendered,
|
content: item.content.rendered,
|
||||||
description: item.excerpt.rendered,
|
description: item.excerpt.rendered,
|
||||||
image,
|
image,
|
||||||
@@ -217,6 +218,7 @@ class WordpressClient implements CmsClient {
|
|||||||
description: item.excerpt.rendered,
|
description: item.excerpt.rendered,
|
||||||
children: [],
|
children: [],
|
||||||
title: item.title.rendered,
|
title: item.title.rendered,
|
||||||
|
label: item.title.rendered,
|
||||||
content: item.content.rendered,
|
content: item.content.rendered,
|
||||||
slug: item.slug,
|
slug: item.slug,
|
||||||
publishedAt: item.date,
|
publishedAt: item.date,
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ const Sidebar = React.forwardRef<
|
|||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.ComponentProps<'div'> & {
|
React.ComponentProps<'div'> & {
|
||||||
side?: 'left' | 'right';
|
side?: 'left' | 'right';
|
||||||
variant?: 'sidebar' | 'floating' | 'inset';
|
variant?: 'sidebar' | 'floating' | 'inset' | 'ghost';
|
||||||
collapsible?: 'offcanvas' | 'icon' | 'none';
|
collapsible?: 'offcanvas' | 'icon' | 'none';
|
||||||
}
|
}
|
||||||
>(
|
>(
|
||||||
@@ -285,7 +285,13 @@ const Sidebar = React.forwardRef<
|
|||||||
<SheetContent
|
<SheetContent
|
||||||
data-sidebar="sidebar"
|
data-sidebar="sidebar"
|
||||||
data-mobile="true"
|
data-mobile="true"
|
||||||
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
className={cn(
|
||||||
|
'w-[--sidebar-width] p-0 text-sidebar-foreground [&>button]:hidden',
|
||||||
|
{
|
||||||
|
'bg-background': variant === 'ghost',
|
||||||
|
'bg-sidebar': variant !== 'ghost',
|
||||||
|
},
|
||||||
|
)}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
|
'--sidebar-width': SIDEBAR_WIDTH_MOBILE,
|
||||||
@@ -313,12 +319,15 @@ const Sidebar = React.forwardRef<
|
|||||||
{/* This is what handles the sidebar gap on desktop */}
|
{/* This is what handles the sidebar gap on desktop */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative h-svh w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear',
|
'relative w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear',
|
||||||
'group-data-[collapsible=offcanvas]:w-0',
|
'group-data-[collapsible=offcanvas]:w-0',
|
||||||
'group-data-[side=right]:rotate-180',
|
'group-data-[side=right]:rotate-180',
|
||||||
variant === 'floating' || variant === 'inset'
|
variant === 'floating' || variant === 'inset'
|
||||||
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]'
|
? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]'
|
||||||
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]',
|
: 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]',
|
||||||
|
{
|
||||||
|
'h-svh': variant !== 'ghost',
|
||||||
|
},
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
@@ -337,7 +346,12 @@ const Sidebar = React.forwardRef<
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
data-sidebar="sidebar"
|
data-sidebar="sidebar"
|
||||||
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
|
className={cn(
|
||||||
|
'flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow',
|
||||||
|
{
|
||||||
|
'bg-background': variant === 'ghost',
|
||||||
|
},
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
@@ -862,11 +876,7 @@ export function SidebarNavigation({
|
|||||||
|
|
||||||
const ContentContainer = (props: React.PropsWithChildren) => {
|
const ContentContainer = (props: React.PropsWithChildren) => {
|
||||||
if (item.collapsible) {
|
if (item.collapsible) {
|
||||||
return (
|
return <CollapsibleContent>{props.children}</CollapsibleContent>;
|
||||||
<CollapsibleContent>
|
|
||||||
{props.children}
|
|
||||||
</CollapsibleContent>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return props.children;
|
return props.children;
|
||||||
|
|||||||
Reference in New Issue
Block a user