Unify workspace dropdowns; Update layouts (#458)

Unified Account and Workspace drop-downs; Layout updates, now header lives within the PageBody component; Sidebars now use floating variant
This commit is contained in:
Giancarlo Buomprisco
2026-03-11 14:45:42 +08:00
committed by GitHub
parent ca585e09be
commit 4bc8448a1d
530 changed files with 14398 additions and 11198 deletions

View File

@@ -1,94 +0,0 @@
import { cache } from 'react';
import { notFound } from 'next/navigation';
import { ContentRenderer, createCmsClient } from '@kit/cms';
import { If } from '@kit/ui/if';
import { Separator } from '@kit/ui/separator';
import { cn } from '@kit/ui/utils';
import { withI18n } from '~/lib/i18n/with-i18n';
// local imports
import { DocsCards } from '../_components/docs-cards';
const getPageBySlug = cache(pageLoader);
interface DocumentationPageProps {
params: Promise<{ slug: string[] }>;
}
async function pageLoader(slug: string) {
const client = await createCmsClient();
return client.getContentItemBySlug({ slug, collection: 'documentation' });
}
export const generateMetadata = async ({ params }: DocumentationPageProps) => {
const slug = (await params).slug.join('/');
const page = await getPageBySlug(slug);
if (!page) {
notFound();
}
const { title, description } = page;
return {
title,
description,
};
};
async function DocumentationPage({ params }: DocumentationPageProps) {
const slug = (await params).slug.join('/');
const page = await getPageBySlug(slug);
if (!page) {
notFound();
}
const description = page?.description ?? '';
return (
<div className={'flex flex-1 flex-col gap-y-4 overflow-y-hidden'}>
<div className={'flex size-full overflow-y-hidden'}>
<div className="relative size-full">
<article
className={cn(
'absolute size-full w-full gap-y-12 overflow-y-auto pt-4 pb-36',
)}
>
<section
className={'flex flex-col gap-y-1 border-b border-dashed pb-4'}
>
<h1
className={
'text-foreground text-3xl font-semibold tracking-tighter'
}
>
{page.title}
</h1>
<h2 className={'text-secondary-foreground/80 text-lg'}>
{description}
</h2>
</section>
<div className={'markdoc'}>
<ContentRenderer content={page.content} />
</div>
</article>
</div>
</div>
<If condition={page.children.length > 0}>
<Separator />
<DocsCards cards={page.children ?? []} />
</If>
</div>
);
}
export default withI18n(DocumentationPage);

View File

@@ -1,32 +0,0 @@
import Link from 'next/link';
export function DocsCard({
title,
subtitle,
children,
link,
}: React.PropsWithChildren<{
title: string;
subtitle?: string | null;
link: { url: string; label?: string };
}>) {
return (
<Link href={link.url} className="flex flex-col">
<div
className={`hover:bg-muted/70 flex grow flex-col gap-y-0.5 rounded border p-4`}
>
<h3 className="mt-0 text-lg font-medium hover:underline dark:text-white">
{title}
</h3>
{subtitle && (
<div className="text-muted-foreground text-sm">
<p dangerouslySetInnerHTML={{ __html: subtitle }}></p>
</div>
)}
{children && <div className="text-sm">{children}</div>}
</div>
</Link>
);
}

View File

@@ -1,24 +0,0 @@
import { Cms } from '@kit/cms';
import { DocsCard } from './docs-card';
export function DocsCards({ cards }: { cards: Cms.ContentItem[] }) {
const cardsSortedByOrder = [...cards].sort((a, b) => a.order - b.order);
return (
<div className={'absolute flex w-full flex-col gap-4 pb-48 lg:max-w-2xl'}>
{cardsSortedByOrder.map((item) => {
return (
<DocsCard
key={item.title}
title={item.title}
subtitle={item.description}
link={{
url: `/docs/${item.slug}`,
}}
/>
);
})}
</div>
);
}

View File

@@ -1,32 +0,0 @@
'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,
children,
}: React.PropsWithChildren<{ label: string; url: string }>) {
const currentPath = usePathname();
const isCurrent = isRouteActive(url, currentPath, true);
return (
<SidebarMenuItem>
<SidebarMenuButton
asChild
isActive={isCurrent}
className={cn('text-secondary-foreground transition-all')}
>
<Link href={url}>
<span className="block max-w-full truncate">{label}</span>
{children}
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
);
}

View File

@@ -1,32 +0,0 @@
'use client';
import { usePathname } from 'next/navigation';
import { Cms } from '@kit/cms';
import { Collapsible } from '@kit/ui/collapsible';
import { cn, isRouteActive } from '@kit/ui/utils';
export function DocsNavigationCollapsible(
props: React.PropsWithChildren<{
node: Cms.ContentItem;
prefix: string;
}>,
) {
const currentPath = usePathname();
const prefix = props.prefix;
const isChildActive = props.node.children.some((child) =>
isRouteActive(prefix + '/' + child.url, currentPath, false),
);
return (
<Collapsible
className={cn('group/collapsible', {
'group/active': isChildActive,
})}
defaultOpen={isChildActive ? true : !props.node.collapsed}
>
{props.children}
</Collapsible>
);
}

View File

@@ -1,167 +0,0 @@
import { ChevronDown } from 'lucide-react';
import { Cms } from '@kit/cms';
import { CollapsibleContent, CollapsibleTrigger } from '@kit/ui/collapsible';
import {
Sidebar,
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
} from '@kit/ui/shadcn-sidebar';
import { DocsNavLink } from '~/(marketing)/docs/_components/docs-nav-link';
import { DocsNavigationCollapsible } from '~/(marketing)/docs/_components/docs-navigation-collapsible';
import { FloatingDocumentationNavigation } from './floating-docs-navigation';
function Node({
node,
level,
prefix,
}: {
node: Cms.ContentItem;
level: number;
prefix: string;
}) {
const url = `${prefix}/${node.slug}`;
const label = node.label ? node.label : node.title;
return (
<NodeContainer node={node} prefix={prefix}>
<NodeTrigger node={node} label={label} url={url} />
<NodeContentContainer node={node}>
<Tree pages={node.children ?? []} level={level + 1} prefix={prefix} />
</NodeContentContainer>
</NodeContainer>
);
}
function NodeContentContainer({
node,
children,
}: {
node: Cms.ContentItem;
children: React.ReactNode;
}) {
if (node.collapsible) {
return <CollapsibleContent>{children}</CollapsibleContent>;
}
return children;
}
function NodeContainer({
node,
prefix,
children,
}: {
node: Cms.ContentItem;
prefix: string;
children: React.ReactNode;
}) {
if (node.collapsible) {
return (
<DocsNavigationCollapsible node={node} prefix={prefix}>
{children}
</DocsNavigationCollapsible>
);
}
return children;
}
function NodeTrigger({
node,
label,
url,
}: {
node: Cms.ContentItem;
label: string;
url: string;
}) {
if (node.collapsible) {
return (
<CollapsibleTrigger asChild>
<SidebarMenuItem>
<SidebarMenuButton>
{label}
<ChevronDown className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-180" />
</SidebarMenuButton>
</SidebarMenuItem>
</CollapsibleTrigger>
);
}
return <DocsNavLink label={label} url={url} />;
}
function Tree({
pages,
level,
prefix,
}: {
pages: Cms.ContentItem[];
level: number;
prefix: string;
}) {
if (level === 0) {
return pages.map((treeNode, index) => (
<Node key={index} node={treeNode} level={level} prefix={prefix} />
));
}
if (pages.length === 0) {
return null;
}
return (
<SidebarMenuSub>
{pages.map((treeNode, index) => (
<Node key={index} node={treeNode} level={level} prefix={prefix} />
))}
</SidebarMenuSub>
);
}
export function DocsNavigation({
pages,
prefix = '/docs',
}: {
pages: Cms.ContentItem[];
prefix?: string;
}) {
return (
<>
<Sidebar
variant={'ghost'}
className={
'border-border/50 sticky z-1 mt-4 max-h-full overflow-y-auto pr-4'
}
>
<SidebarGroup className="p-0">
<SidebarGroupContent>
<SidebarMenu className={'pb-48'}>
<Tree pages={pages} level={0} prefix={prefix} />
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</Sidebar>
<div className={'lg:hidden'}>
<FloatingDocumentationNavigation>
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
<Tree pages={pages} level={0} prefix={prefix} />
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</FloatingDocumentationNavigation>
</div>
</>
);
}

View File

@@ -1,72 +0,0 @@
'use client';
import { useEffect, useEffectEvent, 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 = useEffectEvent(
() => body && (body.style.overflowY = ''),
);
const disableScrolling = useEffectEvent(
() => body && (body.style.overflowY = 'hidden'),
);
// enable/disable body scrolling when the docs are toggled
useEffect(() => {
if (isVisible) {
disableScrolling();
} else {
enableScrolling();
}
}, [isVisible]);
// hide docs when navigating to another page
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setIsVisible(false);
}, [activePath]);
const onClick = () => {
setIsVisible(!isVisible);
};
return (
<>
<If condition={isVisible}>
<div
className={
'fixed top-0 left-0 z-10 h-screen w-full p-4' +
' dark:bg-background flex flex-col space-y-4 overflow-auto bg-white'
}
>
{props.children}
</div>
</If>
<Button
className={'fixed right-5 bottom-5 z-10 h-16 w-16 rounded-full'}
onClick={onClick}
>
<Menu className={'h-8'} />
</Button>
</>
);
}

View File

@@ -1,31 +0,0 @@
import { cache } from 'react';
import { createCmsClient } from '@kit/cms';
import { getLogger } from '@kit/shared/logger';
/**
* @name getDocs
* @description Load the documentation pages.
* @param language
*/
export const getDocs = cache(docsLoader);
async function docsLoader(language: string | undefined) {
const cms = await createCmsClient();
const logger = await getLogger();
try {
const data = await cms.getContentItems({
collection: 'documentation',
language,
limit: Infinity,
content: false,
});
return data.items;
} catch (error) {
logger.error({ error }, 'Failed to load documentation pages');
return [];
}
}

View File

@@ -1,33 +0,0 @@
import { Cms } from '@kit/cms';
/**
* @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);
}

View File

@@ -1,45 +0,0 @@
import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
// local imports
import { DocsNavigation } from './_components/docs-navigation';
import { getDocs } from './_lib/server/docs.loader';
import { buildDocumentationTree } from './_lib/utils';
async function DocsLayout({ children }: React.PropsWithChildren) {
const { resolvedLanguage } = await createI18nServerInstance();
const docs = await getDocs(resolvedLanguage);
const tree = buildDocumentationTree(docs);
return (
<div className={'container h-[calc(100vh-56px)] overflow-y-hidden'}>
<SidebarProvider
className="lg:gap-x-6"
style={{ '--sidebar-width': '17em' } as React.CSSProperties}
>
<HideFooterStyles />
<DocsNavigation pages={tree} />
{children}
</SidebarProvider>
</div>
);
}
function HideFooterStyles() {
return (
<style
dangerouslySetInnerHTML={{
__html: `
.site-footer {
display: none;
}
`,
}}
/>
);
}
export default DocsLayout;

View File

@@ -1,37 +0,0 @@
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import { SitePageHeader } from '../_components/site-page-header';
import { DocsCards } from './_components/docs-cards';
import { getDocs } from './_lib/server/docs.loader';
export const generateMetadata = async () => {
const { t } = await createI18nServerInstance();
return {
title: t('marketing:documentation'),
};
};
async function DocsPage() {
const { t, resolvedLanguage } = await createI18nServerInstance();
const items = await getDocs(resolvedLanguage);
// Filter out any docs that have a parentId, as these are children of other docs
const cards = items.filter((item) => !item.parentId);
return (
<div className={'flex w-full flex-1 flex-col gap-y-6 xl:gap-y-8'}>
<SitePageHeader
title={t('marketing:documentation')}
subtitle={t('marketing:documentationSubtitle')}
/>
<div className={'relative flex size-full justify-center overflow-y-auto'}>
<DocsCards cards={cards} />
</div>
</div>
);
}
export default withI18n(DocsPage);