mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 10:55:57 +00:00
UI: Introduce --fd-nav-height CSS variable for custom navbar
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'fumadocs-ui': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Introduce `--fd-nav-height` CSS variable for custom navbar
|
||||||
@@ -36,20 +36,47 @@ See [Root Provider](/docs/ui/blocks/root-provider#theme-provider) to learn more.
|
|||||||
|
|
||||||
### RTL Layout
|
### RTL Layout
|
||||||
|
|
||||||
RTL (Right-to-left) layout is supported. To enable RTL, pass the `dir` prop to body.
|
RTL (Right-to-left) layout is supported.
|
||||||
|
|
||||||
|
To enable RTL, set the `dir` prop to `rtl` in body and root provider (required for Radix UI).
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
<body dir="rtl"></body>
|
import { RootProvider } from 'fumadocs-ui/provider';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}): ReactNode {
|
||||||
|
return (
|
||||||
|
<html lang="en" suppressHydrationWarning>
|
||||||
|
<body dir="rtl">
|
||||||
|
<RootProvider dir="rtl">{children}</RootProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Also pass the same prop to root provider to configure Radix UI RTL layout.
|
## CSS Variables
|
||||||
|
|
||||||
```tsx
|
Fumadocs UI provides some CSS variables for customising the layout.
|
||||||
import { RootProvider } from '@fumadocs-ui/provider';
|
|
||||||
|
|
||||||
<RootProvider dir="rtl"></RootProvider>;
|
### Navbar Height
|
||||||
|
|
||||||
|
When the default navbar is replaced, the layout may overlap with your new navbar.
|
||||||
|
|
||||||
|
You should set the `--fd-nav-height` variable to the height of your navbar.
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--fd-nav-height: 40px !important;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> You can use it with CSS media queries.
|
||||||
|
|
||||||
### Layout Width
|
### Layout Width
|
||||||
|
|
||||||
Customise the max width of docs layout with CSS Variables.
|
Customise the max width of docs layout with CSS Variables.
|
||||||
@@ -113,7 +140,9 @@ We use the [Tailwind CSS Typography](https://tailwindcss.com/docs/typography-plu
|
|||||||
The Tailwind CSS plugin overrides the default configuration for the Typography plugin. You can use the `prose` utility directly.
|
The Tailwind CSS plugin overrides the default configuration for the Typography plugin. You can use the `prose` utility directly.
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
<div className="prose">...</div>
|
<div className="prose">
|
||||||
|
<h1>Good Heading</h1>
|
||||||
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Colors
|
### Colors
|
||||||
@@ -135,20 +164,3 @@ export default {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### Layout Width
|
|
||||||
|
|
||||||
Customise the max width of docs layout with Tailwind CSS plugin.
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { createPreset } from 'fumadocs-ui/tailwind-plugin';
|
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
export default {
|
|
||||||
presets: [
|
|
||||||
createPreset({
|
|
||||||
layoutWidth: '1400px',
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|||||||
+15
-4
@@ -1,4 +1,15 @@
|
|||||||
import { toRuntime } from "fumadocs-mdx"
|
import { toRuntime } from 'fumadocs-mdx';
|
||||||
import * as docs_0 from "./index.mdx?collection=docs&hash=hash"
|
import * as docs_0 from './index.mdx?collection=docs&hash=hash';
|
||||||
import * as docs_1 from "./folder/test.mdx?collection=docs&hash=hash"
|
import * as docs_1 from './folder/test.mdx?collection=docs&hash=hash';
|
||||||
export const docs = [toRuntime("doc", docs_0, {"path":"index.mdx","absolutePath":"/Users/xred/dev/fumadocs/packages/mdx/test/fixtures/index.mdx"}),toRuntime("doc", docs_1, {"path":"folder/test.mdx","absolutePath":"/Users/xred/dev/fumadocs/packages/mdx/test/fixtures/folder/test.mdx"})];
|
export const docs = [
|
||||||
|
toRuntime('doc', docs_0, {
|
||||||
|
path: 'index.mdx',
|
||||||
|
absolutePath:
|
||||||
|
'/Users/xred/dev/fumadocs/packages/mdx/test/fixtures/index.mdx',
|
||||||
|
}),
|
||||||
|
toRuntime('doc', docs_1, {
|
||||||
|
path: 'folder/test.mdx',
|
||||||
|
absolutePath:
|
||||||
|
'/Users/xred/dev/fumadocs/packages/mdx/test/fixtures/folder/test.mdx',
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|||||||
@@ -87,7 +87,8 @@ export function APIPlayground({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
if (bodyType !== 'form-data') headers.append('Content-Type', 'application/json');
|
if (bodyType !== 'form-data')
|
||||||
|
headers.append('Content-Type', 'application/json');
|
||||||
|
|
||||||
if (input.authorization) {
|
if (input.authorization) {
|
||||||
headers.append('Authorization', input.authorization);
|
headers.append('Authorization', input.authorization);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Link from 'fumadocs-core/link';
|
|||||||
import { ChevronDown } from 'lucide-react';
|
import { ChevronDown } from 'lucide-react';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { cva } from 'class-variance-authority';
|
import { cva } from 'class-variance-authority';
|
||||||
import { type ReactNode, useState } from 'react';
|
import { HTMLAttributes, type ReactNode, useState } from 'react';
|
||||||
import { useOnChange } from 'fumadocs-core/utils/use-on-change';
|
import { useOnChange } from 'fumadocs-core/utils/use-on-change';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { isActive } from '@/utils/shared';
|
import { isActive } from '@/utils/shared';
|
||||||
@@ -19,19 +19,8 @@ import {
|
|||||||
} from '@/components/ui/popover';
|
} from '@/components/ui/popover';
|
||||||
import { itemVariants } from '@/components/layout/variants';
|
import { itemVariants } from '@/components/layout/variants';
|
||||||
|
|
||||||
const linkItemVariants = cva(
|
const navItemVariants = cva(
|
||||||
'-m-2 inline-flex items-center gap-1 p-2 text-fd-muted-foreground transition-colors [&_svg]:size-4',
|
'-m-2 inline-flex items-center gap-1 p-2 text-fd-muted-foreground transition-colors hover:text-fd-accent-foreground data-[active=true]:!text-fd-primary [&_svg]:size-4',
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
active: {
|
|
||||||
true: 'text-fd-primary',
|
|
||||||
false: 'hover:text-fd-accent-foreground',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
active: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
interface BaseItem {
|
interface BaseItem {
|
||||||
@@ -54,13 +43,13 @@ interface BaseLinkItem extends BaseItem {
|
|||||||
external?: boolean;
|
external?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LinkItemType =
|
interface MainItem extends BaseLinkItem {
|
||||||
| (BaseLinkItem & {
|
|
||||||
type?: 'main';
|
type?: 'main';
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
text: ReactNode;
|
text: ReactNode;
|
||||||
})
|
}
|
||||||
| (BaseLinkItem & {
|
|
||||||
|
interface IconItem extends BaseLinkItem {
|
||||||
type: 'icon';
|
type: 'icon';
|
||||||
/**
|
/**
|
||||||
* `aria-label` of icon button
|
* `aria-label` of icon button
|
||||||
@@ -72,8 +61,9 @@ export type LinkItemType =
|
|||||||
* @defaultValue true
|
* @defaultValue true
|
||||||
*/
|
*/
|
||||||
secondary?: boolean;
|
secondary?: boolean;
|
||||||
})
|
}
|
||||||
| (BaseLinkItem & {
|
|
||||||
|
interface ButtonItem extends BaseLinkItem {
|
||||||
type: 'button';
|
type: 'button';
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
text: ReactNode;
|
text: ReactNode;
|
||||||
@@ -81,8 +71,9 @@ export type LinkItemType =
|
|||||||
* @defaultValue false
|
* @defaultValue false
|
||||||
*/
|
*/
|
||||||
secondary?: boolean;
|
secondary?: boolean;
|
||||||
})
|
}
|
||||||
| (BaseItem & {
|
|
||||||
|
interface MenuItem extends BaseItem {
|
||||||
type: 'menu';
|
type: 'menu';
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
text: ReactNode;
|
text: ReactNode;
|
||||||
@@ -91,49 +82,39 @@ export type LinkItemType =
|
|||||||
* @defaultValue false
|
* @defaultValue false
|
||||||
*/
|
*/
|
||||||
secondary?: boolean;
|
secondary?: boolean;
|
||||||
})
|
}
|
||||||
| (BaseItem & {
|
|
||||||
|
interface CustomItem extends BaseItem {
|
||||||
type: 'custom';
|
type: 'custom';
|
||||||
/**
|
/**
|
||||||
* @defaultValue false
|
* @defaultValue false
|
||||||
*/
|
*/
|
||||||
secondary?: boolean;
|
secondary?: boolean;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
export type LinkItemType =
|
||||||
|
| MainItem
|
||||||
|
| IconItem
|
||||||
|
| ButtonItem
|
||||||
|
| MenuItem
|
||||||
|
| CustomItem;
|
||||||
|
|
||||||
interface LinkItemProps extends React.HTMLAttributes<HTMLElement> {
|
interface LinkItemProps extends React.HTMLAttributes<HTMLElement> {
|
||||||
item: LinkItemType;
|
item: LinkItemType;
|
||||||
on?: 'menu' | 'nav';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LinkItem({
|
export function MenuItem({
|
||||||
item,
|
item,
|
||||||
on = 'nav',
|
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: LinkItemProps): React.ReactNode {
|
}: LinkItemProps): React.ReactNode {
|
||||||
const pathname = usePathname();
|
|
||||||
|
|
||||||
if (item.on && item.on !== 'all' && item.on !== on) return null;
|
|
||||||
|
|
||||||
if (item.type === 'custom')
|
if (item.type === 'custom')
|
||||||
return <div className={cn('grid', className)}>{item.children}</div>;
|
|
||||||
|
|
||||||
if (item.type === 'menu' && on === 'nav') {
|
|
||||||
return (
|
return (
|
||||||
<LinksMenu
|
<div className={cn('grid', className)} {...props}>
|
||||||
items={item.items.map((child, i) => (
|
{item.children}
|
||||||
<LinkItem key={i} item={child} on="menu" />
|
</div>
|
||||||
))}
|
|
||||||
className={cn(linkItemVariants({ className }))}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{item.icon}
|
|
||||||
{item.text}
|
|
||||||
<ChevronDown className="ms-auto !size-3.5" />
|
|
||||||
</LinksMenu>
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (item.type === 'menu') {
|
if (item.type === 'menu') {
|
||||||
return (
|
return (
|
||||||
@@ -149,7 +130,7 @@ export function LinkItem({
|
|||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<div className="ms-2 flex flex-col border-s py-2 ps-2">
|
<div className="ms-2 flex flex-col border-s py-2 ps-2">
|
||||||
{item.items.map((child, i) => (
|
{item.items.map((child, i) => (
|
||||||
<LinkItem key={i} item={child} on="menu" />
|
<MenuItem key={i} item={child} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
@@ -158,62 +139,129 @@ export function LinkItem({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (item.type === 'button') {
|
if (item.type === 'button') {
|
||||||
|
return <ButtonItem item={item} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<BaseLinkItem
|
||||||
href={item.url}
|
item={item}
|
||||||
external={item.external}
|
className={cn(itemVariants(), className)}
|
||||||
className={cn(
|
{...props}
|
||||||
buttonVariants({ color: 'secondary' }),
|
|
||||||
'gap-1.5 [&_svg]:size-4',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
{item.text}
|
{item.text}
|
||||||
</Link>
|
</BaseLinkItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function LinkItem({
|
||||||
|
item,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: LinkItemProps): React.ReactNode {
|
||||||
|
if (item.type === 'custom')
|
||||||
|
return (
|
||||||
|
<div className={cn('grid', className)} {...props}>
|
||||||
|
{item.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (item.type === 'menu') {
|
||||||
|
return (
|
||||||
|
<LinksMenu
|
||||||
|
items={item.items.map((child, i) => (
|
||||||
|
<MenuItem key={i} item={child} />
|
||||||
|
))}
|
||||||
|
className={cn(navItemVariants({ className }))}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
{item.text}
|
||||||
|
<ChevronDown className="ms-auto !size-3.5" />
|
||||||
|
</LinksMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.type === 'button') {
|
||||||
|
return <ButtonItem item={item} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.type === 'icon') {
|
||||||
|
return <IconItem item={item} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseLinkItem
|
||||||
|
item={item}
|
||||||
|
className={cn(navItemVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{item.text}
|
||||||
|
</BaseLinkItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function BaseLinkItem({
|
||||||
|
item,
|
||||||
|
...props
|
||||||
|
}: { item: BaseLinkItem } & HTMLAttributes<HTMLAnchorElement>): ReactNode {
|
||||||
|
const pathname = usePathname();
|
||||||
const activeType = item.active ?? 'url';
|
const activeType = item.active ?? 'url';
|
||||||
const active =
|
const active =
|
||||||
activeType !== 'none' &&
|
activeType !== 'none' &&
|
||||||
isActive(item.url, pathname, activeType === 'nested-url');
|
isActive(item.url, pathname, activeType === 'nested-url');
|
||||||
|
|
||||||
if (item.type === 'icon' && on === 'nav') {
|
return (
|
||||||
|
<Link
|
||||||
|
href={item.url}
|
||||||
|
external={item.external}
|
||||||
|
data-active={active}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IconItem({
|
||||||
|
item,
|
||||||
|
...props
|
||||||
|
}: { item: IconItem } & HTMLAttributes<HTMLAnchorElement>): ReactNode {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
aria-label={item.label}
|
aria-label={item.label}
|
||||||
href={item.url}
|
href={item.url}
|
||||||
external={item.external}
|
external={item.external}
|
||||||
|
{...props}
|
||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({
|
buttonVariants({
|
||||||
size: 'icon',
|
size: 'icon',
|
||||||
color: 'ghost',
|
color: 'ghost',
|
||||||
className,
|
|
||||||
}),
|
}),
|
||||||
|
props.className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
|
||||||
>
|
>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ButtonItem({
|
||||||
|
item,
|
||||||
|
...props
|
||||||
|
}: { item: ButtonItem } & HTMLAttributes<HTMLAnchorElement>): ReactNode {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={item.url}
|
href={item.url}
|
||||||
external={item.external}
|
external={item.external}
|
||||||
className={cn(
|
|
||||||
on === 'nav'
|
|
||||||
? linkItemVariants({
|
|
||||||
active,
|
|
||||||
})
|
|
||||||
: itemVariants({ active }),
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({ color: 'secondary' }),
|
||||||
|
'gap-1.5 [&_svg]:size-4',
|
||||||
|
props.className,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{on === 'menu' ? item.icon : null}
|
{item.icon}
|
||||||
{item.text}
|
{item.text}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export function NavBox({
|
|||||||
<header
|
<header
|
||||||
{...props}
|
{...props}
|
||||||
className={cn(
|
className={cn(
|
||||||
'sticky top-fd-sidebar-top z-40 border-b transition-colors',
|
'sticky top-[var(--fd-banner-height)] z-40 border-b transition-colors',
|
||||||
transparent
|
transparent
|
||||||
? 'border-transparent'
|
? 'border-transparent'
|
||||||
: 'border-fd-foreground/10 bg-fd-background/80 backdrop-blur-md',
|
: 'border-fd-foreground/10 bg-fd-background/80 backdrop-blur-md',
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { cn } from '@/utils/cn';
|
|||||||
import { useTreeContext } from '@/contexts/tree';
|
import { useTreeContext } from '@/contexts/tree';
|
||||||
import { ScrollArea, ScrollViewport } from '@/components/ui/scroll-area';
|
import { ScrollArea, ScrollViewport } from '@/components/ui/scroll-area';
|
||||||
import { hasActive, isActive } from '@/utils/shared';
|
import { hasActive, isActive } from '@/utils/shared';
|
||||||
import { LinkItem, type LinkItemType } from '@/components/layout/link-item';
|
import { type LinkItemType, MenuItem } from '@/components/layout/link-item';
|
||||||
import { LargeSearchToggle } from '@/components/layout/search-toggle';
|
import { LargeSearchToggle } from '@/components/layout/search-toggle';
|
||||||
import { useSearchContext } from '@/contexts/search';
|
import { useSearchContext } from '@/contexts/search';
|
||||||
import { itemVariants } from '@/components/layout/variants';
|
import { itemVariants } from '@/components/layout/variants';
|
||||||
@@ -113,13 +113,15 @@ export function Sidebar({
|
|||||||
blockScrollingWidth={768} // md
|
blockScrollingWidth={768} // md
|
||||||
{...props.aside}
|
{...props.aside}
|
||||||
className={cn(
|
className={cn(
|
||||||
'fixed z-30 flex flex-col bg-fd-card text-sm md:sticky md:top-fd-sidebar-top md:h-fd-sidebar-height md:w-[var(--fd-c-sidebar)] md:min-w-[var(--fd-sidebar-width)] md:border-e md:ps-[var(--fd-sidebar-offset)]',
|
'fixed top-fd-layout-top z-30 flex flex-col bg-fd-card text-sm md:sticky md:h-[var(--fd-sidebar-height)] md:w-[var(--fd-c-sidebar)] md:min-w-[var(--fd-sidebar-width)] md:border-e md:ps-[var(--fd-sidebar-offset)]',
|
||||||
'max-md:inset-0 max-md:top-fd-sidebar-top max-md:bg-fd-background/80 max-md:pt-[var(--fd-nav-height)] max-md:text-[15px] max-md:backdrop-blur-md max-md:data-[open=false]:invisible',
|
'max-md:inset-x-0 max-md:bottom-0 max-md:bg-fd-background/80 max-md:pt-[var(--fd-nav-height)] max-md:text-[15px] max-md:backdrop-blur-md max-md:data-[open=false]:invisible',
|
||||||
props.aside?.className,
|
props.aside?.className,
|
||||||
)}
|
)}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
...props.aside?.style,
|
...props.aside?.style,
|
||||||
|
'--fd-sidebar-height':
|
||||||
|
'calc(100dvh - var(--fd-banner-height) - var(--fd-nav-height))',
|
||||||
'--fd-sidebar-offset':
|
'--fd-sidebar-offset':
|
||||||
'calc(var(--fd-c-sidebar) - var(--fd-sidebar-width))',
|
'calc(var(--fd-c-sidebar) - var(--fd-sidebar-width))',
|
||||||
} as object
|
} as object
|
||||||
@@ -140,7 +142,15 @@ export function Sidebar({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<ViewportContent items={items} />
|
<ViewportContent>
|
||||||
|
{items.length > 0 ? (
|
||||||
|
<div className="flex flex-col px-4 pt-6 md:hidden">
|
||||||
|
{items.map((item, i) => (
|
||||||
|
<MenuItem key={i} item={item} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</ViewportContent>
|
||||||
{props.footer ? (
|
{props.footer ? (
|
||||||
<div
|
<div
|
||||||
{...props.footerProps}
|
{...props.footerProps}
|
||||||
@@ -158,9 +168,9 @@ export function Sidebar({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ViewportContent({
|
function ViewportContent({
|
||||||
items,
|
children,
|
||||||
}: {
|
}: {
|
||||||
items: LinkItemType[];
|
children: React.ReactNode;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const { root } = useTreeContext();
|
const { root } = useTreeContext();
|
||||||
|
|
||||||
@@ -171,13 +181,7 @@ function ViewportContent({
|
|||||||
maskImage: 'linear-gradient(to bottom, transparent 2px, white 24px)',
|
maskImage: 'linear-gradient(to bottom, transparent 2px, white 24px)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{items.length > 0 ? (
|
{children}
|
||||||
<div className="flex flex-col px-4 pt-6 md:hidden">
|
|
||||||
{items.map((item, i) => (
|
|
||||||
<LinkItem key={i} item={item} on="menu" />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<NodeList items={root.children} className="px-4 py-6 md:px-3" />
|
<NodeList items={root.children} className="px-4 py-6 md:px-3" />
|
||||||
</ScrollViewport>
|
</ScrollViewport>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
@@ -227,7 +231,8 @@ function PageNode({
|
|||||||
<Link
|
<Link
|
||||||
href={url}
|
href={url}
|
||||||
external={external}
|
external={external}
|
||||||
className={cn(itemVariants({ active }))}
|
data-active={active}
|
||||||
|
className={cn(itemVariants())}
|
||||||
prefetch={prefetch}
|
prefetch={prefetch}
|
||||||
>
|
>
|
||||||
{icon ?? (external ? <ExternalLinkIcon /> : null)}
|
{icon ?? (external ? <ExternalLinkIcon /> : null)}
|
||||||
@@ -289,7 +294,8 @@ function FolderNode({
|
|||||||
<Collapsible open={open} onOpenChange={setOpen}>
|
<Collapsible open={open} onOpenChange={setOpen}>
|
||||||
{item.index ? (
|
{item.index ? (
|
||||||
<Link
|
<Link
|
||||||
className={cn(itemVariants({ active }))}
|
data-active={active}
|
||||||
|
className={cn(itemVariants())}
|
||||||
href={item.index.url}
|
href={item.index.url}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
prefetch={prefetch}
|
prefetch={prefetch}
|
||||||
@@ -297,7 +303,7 @@ function FolderNode({
|
|||||||
{content}
|
{content}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<CollapsibleTrigger className={cn(itemVariants({ active }))}>
|
<CollapsibleTrigger data-active={active} className={cn(itemVariants())}>
|
||||||
{content}
|
{content}
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function Toc({ header, footer, children }: TOCProps): ReactElement {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-toc=""
|
data-toc=""
|
||||||
className="sticky top-fd-toc-top flex w-[var(--fd-c-toc)] min-w-[var(--fd-toc-width)] flex-col gap-3 pb-2 pe-[var(--fd-toc-offset)] pt-12 max-lg:hidden md:h-fd-toc-height"
|
className="sticky top-fd-layout-top flex h-fd-toc-height w-[var(--fd-c-toc)] min-w-[var(--fd-toc-width)] flex-col gap-3 pb-2 pe-[var(--fd-toc-offset)] pt-12 max-lg:hidden"
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
'--fd-toc-offset':
|
'--fd-toc-offset':
|
||||||
|
|||||||
@@ -1,17 +1,7 @@
|
|||||||
import { cva } from 'class-variance-authority';
|
import { cva } from 'class-variance-authority';
|
||||||
|
|
||||||
export const itemVariants = cva(
|
export const itemVariants = cva([
|
||||||
'flex w-full flex-row items-center gap-2 rounded-md px-2 py-1.5 text-fd-muted-foreground transition-colors duration-100 [overflow-wrap:anywhere] [&_svg]:size-4',
|
'flex w-full flex-row items-center gap-2 rounded-md px-2 py-1.5 text-fd-muted-foreground transition-colors duration-100 [overflow-wrap:anywhere] [&_svg]:size-4',
|
||||||
{
|
|
||||||
variants: {
|
|
||||||
active: {
|
|
||||||
true: 'bg-fd-primary/10 font-medium text-fd-primary',
|
|
||||||
false:
|
|
||||||
'hover:bg-fd-accent/50 hover:text-fd-accent-foreground/80 hover:transition-none',
|
'hover:bg-fd-accent/50 hover:text-fd-accent-foreground/80 hover:transition-none',
|
||||||
},
|
'data-[active=true]:!bg-fd-primary/10 data-[active=true]:font-medium data-[active=true]:!text-fd-primary data-[active=true]:hover:transition-colors',
|
||||||
},
|
]);
|
||||||
defaultVariants: {
|
|
||||||
active: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -25,10 +25,7 @@ export function SubNav({
|
|||||||
<NavBox
|
<NavBox
|
||||||
id="nd-subnav"
|
id="nd-subnav"
|
||||||
{...props}
|
{...props}
|
||||||
className={cn(
|
className={cn('flex flex-row items-center px-4', props.className)}
|
||||||
'flex h-[var(--fd-nav-height)] flex-row items-center px-4',
|
|
||||||
props.className,
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<Title url={url} title={title} />
|
<Title url={url} title={title} />
|
||||||
<div className="flex flex-1 flex-row items-center gap-1">
|
<div className="flex flex-1 flex-row items-center gap-1">
|
||||||
@@ -76,7 +73,12 @@ export function SidebarCollapseTrigger(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { LinksMenu, LinkItem } from '@/components/layout/link-item';
|
export {
|
||||||
|
LinksMenu,
|
||||||
|
LinkItem,
|
||||||
|
IconItem,
|
||||||
|
MenuItem,
|
||||||
|
} from '@/components/layout/link-item';
|
||||||
export { RootToggle } from '@/components/layout/root-toggle';
|
export { RootToggle } from '@/components/layout/root-toggle';
|
||||||
export { Sidebar } from '@/components/layout/sidebar';
|
export { Sidebar } from '@/components/layout/sidebar';
|
||||||
export { TreeContextProvider } from '@/contexts/tree';
|
export { TreeContextProvider } from '@/contexts/tree';
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ declare const {
|
|||||||
LanguageToggleText,
|
LanguageToggleText,
|
||||||
LinksMenu,
|
LinksMenu,
|
||||||
RootToggle,
|
RootToggle,
|
||||||
LinkItem,
|
|
||||||
DynamicSidebar,
|
DynamicSidebar,
|
||||||
Sidebar,
|
Sidebar,
|
||||||
|
IconItem,
|
||||||
|
MenuItem,
|
||||||
}: typeof import('./docs.client');
|
}: typeof import('./docs.client');
|
||||||
|
|
||||||
export type { LinkItemType };
|
export type { LinkItemType };
|
||||||
@@ -85,7 +86,7 @@ export function DocsLayout({
|
|||||||
<LinksMenu
|
<LinksMenu
|
||||||
key="links"
|
key="links"
|
||||||
items={links.map((item, i) => (
|
items={links.map((item, i) => (
|
||||||
<LinkItem key={i} item={item} on="menu" />
|
<MenuItem key={i} item={item} />
|
||||||
))}
|
))}
|
||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({
|
buttonVariants({
|
||||||
@@ -121,11 +122,10 @@ export function DocsLayout({
|
|||||||
<Fragment key="links">
|
<Fragment key="links">
|
||||||
{links
|
{links
|
||||||
.filter((v) => v.type === 'icon')
|
.filter((v) => v.type === 'icon')
|
||||||
.map((v, i) => (
|
.map((item, i) => (
|
||||||
<LinkItem
|
<IconItem
|
||||||
key={i}
|
key={i}
|
||||||
item={v}
|
item={item}
|
||||||
on="nav"
|
|
||||||
className="text-fd-muted-foreground md:hidden"
|
className="text-fd-muted-foreground md:hidden"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -147,11 +147,17 @@ export function DocsLayout({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TreeContextProvider tree={props.tree}>
|
<TreeContextProvider tree={props.tree}>
|
||||||
{replaceOrDefault(nav, <SubNav className="md:hidden" {...nav} />)}
|
{replaceOrDefault(nav, <SubNav className="h-14 md:hidden" {...nav} />)}
|
||||||
<main
|
<main
|
||||||
id="nd-docs-layout"
|
id="nd-docs-layout"
|
||||||
{...props.containerProps}
|
{...props.containerProps}
|
||||||
className={cn('flex flex-1 flex-row', props.containerProps?.className)}
|
className={cn(
|
||||||
|
'flex flex-1 flex-row',
|
||||||
|
!nav.component &&
|
||||||
|
nav.enabled !== false &&
|
||||||
|
'[--fd-nav-height:3.5rem] md:[--fd-nav-height:0px]',
|
||||||
|
props.containerProps?.className,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{replaceOrDefault(
|
{replaceOrDefault(
|
||||||
{ enabled: sidebarEnabled, component: sidebarReplace },
|
{ enabled: sidebarEnabled, component: sidebarReplace },
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChevronDown, Languages, MoreVertical } from 'lucide-react';
|
import { ChevronDown, Languages, MoreVertical } from 'lucide-react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { useSearchContext } from '@/contexts/search';
|
import { useSearchContext } from '@/contexts/search';
|
||||||
import {
|
import {
|
||||||
LinkItem,
|
LinkItem,
|
||||||
type LinkItemType,
|
type LinkItemType,
|
||||||
LinksMenu,
|
LinksMenu,
|
||||||
|
MenuItem,
|
||||||
} from '@/components/layout/link-item';
|
} from '@/components/layout/link-item';
|
||||||
import {
|
import {
|
||||||
LargeSearchToggle,
|
LargeSearchToggle,
|
||||||
@@ -31,6 +33,13 @@ export function Nav({
|
|||||||
items: LinkItemType[];
|
items: LinkItemType[];
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const search = useSearchContext();
|
const search = useSearchContext();
|
||||||
|
const [navItems, menuItems] = useMemo(
|
||||||
|
() => [
|
||||||
|
items.filter((item) => ['nav', 'all'].includes(item.on ?? 'all')),
|
||||||
|
items.filter((item) => ['menu', 'all'].includes(item.on ?? 'all')),
|
||||||
|
],
|
||||||
|
[items],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavBox
|
<NavBox
|
||||||
@@ -41,7 +50,7 @@ export function Nav({
|
|||||||
<nav className="mx-auto flex size-full max-w-fd-container flex-row items-center gap-6 px-4">
|
<nav className="mx-auto flex size-full max-w-fd-container flex-row items-center gap-6 px-4">
|
||||||
<Title title={props.title} url={props.url} />
|
<Title title={props.title} url={props.url} />
|
||||||
{props.children}
|
{props.children}
|
||||||
{items
|
{navItems
|
||||||
.filter((item) => !isSecondary(item))
|
.filter((item) => !isSecondary(item))
|
||||||
.map((item, i) => (
|
.map((item, i) => (
|
||||||
<LinkItem key={i} item={item} className="text-sm max-sm:hidden" />
|
<LinkItem key={i} item={item} className="text-sm max-sm:hidden" />
|
||||||
@@ -62,18 +71,17 @@ export function Nav({
|
|||||||
</LanguageToggle>
|
</LanguageToggle>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{items.filter(isSecondary).map((item, i) => (
|
{navItems.filter(isSecondary).map((item, i) => (
|
||||||
<LinkItem key={i} item={item} className="max-lg:hidden" />
|
<LinkItem key={i} item={item} className="max-lg:hidden" />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<LinksMenu
|
<LinksMenu
|
||||||
items={
|
items={
|
||||||
<>
|
<>
|
||||||
{items.map((item, i) => (
|
{menuItems.map((item, i) => (
|
||||||
<LinkItem
|
<MenuItem
|
||||||
key={i}
|
key={i}
|
||||||
item={item}
|
item={item}
|
||||||
on="menu"
|
|
||||||
className={cn(!isSecondary(item) && 'sm:hidden')}
|
className={cn(!isSecondary(item) && 'sm:hidden')}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export function HomeLayout({
|
|||||||
disableThemeSwitch={props.disableThemeSwitch}
|
disableThemeSwitch={props.disableThemeSwitch}
|
||||||
{...nav}
|
{...nav}
|
||||||
>
|
>
|
||||||
|
<style>{`:root { --fd-nav-height 3.5rem; }`}</style>
|
||||||
{nav.children}
|
{nav.children}
|
||||||
</Nav>,
|
</Nav>,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export function DocsPage({
|
|||||||
<div
|
<div
|
||||||
id="nd-tocnav"
|
id="nd-tocnav"
|
||||||
className={cn(
|
className={cn(
|
||||||
'sticky top-fd-toc-top z-10 border-b bg-fd-background/60 text-sm backdrop-blur-md md:top-[calc(4px+var(--fd-banner-height)+var(--fd-nav-height))] md:mx-3 md:rounded-full md:border md:shadow-md',
|
'sticky top-fd-layout-top z-10 border-b bg-fd-background/60 text-sm backdrop-blur-md md:top-[calc(4px+var(--fd-banner-height)+var(--fd-nav-height))] md:mx-3 md:rounded-full md:border md:shadow-md',
|
||||||
tocPopoverOptions.enabled !== true && 'lg:hidden',
|
tocPopoverOptions.enabled !== true && 'lg:hidden',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -126,9 +126,9 @@ export const docsUi = plugin.withOptions<DocsUIOptions>(
|
|||||||
'--fd-sidebar-width': '0px',
|
'--fd-sidebar-width': '0px',
|
||||||
'--fd-toc-width': '0px',
|
'--fd-toc-width': '0px',
|
||||||
'--fd-layout-width': layoutWidth,
|
'--fd-layout-width': layoutWidth,
|
||||||
|
|
||||||
'--fd-nav-height': '56px',
|
|
||||||
'--fd-banner-height': '0px',
|
'--fd-banner-height': '0px',
|
||||||
|
'--fd-nav-height': '0px',
|
||||||
|
|
||||||
// computed
|
// computed
|
||||||
'--fd-doc':
|
'--fd-doc':
|
||||||
'calc(min(100vw, var(--fd-layout-width)) - var(--fd-toc-width) - var(--fd-sidebar-width))',
|
'calc(min(100vw, var(--fd-layout-width)) - var(--fd-toc-width) - var(--fd-sidebar-width))',
|
||||||
@@ -150,7 +150,6 @@ export const docsUi = plugin.withOptions<DocsUIOptions>(
|
|||||||
'@screen md': {
|
'@screen md': {
|
||||||
':root': {
|
':root': {
|
||||||
'--fd-sidebar-width': '260px',
|
'--fd-sidebar-width': '260px',
|
||||||
'--fd-nav-height': '0px',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'@screen lg': {
|
'@screen lg': {
|
||||||
@@ -202,7 +201,7 @@ export const docsUi = plugin.withOptions<DocsUIOptions>(
|
|||||||
'.shiki code .diff': {
|
'.shiki code .diff': {
|
||||||
margin: '0 -16px',
|
margin: '0 -16px',
|
||||||
padding: '0 16px',
|
padding: '0 16px',
|
||||||
display: 'inline-block',
|
position: 'relative',
|
||||||
},
|
},
|
||||||
'.shiki .highlighted': {
|
'.shiki .highlighted': {
|
||||||
margin: '0 -16px',
|
margin: '0 -16px',
|
||||||
@@ -278,11 +277,10 @@ export const docsUi = plugin.withOptions<DocsUIOptions>(
|
|||||||
height: {
|
height: {
|
||||||
'fd-toc-height':
|
'fd-toc-height':
|
||||||
'calc(100dvh - var(--fd-banner-height) - var(--fd-nav-height))',
|
'calc(100dvh - var(--fd-banner-height) - var(--fd-nav-height))',
|
||||||
'fd-sidebar-height': 'calc(100dvh - var(--fd-banner-height))',
|
|
||||||
},
|
},
|
||||||
spacing: {
|
spacing: {
|
||||||
'fd-toc-top': 'calc(var(--fd-banner-height) + var(--fd-nav-height))',
|
'fd-layout-top':
|
||||||
'fd-sidebar-top': 'var(--fd-banner-height)',
|
'calc(var(--fd-banner-height) + var(--fd-nav-height))',
|
||||||
},
|
},
|
||||||
margin: {
|
margin: {
|
||||||
// the offset given to docs content when the sidebar is collapsed
|
// the offset given to docs content when the sidebar is collapsed
|
||||||
|
|||||||
Reference in New Issue
Block a user