UI: Fix i18n on Notebook layout

This commit is contained in:
Fuma Nama
2025-02-16 10:38:27 +08:00
parent 7a964dc1bc
commit e6df8aa9b5
9 changed files with 155 additions and 94 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-mdx': patch
---
Improve performance
+43 -1
View File
@@ -139,7 +139,49 @@ export default function Layout({ children }: { children: ReactNode }) {
}
```
### Replacing Navbar
### Nav Mode
Customise the position of navbar.
```tsx title="layout.tsx"
import { DocsLayout } from 'fumadocs-ui/layouts/notebook';
import { baseOptions } from '@/app/layout.config';
import { source } from '@/lib/source';
import type { ReactNode } from 'react';
export default function Layout({ children }: { children: ReactNode }) {
return (
<DocsLayout
{...baseOptions}
nav={{ ...baseOptions, mode: 'top' }}
tree={source.pageTree}
>
{children}
</DocsLayout>
);
}
```
### Tab Mode
Customise the position of tabs (Sidebar Tabs).
```tsx title="layout.tsx"
import { DocsLayout } from 'fumadocs-ui/layouts/notebook';
import { baseOptions } from '@/app/layout.config';
import { source } from '@/lib/source';
import type { ReactNode } from 'react';
export default function Layout({ children }: { children: ReactNode }) {
return (
<DocsLayout {...baseOptions} tabMode="navbar" tree={source.pageTree}>
{children}
</DocsLayout>
);
}
```
## Replacing Navbar
Fumadocs uses CSS Variables to share the size of layout components, and fit each layout component into appropriate position.
+33 -44
View File
@@ -4,16 +4,9 @@ import * as Primitive from 'fumadocs-core/toc';
import { useEffect, useRef, useState } from 'react';
import { cn } from '@/utils/cn';
import { TocThumb } from '@/components/layout/toc-thumb';
import { ScrollArea, ScrollViewport } from '../ui/scroll-area';
import { TocItemsEmpty } from '@/components/layout/toc';
export default function ClerkTOCItems({
items,
}: {
items: TOCItemType[];
isMenu?: boolean;
}) {
const viewRef = useRef<HTMLDivElement>(null);
export default function ClerkTOCItems({ items }: { items: TOCItemType[] }) {
const containerRef = useRef<HTMLDivElement>(null);
const [svg, setSvg] = useState<{
@@ -71,42 +64,38 @@ export default function ClerkTOCItems({
if (items.length === 0) return <TocItemsEmpty />;
return (
<ScrollArea className="flex flex-col ps-px">
<ScrollViewport className="relative min-h-0" ref={viewRef}>
{svg ? (
<div
className="absolute start-0 top-0 rtl:-scale-x-100"
style={{
width: svg.width,
height: svg.height,
maskImage: `url("data:image/svg+xml,${
// Inline SVG
encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${svg.width} ${svg.height}"><path d="${svg.path}" stroke="black" stroke-width="1" fill="none" /></svg>`,
)
}")`,
}}
>
<TocThumb
containerRef={containerRef}
className="mt-(--fd-top) h-(--fd-height) bg-fd-primary transition-all"
/>
</div>
) : null}
<Primitive.ScrollProvider containerRef={viewRef}>
<div className="flex flex-col" ref={containerRef}>
{items.map((item, i) => (
<TOCItem
key={item.url}
item={item}
upper={items[i - 1]?.depth}
lower={items[i + 1]?.depth}
/>
))}
</div>
</Primitive.ScrollProvider>
</ScrollViewport>
</ScrollArea>
<>
{svg ? (
<div
className="absolute start-0 top-0 rtl:-scale-x-100"
style={{
width: svg.width,
height: svg.height,
maskImage: `url("data:image/svg+xml,${
// Inline SVG
encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${svg.width} ${svg.height}"><path d="${svg.path}" stroke="black" stroke-width="1" fill="none" /></svg>`,
)
}")`,
}}
>
<TocThumb
containerRef={containerRef}
className="mt-(--fd-top) h-(--fd-height) bg-fd-primary transition-all"
/>
</div>
) : null}
<div className="flex flex-col" ref={containerRef}>
{items.map((item, i) => (
<TOCItem
key={item.url}
item={item}
upper={items[i - 1]?.depth}
lower={items[i + 1]?.depth}
/>
))}
</div>
</>
);
}
+33 -24
View File
@@ -77,46 +77,55 @@ export function TocItemsEmpty() {
);
}
export function TOCItems({
items,
export function TOCScrollArea({
isMenu,
}: {
items: TOCItemType[];
isMenu?: boolean;
}) {
const containerRef = useRef<HTMLDivElement>(null);
...props
}: ComponentProps<typeof ScrollArea> & { isMenu?: boolean }) {
const viewRef = useRef<HTMLDivElement>(null);
if (items.length === 0) return <TocItemsEmpty />;
return (
<ScrollArea className="flex flex-col ps-px">
<ScrollArea
{...props}
className={cn('flex flex-col ps-px', props.className)}
>
<Primitive.ScrollProvider containerRef={viewRef}>
<ScrollViewport
className={cn(
'relative min-h-0 text-sm',
'relative min-h-0 text-sm',
isMenu && 'mt-2 mb-4 mx-4 md:mx-6',
)}
ref={viewRef}
>
<TocThumb
containerRef={containerRef}
className="absolute start-0 mt-(--fd-top) h-(--fd-height) w-px bg-fd-primary transition-all"
/>
<div
ref={containerRef}
className="flex flex-col border-s border-fd-foreground/10"
>
{items.map((item) => (
<TOCItem key={item.url} item={item} />
))}
</div>
{props.children}
</ScrollViewport>
</Primitive.ScrollProvider>
</ScrollArea>
);
}
export function TOCItems({ items }: { items: TOCItemType[] }) {
const containerRef = useRef<HTMLDivElement>(null);
if (items.length === 0) return <TocItemsEmpty />;
return (
<>
<TocThumb
containerRef={containerRef}
className="absolute start-0 mt-(--fd-top) h-(--fd-height) w-px bg-fd-primary transition-all"
/>
<div
ref={containerRef}
className="flex flex-col border-s border-fd-foreground/10"
>
{items.map((item) => (
<TOCItem key={item.url} item={item} />
))}
</div>
</>
);
}
function TOCItem({ item }: { item: TOCItemType }) {
return (
<Primitive.TOCItem
@@ -178,7 +187,7 @@ export function TocPopoverTrigger({
<CollapsibleTrigger
{...props}
className={cn(
'inline-flex items-center text-sm gap-2 text-nowrap px-4 py-2 text-start md:px-6 md:py-3',
'inline-flex items-center text-sm gap-2 text-nowrap px-4 py-2 text-start md:px-6 md:py-3 focus-visible:outline-none',
props.className,
)}
>
+1
View File
@@ -15,6 +15,7 @@ export const buttonVariants = cva(
size: {
sm: 'gap-1 p-0.5 text-xs',
icon: 'p-1.5 [&_svg]:size-5',
'icon-sm': 'p-1.5 [&_svg]:size-4.5',
},
},
},
+3 -3
View File
@@ -203,9 +203,9 @@ function SidebarHeaderItems({
className={cn(
buttonVariants({
color: 'ghost',
size: 'icon',
size: 'icon-sm',
}),
'ms-auto text-fd-muted-foreground [&_svg]:size-4.5 max-md:hidden',
'ms-auto text-fd-muted-foreground max-md:hidden',
)}
/>
)}
@@ -245,7 +245,7 @@ function SidebarFooterItems({
<div role="separator" className="flex-1" />
{i18n ? (
<LanguageToggle className="me-1.5">
<Languages className="size-5" />
<Languages className="size-4.5" />
<LanguageToggleText className="md:hidden" />
</LanguageToggle>
) : null}
+21 -11
View File
@@ -151,12 +151,19 @@ export function DocsLayout({
<SidebarPageTree components={sidebarComponents} />
</SidebarViewport>
<SidebarFooter className={cn(!sidebarFooter && 'md:hidden')}>
<SidebarFooter
className={cn(
'flex flex-row items-center',
!sidebarFooter && 'md:hidden',
)}
>
{i18n ? (
<LanguageToggle className="me-auto md:hidden">
<Languages className="size-5 text-fd-muted-foreground" />
</LanguageToggle>
) : null}
{!props.disableThemeSwitch ? (
<ThemeToggle
className="w-fit md:hidden"
mode="light-dark-system"
/>
<ThemeToggle className="md:hidden" mode="light-dark-system" />
) : null}
{sidebarFooter}
</SidebarFooter>
@@ -234,7 +241,7 @@ function DocsNavbar({
)}
/>
<div className="flex flex-1 flex-row items-center justify-end md:gap-2">
<div className="flex flex-1 flex-row items-center justify-end">
<div className="flex flex-row items-center gap-6 px-4 empty:hidden max-lg:hidden">
{links
.filter((item) => item.type !== 'icon')
@@ -256,8 +263,8 @@ function DocsNavbar({
key={i}
item={item}
className={cn(
buttonVariants({ size: 'icon', color: 'ghost' }),
'text-fd-muted-foreground [&_svg]:size-4.5 max-lg:hidden',
buttonVariants({ size: 'icon-sm', color: 'ghost' }),
'text-fd-muted-foreground max-lg:hidden',
)}
aria-label={item.label}
>
@@ -265,11 +272,14 @@ function DocsNavbar({
</BaseLinkItem>
))}
{i18n ? (
<LanguageToggle>
<Languages className="size-5" />
<LanguageToggle className="max-md:hidden">
<Languages className="size-4.5 text-fd-muted-foreground" />
</LanguageToggle>
) : null}
<ThemeToggle className="max-md:hidden" mode="light-dark-system" />
<ThemeToggle
className="ms-2 max-md:hidden"
mode="light-dark-system"
/>
</div>
</div>
{tabs.length > 0 ? (
+1 -1
View File
@@ -230,7 +230,7 @@ export function Breadcrumb(options: BreadcrumbProps) {
if (items.length === 0) return null;
return (
<div className="flex flex-row items-center gap-1.5 text-[15px] text-fd-muted-foreground -mb-1.5">
<div className="flex flex-row items-center gap-1.5 text-[15px] text-fd-muted-foreground">
{items.map((item, i) => {
const className = cn(
'truncate',
+15 -10
View File
@@ -26,6 +26,7 @@ import {
TocPopoverTrigger,
TocPopoverContent,
type TOCProps,
TOCScrollArea,
} from '@/components/layout/toc';
import { buttonVariants } from '@/components/ui/button';
import { Edit, Text } from 'lucide-react';
@@ -158,11 +159,13 @@ export function DocsPage({
<TocPopoverTrigger className="w-full" items={toc} />
<TocPopoverContent>
{tocPopoverOptions.header}
{tocPopoverOptions.style === 'clerk' ? (
<ClerkTOCItems items={toc} isMenu />
) : (
<TOCItems items={toc} isMenu />
)}
<TOCScrollArea isMenu>
{tocPopoverOptions.style === 'clerk' ? (
<ClerkTOCItems items={toc} />
) : (
<TOCItems items={toc} />
)}
</TOCScrollArea>
{tocPopoverOptions.footer}
</TocPopoverContent>
</TocPopoverHeader>,
@@ -206,11 +209,13 @@ export function DocsPage({
<Text className="size-4" />
<I18nLabel label="toc" />
</h3>
{tocOptions.style === 'clerk' ? (
<ClerkTOCItems items={toc} />
) : (
<TOCItems items={toc} />
)}
<TOCScrollArea>
{tocOptions.style === 'clerk' ? (
<ClerkTOCItems items={toc} />
) : (
<TOCItems items={toc} />
)}
</TOCScrollArea>
{tocOptions.footer}
</Toc>,
{