mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-07-27 04:31:57 +00:00
feat: merge upstream fumadocs 16.6+ (182 commits) and rename to @hanzo/docs-*
Upstream changes include: - UI: improved TOC detection, clerk variant, slot support - Base UI: redesigned home layout navigation menu - CLI: slot support in customise command - OpenAPI: unsetting auth fields in playground - Multiple version package releases All fumadocs-* imports renamed to @hanzo/docs-* namespace.
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'fumadocs-typescript': patch
|
||||
---
|
||||
|
||||
More options for simplifying types
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
'fumadocs-ui': minor
|
||||
'@fumadocs/base-ui': minor
|
||||
---
|
||||
|
||||
Implement renderer API for replacing layout components, deprecate old options
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'fumadocs-openapi': minor
|
||||
---
|
||||
|
||||
Implement i18n
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'fumadocs-mdx': patch
|
||||
---
|
||||
|
||||
Fix dynamic index generation so generated `dynamic.ts` imports `node:path` and passes lazy entries to `create.doc()` / `create.docs()` as arrays.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
'fumadocs-ui': minor
|
||||
'@fumadocs/base-ui': minor
|
||||
---
|
||||
|
||||
Improved `defineI18nUI()` usage: allow language translations to be defined at root config.
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
'fumadocs-ui': patch
|
||||
'@fumadocs/base-ui': patch
|
||||
'fumadocs-core': patch
|
||||
---
|
||||
|
||||
Improve `<GithubInfo />` component
|
||||
Improve TOC detection logic
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
'fumadocs-typescript': minor
|
||||
'fumadocs-openapi': minor
|
||||
---
|
||||
|
||||
Bump deps
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
'fumadocs-ui': minor
|
||||
'@fumadocs/base-ui': minor
|
||||
'fumadocs-core': minor
|
||||
---
|
||||
|
||||
stablize Shiki factory API
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'fumadocs-core': patch
|
||||
---
|
||||
|
||||
Support `placeholder()` API in llms.txt generation
|
||||
@@ -372,14 +372,14 @@ export function AISearchPanel() {
|
||||
<div
|
||||
className={cn(
|
||||
'overflow-hidden z-30 bg-fd-card text-fd-card-foreground [--ai-chat-width:400px] 2xl:[--ai-chat-width:460px]',
|
||||
'max-lg:fixed max-lg:inset-x-2 max-lg:top-4 max-lg:border max-lg:rounded-2xl max-lg:shadow-xl',
|
||||
'max-lg:fixed max-lg:inset-x-2 max-lg:inset-y-4 max-lg:border max-lg:rounded-2xl max-lg:shadow-xl',
|
||||
'lg:sticky lg:top-0 lg:h-dvh lg:border-s lg:ms-auto lg:in-[#nd-docs-layout]:[grid-area:toc] lg:in-[#nd-notebook-layout]:row-span-full lg:in-[#nd-notebook-layout]:col-start-5',
|
||||
open
|
||||
? 'animate-fd-dialog-in lg:animate-[ask-ai-open_200ms]'
|
||||
: 'animate-fd-dialog-out lg:animate-[ask-ai-close_200ms]',
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col size-full p-2 max-lg:max-h-[80dvh] lg:p-3 lg:w-(--ai-chat-width)">
|
||||
<div className="flex flex-col size-full p-2 lg:p-3 lg:w-(--ai-chat-width)">
|
||||
<AISearchPanelHeader />
|
||||
<AISearchPanelList className="flex-1" />
|
||||
<div className="rounded-xl border bg-fd-secondary text-fd-secondary-foreground shadow-sm has-focus-visible:shadow-md">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Footer } from '@/components/footer';
|
||||
|
||||
export default function Layout({ children }: LayoutProps<'/'>) {
|
||||
return (
|
||||
<<<<<<< HEAD
|
||||
<>
|
||||
<HomeLayout
|
||||
{...baseOptions()}
|
||||
@@ -24,5 +25,97 @@ export default function Layout({ children }: LayoutProps<'/'>) {
|
||||
</HomeLayout>
|
||||
<Footer />
|
||||
</>
|
||||
=======
|
||||
<HomeLayout
|
||||
{...baseOptions()}
|
||||
links={[
|
||||
{
|
||||
type: 'menu',
|
||||
on: 'menu',
|
||||
text: 'Documentation',
|
||||
items: [
|
||||
{
|
||||
text: 'Getting Started',
|
||||
url: '/docs',
|
||||
icon: <Book />,
|
||||
},
|
||||
{
|
||||
text: 'Components',
|
||||
url: '/docs/ui/components',
|
||||
icon: <ComponentIcon />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'custom',
|
||||
on: 'nav',
|
||||
children: (
|
||||
<NavbarMenu>
|
||||
<NavbarMenuTrigger>
|
||||
<Link href="/docs">Documentation</Link>
|
||||
</NavbarMenuTrigger>
|
||||
<NavbarMenuContent>
|
||||
<NavbarMenuLink href="/docs" className="md:row-span-2">
|
||||
<div className="-mx-3 -mt-3">
|
||||
<Image
|
||||
src={Preview}
|
||||
alt="Preview"
|
||||
className="rounded-t-lg object-cover"
|
||||
style={{
|
||||
maskImage: 'linear-gradient(to bottom,white 60%,transparent)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<p className="font-medium">Getting Started</p>
|
||||
<p className="text-fd-muted-foreground text-sm">
|
||||
Learn to use Fumadocs on your docs site.
|
||||
</p>
|
||||
</NavbarMenuLink>
|
||||
|
||||
<NavbarMenuLink href="/docs/ui/components" className="lg:col-start-2">
|
||||
<ComponentIcon className="bg-fd-primary text-fd-primary-foreground p-1 mb-2 rounded-md" />
|
||||
<p className="font-medium">Components</p>
|
||||
<p className="text-fd-muted-foreground text-sm">
|
||||
Add interactive experience to your docs.
|
||||
</p>
|
||||
</NavbarMenuLink>
|
||||
|
||||
<NavbarMenuLink href="/docs/openapi" className="lg:col-start-2">
|
||||
<Server className="bg-fd-primary text-fd-primary-foreground p-1 mb-2 rounded-md" />
|
||||
<p className="font-medium">OpenAPI</p>
|
||||
<p className="text-fd-muted-foreground text-sm">
|
||||
Generate interactive playgrounds and docs for your OpenAPI schema.
|
||||
</p>
|
||||
</NavbarMenuLink>
|
||||
|
||||
<NavbarMenuLink href="/docs/markdown" className="lg:col-start-3 lg:row-start-1">
|
||||
<Pencil className="bg-fd-primary text-fd-primary-foreground p-1 mb-2 rounded-md" />
|
||||
<p className="font-medium">Markdown</p>
|
||||
<p className="text-fd-muted-foreground text-sm">
|
||||
Learn the writing format/syntax of Fumadocs.
|
||||
</p>
|
||||
</NavbarMenuLink>
|
||||
|
||||
<NavbarMenuLink
|
||||
href="/docs/manual-installation"
|
||||
className="lg:col-start-3 lg:row-start-2"
|
||||
>
|
||||
<PlusIcon className="bg-fd-primary text-fd-primary-foreground p-1 mb-2 rounded-md" />
|
||||
<p className="font-medium">Manual Installation</p>
|
||||
<p className="text-fd-muted-foreground text-sm">
|
||||
Setup Fumadocs for your existing Next.js app.
|
||||
</p>
|
||||
</NavbarMenuLink>
|
||||
</NavbarMenuContent>
|
||||
</NavbarMenu>
|
||||
),
|
||||
},
|
||||
...linkItems,
|
||||
]}
|
||||
className="dark:bg-neutral-950 dark:[--color-fd-background:var(--color-neutral-950)] [--color-fd-primary:var(--color-brand)]"
|
||||
>
|
||||
{children}
|
||||
</HomeLayout>
|
||||
>>>>>>> dev
|
||||
);
|
||||
}
|
||||
|
||||
@@ -212,50 +212,6 @@ export default async function Page() {
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://openai.com" rel="noreferrer noopener">
|
||||
<svg
|
||||
width="1604"
|
||||
height="718"
|
||||
viewBox="0 0 1604 718"
|
||||
className="w-32 h-auto"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipPath="url(#clip0_1637_2932)">
|
||||
<path
|
||||
d="M358.8 239.3C293.02 239.3 239.2 293.12 239.2 358.9C239.2 424.68 293.02 478.5 358.8 478.5C424.58 478.5 478.4 425.012 478.4 358.9C478.4 292.787 424.912 239.3 358.8 239.3ZM358.8 435.643C317.937 435.643 285.046 402.089 285.046 358.9C285.046 315.711 317.937 282.156 358.8 282.156C399.663 282.156 432.553 315.711 432.553 358.9C432.553 402.089 399.663 435.643 358.8 435.643Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M599.326 305.744C577.732 305.744 556.802 314.382 545.838 329V309.066H502.649V541.622H545.838V457.57C556.802 471.191 577.067 478.5 599.326 478.5C645.837 478.5 682.382 441.955 682.382 392.122C682.382 342.289 645.837 305.744 599.326 305.744ZM592.017 440.959C567.433 440.959 545.506 421.69 545.506 392.122C545.506 362.554 567.433 343.285 592.017 343.285C616.602 343.285 638.528 362.554 638.528 392.122C638.528 421.69 616.602 440.959 592.017 440.959Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M784.046 305.744C736.871 305.744 699.662 342.621 699.662 392.122C699.662 441.623 732.22 478.5 785.375 478.5C828.896 478.5 856.803 452.254 865.441 422.686H823.249C817.933 434.979 802.983 443.616 785.043 443.616C762.784 443.616 745.841 428.002 741.854 405.743H867.434V388.8C867.434 343.617 835.873 305.744 784.046 305.744ZM742.186 375.179C746.838 354.249 764.113 340.627 785.043 340.627C807.302 340.627 824.245 355.245 826.239 375.179H742.186Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M982.38 305.744C963.111 305.744 942.845 314.382 933.543 328.667V309.066H890.354V475.177H933.543V385.81C933.543 359.896 947.496 342.953 970.087 342.953C991.017 342.953 1002.31 358.9 1002.31 381.159V475.177H1045.5V374.182C1045.5 332.986 1020.25 305.744 982.38 305.744Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M1156.12 242.628L1062.1 475.184H1108.28L1128.21 424.354H1235.19L1255.12 475.184H1301.96L1208.61 242.628H1156.12ZM1143.16 385.816L1181.7 288.475L1219.9 385.816H1143.16Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path d="M1363.42 242.628H1319.57V475.184H1363.42V242.628Z" fill="currentColor" />
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1637_2932">
|
||||
<rect
|
||||
width="1603.2"
|
||||
height="717.6"
|
||||
fill="white"
|
||||
transform="translate(0 0.0999756)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h2 className="mt-12 font-mono text-xs">Individual Sponsors</h2>
|
||||
|
||||
@@ -80,7 +80,7 @@ export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
|
||||
return (
|
||||
<DocsPage
|
||||
toc={toc}
|
||||
TOC={{
|
||||
tableOfContent={{
|
||||
style: 'clerk',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function Layout({ children }: LayoutProps<'/docs'>) {
|
||||
const base = baseOptions();
|
||||
|
||||
return (
|
||||
<<<<<<< HEAD
|
||||
<>
|
||||
<DocsLayout
|
||||
{...base}
|
||||
@@ -50,6 +51,47 @@ export default function Layout({ children }: LayoutProps<'/docs'>) {
|
||||
const meta = source.getNodeMeta(node);
|
||||
if (!meta || !node.icon) return option;
|
||||
const color = `var(--${getSection(meta.path)}-color, var(--color-fd-foreground))`;
|
||||
=======
|
||||
<DocsLayout
|
||||
{...base}
|
||||
tree={source.getPageTree()}
|
||||
// just icon items
|
||||
links={linkItems.filter((item) => item.type === 'icon')}
|
||||
nav={{
|
||||
...base.nav,
|
||||
title: (
|
||||
<>
|
||||
{logo}
|
||||
<span className="font-medium in-[.uwu]:hidden max-md:hidden">Fumadocs</span>
|
||||
</>
|
||||
),
|
||||
}}
|
||||
tabs={{
|
||||
transform(option, node) {
|
||||
const meta = source.getNodeMeta(node);
|
||||
if (!meta || !node.icon) return option;
|
||||
const color = `var(--${getSection(meta.path)}-color, var(--color-fd-foreground))`;
|
||||
|
||||
return {
|
||||
...option,
|
||||
icon: (
|
||||
<div
|
||||
className="[&_svg]:size-full rounded-lg size-full text-(--tab-color) max-md:bg-(--tab-color)/10 max-md:border max-md:p-1.5"
|
||||
style={
|
||||
{
|
||||
'--tab-color': color,
|
||||
} as object
|
||||
}
|
||||
>
|
||||
{node.icon}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
>>>>>>> dev
|
||||
|
||||
return {
|
||||
...option,
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { baseOptions, linkItems, logo } from '@/components/layouts/shared';
|
||||
import { source } from '@/lib/source';
|
||||
import { getSection } from '@/lib/source/navigation';
|
||||
<<<<<<< HEAD
|
||||
import { getSidebarTabs } from '@hanzo/docs-base-ui/components/sidebar/tabs/index';
|
||||
=======
|
||||
>>>>>>> dev
|
||||
import type { ReactNode } from 'react';
|
||||
import { LayoutClient } from './client';
|
||||
import { getLayoutTabs } from '@hanzo/docs-ui/layouts/shared';
|
||||
|
||||
export function FluxLayout({ children }: { children: ReactNode }) {
|
||||
const base = baseOptions();
|
||||
@@ -24,7 +28,7 @@ export function FluxLayout({ children }: { children: ReactNode }) {
|
||||
),
|
||||
}}
|
||||
sidebar={{
|
||||
tabs: getSidebarTabs(source.getPageTree(), {
|
||||
tabs: getLayoutTabs(source.getPageTree(), {
|
||||
transform(option, node) {
|
||||
const meta = source.getNodeMeta(node);
|
||||
if (!meta || !node.icon) return option;
|
||||
|
||||
@@ -364,14 +364,14 @@ export function AISearchPanel() {
|
||||
<div
|
||||
className={cn(
|
||||
'overflow-hidden z-30 bg-fd-card text-fd-card-foreground [--ai-chat-width:400px] 2xl:[--ai-chat-width:460px]',
|
||||
'max-lg:fixed max-lg:inset-x-2 max-lg:top-4 max-lg:border max-lg:rounded-2xl max-lg:shadow-xl',
|
||||
'max-lg:fixed max-lg:inset-x-2 max-lg:inset-y-4 max-lg:border max-lg:rounded-2xl max-lg:shadow-xl',
|
||||
'lg:sticky lg:top-0 lg:h-dvh lg:border-s lg:ms-auto lg:in-[#nd-docs-layout]:[grid-area:toc] lg:in-[#nd-notebook-layout]:row-span-full lg:in-[#nd-notebook-layout]:col-start-5',
|
||||
open
|
||||
? 'animate-fd-dialog-in lg:animate-[ask-ai-open_200ms]'
|
||||
: 'animate-fd-dialog-out lg:animate-[ask-ai-close_200ms]',
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col size-full p-2 max-lg:max-h-[80dvh] lg:p-3 lg:w-(--ai-chat-width)">
|
||||
<div className="flex flex-col size-full p-2 lg:p-3 lg:w-(--ai-chat-width)">
|
||||
<AISearchPanelHeader />
|
||||
<AISearchPanelList className="flex-1" />
|
||||
<div className="rounded-xl border bg-fd-secondary text-fd-secondary-foreground shadow-sm has-focus-visible:shadow-md">
|
||||
|
||||
@@ -45,14 +45,14 @@ export const registry: Registry = {
|
||||
description: 'Replace Docs Layout (Minimal)',
|
||||
files: [
|
||||
{
|
||||
type: 'block',
|
||||
type: 'layout',
|
||||
path: 'components/registry/layout/docs-min.tsx',
|
||||
target: 'components/layout/docs/index.tsx',
|
||||
target: '<dir>/docs/index.tsx',
|
||||
},
|
||||
{
|
||||
type: 'block',
|
||||
type: 'layout',
|
||||
path: 'components/registry/layout/page-min.tsx',
|
||||
target: 'components/layout/docs/page.tsx',
|
||||
target: '<dir>/docs/page.tsx',
|
||||
},
|
||||
],
|
||||
unlisted: true,
|
||||
|
||||
@@ -145,7 +145,7 @@ export const source = loader({
|
||||
</Accordion>
|
||||
|
||||
<Accordion id='multi-docs' title="How to implement multi-docs?">
|
||||
We recommend to use [Sidebar Tabs](/docs/ui/layouts/docs#sidebar-tabs).
|
||||
We recommend using [Layout Tabs](/docs/ui/layouts/docs#layout-tabs).
|
||||
</Accordion>
|
||||
|
||||
</Accordions>
|
||||
|
||||
@@ -59,7 +59,6 @@ export const i18nUI = defineI18nUI(i18n, {
|
||||
// [!code highlight]
|
||||
export function baseOptions(locale: string): BaseLayoutProps {
|
||||
return {
|
||||
LanguageSwitch: true, // recommended [!code ++]
|
||||
// different props based on `locale`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -84,7 +84,6 @@ export const i18nUI = defineI18nUI(i18n, {
|
||||
// [!code highlight]
|
||||
export function baseOptions(locale: string): BaseLayoutProps {
|
||||
return {
|
||||
LanguageSwitch: true, // recommended [!code ++]
|
||||
// different props based on `locale`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@ export const i18nUI = defineI18nUI(i18n, {
|
||||
// [!code highlight]
|
||||
export function baseOptions(locale: string): BaseLayoutProps {
|
||||
return {
|
||||
LanguageSwitch: true, // recommended [!code ++]
|
||||
// different props based on `locale`
|
||||
};
|
||||
}
|
||||
|
||||
@@ -53,8 +53,8 @@ For example:
|
||||
</Files>
|
||||
|
||||
<Callout title="Good to Know">
|
||||
When grouping with folders, you can display them as tabs [using Sidebar
|
||||
Tabs](/docs/ui/layouts/docs#sidebar-tabs).
|
||||
When grouping with folders, you can display them as tabs [using Layout
|
||||
Tabs](/docs/ui/layouts/docs#layout-tabs).
|
||||
</Callout>
|
||||
|
||||
### Full Versioning
|
||||
|
||||
@@ -100,7 +100,11 @@ For example, when you are opening root folder `framework`, the other folders (e.
|
||||
|
||||
<Callout title='Hanzo Docs UI'>
|
||||
|
||||
<<<<<<< HEAD
|
||||
Hanzo Docs UI renders root folders as [Sidebar Tabs](/docs/ui/layouts/docs#sidebar-tabs), which allows user to switch between them.
|
||||
=======
|
||||
Fumadocs UI renders root folders as [Layout Tabs](/docs/ui/layouts/docs#layout-tabs), which allows users to switch between them.
|
||||
>>>>>>> dev
|
||||
|
||||
</Callout>
|
||||
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
---
|
||||
title: Docs Layout
|
||||
description: The layout of documentation
|
||||
---
|
||||
|
||||
The layout of documentation pages, it includes a sidebar and **mobile-only** navbar/header.
|
||||
|
||||

|
||||
|
||||
<Customisation />
|
||||
|
||||
## Usage
|
||||
|
||||
Pass your page tree to the component.
|
||||
|
||||
```tsx title="layout.tsx"
|
||||
import { DocsLayout } from '@hanzo/docs-ui/layouts/docs';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<DocsLayout {...baseOptions()} tree={tree}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
See detailed docs for [`links`](/docs/ui/layouts/links) and [`nav`](/docs/ui/layouts/nav) options.
|
||||
|
||||
### References
|
||||
|
||||
<auto-type-table
|
||||
cwd path="content/docs/ui/props.ts"
|
||||
type="Omit<DocsLayoutProps, 'children' | 'disableThemeSwitch'>"
|
||||
/>
|
||||
|
||||
## Sidebar
|
||||
|
||||
```tsx title="layout.tsx"
|
||||
import { DocsLayout } from '@hanzo/docs-ui/layouts/docs';
|
||||
|
||||
<DocsLayout
|
||||
sidebar={{
|
||||
enabled: true,
|
||||
}}
|
||||
/>;
|
||||
```
|
||||
|
||||
### Sidebar Items
|
||||
|
||||
Customise sidebar navigation links.
|
||||
|
||||
<div className='flex justify-center items-center *:max-w-[200px] bg-linear-to-br from-fd-primary/10 rounded-xl border'>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Sidebar items are rendered from the page tree you passed to `<DocsLayout />`.
|
||||
|
||||
For page tree from [`loader()`](/docs/headless/source-api), it generates the tree from your file structure, see [available patterns](/docs/page-conventions).
|
||||
|
||||
```tsx title="layout.tsx"
|
||||
import { DocsLayout } from '@hanzo/docs-ui/layouts/docs';
|
||||
import { source } from '@/lib/source';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<DocsLayout
|
||||
tree={source.getPageTree()}
|
||||
// other props
|
||||
>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
You may hardcode it too:
|
||||
|
||||
```tsx title="layout.tsx"
|
||||
import { DocsLayout } from '@hanzo/docs-ui/layouts/docs';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<DocsLayout
|
||||
tree={{
|
||||
name: 'docs',
|
||||
children: [],
|
||||
}}
|
||||
// other props
|
||||
>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Layout Tabs (Dropdown) [#layout-tabs]
|
||||
|
||||
Layout Tabs are folders with tab-like behaviours, only the content of opened tab will be visible.
|
||||
|
||||
<div className='flex justify-center items-center *:max-w-[360px] bg-linear-to-br from-fd-primary/10 rounded-xl border'>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
By default, the tab trigger will be displayed as a **Dropdown** component (hidden unless one of its items is active).
|
||||
|
||||
You can add items by marking folders as [Root Folders](/docs/page-conventions#root-folder), create a `meta.json` file in the folder:
|
||||
|
||||
```json title="content/docs/my-folder/meta.json"
|
||||
{
|
||||
"title": "Name of Folder",
|
||||
"description": "The description of root folder (optional)",
|
||||
"root": true
|
||||
}
|
||||
```
|
||||
|
||||
Or specify them explicitly:
|
||||
|
||||
```tsx title="/app/docs/layout.tsx"
|
||||
import { DocsLayout } from '@hanzo/docs-ui/layouts/docs';
|
||||
|
||||
<DocsLayout
|
||||
tabs={[
|
||||
{
|
||||
title: 'Components',
|
||||
description: 'Hello World!',
|
||||
// active for `/docs/components` and sub routes like `/docs/components/button`
|
||||
url: '/docs/components',
|
||||
|
||||
// optionally, you can specify a set of urls which activates the item
|
||||
// urls: new Set(['/docs/test', '/docs/components']),
|
||||
},
|
||||
]}
|
||||
/>;
|
||||
```
|
||||
|
||||
Set it to `false` to disable:
|
||||
|
||||
```tsx
|
||||
import { DocsLayout } from '@hanzo/docs-ui/layouts/docs';
|
||||
|
||||
<DocsLayout tabs={false} />;
|
||||
```
|
||||
|
||||
<Callout title="Want further customisations?">
|
||||
|
||||
You can specify a `banner` to the [Docs Layout](/docs/ui/layouts/docs) component.
|
||||
|
||||
```tsx
|
||||
import { DocsLayout, type DocsLayoutProps } from '@hanzo/docs-ui/layouts/docs';
|
||||
import type { ReactNode } from 'react';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { source } from '@/lib/source';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<DocsLayout
|
||||
{...baseOptions()}
|
||||
tree={source.getPageTree()}
|
||||
sidebar={{
|
||||
// [!code ++]
|
||||
banner: <div>Hello World</div>,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
</Callout>
|
||||
|
||||
#### Decoration
|
||||
|
||||
Change the icon/styles of layout tabs.
|
||||
|
||||
```tsx
|
||||
import { DocsLayout } from '@hanzo/docs-ui/layouts/docs';
|
||||
|
||||
<DocsLayout
|
||||
tabs={{
|
||||
transform: (option, node) => ({
|
||||
...option,
|
||||
icon: <MyIcon />,
|
||||
}),
|
||||
}}
|
||||
/>;
|
||||
```
|
||||
|
||||
### Sidebar Components
|
||||
|
||||
You can replace certain components for rendering page tree.
|
||||
|
||||
```tsx tab="layout.tsx"
|
||||
import { DocsLayout } from '@hanzo/docs-ui/layouts/docs';
|
||||
import { SidebarSeparator } from './layout.client';
|
||||
|
||||
<DocsLayout
|
||||
sidebar={{
|
||||
enabled: true,
|
||||
components: {
|
||||
Separator: SidebarSeparator,
|
||||
},
|
||||
}}
|
||||
/>;
|
||||
```
|
||||
|
||||
```tsx tab="layout.client.tsx"
|
||||
'use client';
|
||||
import * as Base from '@hanzo/docs-ui/components/sidebar/base';
|
||||
|
||||
export function SidebarSeparator({ className, style, children, ...props }: ComponentProps<'p'>) {
|
||||
const depth = Base.useFolderDepth();
|
||||
|
||||
return (
|
||||
<Base.SidebarSeparator
|
||||
className={cn('[&_svg]:size-4 [&_svg]:shrink-0', className)}
|
||||
style={{
|
||||
paddingInlineStart: `calc(${2 + 3 * depth} * var(--spacing))`,
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Base.SidebarSeparator>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### References
|
||||
|
||||
<auto-type-table cwd path="content/docs/ui/props.ts" name="SidebarProps" />
|
||||
|
||||
## Advanced
|
||||
|
||||
### Prefetching
|
||||
|
||||
Fumadocs use the `<Link />` component of your React framework, and keep their default prefetch behaviours.
|
||||
|
||||
On Vercel, prefetch requests may cause a higher usage of serverless functions and Data Cache.
|
||||
It can also hit the limits of some other hosting platforms.
|
||||
|
||||
You can disable prefetching to reduce the amount of prefetch requests, or enable explicitly:
|
||||
|
||||
```tsx
|
||||
import { DocsLayout } from '@hanzo/docs-ui/layouts/docs';
|
||||
|
||||
<DocsLayout sidebar={{ prefetch: false }} />;
|
||||
```
|
||||
|
||||
### The Layout System
|
||||
|
||||
Handling layout is challenging, Fumadocs UI needed an approach that is:
|
||||
|
||||
- **Composable:** Layout components should manage their position and size effortlessly, ideally in place.
|
||||
- **Flexible:** The system should avoid reliance on fixed values or heights, enabling seamless integration of external components, such as AI chat interfaces.
|
||||
- **Cohesive:** Components should respond to changes in others, for instance, by animating sidebar collapses.
|
||||
- **Predictable:** Layout properties should remain centralized, allowing the final result to be readily anticipated from the source code.
|
||||
- **Compatible:** The solution should work on older browsers by leveraging only Baseline Widely Available CSS features.
|
||||
|
||||
Fumadocs UI does this with a grid system:
|
||||
|
||||
```css
|
||||
#nd-docs-layout {
|
||||
grid-template:
|
||||
'sidebar header toc'
|
||||
'sidebar toc-popover toc'
|
||||
'sidebar main toc' 1fr / minmax(var(--fd-sidebar-col), 1fr) minmax(0, var(--fd-page-col))
|
||||
minmax(min-content, 1fr);
|
||||
|
||||
--fd-docs-row-1: var(--fd-banner-height, 0px);
|
||||
--fd-docs-row-2: calc(var(--fd-docs-row-1) + var(--fd-header-height));
|
||||
--fd-docs-row-3: calc(var(--fd-docs-row-2) + var(--fd-toc-popover-height));
|
||||
--fd-sidebar-col: var(--fd-sidebar-width);
|
||||
--fd-page-col: calc(
|
||||
var(--fd-layout-width, 97rem) - var(--fd-sidebar-width) - var(--fd-toc-width)
|
||||
);
|
||||
--fd-sidebar-width: 0px;
|
||||
--fd-toc-width: 0px;
|
||||
|
||||
--fd-header-height: 0px;
|
||||
--fd-toc-popover-height: 0px;
|
||||
}
|
||||
```
|
||||
|
||||
- The layout container uses grid layout, `grid-template` is set to produce predictable result.
|
||||
- `--fd-docs-row-*` define the top offset of each row, allowing elements with `position: sticky` to hook a correct top offset.
|
||||
- `--fd-*-width` and `--fd-*-height` are set by layout components using CSS, they are essential to maintain the grid structure, or calculating `--fd-docs-row-*`.
|
||||
- `--fd-*-col` are dynamic values, updated as state changes (e.g. `--fd-sidebar-col` becomes `0px` when the sidebar is collapsed).
|
||||
|
||||
Both default and the notebook layout utilize this system.
|
||||
@@ -0,0 +1,95 @@
|
||||
---
|
||||
title: Notebook Layout
|
||||
description: A compact version of Docs Layout
|
||||
---
|
||||
|
||||
A compact version of [`<DocsLayout />`](/docs/ui/layouts/docs).
|
||||
|
||||

|
||||
|
||||
<Customisation />
|
||||
|
||||
## Usage
|
||||
|
||||
Enable the notebook layout with `@hanzo/docs-ui/layouts/notebook`.
|
||||
|
||||
```tsx title="layout.tsx"
|
||||
import { DocsLayout } from '@hanzo/docs-ui/layouts/notebook'; // [!code highlight]
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { source } from '@/lib/source';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<DocsLayout {...baseOptions()} tree={source.getPageTree()}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Make sure to update your page import too:
|
||||
|
||||
```tsx title="page.tsx"
|
||||
import { ... } from '@hanzo/docs-ui/layouts/docs/page'; // [!code --]
|
||||
import { ... } from '@hanzo/docs-ui/layouts/notebook/page'; // [!code ++]
|
||||
```
|
||||
|
||||
## Configurations
|
||||
|
||||
The options are inherited from [Docs Layout](/docs/ui/layouts/docs), with minor differences:
|
||||
|
||||
- sidebar/navbar cannot be replaced, Notebook layout is more opinionated than the default one.
|
||||
- additional options (see below).
|
||||
|
||||
### Tab Mode
|
||||
|
||||
Configure the style of [Layout Tabs](/docs/ui/layouts/docs#layout-tabs).
|
||||
|
||||

|
||||
|
||||
```tsx title="layout.tsx"
|
||||
import { DocsLayout } from '@hanzo/docs-ui/layouts/notebook';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { source } from '@/lib/source';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<DocsLayout
|
||||
{...baseOptions()}
|
||||
tabMode="navbar" // [!code ++]
|
||||
tree={source.getPageTree()}
|
||||
>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Nav Mode
|
||||
|
||||
Configure the style of navbar.
|
||||
|
||||

|
||||
|
||||
```tsx title="layout.tsx"
|
||||
import { DocsLayout } from '@hanzo/docs-ui/layouts/notebook';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { source } from '@/lib/source';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
const { nav, ...base } = baseOptions();
|
||||
|
||||
return (
|
||||
<DocsLayout
|
||||
{...base}
|
||||
nav={{ ...nav, mode: 'top' }} // [!code ++]
|
||||
tree={source.getPageTree()}
|
||||
>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
```
|
||||
@@ -45,11 +45,12 @@ This will render the title in the MDX body.
|
||||
|
||||
</Callout>
|
||||
|
||||
### Edit on GitHub
|
||||
### Page Actions
|
||||
|
||||
Link to the original GitHub file with your component.
|
||||
Show GitHub link, and shortcut links for AIs.
|
||||
|
||||
```tsx
|
||||
<<<<<<< HEAD
|
||||
import { DocsPage } from '@hanzo/docs-ui/layouts/<layout>/page';
|
||||
|
||||
<DocsPage>
|
||||
@@ -61,6 +62,14 @@ import { DocsPage } from '@hanzo/docs-ui/layouts/<layout>/page';
|
||||
>
|
||||
Edit on GitHub
|
||||
</a>
|
||||
=======
|
||||
import { DocsPage, ViewOptionsPopover } from '@hanzo/docs-ui/layouts/<layout>/page';
|
||||
|
||||
const githubUrl = `https://github.com/fuma-nama/fumadocs/blob/main/content/docs/${page.path}`
|
||||
|
||||
<DocsPage>
|
||||
<ViewOptionsPopover githubUrl={githubUrl} />
|
||||
>>>>>>> dev
|
||||
</DocsPage>;
|
||||
```
|
||||
|
||||
@@ -142,27 +151,28 @@ import { DocsPage } from '@hanzo/docs-ui/layouts/<layout>/page';
|
||||
<DocsPage toc={headings}>...</DocsPage>;
|
||||
```
|
||||
|
||||
You can customise or disable it with the `TOC` prop, or with `TOCPopover` on smaller devices.
|
||||
You can customise it with `tableOfContent`, or with `tableOfContentPopover` on smaller devices.
|
||||
|
||||
<<<<<<< HEAD
|
||||
```tsx
|
||||
import { DocsPage, type TOCMainProps } from '@hanzo/docs-ui/layouts/<layout>/page';
|
||||
=======
|
||||
```tsx title="page.tsx"
|
||||
import { DocsPage } from '@hanzo/docs-ui/layouts/<layout>/page';
|
||||
>>>>>>> dev
|
||||
|
||||
// pass options:
|
||||
return (
|
||||
<DocsPage TOC={options} TOCPopover={options}>
|
||||
<DocsPage tableOfContent={options} tableOfContentPopover={options}>
|
||||
...
|
||||
</DocsPage>
|
||||
);
|
||||
|
||||
// or to replace renderer:
|
||||
function TOC(props: TOCMainProps) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
return <DocsPage TOC={TOC}>...</DocsPage>;
|
||||
```
|
||||
|
||||
<<<<<<< HEAD
|
||||
<auto-type-table type="import('@hanzo/docs-ui/layouts/docs/page').TOCMainProps" />
|
||||
=======
|
||||
<auto-type-table type="import('@hanzo/docs-ui/layouts/docs/page/slots/toc').TOCProps" />
|
||||
>>>>>>> dev
|
||||
|
||||
#### Style
|
||||
|
||||
@@ -172,7 +182,7 @@ You can choose another style for TOC, like `clerk` inspired by https://clerk.com
|
||||
import { DocsPage } from '@hanzo/docs-ui/layouts/<layout>/page';
|
||||
|
||||
<DocsPage
|
||||
TOC={{
|
||||
tableOfContent={{
|
||||
style: 'clerk',
|
||||
}}
|
||||
>
|
||||
@@ -180,24 +190,208 @@ import { DocsPage } from '@hanzo/docs-ui/layouts/<layout>/page';
|
||||
</DocsPage>;
|
||||
```
|
||||
|
||||
You can also style the TOC title with the `toc-title` ID.
|
||||
#### Replace TOC
|
||||
|
||||
To replace the component:
|
||||
|
||||
```tsx tab="page.tsx"
|
||||
import { TOCProvider, TOC, TOCPopover } from './my-toc';
|
||||
|
||||
return (
|
||||
<DocsPage
|
||||
slots={{
|
||||
toc: {
|
||||
provider: TOCProvider,
|
||||
main: TOC,
|
||||
popover: TOCPopover,
|
||||
},
|
||||
}}
|
||||
>
|
||||
...
|
||||
</DocsPage>
|
||||
);
|
||||
```
|
||||
|
||||
```tsx tab="my-toc.tsx"
|
||||
'use client';
|
||||
import type {
|
||||
TOCProps,
|
||||
TOCPopoverProps,
|
||||
TOCProviderProps,
|
||||
} from '@hanzo/docs-ui/layouts/<layout>/page/slots/toc';
|
||||
|
||||
export function TOCProvider({ toc, children }: TOCProviderProps) {
|
||||
// receive TOC items, you can pass it down via React contexts
|
||||
console.log(toc);
|
||||
return children;
|
||||
}
|
||||
|
||||
export function TOC(props: TOCProps) {
|
||||
return <div>Hello World</div>;
|
||||
}
|
||||
|
||||
export function TOCPopover(props: TOCPopoverProps) {
|
||||
return <div>Popover</div>;
|
||||
}
|
||||
```
|
||||
|
||||
You can start from the default implementation with Fumadocs CLI:
|
||||
|
||||
```bash tab="Docs Layout"
|
||||
npx @hanzo/docs-cli add slots/docs/page/toc
|
||||
```
|
||||
|
||||
```bash tab="Notebook Layout"
|
||||
npx @hanzo/docs-cli add slots/notebook/page/toc
|
||||
```
|
||||
|
||||
```bash tab="Flux Layout"
|
||||
npx @hanzo/docs-cli add slots/flux/page/toc
|
||||
```
|
||||
|
||||
### Footer
|
||||
|
||||
Footer is a navigation element that has two buttons to jump to the next and previous pages. When not specified, it shows the neighbour pages found from page tree.
|
||||
|
||||
Customise the footer with the `Footer` prop.
|
||||
Customise the footer with the `footer` prop:
|
||||
|
||||
```tsx
|
||||
import { DocsPage } from '@hanzo/docs-ui/layouts/<layout>/page';
|
||||
|
||||
<DocsPage Footer={options}>...</DocsPage>;
|
||||
<DocsPage footer={options}>...</DocsPage>;
|
||||
```
|
||||
|
||||
<auto-type-table type="Omit<import('@hanzo/docs-ui/layouts/docs/page').FooterProps, keyof React.ComponentProps<'div'>>" />
|
||||
|
||||
#### Replace Footer
|
||||
|
||||
To replace the component:
|
||||
|
||||
```tsx tab="page.tsx"
|
||||
import { DocsPage } from '@hanzo/docs-ui/layouts/<layout>/page';
|
||||
import { Footer } from './my-footer';
|
||||
|
||||
<DocsPage
|
||||
slots={{
|
||||
footer: Footer,
|
||||
}}
|
||||
/>;
|
||||
```
|
||||
|
||||
```tsx tab="my-footer.tsx"
|
||||
'use client';
|
||||
import type { FooterProps } from '@hanzo/docs-ui/layouts/<layout>/page/slots/footer';
|
||||
|
||||
export function Footer(props: FooterProps) {
|
||||
return <div>Hello World</div>;
|
||||
}
|
||||
```
|
||||
|
||||
You can start from the default implementation with Fumadocs CLI:
|
||||
|
||||
```bash tab="Docs Layout"
|
||||
npx @hanzo/docs-cli add slots/docs/page/footer
|
||||
```
|
||||
|
||||
```bash tab="Notebook Layout"
|
||||
npx @hanzo/docs-cli add slots/notebook/page/footer
|
||||
```
|
||||
|
||||
```bash tab="Flux Layout"
|
||||
npx @hanzo/docs-cli add slots/flux/page/footer
|
||||
```
|
||||
|
||||
### Breadcrumb
|
||||
|
||||
A navigation element, shown only when user is navigating in folders.
|
||||
|
||||
<<<<<<< HEAD
|
||||
<auto-type-table type="Omit<import('@hanzo/docs-ui/layouts/docs/page').BreadcrumbProps, keyof React.ComponentProps<'div'>>" />
|
||||
=======
|
||||
Customise the breadcrumb with the `breadcrumb` option:
|
||||
|
||||
```tsx
|
||||
import { DocsPage } from '@hanzo/docs-ui/layouts/<layout>/page';
|
||||
|
||||
<DocsPage breadcrumb={options}>...</DocsPage>;
|
||||
```
|
||||
|
||||
<auto-type-table type="Omit<import('@hanzo/docs-ui/layouts/docs/page').BreadcrumbProps, keyof React.ComponentProps<'div'>>" />
|
||||
|
||||
#### Replace Breadcrumb
|
||||
|
||||
To replace the component:
|
||||
|
||||
```tsx tab="page.tsx"
|
||||
import { DocsPage } from '@hanzo/docs-ui/layouts/<layout>/page';
|
||||
import { Breadcrumb } from './my-breadcrumb';
|
||||
|
||||
<DocsPage
|
||||
slots={{
|
||||
breadcrumb: Breadcrumb,
|
||||
}}
|
||||
/>;
|
||||
```
|
||||
|
||||
```tsx tab="my-breadcrumb.tsx"
|
||||
'use client';
|
||||
import type { BreadcrumbProps } from '@hanzo/docs-ui/layouts/<layout>/page/slots/breadcrumb';
|
||||
|
||||
export function Breadcrumb(props: BreadcrumbProps) {
|
||||
return <div>Hello World</div>;
|
||||
}
|
||||
```
|
||||
|
||||
You can start from the default implementation with Fumadocs CLI:
|
||||
|
||||
```bash tab="Docs Layout"
|
||||
npx @hanzo/docs-cli add slots/docs/page/breadcrumb
|
||||
```
|
||||
|
||||
```bash tab="Notebook Layout"
|
||||
npx @hanzo/docs-cli add slots/notebook/page/breadcrumb
|
||||
```
|
||||
|
||||
```bash tab="Flux Layout"
|
||||
npx @hanzo/docs-cli add slots/flux/page/breadcrumb
|
||||
```
|
||||
|
||||
### Container
|
||||
|
||||
Container controls the article wrapper around breadcrumb, body, and footer.
|
||||
|
||||
To replace the page container:
|
||||
|
||||
```tsx tab="page.tsx"
|
||||
import { DocsPage } from '@hanzo/docs-ui/layouts/<layout>/page';
|
||||
import { Container } from './my-container';
|
||||
|
||||
<DocsPage
|
||||
slots={{
|
||||
container: Container,
|
||||
}}
|
||||
/>;
|
||||
```
|
||||
|
||||
```tsx tab="my-container.tsx"
|
||||
'use client';
|
||||
|
||||
export function Container(props: React.ComponentProps<'article'>) {
|
||||
return <div />;
|
||||
}
|
||||
```
|
||||
|
||||
You can start from the default implementation with Fumadocs CLI:
|
||||
|
||||
```bash tab="Docs Layout"
|
||||
npx @hanzo/docs-cli add slots/docs/page/container
|
||||
```
|
||||
|
||||
```bash tab="Notebook Layout"
|
||||
npx @hanzo/docs-cli add slots/notebook/page/container
|
||||
```
|
||||
|
||||
```bash tab="Flux Layout"
|
||||
npx @hanzo/docs-cli add slots/flux/page/container
|
||||
```
|
||||
>>>>>>> dev
|
||||
|
||||
@@ -53,9 +53,9 @@ const config: NextConfig = {
|
||||
|
||||
// (a) fumadocs -> @hanzo/docs equivalents (real components/APIs)
|
||||
|
||||
'fumadocs-ui': '@hanzo/docs-base-ui',
|
||||
'fumadocs-core': '@hanzo/docs-core',
|
||||
'fumadocs-mdx': '@hanzo/docs-mdx',
|
||||
'@hanzo/docs-ui': '@hanzo/docs-base-ui',
|
||||
'@hanzo/docs-core': '@hanzo/docs-core',
|
||||
'@hanzo/docs-mdx': '@hanzo/docs-mdx',
|
||||
'@hanzo/docs-core': '@hanzo/docs-core',
|
||||
'@hanzo/docs-mdx': '@hanzo/docs-mdx',
|
||||
|
||||
@@ -164,7 +164,7 @@ const config: NextConfig = {
|
||||
],
|
||||
typescript: {
|
||||
|
||||
// Upstream project docs import fumadocs-ui which is aliased to @hanzo/docs-base-ui
|
||||
// Upstream project docs import @hanzo/docs-ui which is aliased to @hanzo/docs-base-ui
|
||||
// at webpack level, but TS type-checker doesn't see webpack aliases.
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai-compatible": "^2.0.37",
|
||||
"@ai-sdk/react": "^3.0.136",
|
||||
<<<<<<< HEAD
|
||||
"@hanzo/docs": "workspace:*",
|
||||
"@hanzo/docs-base-ui": "workspace:*",
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
@@ -30,6 +31,13 @@
|
||||
"@hanzo/logo": "^1.0.5",
|
||||
"@mixedbread/sdk": "^0.60.0",
|
||||
"@openrouter/ai-sdk-provider": "^2.3.3",
|
||||
=======
|
||||
"@hanzo/docs-story": "workspace:*",
|
||||
"@mixedbread/sdk": "^0.60.0",
|
||||
"@openrouter/ai-sdk-provider": "^2.3.3",
|
||||
"@orama/core": "^1.2.19",
|
||||
"@orama/orama": "^3.1.18",
|
||||
>>>>>>> dev
|
||||
"@paper-design/shaders-react": "^0.0.72",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.15",
|
||||
@@ -71,7 +79,11 @@
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
<<<<<<< HEAD
|
||||
"@hanzo/docs-cli": "workspace:*",
|
||||
=======
|
||||
"@hanzo/docs-cli": "workspace:*",
|
||||
>>>>>>> dev
|
||||
"@next/bundle-analyzer": "16.2.1",
|
||||
"@next/env": "16.2.1",
|
||||
"@next/eslint-plugin-next": "16.2.1",
|
||||
|
||||
@@ -11,20 +11,26 @@
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^5.0.0",
|
||||
"@astrojs/react": "^5.0.0",
|
||||
"@astrojs/mdx": "^5.0.2",
|
||||
"@astrojs/react": "^5.0.1",
|
||||
"@orama/orama": "^3.1.18",
|
||||
<<<<<<< HEAD
|
||||
"astro": "^6.0.4",
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-ui": "workspace:*",
|
||||
=======
|
||||
"astro": "^6.0.8",
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-ui": "workspace:*",
|
||||
>>>>>>> dev
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"sharp": "^0.34.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"tailwindcss": "^4.2.1"
|
||||
"tailwindcss": "^4.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,19 +13,19 @@
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"lucide-react": "^0.577.0",
|
||||
"next": "16.1.6",
|
||||
"next": "16.2.1",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"tailwind-merge": "^3.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.2.1",
|
||||
"@tailwindcss/postcss": "^4.2.2",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"postcss": "^8.5.8",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,17 @@
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
<<<<<<< HEAD
|
||||
"@hanzo/docs-content-collections": "workspace:*",
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-base-ui": "workspace:*",
|
||||
"next": "16.1.6",
|
||||
=======
|
||||
"@hanzo/docs-content-collections": "workspace:*",
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-ui": "workspace:*",
|
||||
"next": "16.2.1",
|
||||
>>>>>>> dev
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4"
|
||||
},
|
||||
|
||||
@@ -20,7 +20,6 @@ export const i18nUI = defineI18nUI(i18n, {
|
||||
|
||||
export function baseOptions(locale: string): BaseLayoutProps {
|
||||
return {
|
||||
LanguageSwitch: true,
|
||||
nav: {
|
||||
title: locale === 'cn' ? 'Chinese Docs' : 'English Docs',
|
||||
url: `/${locale}`,
|
||||
|
||||
@@ -10,10 +10,17 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@orama/tokenizers": "^3.1.18",
|
||||
<<<<<<< HEAD
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"@hanzo/docs-base-ui": "workspace:*",
|
||||
"next": "16.1.6",
|
||||
=======
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"@hanzo/docs-ui": "workspace:*",
|
||||
"next": "16.2.1",
|
||||
>>>>>>> dev
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4"
|
||||
},
|
||||
|
||||
@@ -13,18 +13,18 @@
|
||||
"@hanzo/docs-base-ui": "workspace:*",
|
||||
"github-slugger": "^2.0.0",
|
||||
"html-react-parser": "^5.2.17",
|
||||
"next": "16.1.6",
|
||||
"next": "16.2.1",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"shiki": "^4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.2.1",
|
||||
"@tailwindcss/postcss": "^4.2.2",
|
||||
"@types/node": "25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"postcss": "^8.5.8",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,20 +9,27 @@
|
||||
"types:check": "@hanzo/docs-mdx && next typegen && tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
<<<<<<< HEAD
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"@hanzo/docs-base-ui": "workspace:*",
|
||||
"next": "16.1.6",
|
||||
=======
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"@hanzo/docs-ui": "workspace:*",
|
||||
"next": "16.2.1",
|
||||
>>>>>>> dev
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.2.1",
|
||||
"@tailwindcss/postcss": "^4.2.2",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"postcss": "^8.5.8",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,20 +14,20 @@
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"@hanzo/docs-ui": "workspace:*",
|
||||
"lucide-react": "^0.577.0",
|
||||
"next": "16.1.6",
|
||||
"next": "16.2.1",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"tailwind-merge": "^3.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.2.1",
|
||||
"@tailwindcss/postcss": "^4.2.2",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"postcss": "^8.5.8",
|
||||
"serve": "^14.2.6",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,19 +13,19 @@
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"@hanzo/docs-ui": "workspace:*",
|
||||
"lucide-react": "^0.577.0",
|
||||
"next": "16.1.6",
|
||||
"next": "16.2.1",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"tailwind-merge": "^3.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.2.1",
|
||||
"@tailwindcss/postcss": "^4.2.2",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"postcss": "^8.5.8",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,21 +9,29 @@
|
||||
"types:check": "@hanzo/docs-mdx && next typegen && tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
<<<<<<< HEAD
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"@hanzo/docs-obsidian": "workspace:*",
|
||||
"@hanzo/docs-base-ui": "workspace:*",
|
||||
"next": "16.1.6",
|
||||
=======
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"fumadocs-obsidian": "workspace:*",
|
||||
"@hanzo/docs-ui": "workspace:*",
|
||||
"next": "16.2.1",
|
||||
>>>>>>> dev
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.2.1",
|
||||
"@tailwindcss/postcss": "^4.2.2",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"postcss": "^8.5.8",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,23 +10,31 @@
|
||||
"types:check": "@hanzo/docs-mdx && tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
<<<<<<< HEAD
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"@hanzo/docs-openapi": "workspace:*",
|
||||
"@hanzo/docs-base-ui": "workspace:*",
|
||||
"next": "16.1.6",
|
||||
=======
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"@hanzo/docs-openapi": "workspace:*",
|
||||
"@hanzo/docs-ui": "workspace:*",
|
||||
"next": "16.2.1",
|
||||
>>>>>>> dev
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"shiki": "^4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.2.1",
|
||||
"@tailwindcss/postcss": "^4.2.2",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"postcss": "^8.5.8",
|
||||
"rimraf": "^6.1.3",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,22 +11,30 @@
|
||||
"types:check": "@hanzo/docs-mdx && tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
<<<<<<< HEAD
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"@hanzo/docs-python": "workspace:*",
|
||||
"@hanzo/docs-ui": "workspace:*",
|
||||
"next": "16.1.6",
|
||||
=======
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"fumadocs-python": "workspace:*",
|
||||
"@hanzo/docs-ui": "workspace:*",
|
||||
"next": "16.2.1",
|
||||
>>>>>>> dev
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.2.1",
|
||||
"@tailwindcss/postcss": "^4.2.2",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"postcss": "^8.5.8",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { i18n } from '@/lib/i18n';
|
||||
|
||||
export function baseOptions(locale = i18n.defaultLanguage): BaseLayoutProps {
|
||||
return {
|
||||
LanguageSwitch: true,
|
||||
nav: {
|
||||
title: `React Router ${locale}`,
|
||||
},
|
||||
|
||||
@@ -22,14 +22,14 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-router/dev": "^7.13.1",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"react-router-devtools": "^6.2.0",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^8.0.0"
|
||||
"vite": "^8.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,14 +21,14 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-router/dev": "^7.13.1",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"react-router-devtools": "^6.2.0",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^8.0.0"
|
||||
"vite": "^8.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,15 +21,15 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-router/dev": "^7.13.1",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-rsc": "^0.5.21",
|
||||
"react-router-devtools": "^6.2.0",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^8.0.0"
|
||||
"vite": "^8.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,15 +23,15 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-router/dev": "^7.13.1",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"react-router-devtools": "^6.2.0",
|
||||
"serve": "^14.2.6",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^8.0.0"
|
||||
"vite": "^8.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,17 @@
|
||||
"dependencies": {
|
||||
"@react-router/node": "^7.13.1",
|
||||
"@react-router/serve": "^7.13.1",
|
||||
<<<<<<< HEAD
|
||||
"@takumi-rs/image-response": "^0.72.0",
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"@hanzo/docs-ui": "workspace:*",
|
||||
=======
|
||||
"@takumi-rs/image-response": "^0.73.1",
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"@hanzo/docs-ui": "workspace:*",
|
||||
>>>>>>> dev
|
||||
"isbot": "^5.1.36",
|
||||
"lucide-react": "^0.577.0",
|
||||
"react": "^19.2.4",
|
||||
@@ -24,14 +31,14 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-router/dev": "^7.13.1",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"react-router-devtools": "^6.2.0",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^8.0.0"
|
||||
"vite": "^8.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,26 +11,35 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@orama/tokenizers": "^3.1.18",
|
||||
<<<<<<< HEAD
|
||||
"@tanstack/react-router": "1.166.7",
|
||||
"@tanstack/react-router-devtools": "1.166.7",
|
||||
"@tanstack/react-start": "1.166.8",
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"@hanzo/docs-ui": "workspace:*",
|
||||
=======
|
||||
"@tanstack/react-router": "1.168.1",
|
||||
"@tanstack/react-router-devtools": "1.166.10",
|
||||
"@tanstack/react-start": "1.167.2",
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"@hanzo/docs-ui": "workspace:*",
|
||||
>>>>>>> dev
|
||||
"lucide-static": "^0.577.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"vite": "^8.0.0"
|
||||
"vite": "^8.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { i18n } from '@/lib/i18n';
|
||||
|
||||
export function baseOptions(locale: string = i18n.defaultLanguage): BaseLayoutProps {
|
||||
return {
|
||||
LanguageSwitch: true,
|
||||
nav: {
|
||||
title: `Tanstack Start ${locale}`,
|
||||
},
|
||||
|
||||
@@ -11,27 +11,36 @@
|
||||
"types:check": "@hanzo/docs-mdx && tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
<<<<<<< HEAD
|
||||
"@tanstack/react-router": "1.166.7",
|
||||
"@tanstack/react-router-devtools": "1.166.7",
|
||||
"@tanstack/react-start": "1.166.8",
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"@hanzo/docs-ui": "workspace:*",
|
||||
=======
|
||||
"@tanstack/react-router": "1.168.1",
|
||||
"@tanstack/react-router-devtools": "1.166.10",
|
||||
"@tanstack/react-start": "1.167.2",
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"@hanzo/docs-ui": "workspace:*",
|
||||
>>>>>>> dev
|
||||
"lucide-react": "^0.577.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"vite": "^8.0.0"
|
||||
"vite": "^8.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"nitro": "^3.0.260311-beta",
|
||||
"srvx": "^0.11.9",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"srvx": "^0.11.12",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@orama/orama": "^3.1.18",
|
||||
<<<<<<< HEAD
|
||||
"@tanstack/react-router": "1.166.7",
|
||||
"@tanstack/react-router-devtools": "1.166.7",
|
||||
"@tanstack/react-start": "1.166.8",
|
||||
@@ -19,14 +20,23 @@
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"@hanzo/docs-ui": "workspace:*",
|
||||
=======
|
||||
"@tanstack/react-router": "1.168.1",
|
||||
"@tanstack/react-router-devtools": "1.166.10",
|
||||
"@tanstack/react-start": "1.167.2",
|
||||
"@tanstack/start-static-server-functions": "1.166.17",
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"@hanzo/docs-ui": "workspace:*",
|
||||
>>>>>>> dev
|
||||
"lucide-react": "^0.577.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"vite": "^8.0.0"
|
||||
"vite": "^8.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
@@ -34,8 +44,8 @@
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"nitro": "^3.0.260311-beta",
|
||||
"serve": "^14.2.6",
|
||||
"srvx": "^0.11.9",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"srvx": "^0.11.12",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"types:check": "@hanzo/docs-mdx && tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
<<<<<<< HEAD
|
||||
"@hanzo/docs-story": "workspace:*",
|
||||
"@tanstack/react-router": "1.166.7",
|
||||
"@tanstack/react-router-devtools": "1.166.7",
|
||||
@@ -17,21 +18,30 @@
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"@hanzo/docs-ui": "workspace:*",
|
||||
=======
|
||||
"@hanzo/docs-story": "workspace:*",
|
||||
"@tanstack/react-router": "1.168.1",
|
||||
"@tanstack/react-router-devtools": "1.166.10",
|
||||
"@tanstack/react-start": "1.167.2",
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"@hanzo/docs-ui": "workspace:*",
|
||||
>>>>>>> dev
|
||||
"lucide-react": "^0.577.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"ts-morph": "^27.0.2",
|
||||
"vite": "^8.0.0"
|
||||
"vite": "^8.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,27 +11,36 @@
|
||||
"types:check": "@hanzo/docs-mdx && tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
<<<<<<< HEAD
|
||||
"@tanstack/react-router": "1.166.7",
|
||||
"@tanstack/react-router-devtools": "1.166.7",
|
||||
"@tanstack/react-start": "1.166.8",
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"@hanzo/docs-ui": "workspace:*",
|
||||
=======
|
||||
"@tanstack/react-router": "1.168.1",
|
||||
"@tanstack/react-router-devtools": "1.166.10",
|
||||
"@tanstack/react-start": "1.167.2",
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"@hanzo/docs-ui": "workspace:*",
|
||||
>>>>>>> dev
|
||||
"lucide-react": "^0.577.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"vite": "^8.0.0"
|
||||
"vite": "^8.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"nitro": "^3.0.260311-beta",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,14 +20,14 @@
|
||||
"waku": "1.0.0-alpha.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^8.0.0",
|
||||
"vite": "^8.0.1",
|
||||
"vite-tsconfig-paths": "^6.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,5 @@ export function baseOptions(locale: string): BaseLayoutProps {
|
||||
title: 'Waku',
|
||||
url: `/${locale}`,
|
||||
},
|
||||
LanguageSwitch: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,10 +10,17 @@
|
||||
"types:check": "@hanzo/docs-mdx && tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
<<<<<<< HEAD
|
||||
"@takumi-rs/image-response": "^0.72.0",
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"@hanzo/docs-ui": "workspace:*",
|
||||
=======
|
||||
"@takumi-rs/image-response": "^0.73.1",
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"@hanzo/docs-mdx": "workspace:*",
|
||||
"@hanzo/docs-ui": "workspace:*",
|
||||
>>>>>>> dev
|
||||
"lucide-react": "^0.577.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
@@ -22,14 +29,14 @@
|
||||
"waku": "1.0.0-alpha.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"tailwindcss": "^4 .2.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^8.0.0",
|
||||
"vite": "^8.0.1",
|
||||
"vite-tsconfig-paths": "^6.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -13,23 +13,23 @@
|
||||
"format": "oxfmt",
|
||||
"release": "turbo run build --filter=./packages/* && changeset publish && pnpm -r run postpublish",
|
||||
"test": "vitest",
|
||||
"types:check": "turbo run types:check",
|
||||
"types:check": "turbo run types:check --continue",
|
||||
"version": "changeset version && pnpm install --lockfile-only"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.30.0",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"bun": "1.3.10",
|
||||
"bun": "1.3.11",
|
||||
"concurrently": "^9.2.1",
|
||||
"eslint": "^10.0.3",
|
||||
"eslint": "^10.1.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "7.0.1",
|
||||
"eslint-plugin-tailwindcss": "^3.18.2",
|
||||
"oxfmt": "^0.40.0",
|
||||
"oxfmt": "^0.41.0",
|
||||
"rimraf": "^6.1.3",
|
||||
"turbo": "2.8.16",
|
||||
"turbo": "2.8.20",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.57.0",
|
||||
"typescript-eslint": "^8.57.1",
|
||||
"vitest": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -1,5 +1,50 @@
|
||||
# @fumadocs/base-ui
|
||||
|
||||
## 16.7.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 57c83a5: fix toc
|
||||
- fumadocs-core@16.7.4
|
||||
|
||||
## 16.7.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 9580621: fix sidebar scroll area
|
||||
- 7aa66f2: Redesign home layout navigation menu
|
||||
- fumadocs-core@16.7.3
|
||||
|
||||
## 16.7.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 652c725: Simplify internal types
|
||||
- fumadocs-core@16.7.2
|
||||
|
||||
## 16.7.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 11b8691: hotfix
|
||||
- 75b0b94: Refactor TOC slot
|
||||
- fumadocs-core@16.7.1
|
||||
|
||||
## 16.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 8bdee70: Implement renderer API for replacing layout components, deprecate old options
|
||||
- bdffeba: Improved `defineI18nUI()` usage: allow language translations to be defined at root config.
|
||||
- f45d703: stabilize Shiki factory API
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3d17757: Improve `<GithubInfo />` component
|
||||
- Updated dependencies [f45d703]
|
||||
- Updated dependencies [45aa454]
|
||||
- fumadocs-core@16.7.0
|
||||
|
||||
## 16.6.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
+103
-79
@@ -1,6 +1,11 @@
|
||||
{
|
||||
<<<<<<< HEAD
|
||||
"name": "@hanzo/docs-base-ui",
|
||||
"version": "16.6.17",
|
||||
=======
|
||||
"name": "@hanzo/docs-base-ui",
|
||||
"version": "16.7.4",
|
||||
>>>>>>> dev
|
||||
"description": "The Base UI version of Fumadocs UI",
|
||||
"keywords": [
|
||||
"Docs",
|
||||
@@ -16,87 +21,100 @@
|
||||
],
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./style.css": "./dist/style.css",
|
||||
"./css/*": "./css/*",
|
||||
"./image-zoom.css": "./dist/image-zoom.css",
|
||||
"./components/*": {
|
||||
"types": "./dist/components/*.d.ts",
|
||||
"import": "./dist/components/*.js",
|
||||
"default": "./dist/components/*.js"
|
||||
},
|
||||
"./i18n": {
|
||||
"types": "./dist/i18n.d.ts",
|
||||
"import": "./dist/i18n.js"
|
||||
},
|
||||
"./layouts/home": {
|
||||
"types": "./dist/layouts/home/index.d.ts",
|
||||
"import": "./dist/layouts/home/index.js"
|
||||
},
|
||||
"./layouts/home/not-found": {
|
||||
"types": "./dist/layouts/home/not-found.d.ts",
|
||||
"import": "./dist/layouts/home/not-found.js"
|
||||
},
|
||||
"./layouts/home/navbar": {
|
||||
"types": "./dist/layouts/home/navbar.d.ts",
|
||||
"import": "./dist/layouts/home/navbar.js"
|
||||
},
|
||||
"./layouts/docs": {
|
||||
"types": "./dist/layouts/docs/index.d.ts",
|
||||
"import": "./dist/layouts/docs/index.js"
|
||||
},
|
||||
"./layouts/docs/page": {
|
||||
"types": "./dist/layouts/docs/page/index.d.ts",
|
||||
"import": "./dist/layouts/docs/page/index.js"
|
||||
},
|
||||
"./layouts/flux": {
|
||||
"types": "./dist/layouts/flux/index.d.ts",
|
||||
"import": "./dist/layouts/flux/index.js"
|
||||
},
|
||||
"./layouts/flux/page": {
|
||||
"types": "./dist/layouts/flux/page/index.d.ts",
|
||||
"import": "./dist/layouts/flux/page/index.js"
|
||||
},
|
||||
"./layouts/notebook": {
|
||||
"types": "./dist/layouts/notebook/index.d.ts",
|
||||
"import": "./dist/layouts/notebook/index.js"
|
||||
},
|
||||
"./layouts/notebook/page": {
|
||||
"types": "./dist/layouts/notebook/page/index.d.ts",
|
||||
"import": "./dist/layouts/notebook/page/index.js"
|
||||
},
|
||||
"./layouts/shared": {
|
||||
"types": "./dist/layouts/shared/index.d.ts",
|
||||
"import": "./dist/layouts/shared/index.js"
|
||||
},
|
||||
"./page": {
|
||||
"types": "./dist/page.d.ts",
|
||||
"import": "./dist/page.js"
|
||||
},
|
||||
"./provider/*": {
|
||||
"types": "./dist/provider/*.d.ts",
|
||||
"import": "./dist/provider/*.js"
|
||||
},
|
||||
"./contexts/*": {
|
||||
"types": "./dist/contexts/*.d.ts",
|
||||
"import": "./dist/contexts/*.js"
|
||||
},
|
||||
"./components/accordion": "./dist/components/accordion.js",
|
||||
"./components/banner": "./dist/components/banner.js",
|
||||
"./components/callout": "./dist/components/callout.js",
|
||||
"./components/card": "./dist/components/card.js",
|
||||
"./components/codeblock": "./dist/components/codeblock.js",
|
||||
"./components/codeblock.rsc": "./dist/components/codeblock.rsc.js",
|
||||
"./components/dialog/search": "./dist/components/dialog/search.js",
|
||||
"./components/dialog/search-algolia": "./dist/components/dialog/search-algolia.js",
|
||||
"./components/dialog/search-default": "./dist/components/dialog/search-default.js",
|
||||
"./components/dialog/search-orama": "./dist/components/dialog/search-orama.js",
|
||||
"./components/dynamic-codeblock": "./dist/components/dynamic-codeblock.js",
|
||||
"./components/dynamic-codeblock.core": "./dist/components/dynamic-codeblock.core.js",
|
||||
"./components/files": "./dist/components/files.js",
|
||||
"./components/github-info": "./dist/components/github-info.js",
|
||||
"./components/heading": "./dist/components/heading.js",
|
||||
"./components/image-zoom": "./dist/components/image-zoom.js",
|
||||
"./components/inline-toc": "./dist/components/inline-toc.js",
|
||||
"./components/sidebar/base": "./dist/components/sidebar/base.js",
|
||||
"./components/sidebar/link-item": "./dist/components/sidebar/link-item.js",
|
||||
"./components/sidebar/page-tree": "./dist/components/sidebar/page-tree.js",
|
||||
"./components/sidebar/tabs": "./dist/components/sidebar/tabs/index.js",
|
||||
"./components/sidebar/tabs/dropdown": "./dist/components/sidebar/tabs/dropdown.js",
|
||||
"./components/steps": "./dist/components/steps.js",
|
||||
"./components/tabs": "./dist/components/tabs.js",
|
||||
"./components/toc": "./dist/components/toc/index.js",
|
||||
"./components/toc/clerk": "./dist/components/toc/clerk.js",
|
||||
"./components/toc/default": "./dist/components/toc/default.js",
|
||||
"./components/type-table": "./dist/components/type-table.js",
|
||||
"./components/ui/accordion": "./dist/components/ui/accordion.js",
|
||||
"./components/ui/button": "./dist/components/ui/button.js",
|
||||
"./components/ui/collapsible": "./dist/components/ui/collapsible.js",
|
||||
"./components/ui/navigation-menu": "./dist/components/ui/navigation-menu.js",
|
||||
"./components/ui/popover": "./dist/components/ui/popover.js",
|
||||
"./components/ui/scroll-area": "./dist/components/ui/scroll-area.js",
|
||||
"./components/ui/tabs": "./dist/components/ui/tabs.js",
|
||||
"./contexts/i18n": "./dist/contexts/i18n.js",
|
||||
"./contexts/search": "./dist/contexts/search.js",
|
||||
"./contexts/tree": "./dist/contexts/tree.js",
|
||||
"./i18n": "./dist/i18n.js",
|
||||
"./layouts/docs": "./dist/layouts/docs/index.js",
|
||||
"./layouts/docs/page": "./dist/layouts/docs/page/index.js",
|
||||
"./layouts/docs/page/slots/breadcrumb": "./dist/layouts/docs/page/slots/breadcrumb.js",
|
||||
"./layouts/docs/page/slots/container": "./dist/layouts/docs/page/slots/container.js",
|
||||
"./layouts/docs/page/slots/footer": "./dist/layouts/docs/page/slots/footer.js",
|
||||
"./layouts/docs/page/slots/toc": "./dist/layouts/docs/page/slots/toc.js",
|
||||
"./layouts/docs/slots/container": "./dist/layouts/docs/slots/container.js",
|
||||
"./layouts/docs/slots/header": "./dist/layouts/docs/slots/header.js",
|
||||
"./layouts/docs/slots/sidebar": "./dist/layouts/docs/slots/sidebar.js",
|
||||
"./layouts/flux": "./dist/layouts/flux/index.js",
|
||||
"./layouts/flux/page": "./dist/layouts/flux/page/index.js",
|
||||
"./layouts/flux/page/slots/breadcrumb": "./dist/layouts/flux/page/slots/breadcrumb.js",
|
||||
"./layouts/flux/page/slots/container": "./dist/layouts/flux/page/slots/container.js",
|
||||
"./layouts/flux/page/slots/footer": "./dist/layouts/flux/page/slots/footer.js",
|
||||
"./layouts/flux/page/slots/toc": "./dist/layouts/flux/page/slots/toc.js",
|
||||
"./layouts/flux/slots/container": "./dist/layouts/flux/slots/container.js",
|
||||
"./layouts/flux/slots/sidebar": "./dist/layouts/flux/slots/sidebar.js",
|
||||
"./layouts/flux/slots/tab-dropdown": "./dist/layouts/flux/slots/tab-dropdown.js",
|
||||
"./layouts/home": "./dist/layouts/home/index.js",
|
||||
"./layouts/home/navbar": "./dist/layouts/home/navbar.js",
|
||||
"./layouts/home/not-found": "./dist/layouts/home/not-found.js",
|
||||
"./layouts/home/slots/container": "./dist/layouts/home/slots/container.js",
|
||||
"./layouts/home/slots/header": "./dist/layouts/home/slots/header.js",
|
||||
"./layouts/notebook": "./dist/layouts/notebook/index.js",
|
||||
"./layouts/notebook/page": "./dist/layouts/notebook/page/index.js",
|
||||
"./layouts/notebook/page/slots/breadcrumb": "./dist/layouts/notebook/page/slots/breadcrumb.js",
|
||||
"./layouts/notebook/page/slots/container": "./dist/layouts/notebook/page/slots/container.js",
|
||||
"./layouts/notebook/page/slots/footer": "./dist/layouts/notebook/page/slots/footer.js",
|
||||
"./layouts/notebook/page/slots/toc": "./dist/layouts/notebook/page/slots/toc.js",
|
||||
"./layouts/notebook/slots/container": "./dist/layouts/notebook/slots/container.js",
|
||||
"./layouts/notebook/slots/header": "./dist/layouts/notebook/slots/header.js",
|
||||
"./layouts/notebook/slots/sidebar": "./dist/layouts/notebook/slots/sidebar.js",
|
||||
"./layouts/shared": "./dist/layouts/shared/index.js",
|
||||
"./layouts/shared/slots/language-select": "./dist/layouts/shared/slots/language-select.js",
|
||||
"./layouts/shared/slots/search-trigger": "./dist/layouts/shared/slots/search-trigger.js",
|
||||
"./layouts/shared/slots/theme-switch": "./dist/layouts/shared/slots/theme-switch.js",
|
||||
"./mdx": {
|
||||
"types": "./dist/mdx.d.ts",
|
||||
"node": "./dist/mdx.server.js",
|
||||
"import": "./dist/mdx.js"
|
||||
},
|
||||
"./og": {
|
||||
"types": "./dist/og.d.ts",
|
||||
"import": "./dist/og.js"
|
||||
},
|
||||
"./og/*": {
|
||||
"types": "./dist/og/*.d.ts",
|
||||
"import": "./dist/og/*.js"
|
||||
},
|
||||
"./utils/*": {
|
||||
"types": "./dist/utils/*.d.ts",
|
||||
"import": "./dist/utils/*.js"
|
||||
}
|
||||
"./og": "./dist/og.js",
|
||||
"./og/takumi": "./dist/og/takumi.js",
|
||||
"./page": "./dist/page.js",
|
||||
"./provider/base": "./dist/provider/base.js",
|
||||
"./provider/next": "./dist/provider/next.js",
|
||||
"./provider/react-router": "./dist/provider/react-router.js",
|
||||
"./provider/tanstack": "./dist/provider/tanstack.js",
|
||||
"./provider/waku": "./dist/provider/waku.js",
|
||||
"./utils/use-copy-button": "./dist/utils/use-copy-button.js",
|
||||
"./utils/use-footer-items": "./dist/utils/use-footer-items.js",
|
||||
"./utils/use-is-scroll-top": "./dist/utils/use-is-scroll-top.js",
|
||||
"./package.json": "./package.json",
|
||||
"./style.css": "./dist/style.css",
|
||||
"./css/*": "./css/*"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -115,7 +133,7 @@
|
||||
"@hanzo/docs-tailwind": "workspace:*",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"lucide-react": "^0.577.0",
|
||||
"motion": "^12.36.0",
|
||||
"motion": "^12.38.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react-medium-image-zoom": "^5.4.1",
|
||||
"react-remove-scroll": "^2.7.2",
|
||||
@@ -125,9 +143,15 @@
|
||||
"unist-util-visit": "^5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
<<<<<<< HEAD
|
||||
"@hanzo/docs-cli": "workspace:*",
|
||||
"@tailwindcss/cli": "^4.2.1",
|
||||
"@tsdown/css": "^0.21.2",
|
||||
=======
|
||||
"@hanzo/docs-cli": "workspace:*",
|
||||
"@tailwindcss/cli": "^4.2.2",
|
||||
"@tsdown/css": "^0.21.4",
|
||||
>>>>>>> dev
|
||||
"@types/hast": "^3.0.4",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^25.5.0",
|
||||
@@ -136,9 +160,9 @@
|
||||
"eslint-config-custom": "workspace:*",
|
||||
"@hanzo/docs-core": "workspace:*",
|
||||
"shiki": "^4.0.2",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"tsconfig": "workspace:*",
|
||||
"tsdown": "0.21.2",
|
||||
"tsdown": "0.21.4",
|
||||
"unified": "^11.0.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,77 +1,68 @@
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import type { Registry } from '@hanzo/docs-cli/build';
|
||||
import * as path from 'node:path';
|
||||
import { commonComponents, resolveExternal } from '../../shared/registry';
|
||||
import { commonComponents, findSlotComponents, resolveExternal } from '../../shared/registry';
|
||||
|
||||
const srcDir = path.join(path.dirname(fileURLToPath(import.meta.url)), '../src');
|
||||
const dir = path.join(path.dirname(fileURLToPath(import.meta.url)), '../src');
|
||||
|
||||
// in shadcn cli, the order of files matters when writing import paths on consumer's codebase
|
||||
export const registry: Registry = {
|
||||
<<<<<<< HEAD
|
||||
name: 'hanzo-docs/base-ui',
|
||||
dir: srcDir,
|
||||
=======
|
||||
name: 'fumadocs/base-ui',
|
||||
dir,
|
||||
>>>>>>> dev
|
||||
tsconfigPath: '../tsconfig.json',
|
||||
packageJson: '../package.json',
|
||||
env: {
|
||||
ui: '@hanzo/docs-base-ui',
|
||||
},
|
||||
onResolve(ref) {
|
||||
<<<<<<< HEAD
|
||||
return resolveExternal(ref, '@hanzo/docs-base-ui', srcDir) ?? ref;
|
||||
=======
|
||||
return resolveExternal(ref, '@hanzo/docs-ui', dir) ?? ref;
|
||||
>>>>>>> dev
|
||||
},
|
||||
components: [
|
||||
...commonComponents,
|
||||
...(await findSlotComponents(dir)),
|
||||
{
|
||||
name: 'layouts/sidebar',
|
||||
unlisted: true,
|
||||
files: [
|
||||
{
|
||||
type: 'components',
|
||||
path: 'components/sidebar/base.tsx',
|
||||
target: '<dir>/docs-sidebar/base.tsx',
|
||||
},
|
||||
{
|
||||
type: 'components',
|
||||
path: 'components/sidebar/page-tree.tsx',
|
||||
target: '<dir>/docs-sidebar/page-tree.tsx',
|
||||
},
|
||||
{
|
||||
type: 'components',
|
||||
path: 'components/sidebar/link-item.tsx',
|
||||
target: '<dir>/docs-sidebar/link-item.tsx',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'layouts/shared',
|
||||
unlisted: true,
|
||||
files: [
|
||||
{
|
||||
type: 'components',
|
||||
type: 'layout',
|
||||
path: 'layouts/shared/index.tsx',
|
||||
target: '<dir>/layout/shared.tsx',
|
||||
target: '<dir>/shared/index.tsx',
|
||||
},
|
||||
{
|
||||
type: 'components',
|
||||
path: 'layouts/shared/language-toggle.tsx',
|
||||
target: '<dir>/layout/language-toggle.tsx',
|
||||
},
|
||||
{
|
||||
type: 'components',
|
||||
path: 'layouts/shared/page-actions.tsx',
|
||||
target: '<dir>/layout/page-actions.tsx',
|
||||
},
|
||||
{
|
||||
type: 'components',
|
||||
path: 'layouts/shared/search-toggle.tsx',
|
||||
target: '<dir>/layout/search-toggle.tsx',
|
||||
},
|
||||
{
|
||||
type: 'components',
|
||||
path: 'layouts/shared/theme-toggle.tsx',
|
||||
target: '<dir>/layout/theme-toggle.tsx',
|
||||
},
|
||||
{
|
||||
type: 'components',
|
||||
path: 'components/sidebar/base.tsx',
|
||||
target: '<dir>/layout/sidebar/base.tsx',
|
||||
},
|
||||
{
|
||||
type: 'components',
|
||||
path: 'components/sidebar/page-tree.tsx',
|
||||
target: '<dir>/layout/sidebar/page-tree.tsx',
|
||||
},
|
||||
{
|
||||
type: 'components',
|
||||
path: 'components/sidebar/link-item.tsx',
|
||||
target: '<dir>/layout/sidebar/link-item.tsx',
|
||||
},
|
||||
{
|
||||
type: 'components',
|
||||
path: 'components/sidebar/tabs/index.tsx',
|
||||
target: '<dir>/layout/sidebar/tabs/index.tsx',
|
||||
},
|
||||
{
|
||||
type: 'components',
|
||||
path: 'components/sidebar/tabs/dropdown.tsx',
|
||||
target: '<dir>/layout/sidebar/tabs/dropdown.tsx',
|
||||
type: 'layout',
|
||||
path: 'layouts/shared/client.tsx',
|
||||
target: '<dir>/shared/client.tsx',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -79,29 +70,19 @@ export const registry: Registry = {
|
||||
name: 'layouts/docs',
|
||||
files: [
|
||||
{
|
||||
type: 'components',
|
||||
type: 'layout',
|
||||
path: 'layouts/docs/index.tsx',
|
||||
target: '<dir>/layout/docs/index.tsx',
|
||||
target: '<dir>/docs/index.tsx',
|
||||
},
|
||||
{
|
||||
type: 'components',
|
||||
type: 'layout',
|
||||
path: 'layouts/docs/client.tsx',
|
||||
target: '<dir>/layout/docs/client.tsx',
|
||||
target: '<dir>/docs/client.tsx',
|
||||
},
|
||||
{
|
||||
type: 'components',
|
||||
path: 'layouts/docs/sidebar.tsx',
|
||||
target: '<dir>/layout/docs/sidebar.tsx',
|
||||
},
|
||||
{
|
||||
type: 'components',
|
||||
type: 'layout',
|
||||
path: 'layouts/docs/page/index.tsx',
|
||||
target: '<dir>/layout/docs/page/index.tsx',
|
||||
},
|
||||
{
|
||||
type: 'components',
|
||||
path: 'layouts/docs/page/client.tsx',
|
||||
target: '<dir>/layout/docs/page/client.tsx',
|
||||
target: '<dir>/docs/page/index.tsx',
|
||||
},
|
||||
],
|
||||
unlisted: true,
|
||||
@@ -110,29 +91,14 @@ export const registry: Registry = {
|
||||
name: 'layouts/flux',
|
||||
files: [
|
||||
{
|
||||
type: 'components',
|
||||
type: 'layout',
|
||||
path: 'layouts/flux/index.tsx',
|
||||
target: '<dir>/layout/flux/index.tsx',
|
||||
target: '<dir>/flux/index.tsx',
|
||||
},
|
||||
{
|
||||
type: 'components',
|
||||
path: 'layouts/flux/tab-dropdown.tsx',
|
||||
target: '<dir>/layout/flux/tab-dropdown.tsx',
|
||||
},
|
||||
{
|
||||
type: 'components',
|
||||
path: 'layouts/flux/sidebar.tsx',
|
||||
target: '<dir>/layout/flux/sidebar.tsx',
|
||||
},
|
||||
{
|
||||
type: 'components',
|
||||
type: 'layout',
|
||||
path: 'layouts/flux/page/index.tsx',
|
||||
target: '<dir>/layout/flux/page/index.tsx',
|
||||
},
|
||||
{
|
||||
type: 'components',
|
||||
path: 'layouts/flux/page/client.tsx',
|
||||
target: '<dir>/layout/flux/page/client.tsx',
|
||||
target: '<dir>/flux/page/index.tsx',
|
||||
},
|
||||
],
|
||||
unlisted: true,
|
||||
@@ -141,29 +107,19 @@ export const registry: Registry = {
|
||||
name: 'layouts/notebook',
|
||||
files: [
|
||||
{
|
||||
type: 'components',
|
||||
type: 'layout',
|
||||
path: 'layouts/notebook/index.tsx',
|
||||
target: '<dir>/layout/notebook/index.tsx',
|
||||
target: '<dir>/notebook/index.tsx',
|
||||
},
|
||||
{
|
||||
type: 'components',
|
||||
type: 'layout',
|
||||
path: 'layouts/notebook/client.tsx',
|
||||
target: '<dir>/layout/notebook/client.tsx',
|
||||
target: '<dir>/notebook/client.tsx',
|
||||
},
|
||||
{
|
||||
type: 'components',
|
||||
path: 'layouts/notebook/sidebar.tsx',
|
||||
target: '<dir>/layout/notebook/sidebar.tsx',
|
||||
},
|
||||
{
|
||||
type: 'components',
|
||||
type: 'layout',
|
||||
path: 'layouts/notebook/page/index.tsx',
|
||||
target: '<dir>/layout/notebook/page/index.tsx',
|
||||
},
|
||||
{
|
||||
type: 'components',
|
||||
path: 'layouts/notebook/page/client.tsx',
|
||||
target: '<dir>/layout/notebook/page/client.tsx',
|
||||
target: '<dir>/notebook/page/index.tsx',
|
||||
},
|
||||
],
|
||||
unlisted: true,
|
||||
@@ -172,14 +128,9 @@ export const registry: Registry = {
|
||||
name: 'layouts/home',
|
||||
files: [
|
||||
{
|
||||
type: 'components',
|
||||
type: 'layout',
|
||||
path: 'layouts/home/index.tsx',
|
||||
target: '<dir>/layout/home/index.tsx',
|
||||
},
|
||||
{
|
||||
type: 'components',
|
||||
path: 'layouts/home/client.tsx',
|
||||
target: '<dir>/layout/home/client.tsx',
|
||||
target: '<dir>/home/index.tsx',
|
||||
},
|
||||
{
|
||||
type: 'ui',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Check, Link as LinkIcon } from 'lucide-react';
|
||||
import { ComponentProps, type ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import { Check, LinkIcon } from 'lucide-react';
|
||||
import { type ComponentProps, type ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useCopyButton } from '@/utils/use-copy-button';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { ChevronRight, Hash, Search as SearchIcon } from 'lucide-react';
|
||||
import { ChevronRight, Hash, SearchIcon } from 'lucide-react';
|
||||
import {
|
||||
type ComponentProps,
|
||||
createContext,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { File as FileIcon, Folder as FolderIcon, FolderOpen } from 'lucide-react';
|
||||
import { FileIcon, FolderIcon, FolderOpen } from 'lucide-react';
|
||||
import { type HTMLAttributes, type ReactNode, useState } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Link } from 'lucide-react';
|
||||
import { LinkIcon } from 'lucide-react';
|
||||
import type { ComponentPropsWithoutRef } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
@@ -21,7 +21,7 @@ export function Heading<T extends Types = 'h1'>({
|
||||
<a data-card="" href={`#${props.id}`} className="peer">
|
||||
{props.children}
|
||||
</a>
|
||||
<Link
|
||||
<LinkIcon
|
||||
aria-hidden
|
||||
className="size-3.5 shrink-0 text-fd-muted-foreground opacity-0 transition-opacity peer-hover:opacity-100"
|
||||
/>
|
||||
|
||||
@@ -226,15 +226,7 @@ export function SidebarViewport({ className, ...props }: ScrollAreaProps) {
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<ScrollViewport
|
||||
className="p-4 overscroll-contain"
|
||||
style={
|
||||
{
|
||||
maskImage:
|
||||
'linear-gradient(to bottom, transparent, white 12px, white calc(100% - 12px), transparent)',
|
||||
} as object
|
||||
}
|
||||
>
|
||||
<ScrollViewport className="p-4 overscroll-contain mask-[linear-gradient(to_bottom,transparent,white_12px,white_calc(100%-12px),transparent)]">
|
||||
{props.children}
|
||||
</ScrollViewport>
|
||||
</ScrollArea>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use client';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import type * as Base from './base';
|
||||
import { useLinkItemActive, type LinkItemType } from '@/utils/link-item';
|
||||
import { usePathname } from '@hanzo/docs-core/framework';
|
||||
import { isLinkItemActive, type LinkItemType } from '@/layouts/shared';
|
||||
|
||||
type InternalComponents = Pick<
|
||||
typeof Base,
|
||||
@@ -28,7 +29,8 @@ export function createLinkItemRenderer({
|
||||
}: HTMLAttributes<HTMLElement> & {
|
||||
item: Exclude<LinkItemType, { type: 'icon' }>;
|
||||
}) {
|
||||
const active = useLinkItemActive(item);
|
||||
const pathname = usePathname();
|
||||
const active = isLinkItemActive(item, pathname);
|
||||
if (item.type === 'custom') return <div {...props}>{item.children}</div>;
|
||||
|
||||
if (item.type === 'menu')
|
||||
|
||||
@@ -4,14 +4,11 @@ import { type ComponentProps, type ReactNode, useMemo, useState } from 'react';
|
||||
import Link from '@hanzo/docs-core/link';
|
||||
import { usePathname } from '@hanzo/docs-core/framework';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { normalize, isActive } from '@/utils/urls';
|
||||
import { useSidebar } from '@/components/sidebar/base';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import type { SidebarTab } from '.';
|
||||
import { isLayoutTabActive, type LayoutTab } from '@/layouts/shared';
|
||||
|
||||
export interface SidebarTabWithProps extends SidebarTab {
|
||||
props?: ComponentProps<'a'>;
|
||||
}
|
||||
export type SidebarTabWithProps = LayoutTab;
|
||||
|
||||
export function SidebarTabsDropdown({
|
||||
options,
|
||||
@@ -100,8 +97,4 @@ export function SidebarTabsDropdown({
|
||||
);
|
||||
}
|
||||
|
||||
export function isTabActive(tab: SidebarTab, pathname: string) {
|
||||
if (tab.urls) return tab.urls.has(normalize(pathname));
|
||||
|
||||
return isActive(tab.url, pathname, true);
|
||||
}
|
||||
export const isTabActive = isLayoutTabActive;
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
<<<<<<< HEAD
|
||||
import type * as PageTree from '@hanzo/docs-core/page-tree';
|
||||
import type { ReactNode } from 'react';
|
||||
=======
|
||||
import type { LayoutTab } from '@/layouts/shared';
|
||||
import type * as PageTree from '@hanzo/docs-core/page-tree';
|
||||
>>>>>>> dev
|
||||
|
||||
export interface SidebarTab {
|
||||
/**
|
||||
* Redirect URL of the folder, usually the index page
|
||||
*/
|
||||
url: string;
|
||||
|
||||
icon?: ReactNode;
|
||||
title: ReactNode;
|
||||
description?: ReactNode;
|
||||
|
||||
/**
|
||||
* Detect from a list of urls
|
||||
*/
|
||||
urls?: Set<string>;
|
||||
unlisted?: boolean;
|
||||
}
|
||||
export type SidebarTab = LayoutTab;
|
||||
|
||||
export interface GetSidebarTabsOptions {
|
||||
transform?: (option: SidebarTab, node: PageTree.Folder) => SidebarTab | null;
|
||||
|
||||
@@ -49,39 +49,39 @@ function useTabContext() {
|
||||
return ctx;
|
||||
}
|
||||
|
||||
export const TabsList = React.forwardRef<
|
||||
React.ComponentRef<typeof Unstyled.TabsList>,
|
||||
React.ComponentPropsWithoutRef<typeof Unstyled.TabsList>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<Unstyled.TabsList
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={(s) =>
|
||||
cn(
|
||||
'flex gap-3.5 text-fd-secondary-foreground overflow-x-auto px-4 not-prose',
|
||||
typeof className === 'function' ? className(s) : className,
|
||||
)
|
||||
}
|
||||
/>
|
||||
));
|
||||
TabsList.displayName = 'TabsList';
|
||||
export function TabsList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithRef<typeof Unstyled.TabsList>) {
|
||||
return (
|
||||
<Unstyled.TabsList
|
||||
{...props}
|
||||
className={(s) =>
|
||||
cn(
|
||||
'flex gap-3.5 text-fd-secondary-foreground overflow-x-auto px-4 not-prose',
|
||||
typeof className === 'function' ? className(s) : className,
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const TabsTrigger = React.forwardRef<
|
||||
React.ComponentRef<typeof Unstyled.TabsTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof Unstyled.TabsTrigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<Unstyled.TabsTrigger
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={(s) =>
|
||||
cn(
|
||||
'inline-flex items-center gap-2 whitespace-nowrap text-fd-muted-foreground border-b border-transparent py-2 text-sm font-medium transition-colors [&_svg]:size-4 hover:text-fd-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[active]:border-fd-primary data-[active]:text-fd-primary',
|
||||
typeof className === 'function' ? className(s) : className,
|
||||
)
|
||||
}
|
||||
/>
|
||||
));
|
||||
TabsTrigger.displayName = 'TabsTrigger';
|
||||
export function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithRef<typeof Unstyled.TabsTrigger>) {
|
||||
return (
|
||||
<Unstyled.TabsTrigger
|
||||
{...props}
|
||||
className={(s) =>
|
||||
cn(
|
||||
'inline-flex items-center gap-2 whitespace-nowrap text-fd-muted-foreground border-b border-transparent py-2 text-sm font-medium transition-colors [&_svg]:size-4 hover:text-fd-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[active]:border-fd-primary data-[active]:text-fd-primary',
|
||||
typeof className === 'function' ? className(s) : className,
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Tabs({
|
||||
ref,
|
||||
|
||||
@@ -1,64 +1,78 @@
|
||||
'use client';
|
||||
<<<<<<< HEAD
|
||||
import * as Primitive from '@hanzo/docs-core/toc';
|
||||
import { type ComponentProps, useEffect, useRef, useState } from 'react';
|
||||
=======
|
||||
import * as Primitive from '@hanzo/docs-core/toc';
|
||||
import { type ComponentProps, useEffect, useEffectEvent, useRef, useState } from 'react';
|
||||
>>>>>>> dev
|
||||
import { cn } from '@/utils/cn';
|
||||
import { TocThumb, useTOCItems } from '.';
|
||||
import { mergeRefs } from '@/utils/merge-refs';
|
||||
import { useI18n } from '@/contexts/i18n';
|
||||
|
||||
interface ComputedSVG {
|
||||
d: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export function TOCItems({ ref, className, ...props }: ComponentProps<'div'>) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const items = useTOCItems();
|
||||
const { text } = useI18n();
|
||||
const [svg, setSvg] = useState<ComputedSVG>();
|
||||
|
||||
const [svg, setSvg] = useState<{
|
||||
path: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}>();
|
||||
const onResize = useEffectEvent(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container || container.clientHeight === 0) return;
|
||||
let w = 0;
|
||||
let h = 0;
|
||||
let b0 = 0;
|
||||
let d = '';
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const element: HTMLElement | null = container.querySelector(
|
||||
`a[href="#${items[i].url.slice(1)}"]`,
|
||||
);
|
||||
if (!element) continue;
|
||||
|
||||
const styles = getComputedStyle(element);
|
||||
const offset = getLineOffset(items[i].depth) + 1,
|
||||
top = element.offsetTop + parseFloat(styles.paddingTop),
|
||||
bottom = element.offsetTop + element.clientHeight - parseFloat(styles.paddingBottom);
|
||||
|
||||
w = Math.max(offset, w);
|
||||
h = Math.max(h, bottom);
|
||||
|
||||
if (i === 0) {
|
||||
d += ` M${offset} ${top} L${offset} ${bottom}`;
|
||||
} else {
|
||||
const pOffset = getLineOffset(items[i - 1].depth) + 1;
|
||||
d += ` C ${pOffset} ${top - 4} ${offset} ${b0! + 4} ${offset} ${top} L${offset} ${bottom}`;
|
||||
}
|
||||
|
||||
b0 = bottom;
|
||||
}
|
||||
|
||||
w += 1;
|
||||
setSvg({
|
||||
d,
|
||||
width: w,
|
||||
height: h,
|
||||
});
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
const container = containerRef.current;
|
||||
|
||||
function onResize(): void {
|
||||
if (container.clientHeight === 0) return;
|
||||
let w = 0,
|
||||
h = 0;
|
||||
const d: string[] = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const element: HTMLElement | null = container.querySelector(
|
||||
`a[href="#${items[i].url.slice(1)}"]`,
|
||||
);
|
||||
if (!element) continue;
|
||||
|
||||
const styles = getComputedStyle(element);
|
||||
const offset = getLineOffset(items[i].depth) + 1,
|
||||
top = element.offsetTop + parseFloat(styles.paddingTop),
|
||||
bottom = element.offsetTop + element.clientHeight - parseFloat(styles.paddingBottom);
|
||||
|
||||
w = Math.max(offset, w);
|
||||
h = Math.max(h, bottom);
|
||||
|
||||
d.push(`${i === 0 ? 'M' : 'L'}${offset} ${top}`);
|
||||
d.push(`L${offset} ${bottom}`);
|
||||
}
|
||||
|
||||
setSvg({
|
||||
path: d.join(' '),
|
||||
width: w + 1,
|
||||
height: h,
|
||||
});
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver(onResize);
|
||||
onResize();
|
||||
|
||||
observer.observe(container);
|
||||
observer.observe(containerRef.current);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [items]);
|
||||
}, []);
|
||||
|
||||
if (items.length === 0)
|
||||
return (
|
||||
@@ -70,24 +84,28 @@ export function TOCItems({ ref, className, ...props }: ComponentProps<'div'>) {
|
||||
return (
|
||||
<>
|
||||
{svg && (
|
||||
<div
|
||||
className="absolute start-0 top-0 rtl:-scale-x-100"
|
||||
<TocThumb
|
||||
containerRef={containerRef}
|
||||
className="absolute top-0 inset-s-0"
|
||||
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="absolute w-full top-(--fd-top) h-(--fd-height) bg-fd-primary transition-[top,height]"
|
||||
/>
|
||||
</div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox={`0 0 ${svg.width} ${svg.height}`}
|
||||
className="absolute transition-[clip-path]"
|
||||
style={{
|
||||
width: svg.width,
|
||||
height: svg.height,
|
||||
clipPath: `polygon(0 var(--fd-top), 100% var(--fd-top), 100% calc(var(--fd-top) + var(--fd-height)), 0 calc(var(--fd-top) + var(--fd-height)))`,
|
||||
}}
|
||||
>
|
||||
<path d={svg.d} className="stroke-fd-primary" strokeWidth="1" fill="none" />
|
||||
</svg>
|
||||
<ThumbBox />
|
||||
</TocThumb>
|
||||
)}
|
||||
<div ref={mergeRefs(containerRef, ref)} className={cn('flex flex-col', className)} {...props}>
|
||||
{items.map((item, i) => (
|
||||
@@ -103,6 +121,34 @@ export function TOCItems({ ref, className, ...props }: ComponentProps<'div'>) {
|
||||
);
|
||||
}
|
||||
|
||||
function ThumbBox() {
|
||||
const itemInfos = Primitive.useItems();
|
||||
const startIdx = itemInfos.findIndex((info) => info.active);
|
||||
const endIdx = itemInfos.findLastIndex((info) => info.active);
|
||||
if (startIdx === -1) return;
|
||||
|
||||
let lastInactiveIdx = -1;
|
||||
for (let i = 0; i < itemInfos.length; i++) {
|
||||
const item = itemInfos[i];
|
||||
if (item.active) continue;
|
||||
if (lastInactiveIdx === -1 || itemInfos[lastInactiveIdx].t < item.t) {
|
||||
lastInactiveIdx = i;
|
||||
}
|
||||
}
|
||||
const isStart = endIdx < lastInactiveIdx;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute size-1 bg-fd-primary rounded-full transition-transform"
|
||||
style={{
|
||||
translate: `calc(${getLineOffset(itemInfos[isStart ? startIdx : endIdx].original.depth)}px - 1.25px) ${
|
||||
isStart ? 'var(--fd-top)' : 'calc(var(--fd-top) + var(--fd-height))'
|
||||
}`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getItemOffset(depth: number): number {
|
||||
if (depth <= 2) return 14;
|
||||
if (depth === 3) return 26;
|
||||
@@ -110,7 +156,9 @@ function getItemOffset(depth: number): number {
|
||||
}
|
||||
|
||||
function getLineOffset(depth: number): number {
|
||||
return depth >= 3 ? 10 : 0;
|
||||
if (depth <= 2) return 2;
|
||||
if (depth === 3) return 10;
|
||||
return 20;
|
||||
}
|
||||
|
||||
function TOCItem({
|
||||
@@ -132,21 +180,25 @@ function TOCItem({
|
||||
style={{
|
||||
paddingInlineStart: getItemOffset(item.depth),
|
||||
}}
|
||||
className="prose relative py-1.5 text-sm text-fd-muted-foreground hover:text-fd-accent-foreground transition-colors wrap-anywhere first:pt-0 last:pb-0 data-[active=true]:text-fd-primary"
|
||||
className="prose relative py-1.5 text-sm scroll-m-4 text-fd-muted-foreground hover:text-fd-accent-foreground transition-colors wrap-anywhere first:pt-0 last:pb-0 data-[active=true]:text-fd-primary"
|
||||
>
|
||||
{offset !== upperOffset && (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
className="absolute -top-1.5 start-0 size-4 rtl:-scale-x-100"
|
||||
viewBox={`${Math.min(offset, upperOffset)} 0 ${Math.abs(upperOffset - offset)} 12`}
|
||||
className="absolute -top-1.5"
|
||||
style={{
|
||||
width: Math.abs(upperOffset - offset) + 1,
|
||||
height: 12,
|
||||
insetInlineStart: Math.min(offset, upperOffset),
|
||||
}}
|
||||
>
|
||||
<line
|
||||
x1={upperOffset}
|
||||
y1="0"
|
||||
x2={offset}
|
||||
y2="12"
|
||||
className="stroke-fd-foreground/10"
|
||||
<path
|
||||
d={`M ${upperOffset} 0 C ${upperOffset} 8 ${offset} 4 ${offset} 12`}
|
||||
stroke="black"
|
||||
strokeWidth="1"
|
||||
fill="none"
|
||||
className="stroke-fd-foreground/10"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
|
||||
@@ -19,21 +19,23 @@ export function TOCItems({ ref, className, ...props }: ComponentProps<'div'>) {
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={mergeRefs(ref, containerRef)}
|
||||
className={cn('relative flex flex-col border-s border-fd-foreground/10', className)}
|
||||
{...props}
|
||||
>
|
||||
<TocThumb
|
||||
containerRef={containerRef}
|
||||
className="absolute top-(--fd-top) h-(--fd-height) w-0.5 rounded-e-sm bg-fd-primary transition-[top,height] ease-linear"
|
||||
className="absolute inset-y-0 inset-s-0 bg-fd-primary w-px transition-[clip-path]"
|
||||
style={{
|
||||
clipPath:
|
||||
'polygon(0 var(--fd-top), 100% var(--fd-top), 100% calc(var(--fd-top) + var(--fd-height)), 0 calc(var(--fd-top) + var(--fd-height)))',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
ref={mergeRefs(ref, containerRef)}
|
||||
className={cn('flex flex-col border-s border-fd-foreground/10', className)}
|
||||
{...props}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<TOCItem key={item.url} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
{items.map((item) => (
|
||||
<TOCItem key={item.url} item={item} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,7 +44,7 @@ function TOCItem({ item }: { item: Primitive.TOCItemType }) {
|
||||
<Primitive.TOCItem
|
||||
href={item.url}
|
||||
className={cn(
|
||||
'prose py-1.5 text-sm text-fd-muted-foreground transition-colors wrap-anywhere first:pt-0 last:pb-0 data-[active=true]:text-fd-primary',
|
||||
'prose py-1.5 text-sm text-fd-muted-foreground scroll-m-4 transition-colors wrap-anywhere first:pt-0 last:pb-0 data-[active=true]:text-fd-primary hover:text-fd-accent-foreground',
|
||||
item.depth <= 2 && 'ps-3',
|
||||
item.depth === 3 && 'ps-6',
|
||||
item.depth >= 4 && 'ps-8',
|
||||
|
||||
@@ -19,11 +19,11 @@ export function useTOCItems(): Primitive.TOCItemType[] {
|
||||
return use(TOCContext);
|
||||
}
|
||||
|
||||
export function TOCProvider({
|
||||
toc,
|
||||
children,
|
||||
...props
|
||||
}: ComponentProps<typeof Primitive.AnchorProvider>) {
|
||||
export type TOCProviderProps = Primitive.AnchorProviderProps;
|
||||
|
||||
export const { useActiveAnchor, useActiveAnchors, useItems } = Primitive;
|
||||
|
||||
export function TOCProvider({ toc, children, ...props }: TOCProviderProps) {
|
||||
return (
|
||||
<TOCContext value={toc}>
|
||||
<Primitive.AnchorProvider toc={toc} {...props}>
|
||||
@@ -50,26 +50,65 @@ export function TOCScrollArea({ ref, className, ...props }: ComponentProps<'div'
|
||||
);
|
||||
}
|
||||
|
||||
type TocThumbType = [top: number, height: number];
|
||||
|
||||
interface RefProps {
|
||||
containerRef: RefObject<HTMLElement | null>;
|
||||
interface TocThumbInfo {
|
||||
top: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export function TocThumb({ containerRef, ...props }: ComponentProps<'div'> & RefProps) {
|
||||
interface TocThumbProps extends ComponentProps<'div'> {
|
||||
containerRef: RefObject<HTMLElement | null>;
|
||||
align?: 'center' | 'end';
|
||||
}
|
||||
|
||||
export function TocThumb({ containerRef, align = 'end', ...props }: TocThumbProps) {
|
||||
const thumbRef = useRef<HTMLDivElement>(null);
|
||||
const active = Primitive.useActiveAnchors();
|
||||
function update(info: TocThumbType): void {
|
||||
const active = useActiveAnchors();
|
||||
|
||||
function update(info: TocThumbInfo): void {
|
||||
const element = thumbRef.current;
|
||||
if (!element) return;
|
||||
element.style.setProperty('--fd-top', `${info[0]}px`);
|
||||
element.style.setProperty('--fd-height', `${info[1]}px`);
|
||||
const container = containerRef.current;
|
||||
if (!element || !container) return;
|
||||
|
||||
element.style.setProperty('--fd-top', `${info.top}px`);
|
||||
element.style.setProperty('--fd-height', `${info.height}px`);
|
||||
}
|
||||
|
||||
function calc(active: string[]): TocThumbInfo | null {
|
||||
const container = containerRef.current;
|
||||
if (!container || container.clientHeight === 0) return null;
|
||||
if (active.length === 0) return { height: 0, top: 0 };
|
||||
|
||||
let upper = Number.MAX_VALUE;
|
||||
let lower = 0;
|
||||
|
||||
for (const item of active) {
|
||||
const element = container.querySelector<HTMLElement>(`a[href="#${item}"]`);
|
||||
if (!element) continue;
|
||||
|
||||
if (align === 'center') {
|
||||
const y = element.offsetTop + element.clientHeight / 2;
|
||||
upper = Math.min(upper, y);
|
||||
lower = Math.max(lower, y);
|
||||
} else {
|
||||
const styles = getComputedStyle(element);
|
||||
|
||||
upper = Math.min(upper, element.offsetTop + parseFloat(styles.paddingTop));
|
||||
lower = Math.max(
|
||||
lower,
|
||||
element.offsetTop + element.clientHeight - parseFloat(styles.paddingBottom),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
top: upper,
|
||||
height: lower - upper,
|
||||
};
|
||||
}
|
||||
|
||||
const onPrint = useEffectEvent(() => {
|
||||
if (containerRef.current) {
|
||||
update(calc(containerRef.current, active));
|
||||
}
|
||||
const result = calc(active);
|
||||
if (result) update(result);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -85,33 +124,9 @@ export function TocThumb({ containerRef, ...props }: ComponentProps<'div'> & Ref
|
||||
}, [containerRef]);
|
||||
|
||||
useOnChange(active, () => {
|
||||
if (containerRef.current) {
|
||||
update(calc(containerRef.current, active));
|
||||
}
|
||||
const result = calc(active);
|
||||
if (result) update(result);
|
||||
});
|
||||
|
||||
return <div ref={thumbRef} data-hidden={active.length === 0} {...props} />;
|
||||
}
|
||||
|
||||
function calc(container: HTMLElement, active: string[]): TocThumbType {
|
||||
if (active.length === 0 || container.clientHeight === 0) {
|
||||
return [0, 0];
|
||||
}
|
||||
|
||||
let upper = Number.MAX_VALUE,
|
||||
lower = 0;
|
||||
|
||||
for (const item of active) {
|
||||
const element = container.querySelector<HTMLElement>(`a[href="#${item}"]`);
|
||||
if (!element) continue;
|
||||
|
||||
const styles = getComputedStyle(element);
|
||||
upper = Math.min(upper, element.offsetTop + parseFloat(styles.paddingTop));
|
||||
lower = Math.max(
|
||||
lower,
|
||||
element.offsetTop + element.clientHeight - parseFloat(styles.paddingBottom),
|
||||
);
|
||||
}
|
||||
|
||||
return [upper, lower - upper];
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export function CollapsibleContent({
|
||||
{...props}
|
||||
className={(s) =>
|
||||
cn(
|
||||
"overflow-hidden [&[hidden]:not([hidden='until-found'])]:hidden h-(--collapsible-panel-height) transition-[height] data-[starting-style]:h-0 data-[ending-style]:h-0",
|
||||
"overflow-hidden [&[hidden]:not([hidden='until-found'])]:hidden h-(--collapsible-panel-height) transition-[height,opacity] data-starting-style:opacity-0 data-starting-style:h-0 data-ending-style:h-0 data-ending-style:opacity-0",
|
||||
typeof className === 'function' ? className(s) : className,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,65 +6,83 @@ import { cn } from '@/utils/cn';
|
||||
export type NavigationMenuContentProps = Primitive.Content.Props;
|
||||
export type NavigationMenuTriggerProps = Primitive.Trigger.Props;
|
||||
|
||||
const NavigationMenuRoot = Primitive.Root;
|
||||
export const NavigationMenu = Primitive.Root;
|
||||
export const NavigationMenuList = Primitive.List;
|
||||
|
||||
const NavigationMenuList = Primitive.List;
|
||||
export function NavigationMenuItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentPropsWithRef<typeof Primitive.Item>) {
|
||||
return (
|
||||
<Primitive.Item
|
||||
className={(s) => cn('list-none', typeof className === 'function' ? className(s) : className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Primitive.Item>
|
||||
);
|
||||
}
|
||||
|
||||
const NavigationMenuItem = React.forwardRef<
|
||||
React.ComponentRef<typeof Primitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<Primitive.Item
|
||||
ref={ref}
|
||||
className={(s) => cn('list-none', typeof className === 'function' ? className(s) : className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Primitive.Item>
|
||||
));
|
||||
export function NavigationMenuTrigger({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentPropsWithRef<typeof Primitive.Trigger>) {
|
||||
return <Primitive.Trigger {...props}>{children}</Primitive.Trigger>;
|
||||
}
|
||||
|
||||
NavigationMenuItem.displayName = Primitive.Item.displayName;
|
||||
export function NavigationMenuContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithRef<typeof Primitive.Content>) {
|
||||
return (
|
||||
<Primitive.Content
|
||||
className={(s) =>
|
||||
cn(
|
||||
'size-full p-3',
|
||||
'transition-[opacity,transform,translate] duration-(--duration) ease-(--easing)',
|
||||
'data-starting-style:opacity-0 data-ending-style:opacity-0',
|
||||
'data-starting-style:data-[activation-direction=left]:-translate-x-1/2',
|
||||
'data-starting-style:data-[activation-direction=right]:translate-x-1/2',
|
||||
'data-ending-style:data-[activation-direction=left]:translate-x-1/2',
|
||||
'data-ending-style:data-[activation-direction=right]:-translate-x-1/2',
|
||||
typeof className === 'function' ? className(s) : className,
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const NavigationMenuTrigger = React.forwardRef<
|
||||
React.ComponentRef<typeof Primitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitive.Trigger>
|
||||
>(({ children, ...props }, ref) => (
|
||||
<Primitive.Trigger ref={ref} {...props}>
|
||||
{children}
|
||||
</Primitive.Trigger>
|
||||
));
|
||||
NavigationMenuTrigger.displayName = Primitive.Trigger.displayName;
|
||||
export function NavigationMenuLink({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentPropsWithRef<typeof Primitive.Link>) {
|
||||
return <Primitive.Link {...props}>{children}</Primitive.Link>;
|
||||
}
|
||||
|
||||
const NavigationMenuContent = React.forwardRef<
|
||||
React.ComponentRef<typeof Primitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<Primitive.Content
|
||||
ref={ref}
|
||||
className={(s) =>
|
||||
cn(
|
||||
'size-full p-4',
|
||||
'transition-[opacity,transform,translate] duration-(--duration) ease-(--easing)',
|
||||
'data-starting-style:opacity-0 data-ending-style:opacity-0',
|
||||
'data-starting-style:data-[activation-direction=left]:-translate-x-1/2',
|
||||
'data-starting-style:data-[activation-direction=right]:translate-x-1/2',
|
||||
'data-ending-style:data-[activation-direction=left]:translate-x-1/2',
|
||||
'data-ending-style:data-[activation-direction=right]:-translate-x-1/2',
|
||||
typeof className === 'function' ? className(s) : className,
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
NavigationMenuContent.displayName = Primitive.Content.displayName;
|
||||
|
||||
const NavigationMenuLink = Primitive.Link;
|
||||
|
||||
export {
|
||||
NavigationMenuRoot,
|
||||
NavigationMenuList,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuTrigger,
|
||||
NavigationMenuLink,
|
||||
};
|
||||
export function NavigationMenuViewport(props: Primitive.Positioner.Props) {
|
||||
return (
|
||||
<Primitive.Portal>
|
||||
<Primitive.Positioner
|
||||
collisionPadding={{ top: 5, bottom: 5, left: 20, right: 20 }}
|
||||
{...props}
|
||||
className={(s) =>
|
||||
cn(
|
||||
"box-border h-(--positioner-height) w-(--anchor-width) max-w-(--available-width) duration-(--duration) ease-(--easing) before:absolute before:content-[''] data-instant:transition-none data-[side=bottom]:before:top-[-10px] data-[side=bottom]:before:right-0 data-[side=bottom]:before:left-0 data-[side=bottom]:before:h-2.5 data-[side=left]:before:top-0 data-[side=left]:before:right-[-10px] data-[side=left]:before:bottom-0 data-[side=left]:before:w-2.5 data-[side=right]:before:top-0 data-[side=right]:before:bottom-0 data-[side=right]:before:left-[-10px] data-[side=right]:before:w-2.5 data-[side=top]:before:right-0 data-[side=top]:before:bottom-[-10px] data-[side=top]:before:left-0 data-[side=top]:before:h-2.5",
|
||||
typeof props.className === 'function' ? props.className(s) : props.className,
|
||||
)
|
||||
}
|
||||
style={{
|
||||
['--duration' as string]: '0.35s',
|
||||
['--easing' as string]: 'cubic-bezier(0.22, 1, 0.36, 1)',
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
<Primitive.Popup className="data-[ending-style]:easing-[ease] relative border h-(--popup-height) origin-(--transform-origin) rounded-xl bg-fd-popover/80 text-fd-popover-foreground backdrop-blur-md shadow-lg transition-[opacity,transform,width,height,scale,translate] duration-(--duration) ease-(--easing) data-ending-style:scale-90 data-ending-style:opacity-0 data-ending-style:duration-150 data-starting-style:scale-90 data-starting-style:opacity-0 w-(--popup-width)">
|
||||
<Primitive.Viewport className="relative size-full overflow-hidden" />
|
||||
</Primitive.Popup>
|
||||
</Primitive.Positioner>
|
||||
</Primitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,32 +3,32 @@ import { Popover as Primitive } from '@base-ui/react/popover';
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
const Popover = Primitive.Root;
|
||||
export const Popover = Primitive.Root;
|
||||
|
||||
const PopoverTrigger = Primitive.Trigger;
|
||||
export const PopoverTrigger = Primitive.Trigger;
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ComponentRef<typeof Primitive.Popup>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitive.Popup> &
|
||||
Pick<Primitive.Positioner.Props, 'sideOffset' | 'align'>
|
||||
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
|
||||
<Primitive.Portal>
|
||||
<Primitive.Positioner align={align} side="bottom" sideOffset={sideOffset} className="z-50">
|
||||
<Primitive.Popup
|
||||
ref={ref}
|
||||
className={(s) =>
|
||||
cn(
|
||||
'z-50 origin-(--transform-origin) overflow-y-auto max-h-(--available-height) min-w-[240px] max-w-[98vw] rounded-xl border bg-fd-popover/60 backdrop-blur-lg p-2 text-sm text-fd-popover-foreground shadow-lg focus-visible:outline-none data-[closed]:animate-fd-popover-out data-[open]:animate-fd-popover-in',
|
||||
typeof className === 'function' ? className(s) : className,
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
</Primitive.Positioner>
|
||||
</Primitive.Portal>
|
||||
));
|
||||
PopoverContent.displayName = Primitive.Popup.displayName;
|
||||
export function PopoverContent({
|
||||
className,
|
||||
align = 'center',
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentPropsWithRef<typeof Primitive.Popup> &
|
||||
Pick<Primitive.Positioner.Props, 'sideOffset' | 'align'>) {
|
||||
return (
|
||||
<Primitive.Portal>
|
||||
<Primitive.Positioner align={align} side="bottom" sideOffset={sideOffset} className="z-50">
|
||||
<Primitive.Popup
|
||||
className={(s) =>
|
||||
cn(
|
||||
'z-50 origin-(--transform-origin) overflow-y-auto max-h-(--available-height) min-w-[240px] max-w-[98vw] rounded-xl border bg-fd-popover/60 backdrop-blur-lg p-2 text-sm text-fd-popover-foreground shadow-lg focus-visible:outline-none data-[closed]:animate-fd-popover-out data-[open]:animate-fd-popover-in',
|
||||
typeof className === 'function' ? className(s) : className,
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
</Primitive.Positioner>
|
||||
</Primitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
const PopoverClose = Primitive.Close;
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverClose };
|
||||
export const PopoverClose = Primitive.Close;
|
||||
|
||||
@@ -2,64 +2,61 @@ import { ScrollArea as Primitive } from '@base-ui/react/scroll-area';
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ComponentRef<typeof Primitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<Primitive.Root
|
||||
ref={ref}
|
||||
className={(s) =>
|
||||
cn('overflow-hidden', typeof className === 'function' ? className(s) : className)
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<Primitive.Corner />
|
||||
<ScrollBar orientation="vertical" />
|
||||
</Primitive.Root>
|
||||
));
|
||||
export function ScrollArea({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentPropsWithRef<typeof Primitive.Root>) {
|
||||
return (
|
||||
<Primitive.Root {...props}>
|
||||
{children}
|
||||
<Primitive.Corner />
|
||||
<ScrollBar orientation="vertical" />
|
||||
</Primitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
ScrollArea.displayName = Primitive.Root.displayName;
|
||||
export function ScrollViewport({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentPropsWithRef<typeof Primitive.Viewport>) {
|
||||
return (
|
||||
<Primitive.Viewport
|
||||
className={(s) =>
|
||||
cn(
|
||||
'size-full rounded-[inherit]',
|
||||
typeof className === 'function' ? className(s) : className,
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Primitive.Viewport>
|
||||
);
|
||||
}
|
||||
|
||||
const ScrollViewport = React.forwardRef<
|
||||
React.ComponentRef<typeof Primitive.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitive.Viewport>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<Primitive.Viewport
|
||||
ref={ref}
|
||||
className={(s) =>
|
||||
cn('size-full rounded-[inherit]', typeof className === 'function' ? className(s) : className)
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Primitive.Viewport>
|
||||
));
|
||||
export function ScrollBar({
|
||||
className,
|
||||
orientation = 'vertical',
|
||||
...props
|
||||
}: React.ComponentPropsWithRef<typeof Primitive.Scrollbar>) {
|
||||
return (
|
||||
<Primitive.Scrollbar
|
||||
orientation={orientation}
|
||||
className={(s) =>
|
||||
cn(
|
||||
'flex select-none transition-opacity',
|
||||
!s.hovering && 'opacity-0',
|
||||
orientation === 'vertical' && 'h-full w-1.5',
|
||||
orientation === 'horizontal' && 'h-1.5 flex-col',
|
||||
typeof className === 'function' ? className(s) : className,
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<Primitive.Thumb className="relative flex-1 rounded-full bg-fd-border" />
|
||||
</Primitive.Scrollbar>
|
||||
);
|
||||
}
|
||||
|
||||
ScrollViewport.displayName = Primitive.Viewport.displayName;
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ComponentRef<typeof Primitive.Scrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof Primitive.Scrollbar>
|
||||
>(({ className, orientation = 'vertical', ...props }, ref) => (
|
||||
<Primitive.Scrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={(s) =>
|
||||
cn(
|
||||
'flex select-none transition-opacity',
|
||||
!s.hovering && 'opacity-0',
|
||||
orientation === 'vertical' && 'h-full w-1.5',
|
||||
orientation === 'horizontal' && 'h-1.5 flex-col',
|
||||
typeof className === 'function' ? className(s) : className,
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
<Primitive.Thumb className="relative flex-1 rounded-full bg-fd-border" />
|
||||
</Primitive.Scrollbar>
|
||||
));
|
||||
ScrollBar.displayName = Primitive.Scrollbar.displayName;
|
||||
|
||||
export { ScrollArea, ScrollBar, ScrollViewport };
|
||||
export type ScrollAreaProps = Primitive.Root.Props;
|
||||
|
||||
@@ -1,19 +1,74 @@
|
||||
'use client';
|
||||
|
||||
import { type ComponentProps, createContext, type ReactNode, use, useMemo } from 'react';
|
||||
import { type ComponentProps, createContext, type FC, use, useMemo } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
<<<<<<< HEAD
|
||||
import { useSidebar } from '@/components/sidebar/base';
|
||||
import { usePathname } from '@hanzo/docs-core/framework';
|
||||
import Link from '@hanzo/docs-core/link';
|
||||
import type { SidebarTab } from '@/components/sidebar/tabs';
|
||||
import { isTabActive } from '@/components/sidebar/tabs/dropdown';
|
||||
=======
|
||||
import { usePathname } from '@hanzo/docs-core/framework';
|
||||
import Link from '@hanzo/docs-core/link';
|
||||
>>>>>>> dev
|
||||
import { useIsScrollTop } from '@/utils/use-is-scroll-top';
|
||||
import { renderer, type Renderer } from '@/utils/renderer';
|
||||
import type { LinkItemType } from '@/layouts/shared';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
type SidebarProps,
|
||||
type SidebarProviderProps,
|
||||
} from './slots/sidebar';
|
||||
import type { DocsLayoutProps } from '.';
|
||||
import {
|
||||
baseSlots,
|
||||
isLayoutTabActive,
|
||||
useLinkItems,
|
||||
type LayoutTab,
|
||||
type BaseSlots,
|
||||
type BaseSlotsProps,
|
||||
} from '../shared';
|
||||
import { TreeContextProvider } from '@/contexts/tree';
|
||||
import { Header } from './slots/header';
|
||||
import { Container } from './slots/container';
|
||||
|
||||
export const LayoutContext = createContext<{
|
||||
export interface DocsSlots extends BaseSlots {
|
||||
container: FC<ComponentProps<'div'>>;
|
||||
header: FC<ComponentProps<'header'>>;
|
||||
sidebar: {
|
||||
provider: FC<SidebarProviderProps>;
|
||||
root: FC<SidebarProps>;
|
||||
trigger: FC<ComponentProps<'button'>>;
|
||||
useSidebar: () => { collapsed: boolean; open: boolean; setOpen: (v: boolean) => void };
|
||||
};
|
||||
}
|
||||
|
||||
const { useProvider } = baseSlots({
|
||||
useProps() {
|
||||
return useDocsLayout().props;
|
||||
},
|
||||
});
|
||||
|
||||
interface SlotsProps extends BaseSlotsProps<DocsLayoutProps> {
|
||||
tabs: LayoutTab[];
|
||||
tabMode: NonNullable<DocsLayoutProps['tabMode']>;
|
||||
}
|
||||
|
||||
const LayoutContext = createContext<{
|
||||
props: SlotsProps;
|
||||
isNavTransparent: boolean;
|
||||
navItems: LinkItemType[];
|
||||
menuItems: LinkItemType[];
|
||||
slots: DocsSlots;
|
||||
} | null>(null);
|
||||
|
||||
export function useIsDocsLayout() {
|
||||
return use(LayoutContext) !== null;
|
||||
}
|
||||
|
||||
export function useDocsLayout() {
|
||||
const context = use(LayoutContext);
|
||||
if (!context)
|
||||
@@ -23,85 +78,79 @@ export function useDocsLayout() {
|
||||
return context;
|
||||
}
|
||||
|
||||
export function LayoutContextProvider({
|
||||
navTransparentMode = 'none',
|
||||
children,
|
||||
}: {
|
||||
navTransparentMode?: 'always' | 'top' | 'none';
|
||||
children: ReactNode;
|
||||
}) {
|
||||
export function LayoutBody(
|
||||
props: Omit<DocsLayoutProps, 'tabs'> & {
|
||||
tabs: LayoutTab[];
|
||||
},
|
||||
) {
|
||||
const {
|
||||
nav: { enabled: navEnabled = true, transparentMode: navTransparentMode = 'none' } = {},
|
||||
sidebar: { enabled: sidebarEnabled = true, defaultOpenLevel, prefetch, ...sidebarProps } = {},
|
||||
slots: defaultSlots,
|
||||
tabs,
|
||||
tabMode = 'auto',
|
||||
tree,
|
||||
containerProps,
|
||||
children,
|
||||
} = props;
|
||||
const isTop = useIsScrollTop({ enabled: navTransparentMode === 'top' }) ?? true;
|
||||
const isNavTransparent = navTransparentMode === 'top' ? isTop : navTransparentMode === 'always';
|
||||
const { baseSlots, baseProps } = useProvider(props);
|
||||
const linkItems = useLinkItems(props);
|
||||
const slots: DocsSlots = {
|
||||
...baseSlots,
|
||||
header: defaultSlots?.header ?? Header,
|
||||
container: defaultSlots?.container ?? Container,
|
||||
sidebar: defaultSlots?.sidebar ?? {
|
||||
provider: SidebarProvider,
|
||||
root: Sidebar,
|
||||
trigger: SidebarTrigger,
|
||||
useSidebar: useSidebar,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<LayoutContext
|
||||
value={useMemo(
|
||||
() => ({
|
||||
<TreeContextProvider tree={tree}>
|
||||
<LayoutContext
|
||||
value={{
|
||||
props: {
|
||||
tabMode,
|
||||
tabs,
|
||||
...baseProps,
|
||||
},
|
||||
isNavTransparent,
|
||||
}),
|
||||
[isNavTransparent],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</LayoutContext>
|
||||
slots,
|
||||
...linkItems,
|
||||
}}
|
||||
>
|
||||
<slots.sidebar.provider defaultOpenLevel={defaultOpenLevel} prefetch={prefetch}>
|
||||
<slots.container {...containerProps}>
|
||||
{navEnabled && <slots.header />}
|
||||
{sidebarEnabled && <slots.sidebar.root {...sidebarProps} />}
|
||||
{tabMode === 'top' && tabs.length > 0 && (
|
||||
<LayoutTabs
|
||||
tabs={tabs}
|
||||
className="z-10 bg-fd-background border-b px-6 pt-3 xl:px-8 max-md:hidden"
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</slots.container>
|
||||
</slots.sidebar.provider>
|
||||
</LayoutContext>
|
||||
</TreeContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function LayoutHeader(props: ComponentProps<'header'>) {
|
||||
const { isNavTransparent } = useDocsLayout();
|
||||
|
||||
return (
|
||||
<header data-transparent={isNavTransparent} {...props}>
|
||||
{props.children}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export function LayoutBody({
|
||||
_,
|
||||
children,
|
||||
}: {
|
||||
_: Renderer<ComponentProps<'div'>>;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const render = renderer(_, 'div');
|
||||
const { collapsed } = useSidebar();
|
||||
|
||||
return render?.(
|
||||
(t) =>
|
||||
({
|
||||
id: 'nd-docs-layout',
|
||||
'data-sidebar-collapsed': collapsed,
|
||||
children,
|
||||
...t,
|
||||
className: cn(
|
||||
'grid transition-[grid-template-columns] overflow-x-clip min-h-(--fd-docs-height) [--fd-docs-height:100dvh] [--fd-header-height:0px] [--fd-toc-popover-height:0px] [--fd-sidebar-width:0px] [--fd-toc-width:0px]',
|
||||
t?.className,
|
||||
),
|
||||
style: {
|
||||
gridTemplate: `"sidebar sidebar header toc toc"
|
||||
"sidebar sidebar toc-popover toc toc"
|
||||
"sidebar sidebar main toc toc" 1fr / minmax(min-content, 1fr) var(--fd-sidebar-col) minmax(0, calc(var(--fd-layout-width,97rem) - var(--fd-sidebar-width) - var(--fd-toc-width))) var(--fd-toc-width) minmax(min-content, 1fr)`,
|
||||
'--fd-docs-row-1': 'var(--fd-banner-height, 0px)',
|
||||
'--fd-docs-row-2': 'calc(var(--fd-docs-row-1) + var(--fd-header-height))',
|
||||
'--fd-docs-row-3': 'calc(var(--fd-docs-row-2) + var(--fd-toc-popover-height))',
|
||||
'--fd-sidebar-col': collapsed ? '0px' : 'var(--fd-sidebar-width)',
|
||||
...t?.style,
|
||||
},
|
||||
}) as ComponentProps<'div'>,
|
||||
);
|
||||
}
|
||||
|
||||
export function LayoutTabs({
|
||||
options,
|
||||
function LayoutTabs({
|
||||
tabs,
|
||||
...props
|
||||
}: ComponentProps<'div'> & {
|
||||
options: SidebarTab[];
|
||||
tabs: LayoutTab[];
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const selected = useMemo(() => {
|
||||
return options.findLast((option) => isTabActive(option, pathname));
|
||||
}, [options, pathname]);
|
||||
return tabs.findLast((option) => isLayoutTabActive(option, pathname));
|
||||
}, [tabs, pathname]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -111,17 +160,17 @@ export function LayoutTabs({
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{options.map((option, i) => (
|
||||
{tabs.map((tab, i) => (
|
||||
<Link
|
||||
key={i}
|
||||
href={option.url}
|
||||
href={tab.url}
|
||||
className={cn(
|
||||
'inline-flex border-b-2 border-transparent transition-colors items-center pb-1.5 font-medium gap-2 text-fd-muted-foreground text-sm text-nowrap hover:text-fd-accent-foreground',
|
||||
option.unlisted && selected !== option && 'hidden',
|
||||
selected === option && 'border-fd-primary text-fd-primary',
|
||||
tab.unlisted && selected !== tab && 'hidden',
|
||||
selected === tab && 'border-fd-primary text-fd-primary',
|
||||
)}
|
||||
>
|
||||
{option.title}
|
||||
{tab.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<<<<<<< HEAD
|
||||
import type * as PageTree from '@hanzo/docs-core/page-tree';
|
||||
import { type ComponentProps, type HTMLAttributes, type ReactNode, useMemo } from 'react';
|
||||
import { Languages, Sidebar as SidebarIcon } from 'lucide-react';
|
||||
@@ -13,273 +14,70 @@ import {
|
||||
SidebarTrigger,
|
||||
SidebarViewport,
|
||||
} from './sidebar';
|
||||
=======
|
||||
import type * as PageTree from '@hanzo/docs-core/page-tree';
|
||||
import { type HTMLAttributes, type ReactNode, useMemo } from 'react';
|
||||
import type { SidebarProps, SidebarProviderProps } from './slots/sidebar';
|
||||
>>>>>>> dev
|
||||
import {
|
||||
getLayoutTabs,
|
||||
type BaseLayoutProps,
|
||||
parseLayoutProps,
|
||||
renderTitleNav,
|
||||
useLinkItems,
|
||||
type GetLayoutTabsOptions,
|
||||
type LayoutTab,
|
||||
} from '@/layouts/shared';
|
||||
import { LinkItem } from '@/utils/link-item';
|
||||
import { LanguageToggle, LanguageToggleText } from '@/layouts/shared/language-toggle';
|
||||
import { LayoutBody, LayoutContextProvider, LayoutHeader, LayoutTabs } from './client';
|
||||
import { TreeContextProvider } from '@/contexts/tree';
|
||||
import { ThemeToggle } from '../shared/theme-toggle';
|
||||
import { LargeSearchToggle, SearchToggle } from '@/layouts/shared/search-toggle';
|
||||
import { getSidebarTabs, type GetSidebarTabsOptions } from '@/components/sidebar/tabs';
|
||||
import type { SidebarPageTreeComponents } from '@/components/sidebar/page-tree';
|
||||
import { SidebarTabsDropdown, type SidebarTabWithProps } from '@/components/sidebar/tabs/dropdown';
|
||||
import { type Renderer, renderer } from '@/utils/renderer';
|
||||
import { type DocsSlots, LayoutBody } from './client';
|
||||
|
||||
export interface DocsLayoutProps extends BaseLayoutProps {
|
||||
tree: PageTree.Root;
|
||||
sidebar?: SidebarOptions;
|
||||
|
||||
tabMode?: 'top' | 'auto';
|
||||
SidebarTrigger?: Renderer<ComponentProps<'button'>>;
|
||||
Container?: Renderer<ComponentProps<'div'>>;
|
||||
|
||||
/**
|
||||
* @deprecated use `Container` instead.
|
||||
*/
|
||||
tabs?: LayoutTab[] | GetLayoutTabsOptions | false;
|
||||
containerProps?: HTMLAttributes<HTMLDivElement>;
|
||||
slots?: Partial<DocsSlots>;
|
||||
}
|
||||
|
||||
interface SidebarOptions
|
||||
extends
|
||||
ComponentProps<'aside'>,
|
||||
Pick<ComponentProps<typeof Sidebar>, 'defaultOpenLevel' | 'prefetch'> {
|
||||
interface SidebarOptions extends SidebarProps, SidebarProviderProps {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @deprecated use `slots.sidebar` instead.
|
||||
*/
|
||||
component?: ReactNode;
|
||||
components?: Partial<SidebarPageTreeComponents>;
|
||||
|
||||
/**
|
||||
* Root Toggle options
|
||||
* @deprecated use layout-level `tabMode` option instead.
|
||||
*/
|
||||
tabs?: SidebarTabWithProps[] | GetSidebarTabsOptions | false;
|
||||
|
||||
banner?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
|
||||
tabMode?: 'auto' | 'top';
|
||||
/**
|
||||
* Support collapsing the sidebar on desktop mode
|
||||
*
|
||||
* @defaultValue true
|
||||
* @deprecated use layout-level `tabs` option instead.
|
||||
*/
|
||||
collapsible?: boolean;
|
||||
tabs?: LayoutTab[] | GetLayoutTabsOptions | false;
|
||||
}
|
||||
|
||||
export function DocsLayout(_: DocsLayoutProps) {
|
||||
const {
|
||||
nav: { transparentMode, ...nav } = {},
|
||||
sidebar: {
|
||||
tabs: sidebarTabs,
|
||||
enabled: sidebarEnabled = true,
|
||||
defaultOpenLevel,
|
||||
prefetch,
|
||||
...sidebarProps
|
||||
} = {},
|
||||
SearchToggle: SearchToggleRenderer,
|
||||
LargeSearchToggle: LargeSearchToggleRenderer,
|
||||
ThemeSwitch,
|
||||
containerProps,
|
||||
Container = containerProps ?? true,
|
||||
tabMode = 'auto',
|
||||
LanguageSwitch,
|
||||
SidebarTrigger: SidebarTriggerRenderer = sidebarEnabled,
|
||||
children,
|
||||
tree,
|
||||
...props
|
||||
} = parseLayoutProps<DocsLayoutProps>(_);
|
||||
|
||||
const renderSearchToggle = renderer(SearchToggleRenderer, SearchToggle);
|
||||
const renderLargeSearchToggle = renderer(LargeSearchToggleRenderer, LargeSearchToggle);
|
||||
const renderThemeSwitch = renderer(ThemeSwitch, ThemeToggle);
|
||||
const renderLanguageSwitch = renderer(LanguageSwitch, LanguageToggle);
|
||||
const renderSidebarTrigger = renderer(SidebarTriggerRenderer, SidebarTrigger);
|
||||
|
||||
export function DocsLayout({
|
||||
tree,
|
||||
sidebar: { tabs: _tabs, tabMode: _tabMode, ...sidebarProps } = {},
|
||||
tabs: layoutTabs = _tabs,
|
||||
tabMode = _tabMode,
|
||||
children,
|
||||
...props
|
||||
}: DocsLayoutProps) {
|
||||
const tabs = useMemo(() => {
|
||||
if (Array.isArray(sidebarTabs)) {
|
||||
return sidebarTabs;
|
||||
if (Array.isArray(layoutTabs)) {
|
||||
return layoutTabs;
|
||||
}
|
||||
if (typeof sidebarTabs === 'object') {
|
||||
return getSidebarTabs(tree, sidebarTabs);
|
||||
if (typeof layoutTabs === 'object') {
|
||||
return getLayoutTabs(tree, layoutTabs);
|
||||
}
|
||||
if (sidebarTabs !== false) {
|
||||
return getSidebarTabs(tree);
|
||||
if (layoutTabs !== false) {
|
||||
return getLayoutTabs(tree);
|
||||
}
|
||||
return [];
|
||||
}, [tree, sidebarTabs]);
|
||||
const { menuItems } = useLinkItems(props);
|
||||
|
||||
function sidebar() {
|
||||
const { footer, banner, collapsible = true, component, components, ...rest } = sidebarProps;
|
||||
if (component) return component;
|
||||
|
||||
const iconLinks = menuItems.filter((item) => item.type === 'icon');
|
||||
const viewport = (
|
||||
<SidebarViewport>
|
||||
{menuItems
|
||||
.filter((v) => v.type !== 'icon')
|
||||
.map((item, i, list) => (
|
||||
<SidebarLinkItem key={i} item={item} className={cn(i === list.length - 1 && 'mb-4')} />
|
||||
))}
|
||||
<SidebarPageTree {...components} />
|
||||
</SidebarViewport>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidebarContent {...rest}>
|
||||
<div className="flex flex-col gap-3 p-4 pb-2">
|
||||
<div className="flex">
|
||||
{renderTitleNav(nav, {
|
||||
className: 'inline-flex text-[0.9375rem] items-center gap-2.5 font-medium me-auto',
|
||||
})}
|
||||
{nav.children}
|
||||
{collapsible && (
|
||||
<SidebarCollapseTrigger
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'ghost',
|
||||
size: 'icon-sm',
|
||||
className: 'mb-auto text-fd-muted-foreground',
|
||||
}),
|
||||
)}
|
||||
>
|
||||
<SidebarIcon />
|
||||
</SidebarCollapseTrigger>
|
||||
)}
|
||||
</div>
|
||||
{renderLargeSearchToggle?.((o) => ({
|
||||
hideIfDisabled: true,
|
||||
...o,
|
||||
}))}
|
||||
{tabs.length > 0 && tabMode === 'auto' && <SidebarTabsDropdown options={tabs} />}
|
||||
{banner}
|
||||
</div>
|
||||
{viewport}
|
||||
{(renderLanguageSwitch || iconLinks.length > 0 || renderThemeSwitch || footer) && (
|
||||
<div className="flex flex-col border-t p-4 pt-2 empty:hidden">
|
||||
<div className="flex text-fd-muted-foreground items-center empty:hidden">
|
||||
{renderLanguageSwitch?.((o) => ({
|
||||
children: <Languages className="size-4.5" />,
|
||||
...o,
|
||||
}))}
|
||||
{iconLinks.map((item, i) => (
|
||||
<LinkItem
|
||||
key={i}
|
||||
item={item}
|
||||
className={cn(buttonVariants({ size: 'icon-sm', color: 'ghost' }))}
|
||||
aria-label={item.label}
|
||||
>
|
||||
{item.icon}
|
||||
</LinkItem>
|
||||
))}
|
||||
{renderThemeSwitch?.((t) => ({ ...t, className: cn('ms-auto p-0', t?.className) }))}
|
||||
</div>
|
||||
{footer}
|
||||
</div>
|
||||
)}
|
||||
</SidebarContent>
|
||||
<SidebarDrawer>
|
||||
<div className="flex flex-col gap-3 p-4 pb-2">
|
||||
<div className="flex text-fd-muted-foreground items-center gap-1.5">
|
||||
<div className="flex flex-1">
|
||||
{iconLinks.map((item, i) => (
|
||||
<LinkItem
|
||||
key={i}
|
||||
item={item}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
size: 'icon-sm',
|
||||
color: 'ghost',
|
||||
className: 'p-2',
|
||||
}),
|
||||
)}
|
||||
aria-label={item.label}
|
||||
>
|
||||
{item.icon}
|
||||
</LinkItem>
|
||||
))}
|
||||
</div>
|
||||
{renderLanguageSwitch?.((o) => ({
|
||||
children: (
|
||||
<>
|
||||
<Languages className="size-4.5" />
|
||||
<LanguageToggleText />
|
||||
</>
|
||||
),
|
||||
...o,
|
||||
}))}
|
||||
{renderThemeSwitch?.((t) => ({ ...t, className: cn('p-0', t?.className) }))}
|
||||
{renderSidebarTrigger?.((t) => ({
|
||||
children: <SidebarIcon />,
|
||||
...t,
|
||||
className: cn(
|
||||
buttonVariants({
|
||||
color: 'ghost',
|
||||
size: 'icon-sm',
|
||||
}),
|
||||
'p-2',
|
||||
t?.className,
|
||||
),
|
||||
}))}
|
||||
</div>
|
||||
{tabs.length > 0 && <SidebarTabsDropdown options={tabs} />}
|
||||
{banner}
|
||||
</div>
|
||||
{viewport}
|
||||
<div className="flex flex-col border-t p-4 pt-2 empty:hidden">{footer}</div>
|
||||
</SidebarDrawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}, [tree, layoutTabs]);
|
||||
|
||||
return (
|
||||
<TreeContextProvider tree={tree}>
|
||||
<LayoutContextProvider navTransparentMode={transparentMode}>
|
||||
<Sidebar defaultOpenLevel={defaultOpenLevel} prefetch={prefetch}>
|
||||
<LayoutBody _={Container}>
|
||||
{nav.enabled !== false &&
|
||||
(nav.component ?? (
|
||||
<LayoutHeader
|
||||
id="nd-subnav"
|
||||
className="[grid-area:header] sticky top-(--fd-docs-row-1) z-30 flex items-center ps-4 pe-2.5 border-b transition-colors backdrop-blur-sm h-(--fd-header-height) md:hidden max-md:layout:[--fd-header-height:--spacing(14)] data-[transparent=false]:bg-fd-background/80"
|
||||
>
|
||||
{renderTitleNav(nav, {
|
||||
className: 'inline-flex items-center gap-2.5 font-semibold',
|
||||
})}
|
||||
<div className="flex-1">{nav.children}</div>
|
||||
{renderSearchToggle?.((o) => ({
|
||||
hideIfDisabled: true,
|
||||
...o,
|
||||
className: cn('p-2', o?.className),
|
||||
}))}
|
||||
{renderSidebarTrigger?.((t) => ({
|
||||
children: <SidebarIcon />,
|
||||
...t,
|
||||
className: cn(
|
||||
buttonVariants({
|
||||
color: 'ghost',
|
||||
size: 'icon-sm',
|
||||
}),
|
||||
'p-2',
|
||||
t?.className,
|
||||
),
|
||||
}))}
|
||||
</LayoutHeader>
|
||||
))}
|
||||
{sidebarEnabled && sidebar()}
|
||||
{tabMode === 'top' && tabs.length > 0 && (
|
||||
<LayoutTabs
|
||||
options={tabs}
|
||||
className="z-10 bg-fd-background border-b px-6 pt-3 xl:px-8 max-md:hidden"
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</LayoutBody>
|
||||
</Sidebar>
|
||||
</LayoutContextProvider>
|
||||
</TreeContextProvider>
|
||||
<LayoutBody tree={tree} tabs={tabs} tabMode={tabMode} sidebar={sidebarProps} {...props}>
|
||||
{children}
|
||||
</LayoutBody>
|
||||
);
|
||||
}
|
||||
|
||||
export { type DocsSlots, useDocsLayout } from './client';
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import type { ComponentProps, ReactNode } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { Edit, Text } from 'lucide-react';
|
||||
import { I18nLabel } from '@/contexts/i18n';
|
||||
'use client';
|
||||
import {
|
||||
<<<<<<< HEAD
|
||||
type BreadcrumbProps,
|
||||
type FooterProps,
|
||||
PageBreadcrumb,
|
||||
@@ -17,131 +14,152 @@ import * as TocDefault from '@/components/toc/default';
|
||||
import * as TocClerk from '@/components/toc/clerk';
|
||||
import { TOCProvider, TOCScrollArea } from '@/components/toc';
|
||||
import { ChildrenRenderer, renderer, type Renderer } from '@/utils/renderer';
|
||||
=======
|
||||
type ComponentProps,
|
||||
createContext,
|
||||
type FC,
|
||||
type ReactNode,
|
||||
use,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { I18nLabel, useI18n } from '@/contexts/i18n';
|
||||
import {
|
||||
TOC,
|
||||
TOCPopover,
|
||||
TOCProvider,
|
||||
type TOCProviderProps,
|
||||
type TOCPopoverProps,
|
||||
type TOCProps,
|
||||
} from './slots/toc';
|
||||
import { Footer, type FooterProps } from './slots/footer';
|
||||
import { Breadcrumb, type BreadcrumbProps } from './slots/breadcrumb';
|
||||
import { Container } from './slots/container';
|
||||
import type { TOCItemType } from '@hanzo/docs-core/toc';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { Edit } from 'lucide-react';
|
||||
>>>>>>> dev
|
||||
|
||||
export interface DocsPageProps {
|
||||
export interface DocsPageProps extends ComponentProps<'article'> {
|
||||
toc?: TOCItemType[];
|
||||
/**
|
||||
* - `multiple` (default): Accept multiple active items
|
||||
* - `single`: Only accept one active item at most
|
||||
* */
|
||||
tocMode?: 'single' | 'multiple';
|
||||
|
||||
/**
|
||||
* Extend the page to fill all available space
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
full?: boolean;
|
||||
children?: ReactNode;
|
||||
slots?: Partial<DocsPageSlots>;
|
||||
|
||||
TOC?: Renderer<TOCMainProps>;
|
||||
TOCPopover?: Renderer<TOCPopoverProps>;
|
||||
/** Footer navigation, located under the page body. */
|
||||
Footer?: Renderer<FooterProps>;
|
||||
Container?: Renderer<ComponentProps<'div'>>;
|
||||
Breadcrumb?: Renderer<BreadcrumbProps>;
|
||||
|
||||
/** @deprecated use `Footer` instead. */
|
||||
footer?: FooterOptions;
|
||||
/** @deprecated use `Container` instead. */
|
||||
className?: string;
|
||||
/** @deprecated use `Breadcrumb` instead. */
|
||||
breadcrumb?: BreadcrumbOptions;
|
||||
/** @deprecated use `TOC` instead, or `tocMode` for enabling `single`. */
|
||||
tableOfContent?: TableOfContentOptions;
|
||||
/** @deprecated use `TOCPopover` instead. */
|
||||
tableOfContentPopover?: TableOfContentPopoverOptions;
|
||||
}
|
||||
|
||||
interface BreadcrumbOptions extends BreadcrumbProps {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @deprecated use `slots.breadcrumb` instead.
|
||||
*/
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
interface FooterOptions extends FooterProps {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @deprecated use `slots.footer` instead.
|
||||
*/
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
interface TableOfContentOptions extends Pick<AnchorProviderProps, 'single'>, TOCProps {
|
||||
interface TableOfContentOptions extends Pick<TOCProviderProps, 'single'>, TOCProps {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @deprecated use `slots.toc` instead.
|
||||
*/
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
interface TableOfContentPopoverOptions extends TOCProps {
|
||||
interface TableOfContentPopoverOptions extends TOCPopoverProps {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @deprecated use `slots.tocPopover` instead.
|
||||
*/
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
interface DocsPageSlots {
|
||||
toc: {
|
||||
provider: FC<TOCProviderProps>;
|
||||
main: FC<TOCProps>;
|
||||
popover: FC<TOCPopoverProps>;
|
||||
};
|
||||
container: FC<ComponentProps<'article'>>;
|
||||
footer: FC<FooterProps>;
|
||||
breadcrumb: FC<BreadcrumbProps>;
|
||||
}
|
||||
|
||||
type PageSlotsProps = Pick<DocsPageProps, 'full'>;
|
||||
|
||||
const PageContext = createContext<{
|
||||
props: PageSlotsProps;
|
||||
slots: DocsPageSlots;
|
||||
} | null>(null);
|
||||
|
||||
export function useDocsPage() {
|
||||
const context = use(PageContext);
|
||||
if (!context)
|
||||
throw new Error(
|
||||
'Please use page components under <DocsPage /> (`@hanzo/docs-ui/layouts/docs/page`).',
|
||||
);
|
||||
return context;
|
||||
}
|
||||
|
||||
export function DocsPage({
|
||||
tableOfContent: tocProps = {},
|
||||
TOC: TOCRenderer,
|
||||
tableOfContentPopover: tocPopoverProps = {},
|
||||
TOCPopover: TOCPopoverRenderer,
|
||||
footer = {},
|
||||
Footer = footer.enabled === false
|
||||
? false
|
||||
: footer.component
|
||||
? new ChildrenRenderer(footer.component)
|
||||
: footer,
|
||||
className,
|
||||
Container = { className },
|
||||
breadcrumb = {},
|
||||
Breadcrumb = breadcrumb.enabled === false
|
||||
? false
|
||||
: breadcrumb.component
|
||||
? new ChildrenRenderer(breadcrumb.component)
|
||||
: breadcrumb,
|
||||
tableOfContent: { enabled: tocEnabled, single, ...tocProps } = {},
|
||||
tableOfContentPopover: { enabled: tocPopoverEnabled, ...tocPopoverProps } = {},
|
||||
breadcrumb: { enabled: breadcrumbEnabled = true, ...breadcrumb } = {},
|
||||
footer: { enabled: footerEnabled = true, ...footer } = {},
|
||||
full = false,
|
||||
toc = [],
|
||||
tocMode = tocProps.single ? 'single' : 'multiple',
|
||||
slots: defaultSlots = {},
|
||||
children,
|
||||
...containerProps
|
||||
}: DocsPageProps) {
|
||||
if (!full && (tocProps.enabled ?? (toc.length > 0 || tocProps.footer || tocProps.header))) {
|
||||
TOCRenderer ??= tocProps.component ? new ChildrenRenderer(tocProps.component) : tocProps;
|
||||
} else {
|
||||
// force disable toc in full mode or when no content
|
||||
TOCRenderer = false;
|
||||
}
|
||||
tocEnabled ??= Boolean(!full && (toc.length > 0 || tocProps.footer || tocProps.header));
|
||||
tocPopoverEnabled ??= Boolean(toc.length > 0 || tocPopoverProps.header || tocPopoverProps.footer);
|
||||
|
||||
if (
|
||||
tocPopoverProps.enabled ??
|
||||
(toc.length > 0 || tocPopoverProps.header || tocPopoverProps.footer)
|
||||
) {
|
||||
TOCPopoverRenderer ??= tocPopoverProps.component
|
||||
? new ChildrenRenderer(tocPopoverProps.component)
|
||||
: tocPopoverProps;
|
||||
} else {
|
||||
TOCPopoverRenderer = false;
|
||||
}
|
||||
|
||||
const renderBreadcrumb = renderer(Breadcrumb, PageBreadcrumb);
|
||||
const renderFooter = renderer(Footer, PageFooter);
|
||||
const renderContainer = renderer(Container, 'article');
|
||||
const renderToc = renderer(TOCRenderer, TOC);
|
||||
const renderTocPopover = renderer(TOCPopoverRenderer, TOCPopover);
|
||||
const slots: DocsPageSlots = {
|
||||
breadcrumb: defaultSlots.breadcrumb ?? Breadcrumb,
|
||||
footer: defaultSlots.footer ?? Footer,
|
||||
toc: defaultSlots.toc ?? {
|
||||
provider: TOCProvider,
|
||||
main: TOC,
|
||||
popover: TOCPopover,
|
||||
},
|
||||
container: defaultSlots.container ?? Container,
|
||||
};
|
||||
|
||||
return (
|
||||
<TOCProvider single={tocMode === 'single'} toc={renderToc || renderTocPopover ? toc : []}>
|
||||
{renderTocPopover?.((t) => t ?? {})}
|
||||
{renderContainer?.((t) => ({
|
||||
id: 'nd-page',
|
||||
'data-full': full,
|
||||
children: (
|
||||
<>
|
||||
{renderBreadcrumb?.((t) => t ?? {})}
|
||||
{children}
|
||||
{renderFooter?.((t) => t ?? {})}
|
||||
</>
|
||||
),
|
||||
...t,
|
||||
className: cn(
|
||||
'flex flex-col w-full max-w-[900px] mx-auto [grid-area:main] px-4 py-6 gap-4 md:px-6 md:pt-8 xl:px-8 xl:pt-14',
|
||||
full ? 'max-w-[1168px]' : 'xl:layout:[--fd-toc-width:268px]',
|
||||
t?.className,
|
||||
),
|
||||
}))}
|
||||
{renderToc?.((t) => t ?? {})}
|
||||
</TOCProvider>
|
||||
<PageContext
|
||||
value={{
|
||||
props: { full },
|
||||
slots,
|
||||
}}
|
||||
>
|
||||
<slots.toc.provider single={single} toc={tocEnabled || tocPopoverEnabled ? toc : []}>
|
||||
{tocPopoverEnabled &&
|
||||
(tocPopoverProps.component ?? <slots.toc.popover {...tocPopoverProps} />)}
|
||||
<slots.container {...containerProps}>
|
||||
{breadcrumbEnabled && (breadcrumb.component ?? <slots.breadcrumb {...breadcrumb} />)}
|
||||
{children}
|
||||
{footerEnabled && (footer.component ?? <slots.footer {...footer} />)}
|
||||
</slots.container>
|
||||
{tocEnabled && (tocProps.component ?? <slots.toc.main {...tocProps} />)}
|
||||
</slots.toc.provider>
|
||||
</PageContext>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -155,8 +173,8 @@ export function EditOnGitHub(props: ComponentProps<'a'>) {
|
||||
buttonVariants({
|
||||
color: 'secondary',
|
||||
size: 'sm',
|
||||
className: 'gap-1.5 not-prose',
|
||||
}),
|
||||
'gap-1.5 not-prose',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
@@ -200,86 +218,25 @@ export function DocsTitle({ children, className, ...props }: ComponentProps<'h1'
|
||||
);
|
||||
}
|
||||
|
||||
export interface TOCPopoverProps extends TOCProps {
|
||||
container?: ComponentProps<typeof PageTOCPopover>;
|
||||
trigger?: ComponentProps<typeof PageTOCPopoverTrigger>;
|
||||
content?: ComponentProps<typeof PageTOCPopoverContent>;
|
||||
}
|
||||
export function PageLastUpdate({
|
||||
date: value,
|
||||
...props
|
||||
}: Omit<ComponentProps<'p'>, 'children'> & { date: Date }) {
|
||||
const { text } = useI18n();
|
||||
const [date, setDate] = useState('');
|
||||
|
||||
export interface TOCMainProps extends TOCProps {
|
||||
container?: ComponentProps<typeof PageTOCPopover>;
|
||||
}
|
||||
useEffect(() => {
|
||||
// to the timezone of client
|
||||
setDate(value.toLocaleDateString());
|
||||
}, [value]);
|
||||
|
||||
interface TOCProps {
|
||||
/**
|
||||
* Custom content in TOC container, before the main TOC
|
||||
*/
|
||||
header?: ReactNode;
|
||||
|
||||
/**
|
||||
* Custom content in TOC container, after the main TOC
|
||||
*/
|
||||
footer?: ReactNode;
|
||||
|
||||
/**
|
||||
* @defaultValue 'normal'
|
||||
*/
|
||||
style?: 'normal' | 'clerk';
|
||||
}
|
||||
|
||||
export function TOC({ container, header, footer, style }: TOCMainProps) {
|
||||
return (
|
||||
<div
|
||||
id="nd-toc"
|
||||
{...container}
|
||||
className={cn(
|
||||
'sticky top-(--fd-docs-row-1) h-[calc(var(--fd-docs-height)-var(--fd-docs-row-1))] flex flex-col [grid-area:toc] w-(--fd-toc-width) pt-12 pe-4 pb-2 max-xl:hidden',
|
||||
container?.className,
|
||||
)}
|
||||
>
|
||||
{header}
|
||||
<h3
|
||||
id="toc-title"
|
||||
className="inline-flex items-center gap-1.5 text-sm text-fd-muted-foreground"
|
||||
>
|
||||
<Text className="size-4" />
|
||||
<I18nLabel label="toc" />
|
||||
</h3>
|
||||
<TOCScrollArea>
|
||||
{style === 'clerk' ? <TocClerk.TOCItems /> : <TocDefault.TOCItems />}
|
||||
</TOCScrollArea>
|
||||
{footer}
|
||||
</div>
|
||||
<p {...props} className={cn('text-sm text-fd-muted-foreground', props.className)}>
|
||||
{text.lastUpdate} {date}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export function TOCPopover({
|
||||
container,
|
||||
trigger,
|
||||
content,
|
||||
header,
|
||||
footer,
|
||||
style,
|
||||
}: TOCPopoverProps) {
|
||||
return (
|
||||
<PageTOCPopover {...container}>
|
||||
<PageTOCPopoverTrigger {...trigger} />
|
||||
<PageTOCPopoverContent {...content}>
|
||||
{header}
|
||||
<TOCScrollArea>
|
||||
{style === 'clerk' ? <TocClerk.TOCItems /> : <TocDefault.TOCItems />}
|
||||
</TOCScrollArea>
|
||||
{footer}
|
||||
</PageTOCPopoverContent>
|
||||
</PageTOCPopover>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
PageLastUpdate,
|
||||
PageBreadcrumb,
|
||||
PageFooter,
|
||||
type BreadcrumbProps,
|
||||
type FooterProps,
|
||||
} from './client';
|
||||
export { type BreadcrumbProps, Breadcrumb as PageBreadcrumb } from './slots/breadcrumb';
|
||||
export { type FooterProps, Footer as PageFooter } from './slots/footer';
|
||||
export { MarkdownCopyButton, ViewOptionsPopover } from '@/layouts/shared/page-actions';
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
'use client';
|
||||
|
||||
import { useTreePath, useTreeContext } from '@/contexts/tree';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { type BreadcrumbOptions, getBreadcrumbItemsFromPath } from '@hanzo/docs-core/breadcrumb';
|
||||
import Link from '@hanzo/docs-core/link';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { type ComponentProps, useMemo, Fragment } from 'react';
|
||||
|
||||
export type BreadcrumbProps = BreadcrumbOptions & ComponentProps<'div'>;
|
||||
|
||||
export function Breadcrumb({
|
||||
includeRoot,
|
||||
includeSeparator,
|
||||
includePage,
|
||||
...props
|
||||
}: BreadcrumbProps) {
|
||||
const path = useTreePath();
|
||||
const { root } = useTreeContext();
|
||||
const items = useMemo(() => {
|
||||
return getBreadcrumbItemsFromPath(root, path, {
|
||||
includePage,
|
||||
includeSeparator,
|
||||
includeRoot,
|
||||
});
|
||||
}, [includePage, includeRoot, includeSeparator, path, root]);
|
||||
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={cn('flex items-center gap-1.5 text-sm text-fd-muted-foreground', props.className)}
|
||||
>
|
||||
{items.map((item, i) => {
|
||||
const className = cn('truncate', i === items.length - 1 && 'text-fd-primary font-medium');
|
||||
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
{i !== 0 && <ChevronRight className="size-3.5 shrink-0" />}
|
||||
{item.url ? (
|
||||
<Link
|
||||
href={item.url}
|
||||
className={cn(className, 'transition-opacity hover:opacity-80')}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
) : (
|
||||
<span className={className}>{item.name}</span>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import { useDocsPage } from '..';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
export function Container(props: ComponentProps<'article'>) {
|
||||
const {
|
||||
props: { full },
|
||||
} = useDocsPage();
|
||||
|
||||
return (
|
||||
<article
|
||||
id="nd-page"
|
||||
data-full={full}
|
||||
{...props}
|
||||
className={cn(
|
||||
'flex flex-col w-full max-w-[900px] mx-auto [grid-area:main] px-4 py-6 gap-4 md:px-6 md:pt-8 xl:px-8 xl:pt-14',
|
||||
full ? 'max-w-[1168px]' : 'xl:layout:[--fd-toc-width:268px]',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
'use client';
|
||||
|
||||
import { useI18n } from '@/contexts/i18n';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { isActive } from '@/utils/urls';
|
||||
import { useFooterItems } from '@/utils/use-footer-items';
|
||||
import { usePathname } from '@hanzo/docs-core/framework';
|
||||
import Link from '@hanzo/docs-core/link';
|
||||
import type * as PageTree from '@hanzo/docs-core/page-tree';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { type ComponentProps, useMemo } from 'react';
|
||||
|
||||
type Item = Pick<PageTree.Item, 'name' | 'description' | 'url'>;
|
||||
|
||||
export interface FooterProps extends ComponentProps<'div'> {
|
||||
/**
|
||||
* Items including information for the next and previous page
|
||||
*/
|
||||
items?: {
|
||||
previous?: Item;
|
||||
next?: Item;
|
||||
};
|
||||
}
|
||||
|
||||
export function Footer({ items, children, className, ...props }: FooterProps) {
|
||||
const footerList = useFooterItems();
|
||||
const pathname = usePathname();
|
||||
const { previous, next } = useMemo(() => {
|
||||
if (items) return items;
|
||||
|
||||
const idx = footerList.findIndex((item) => isActive(item.url, pathname));
|
||||
|
||||
if (idx === -1) return {};
|
||||
return {
|
||||
previous: footerList[idx - 1],
|
||||
next: footerList[idx + 1],
|
||||
};
|
||||
}, [footerList, items, pathname]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
'@container grid gap-4',
|
||||
previous && next ? 'grid-cols-2' : 'grid-cols-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{previous && <FooterItem item={previous} index={0} />}
|
||||
{next && <FooterItem item={next} index={1} />}
|
||||
</div>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function FooterItem({ item, index }: { item: Item; index: 0 | 1 }) {
|
||||
const { text } = useI18n();
|
||||
const Icon = index === 0 ? ChevronLeft : ChevronRight;
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={item.url}
|
||||
className={cn(
|
||||
'flex flex-col gap-2 rounded-lg border p-4 text-sm transition-colors hover:bg-fd-accent/80 hover:text-fd-accent-foreground @max-lg:col-span-full',
|
||||
index === 1 && 'text-end',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5 font-medium',
|
||||
index === 1 && 'flex-row-reverse',
|
||||
)}
|
||||
>
|
||||
<Icon className="-mx-1 size-4 shrink-0 rtl:rotate-180" />
|
||||
<p>{item.name}</p>
|
||||
</div>
|
||||
<p className="text-fd-muted-foreground truncate">
|
||||
{item.description ?? (index === 0 ? text.previousPage : text.nextPage)}
|
||||
</p>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
'use client';
|
||||
import * as TocDefault from '@/components/toc/default';
|
||||
import * as TocClerk from '@/components/toc/clerk';
|
||||
import * as Base from '@/components/toc';
|
||||
import { I18nLabel, useI18n } from '@/contexts/i18n';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { ChevronDown, Text } from 'lucide-react';
|
||||
import {
|
||||
createContext,
|
||||
use,
|
||||
useEffect,
|
||||
useEffectEvent,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type ComponentProps,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import { useTreePath } from '@/contexts/tree';
|
||||
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/components/ui/collapsible';
|
||||
import { useDocsLayout } from '../..';
|
||||
|
||||
export type TOCProviderProps = Base.TOCProviderProps;
|
||||
|
||||
export function TOCProvider(props: TOCProviderProps) {
|
||||
return <Base.TOCProvider {...props} />;
|
||||
}
|
||||
|
||||
export interface TOCProps {
|
||||
container?: ComponentProps<'div'>;
|
||||
/**
|
||||
* Custom content in TOC container, before the main TOC
|
||||
*/
|
||||
header?: ReactNode;
|
||||
|
||||
/**
|
||||
* Custom content in TOC container, after the main TOC
|
||||
*/
|
||||
footer?: ReactNode;
|
||||
|
||||
/**
|
||||
* @defaultValue 'normal'
|
||||
*/
|
||||
style?: 'normal' | 'clerk';
|
||||
}
|
||||
|
||||
export function TOC({ container, header, footer, style }: TOCProps) {
|
||||
return (
|
||||
<div
|
||||
id="nd-toc"
|
||||
{...container}
|
||||
className={cn(
|
||||
'sticky top-(--fd-docs-row-1) h-[calc(var(--fd-docs-height)-var(--fd-docs-row-1))] flex flex-col [grid-area:toc] w-(--fd-toc-width) pt-12 pe-4 pb-2 max-xl:hidden',
|
||||
container?.className,
|
||||
)}
|
||||
>
|
||||
{header}
|
||||
<h3
|
||||
id="toc-title"
|
||||
className="inline-flex items-center gap-1.5 text-sm text-fd-muted-foreground"
|
||||
>
|
||||
<Text className="size-4" />
|
||||
<I18nLabel label="toc" />
|
||||
</h3>
|
||||
<Base.TOCScrollArea>
|
||||
{style === 'clerk' ? <TocClerk.TOCItems /> : <TocDefault.TOCItems />}
|
||||
</Base.TOCScrollArea>
|
||||
{footer}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const TocPopoverContext = createContext<{
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
} | null>(null);
|
||||
|
||||
export interface TOCPopoverProps {
|
||||
container?: ComponentProps<'div'>;
|
||||
trigger?: ComponentProps<'button'>;
|
||||
content?: ComponentProps<'div'>;
|
||||
|
||||
/**
|
||||
* Custom content in TOC container, before the main TOC
|
||||
*/
|
||||
header?: ReactNode;
|
||||
|
||||
/**
|
||||
* Custom content in TOC container, after the main TOC
|
||||
*/
|
||||
footer?: ReactNode;
|
||||
|
||||
/**
|
||||
* @defaultValue 'normal'
|
||||
*/
|
||||
style?: 'normal' | 'clerk';
|
||||
}
|
||||
|
||||
export function TOCPopover({
|
||||
container,
|
||||
trigger,
|
||||
content,
|
||||
header,
|
||||
footer,
|
||||
style,
|
||||
}: TOCPopoverProps) {
|
||||
return (
|
||||
<PageTOCPopover {...container}>
|
||||
<PageTOCPopoverTrigger {...trigger} />
|
||||
<PageTOCPopoverContent {...content}>
|
||||
{header}
|
||||
<Base.TOCScrollArea>
|
||||
{style === 'clerk' ? <TocClerk.TOCItems /> : <TocDefault.TOCItems />}
|
||||
</Base.TOCScrollArea>
|
||||
{footer}
|
||||
</PageTOCPopoverContent>
|
||||
</PageTOCPopover>
|
||||
);
|
||||
}
|
||||
|
||||
function PageTOCPopover({ className, children, ...rest }: ComponentProps<'div'>) {
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const { isNavTransparent } = useDocsLayout();
|
||||
|
||||
const onClick = useEffectEvent((e: Event) => {
|
||||
if (!open) return;
|
||||
|
||||
if (ref.current && !ref.current.contains(e.target as HTMLElement)) setOpen(false);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('click', onClick);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('click', onClick);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<TocPopoverContext
|
||||
value={useMemo(
|
||||
() => ({
|
||||
open,
|
||||
setOpen,
|
||||
}),
|
||||
[setOpen, open],
|
||||
)}
|
||||
>
|
||||
<Collapsible
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
data-toc-popover=""
|
||||
className={cn(
|
||||
'sticky top-(--fd-docs-row-2) z-10 [grid-area:toc-popover] h-(--fd-toc-popover-height) xl:hidden max-xl:layout:[--fd-toc-popover-height:--spacing(10)]',
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<header
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'border-b backdrop-blur-sm transition-colors',
|
||||
(!isNavTransparent || open) && 'bg-fd-background/80',
|
||||
open && 'shadow-lg',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</header>
|
||||
</Collapsible>
|
||||
</TocPopoverContext>
|
||||
);
|
||||
}
|
||||
|
||||
function PageTOCPopoverTrigger({ className, ...props }: ComponentProps<'button'>) {
|
||||
const { text } = useI18n();
|
||||
const { open } = use(TocPopoverContext)!;
|
||||
const items = Base.useItems();
|
||||
const selectedIdx = items.findIndex((item) => item.active);
|
||||
const path = useTreePath().at(-1);
|
||||
const showItem = selectedIdx !== -1 && !open;
|
||||
|
||||
return (
|
||||
<CollapsibleTrigger
|
||||
className={cn(
|
||||
'flex w-full h-10 items-center text-sm text-fd-muted-foreground gap-2.5 px-4 py-2.5 text-start focus-visible:outline-none [&_svg]:size-4 md:px-6',
|
||||
className,
|
||||
)}
|
||||
data-toc-popover-trigger=""
|
||||
{...props}
|
||||
>
|
||||
<ProgressCircle
|
||||
value={(selectedIdx + 1) / Math.max(1, items.length)}
|
||||
max={1}
|
||||
className={cn('shrink-0', open && 'text-fd-primary')}
|
||||
/>
|
||||
<span className="grid flex-1 *:my-auto *:row-start-1 *:col-start-1">
|
||||
<span
|
||||
className={cn(
|
||||
'truncate transition-[opacity,translate,color]',
|
||||
open && 'text-fd-foreground',
|
||||
showItem && 'opacity-0 -translate-y-full pointer-events-none',
|
||||
)}
|
||||
>
|
||||
{path?.name ?? text.toc}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'truncate transition-[opacity,translate]',
|
||||
!showItem && 'opacity-0 translate-y-full pointer-events-none',
|
||||
)}
|
||||
>
|
||||
{items[selectedIdx]?.original.title}
|
||||
</span>
|
||||
</span>
|
||||
<ChevronDown className={cn('shrink-0 transition-transform mx-0.5', open && 'rotate-180')} />
|
||||
</CollapsibleTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
interface ProgressCircleProps extends Omit<React.ComponentProps<'svg'>, 'strokeWidth'> {
|
||||
value: number;
|
||||
strokeWidth?: number;
|
||||
size?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
function clamp(input: number, min: number, max: number): number {
|
||||
if (input < min) return min;
|
||||
if (input > max) return max;
|
||||
return input;
|
||||
}
|
||||
|
||||
function ProgressCircle({
|
||||
value,
|
||||
strokeWidth = 2,
|
||||
size = 24,
|
||||
min = 0,
|
||||
max = 100,
|
||||
...restSvgProps
|
||||
}: ProgressCircleProps) {
|
||||
const normalizedValue = clamp(value, min, max);
|
||||
const radius = (size - strokeWidth) / 2;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const progress = (normalizedValue / max) * circumference;
|
||||
const circleProps = {
|
||||
cx: size / 2,
|
||||
cy: size / 2,
|
||||
r: radius,
|
||||
fill: 'none',
|
||||
strokeWidth,
|
||||
};
|
||||
|
||||
return (
|
||||
<svg
|
||||
role="progressbar"
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
aria-valuenow={normalizedValue}
|
||||
aria-valuemin={min}
|
||||
aria-valuemax={max}
|
||||
{...restSvgProps}
|
||||
>
|
||||
<circle {...circleProps} className="stroke-current/25" />
|
||||
<circle
|
||||
{...circleProps}
|
||||
stroke="currentColor"
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={circumference - progress}
|
||||
strokeLinecap="round"
|
||||
transform={`rotate(-90 ${size / 2} ${size / 2})`}
|
||||
className="transition-all"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function PageTOCPopoverContent(props: ComponentProps<'div'>) {
|
||||
return (
|
||||
<CollapsibleContent data-toc-popover-content="" {...props}>
|
||||
<div className="flex flex-col px-4 max-h-[50vh] md:px-6">{props.children}</div>
|
||||
</CollapsibleContent>
|
||||
);
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
'use client';
|
||||
import * as Base from '@/components/sidebar/base';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { type ComponentProps, useRef } from 'react';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { createPageTreeRenderer } from '@/components/sidebar/page-tree';
|
||||
import { createLinkItemRenderer } from '@/components/sidebar/link-item';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { SearchToggle } from '@/layouts/shared/search-toggle';
|
||||
import { Sidebar as SidebarIcon } from 'lucide-react';
|
||||
import { mergeRefs } from '@/utils/merge-refs';
|
||||
|
||||
const itemVariants = cva(
|
||||
'relative flex flex-row items-center gap-2 rounded-lg p-2 text-start text-fd-muted-foreground wrap-anywhere [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
link: 'transition-colors hover:bg-fd-accent/50 hover:text-fd-accent-foreground/80 hover:transition-none data-[active=true]:bg-fd-primary/10 data-[active=true]:text-fd-primary data-[active=true]:hover:transition-colors',
|
||||
button:
|
||||
'transition-colors hover:bg-fd-accent/50 hover:text-fd-accent-foreground/80 hover:transition-none',
|
||||
},
|
||||
highlight: {
|
||||
true: "data-[active=true]:before:content-[''] data-[active=true]:before:bg-fd-primary data-[active=true]:before:absolute data-[active=true]:before:w-px data-[active=true]:before:inset-y-2.5 data-[active=true]:before:start-2.5",
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function getItemOffset(depth: number) {
|
||||
return `calc(${2 + 3 * depth} * var(--spacing))`;
|
||||
}
|
||||
|
||||
export function Sidebar(props: ComponentProps<typeof Base.SidebarProvider>) {
|
||||
return <Base.SidebarProvider {...props} />;
|
||||
}
|
||||
|
||||
export function SidebarFolder(props: ComponentProps<typeof Base.SidebarFolder>) {
|
||||
return <Base.SidebarFolder {...props} />;
|
||||
}
|
||||
|
||||
export function SidebarCollapseTrigger(props: ComponentProps<typeof Base.SidebarCollapseTrigger>) {
|
||||
return <Base.SidebarCollapseTrigger {...props} />;
|
||||
}
|
||||
|
||||
export function SidebarViewport(props: ComponentProps<typeof Base.SidebarViewport>) {
|
||||
return <Base.SidebarViewport {...props} />;
|
||||
}
|
||||
|
||||
export function SidebarTrigger(props: ComponentProps<typeof Base.SidebarTrigger>) {
|
||||
return <Base.SidebarTrigger {...props} />;
|
||||
}
|
||||
|
||||
export function SidebarContent({
|
||||
ref: refProp,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ComponentProps<'aside'>) {
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
|
||||
return (
|
||||
<Base.SidebarContent>
|
||||
{({ collapsed, hovered, ref: asideRef, ...rest }) => (
|
||||
<>
|
||||
<div
|
||||
data-sidebar-placeholder=""
|
||||
className="sticky top-(--fd-docs-row-1) z-20 [grid-area:sidebar] pointer-events-none *:pointer-events-auto h-[calc(var(--fd-docs-height)-var(--fd-docs-row-1))] md:layout:[--fd-sidebar-width:268px] max-md:hidden"
|
||||
>
|
||||
{collapsed && <div className="absolute start-0 inset-y-0 w-4" {...rest} />}
|
||||
<aside
|
||||
id="nd-sidebar"
|
||||
ref={mergeRefs(ref, refProp, asideRef)}
|
||||
data-collapsed={collapsed}
|
||||
data-hovered={collapsed && hovered}
|
||||
className={cn(
|
||||
'absolute flex flex-col w-full start-0 inset-y-0 items-end bg-fd-card text-sm border-e duration-250 *:w-(--fd-sidebar-width)',
|
||||
collapsed && [
|
||||
'inset-y-2 rounded-xl transition-transform border w-(--fd-sidebar-width)',
|
||||
hovered
|
||||
? 'shadow-lg translate-x-2 rtl:-translate-x-2'
|
||||
: '-translate-x-(--fd-sidebar-width) rtl:translate-x-full',
|
||||
],
|
||||
ref.current &&
|
||||
(ref.current.getAttribute('data-collapsed') === 'true') !== collapsed &&
|
||||
'transition-[width,inset-block,translate,background-color]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</aside>
|
||||
</div>
|
||||
<div
|
||||
data-sidebar-panel=""
|
||||
className={cn(
|
||||
'fixed flex top-[calc(--spacing(4)+var(--fd-docs-row-3))] start-4 shadow-lg transition-opacity rounded-xl p-0.5 border bg-fd-muted text-fd-muted-foreground z-10',
|
||||
(!collapsed || hovered) && 'pointer-events-none opacity-0',
|
||||
)}
|
||||
>
|
||||
<Base.SidebarCollapseTrigger
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'ghost',
|
||||
size: 'icon-sm',
|
||||
className: 'rounded-lg',
|
||||
}),
|
||||
)}
|
||||
>
|
||||
<SidebarIcon />
|
||||
</Base.SidebarCollapseTrigger>
|
||||
<SearchToggle className="rounded-lg" hideIfDisabled />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Base.SidebarContent>
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarDrawer({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: ComponentProps<typeof Base.SidebarDrawerContent>) {
|
||||
return (
|
||||
<>
|
||||
<Base.SidebarDrawerOverlay className="fixed z-40 inset-0 backdrop-blur-xs data-[state=open]:animate-fd-fade-in data-[state=closed]:animate-fd-fade-out" />
|
||||
<Base.SidebarDrawerContent
|
||||
className={cn(
|
||||
'fixed text-[0.9375rem] flex flex-col shadow-lg border-s end-0 inset-y-0 w-[85%] max-w-[380px] z-40 bg-fd-background data-[state=open]:animate-fd-sidebar-in data-[state=closed]:animate-fd-sidebar-out',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Base.SidebarDrawerContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarSeparator({ className, style, children, ...props }: ComponentProps<'p'>) {
|
||||
const depth = Base.useFolderDepth();
|
||||
|
||||
return (
|
||||
<Base.SidebarSeparator
|
||||
className={cn('[&_svg]:size-4 [&_svg]:shrink-0', className)}
|
||||
style={{
|
||||
paddingInlineStart: getItemOffset(depth),
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Base.SidebarSeparator>
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarItem({
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
}: ComponentProps<typeof Base.SidebarItem>) {
|
||||
const depth = Base.useFolderDepth();
|
||||
|
||||
return (
|
||||
<Base.SidebarItem
|
||||
className={cn(itemVariants({ variant: 'link', highlight: depth >= 1 }), className)}
|
||||
style={{
|
||||
paddingInlineStart: getItemOffset(depth),
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Base.SidebarItem>
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarFolderTrigger({
|
||||
className,
|
||||
style,
|
||||
...props
|
||||
}: ComponentProps<typeof Base.SidebarFolderTrigger>) {
|
||||
const { depth, collapsible } = Base.useFolder()!;
|
||||
|
||||
return (
|
||||
<Base.SidebarFolderTrigger
|
||||
className={(s) =>
|
||||
cn(
|
||||
itemVariants({ variant: collapsible ? 'button' : null }),
|
||||
'w-full',
|
||||
typeof className === 'function' ? className(s) : className,
|
||||
)
|
||||
}
|
||||
style={{
|
||||
paddingInlineStart: getItemOffset(depth - 1),
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
</Base.SidebarFolderTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarFolderLink({
|
||||
className,
|
||||
style,
|
||||
...props
|
||||
}: ComponentProps<typeof Base.SidebarFolderLink>) {
|
||||
const depth = Base.useFolderDepth();
|
||||
|
||||
return (
|
||||
<Base.SidebarFolderLink
|
||||
className={cn(itemVariants({ variant: 'link', highlight: depth > 1 }), 'w-full', className)}
|
||||
style={{
|
||||
paddingInlineStart: getItemOffset(depth - 1),
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
</Base.SidebarFolderLink>
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarFolderContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ComponentProps<typeof Base.SidebarFolderContent>) {
|
||||
const depth = Base.useFolderDepth();
|
||||
|
||||
return (
|
||||
<Base.SidebarFolderContent
|
||||
className={(s) =>
|
||||
cn(
|
||||
'relative',
|
||||
depth === 1 &&
|
||||
"before:content-[''] before:absolute before:w-px before:inset-y-1 before:bg-fd-border before:start-2.5",
|
||||
typeof className === 'function' ? className(s) : className,
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Base.SidebarFolderContent>
|
||||
);
|
||||
}
|
||||
|
||||
export const SidebarPageTree = createPageTreeRenderer({
|
||||
SidebarFolder,
|
||||
SidebarFolderContent,
|
||||
SidebarFolderLink,
|
||||
SidebarFolderTrigger,
|
||||
SidebarItem,
|
||||
SidebarSeparator,
|
||||
});
|
||||
|
||||
export const SidebarLinkItem = createLinkItemRenderer({
|
||||
SidebarFolder,
|
||||
SidebarFolderContent,
|
||||
SidebarFolderLink,
|
||||
SidebarFolderTrigger,
|
||||
SidebarItem,
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
'use client';
|
||||
import { cn } from '@/utils/cn';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { useDocsLayout } from '..';
|
||||
|
||||
export function Container(props: ComponentProps<'div'>) {
|
||||
const { slots } = useDocsLayout();
|
||||
const { collapsed } = slots.sidebar?.useSidebar?.() ?? {};
|
||||
|
||||
return (
|
||||
<div
|
||||
id="nd-docs-layout"
|
||||
data-sidebar-collapsed={collapsed}
|
||||
{...props}
|
||||
style={
|
||||
{
|
||||
gridTemplate: `"sidebar sidebar header toc toc"
|
||||
"sidebar sidebar toc-popover toc toc"
|
||||
"sidebar sidebar main toc toc" 1fr / minmax(min-content, 1fr) var(--fd-sidebar-col) minmax(0, calc(var(--fd-layout-width,97rem) - var(--fd-sidebar-width) - var(--fd-toc-width))) var(--fd-toc-width) minmax(min-content, 1fr)`,
|
||||
'--fd-docs-row-1': 'var(--fd-banner-height, 0px)',
|
||||
'--fd-docs-row-2': 'calc(var(--fd-docs-row-1) + var(--fd-header-height))',
|
||||
'--fd-docs-row-3': 'calc(var(--fd-docs-row-2) + var(--fd-toc-popover-height))',
|
||||
'--fd-sidebar-col': collapsed ? '0px' : 'var(--fd-sidebar-width)',
|
||||
...props.style,
|
||||
} as object
|
||||
}
|
||||
className={cn(
|
||||
'grid transition-[grid-template-columns] overflow-x-clip min-h-(--fd-docs-height) [--fd-docs-height:100dvh] [--fd-header-height:0px] [--fd-toc-popover-height:0px] [--fd-sidebar-width:0px] [--fd-toc-width:0px]',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import { useDocsLayout } from '../client';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { SidebarIcon } from 'lucide-react';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
|
||||
export function Header(props: ComponentProps<'header'>) {
|
||||
const {
|
||||
isNavTransparent,
|
||||
slots,
|
||||
props: { nav },
|
||||
} = useDocsLayout();
|
||||
|
||||
if (nav?.component) return nav.component;
|
||||
return (
|
||||
<header
|
||||
id="nd-subnav"
|
||||
data-transparent={isNavTransparent}
|
||||
{...props}
|
||||
className={cn(
|
||||
'[grid-area:header] sticky top-(--fd-docs-row-1) z-30 flex items-center ps-4 pe-2.5 border-b transition-colors backdrop-blur-sm h-(--fd-header-height) md:hidden max-md:layout:[--fd-header-height:--spacing(14)] data-[transparent=false]:bg-fd-background/80',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{slots.navTitle && (
|
||||
<slots.navTitle className="inline-flex items-center gap-2.5 font-semibold" />
|
||||
)}
|
||||
<div className="flex-1">{nav?.children}</div>
|
||||
{slots.searchTrigger && <slots.searchTrigger.sm hideIfDisabled className="p-2" />}
|
||||
{slots.sidebar && (
|
||||
<slots.sidebar.trigger
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'ghost',
|
||||
size: 'icon-sm',
|
||||
className: 'p-2',
|
||||
}),
|
||||
)}
|
||||
>
|
||||
<SidebarIcon />
|
||||
</slots.sidebar.trigger>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
'use client';
|
||||
import * as Base from '@/components/sidebar/base';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { type ComponentProps, type ReactNode, useMemo, useRef, useState } from 'react';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import {
|
||||
createPageTreeRenderer,
|
||||
type SidebarPageTreeComponents,
|
||||
} from '@/components/sidebar/page-tree';
|
||||
import { createLinkItemRenderer } from '@/components/sidebar/link-item';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { SearchTrigger } from '@/layouts/shared/slots/search-trigger';
|
||||
import { Check, ChevronsUpDown, Languages, SidebarIcon } from 'lucide-react';
|
||||
import { mergeRefs } from '@/utils/merge-refs';
|
||||
import { useDocsLayout } from '../client';
|
||||
import { LinkItem } from '@/layouts/shared';
|
||||
import { isLayoutTabActive, type LayoutTab } from '@/layouts/shared';
|
||||
import { usePathname } from '@hanzo/docs-core/framework';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import Link from '@hanzo/docs-core/link';
|
||||
|
||||
const itemVariants = cva(
|
||||
'relative flex flex-row items-center gap-2 rounded-lg p-2 text-start text-fd-muted-foreground wrap-anywhere [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
link: 'transition-colors hover:bg-fd-accent/50 hover:text-fd-accent-foreground/80 hover:transition-none data-[active=true]:bg-fd-primary/10 data-[active=true]:text-fd-primary data-[active=true]:hover:transition-colors',
|
||||
button:
|
||||
'transition-colors hover:bg-fd-accent/50 hover:text-fd-accent-foreground/80 hover:transition-none',
|
||||
},
|
||||
highlight: {
|
||||
true: "data-[active=true]:before:content-[''] data-[active=true]:before:bg-fd-primary data-[active=true]:before:absolute data-[active=true]:before:w-px data-[active=true]:before:inset-y-2.5 data-[active=true]:before:start-2.5",
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface SidebarProps extends ComponentProps<'aside'> {
|
||||
components?: Partial<SidebarPageTreeComponents>;
|
||||
banner?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
|
||||
/**
|
||||
* Support collapsing the sidebar on desktop mode
|
||||
*
|
||||
* @defaultValue true
|
||||
*/
|
||||
collapsible?: boolean;
|
||||
}
|
||||
|
||||
export type SidebarProviderProps = Base.SidebarProviderProps;
|
||||
|
||||
export const { useSidebar } = Base;
|
||||
|
||||
export function SidebarProvider(props: SidebarProviderProps) {
|
||||
return <Base.SidebarProvider {...props} />;
|
||||
}
|
||||
|
||||
export function Sidebar({ footer, banner, collapsible = true, components, ...rest }: SidebarProps) {
|
||||
const {
|
||||
menuItems,
|
||||
slots,
|
||||
props: { tabs, nav, tabMode },
|
||||
} = useDocsLayout();
|
||||
const iconLinks = menuItems.filter((item) => item.type === 'icon');
|
||||
const viewport = (
|
||||
<Base.SidebarViewport>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{menuItems
|
||||
.filter((v) => v.type !== 'icon')
|
||||
.map((item, i, list) => (
|
||||
<SidebarLinkItem key={i} item={item} className={cn(i === list.length - 1 && 'mb-4')} />
|
||||
))}
|
||||
<SidebarPageTree {...components} />
|
||||
</div>
|
||||
</Base.SidebarViewport>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidebarContent {...rest}>
|
||||
<div className="flex flex-col gap-3 p-4 pb-2">
|
||||
<div className="flex">
|
||||
{slots.navTitle && (
|
||||
<slots.navTitle className="inline-flex text-[0.9375rem] items-center gap-2.5 font-medium me-auto" />
|
||||
)}
|
||||
{nav?.children}
|
||||
{collapsible && (
|
||||
<SidebarCollapseTrigger
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'ghost',
|
||||
size: 'icon-sm',
|
||||
className: 'mb-auto text-fd-muted-foreground',
|
||||
}),
|
||||
)}
|
||||
>
|
||||
<SidebarIcon />
|
||||
</SidebarCollapseTrigger>
|
||||
)}
|
||||
</div>
|
||||
{slots.searchTrigger && <slots.searchTrigger.full hideIfDisabled />}
|
||||
{tabs.length > 0 && tabMode === 'auto' && <SidebarTabsDropdown tabs={tabs} />}
|
||||
{banner}
|
||||
</div>
|
||||
{viewport}
|
||||
{(slots.languageSelect || iconLinks.length > 0 || slots.themeSwitch || footer) && (
|
||||
<div className="flex flex-col border-t p-4 pt-2 empty:hidden">
|
||||
<div className="flex text-fd-muted-foreground items-center empty:hidden">
|
||||
{slots.languageSelect && (
|
||||
<slots.languageSelect.root>
|
||||
<Languages className="size-4.5" />
|
||||
</slots.languageSelect.root>
|
||||
)}
|
||||
{iconLinks.map((item, i) => (
|
||||
<LinkItem
|
||||
key={i}
|
||||
item={item}
|
||||
className={cn(buttonVariants({ size: 'icon-sm', color: 'ghost' }))}
|
||||
aria-label={item.label}
|
||||
>
|
||||
{item.icon}
|
||||
</LinkItem>
|
||||
))}
|
||||
{slots.themeSwitch && <slots.themeSwitch className="ms-auto p-0" />}
|
||||
</div>
|
||||
{footer}
|
||||
</div>
|
||||
)}
|
||||
</SidebarContent>
|
||||
<SidebarDrawer>
|
||||
<div className="flex flex-col gap-3 p-4 pb-2">
|
||||
<div className="flex text-fd-muted-foreground items-center gap-1.5">
|
||||
<div className="flex flex-1">
|
||||
{iconLinks.map((item, i) => (
|
||||
<LinkItem
|
||||
key={i}
|
||||
item={item}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
size: 'icon-sm',
|
||||
color: 'ghost',
|
||||
className: 'p-2',
|
||||
}),
|
||||
)}
|
||||
aria-label={item.label}
|
||||
>
|
||||
{item.icon}
|
||||
</LinkItem>
|
||||
))}
|
||||
</div>
|
||||
{slots.languageSelect && (
|
||||
<slots.languageSelect.root>
|
||||
<Languages className="size-4.5" />
|
||||
<slots.languageSelect.text />
|
||||
</slots.languageSelect.root>
|
||||
)}
|
||||
{slots.themeSwitch && <slots.themeSwitch className="p-0" />}
|
||||
<SidebarTrigger
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'ghost',
|
||||
size: 'icon-sm',
|
||||
className: 'p-2',
|
||||
}),
|
||||
)}
|
||||
>
|
||||
<SidebarIcon />
|
||||
</SidebarTrigger>
|
||||
</div>
|
||||
{tabs.length > 0 && <SidebarTabsDropdown tabs={tabs} />}
|
||||
{banner}
|
||||
</div>
|
||||
{viewport}
|
||||
<div className="flex flex-col border-t p-4 pt-2 empty:hidden">{footer}</div>
|
||||
</SidebarDrawer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarFolder(props: ComponentProps<typeof Base.SidebarFolder>) {
|
||||
return <Base.SidebarFolder {...props} />;
|
||||
}
|
||||
|
||||
function SidebarCollapseTrigger(props: ComponentProps<typeof Base.SidebarCollapseTrigger>) {
|
||||
return <Base.SidebarCollapseTrigger {...props} />;
|
||||
}
|
||||
|
||||
export function SidebarTrigger(props: ComponentProps<'button'>) {
|
||||
return <Base.SidebarTrigger {...props} />;
|
||||
}
|
||||
|
||||
function SidebarContent({ ref: refProp, className, children, ...props }: ComponentProps<'aside'>) {
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
|
||||
return (
|
||||
<Base.SidebarContent>
|
||||
{({ collapsed, hovered, ref: asideRef, ...rest }) => (
|
||||
<>
|
||||
<div
|
||||
data-sidebar-placeholder=""
|
||||
className="sticky top-(--fd-docs-row-1) z-20 [grid-area:sidebar] pointer-events-none *:pointer-events-auto h-[calc(var(--fd-docs-height)-var(--fd-docs-row-1))] md:layout:[--fd-sidebar-width:268px] max-md:hidden"
|
||||
>
|
||||
{collapsed && <div className="absolute start-0 inset-y-0 w-4" {...rest} />}
|
||||
<aside
|
||||
id="nd-sidebar"
|
||||
ref={mergeRefs(ref, refProp, asideRef)}
|
||||
data-collapsed={collapsed}
|
||||
data-hovered={collapsed && hovered}
|
||||
className={cn(
|
||||
'absolute flex flex-col w-full start-0 inset-y-0 items-end bg-fd-card text-sm border-e duration-250 *:w-(--fd-sidebar-width)',
|
||||
collapsed && [
|
||||
'inset-y-2 rounded-xl transition-transform border w-(--fd-sidebar-width)',
|
||||
hovered
|
||||
? 'shadow-lg translate-x-2 rtl:-translate-x-2'
|
||||
: '-translate-x-(--fd-sidebar-width) rtl:translate-x-full',
|
||||
],
|
||||
ref.current &&
|
||||
(ref.current.getAttribute('data-collapsed') === 'true') !== collapsed &&
|
||||
'transition-[width,inset-block,translate,background-color]',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</aside>
|
||||
</div>
|
||||
<div
|
||||
data-sidebar-panel=""
|
||||
className={cn(
|
||||
'fixed flex top-[calc(--spacing(4)+var(--fd-docs-row-3))] start-4 shadow-lg transition-opacity rounded-xl p-0.5 border bg-fd-muted text-fd-muted-foreground z-10',
|
||||
(!collapsed || hovered) && 'pointer-events-none opacity-0',
|
||||
)}
|
||||
>
|
||||
<Base.SidebarCollapseTrigger
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
color: 'ghost',
|
||||
size: 'icon-sm',
|
||||
className: 'rounded-lg',
|
||||
}),
|
||||
)}
|
||||
>
|
||||
<SidebarIcon />
|
||||
</Base.SidebarCollapseTrigger>
|
||||
<SearchTrigger className="rounded-lg" hideIfDisabled />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Base.SidebarContent>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarDrawer({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: ComponentProps<typeof Base.SidebarDrawerContent>) {
|
||||
return (
|
||||
<>
|
||||
<Base.SidebarDrawerOverlay className="fixed z-40 inset-0 backdrop-blur-xs data-[state=open]:animate-fd-fade-in data-[state=closed]:animate-fd-fade-out" />
|
||||
<Base.SidebarDrawerContent
|
||||
className={cn(
|
||||
'fixed text-[0.9375rem] flex flex-col shadow-lg border-s end-0 inset-y-0 w-[85%] max-w-[380px] z-40 bg-fd-background data-[state=open]:animate-fd-sidebar-in data-[state=closed]:animate-fd-sidebar-out',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Base.SidebarDrawerContent>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarSeparator({ className, style, children, ...props }: ComponentProps<'p'>) {
|
||||
const depth = Base.useFolderDepth();
|
||||
|
||||
return (
|
||||
<Base.SidebarSeparator
|
||||
className={cn(
|
||||
'inline-flex items-center gap-2 mb-1 px-2 mt-6 empty:mb-0 [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
depth === 0 && 'first:mt-0',
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
paddingInlineStart: getItemOffset(depth),
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Base.SidebarSeparator>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarItem({
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
}: ComponentProps<typeof Base.SidebarItem>) {
|
||||
const depth = Base.useFolderDepth();
|
||||
|
||||
return (
|
||||
<Base.SidebarItem
|
||||
className={cn(itemVariants({ variant: 'link', highlight: depth >= 1 }), className)}
|
||||
style={{
|
||||
paddingInlineStart: getItemOffset(depth),
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Base.SidebarItem>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarFolderTrigger({
|
||||
className,
|
||||
style,
|
||||
...props
|
||||
}: ComponentProps<typeof Base.SidebarFolderTrigger>) {
|
||||
const { depth, collapsible } = Base.useFolder()!;
|
||||
|
||||
return (
|
||||
<Base.SidebarFolderTrigger
|
||||
className={(state) =>
|
||||
cn(
|
||||
itemVariants({ variant: collapsible ? 'button' : null }),
|
||||
'w-full',
|
||||
typeof className === 'function' ? className(state) : className,
|
||||
)
|
||||
}
|
||||
style={{
|
||||
paddingInlineStart: getItemOffset(depth - 1),
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
</Base.SidebarFolderTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarFolderLink({
|
||||
className,
|
||||
style,
|
||||
...props
|
||||
}: ComponentProps<typeof Base.SidebarFolderLink>) {
|
||||
const depth = Base.useFolderDepth();
|
||||
|
||||
return (
|
||||
<Base.SidebarFolderLink
|
||||
className={cn(itemVariants({ variant: 'link', highlight: depth > 1 }), 'w-full', className)}
|
||||
style={{
|
||||
paddingInlineStart: getItemOffset(depth - 1),
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
</Base.SidebarFolderLink>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarFolderContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ComponentProps<typeof Base.SidebarFolderContent>) {
|
||||
const depth = Base.useFolderDepth();
|
||||
|
||||
return (
|
||||
<Base.SidebarFolderContent
|
||||
className={(state) =>
|
||||
cn(
|
||||
'relative flex flex-col gap-0.5 pt-0.5',
|
||||
depth === 1 &&
|
||||
"before:content-[''] before:absolute before:w-px before:inset-y-1 before:bg-fd-border before:start-2.5",
|
||||
typeof className === 'function' ? className(state) : className,
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Base.SidebarFolderContent>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarTabsDropdown({
|
||||
tabs,
|
||||
placeholder,
|
||||
...props
|
||||
}: {
|
||||
placeholder?: ReactNode;
|
||||
tabs: LayoutTab[];
|
||||
} & ComponentProps<'button'>) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { closeOnRedirect } = useSidebar();
|
||||
const pathname = usePathname();
|
||||
|
||||
const selected = useMemo(() => {
|
||||
return tabs.findLast((item) => isLayoutTabActive(item, pathname));
|
||||
}, [tabs, pathname]);
|
||||
|
||||
const onClick = () => {
|
||||
closeOnRedirect.current = false;
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const item = selected ? (
|
||||
<>
|
||||
<div className="size-9 shrink-0 empty:hidden md:size-5">{selected.icon}</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">{selected.title}</p>
|
||||
<p className="text-sm text-fd-muted-foreground empty:hidden md:hidden">
|
||||
{selected.description}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
placeholder
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
{item && (
|
||||
<PopoverTrigger
|
||||
{...props}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-lg p-2 border bg-fd-secondary/50 text-start text-fd-secondary-foreground transition-colors hover:bg-fd-accent data-[state=open]:bg-fd-accent data-[state=open]:text-fd-accent-foreground',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{item}
|
||||
<ChevronsUpDown className="shrink-0 ms-auto size-4 text-fd-muted-foreground" />
|
||||
</PopoverTrigger>
|
||||
)}
|
||||
<PopoverContent className="flex flex-col gap-1 w-(--radix-popover-trigger-width) p-1 fd-scroll-container">
|
||||
{tabs.map((item) => {
|
||||
const isActive = selected && item.url === selected.url;
|
||||
if (!isActive && item.unlisted) return;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={item.url}
|
||||
href={item.url}
|
||||
onClick={onClick}
|
||||
{...item.props}
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-lg p-1.5 hover:bg-fd-accent hover:text-fd-accent-foreground',
|
||||
item.props?.className,
|
||||
)}
|
||||
>
|
||||
<div className="shrink-0 size-9 md:mb-auto md:size-5 empty:hidden">{item.icon}</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium leading-none">{item.title}</p>
|
||||
<p className="text-[0.8125rem] text-fd-muted-foreground mt-1 empty:hidden">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Check
|
||||
className={cn(
|
||||
'shrink-0 ms-auto size-3.5 text-fd-primary',
|
||||
!isActive && 'invisible',
|
||||
)}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function getItemOffset(depth: number) {
|
||||
return `calc(${2 + 3 * depth} * var(--spacing))`;
|
||||
}
|
||||
|
||||
const SidebarPageTree = createPageTreeRenderer({
|
||||
SidebarFolder,
|
||||
SidebarFolderContent,
|
||||
SidebarFolderLink,
|
||||
SidebarFolderTrigger,
|
||||
SidebarItem,
|
||||
SidebarSeparator,
|
||||
});
|
||||
|
||||
const SidebarLinkItem = createLinkItemRenderer({
|
||||
SidebarFolder,
|
||||
SidebarFolderContent,
|
||||
SidebarFolderLink,
|
||||
SidebarFolderTrigger,
|
||||
SidebarItem,
|
||||
});
|
||||
@@ -1,218 +1,203 @@
|
||||
'use client';
|
||||
<<<<<<< HEAD
|
||||
import type * as PageTree from '@hanzo/docs-core/page-tree';
|
||||
import type { BaseLayoutProps } from '@/layouts/shared';
|
||||
=======
|
||||
import type * as PageTree from '@hanzo/docs-core/page-tree';
|
||||
import {
|
||||
type BaseLayoutProps,
|
||||
baseSlots,
|
||||
type BaseSlots,
|
||||
type BaseSlotsProps,
|
||||
getLayoutTabs,
|
||||
type GetLayoutTabsOptions,
|
||||
type LayoutTab,
|
||||
useLinkItems,
|
||||
} from '@/layouts/shared';
|
||||
>>>>>>> dev
|
||||
import { TreeContextProvider } from '@/contexts/tree';
|
||||
import { getSidebarTabs, type GetSidebarTabsOptions } from '@/components/sidebar/tabs';
|
||||
import type { SidebarPageTreeComponents } from '@/components/sidebar/page-tree';
|
||||
import { type ComponentProps, type ReactNode, useMemo } from 'react';
|
||||
import { type ComponentProps, createContext, type FC, type ReactNode, use, useMemo } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useSidebar } from '@/components/sidebar/base';
|
||||
import { SidebarTabsDropdown, type SidebarTabWithProps } from './tab-dropdown';
|
||||
import { Sidebar, SidebarContent, SidebarLinkItem, SidebarPageTree } from './sidebar';
|
||||
import { TabDropdown, type TabDropdownProps } from './slots/tab-dropdown';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { Languages, SidebarIcon, XIcon } from 'lucide-react';
|
||||
import { parseLayoutProps, renderTitleNav, useLinkItems } from '../shared';
|
||||
import { LanguageToggle } from '../shared/language-toggle';
|
||||
import { SearchToggle } from '../shared/search-toggle';
|
||||
import { ThemeToggle } from '../shared/theme-toggle';
|
||||
import { LinkItem } from '@/utils/link-item';
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
import { Languages } from 'lucide-react';
|
||||
import { LinkItem, type LinkItemType } from '@/layouts/shared';
|
||||
import { motion } from 'motion/react';
|
||||
import { RemoveScroll } from 'react-remove-scroll';
|
||||
import { useSearchContext } from '@/contexts/search';
|
||||
import { ScrollArea, ScrollViewport } from '@/components/ui/scroll-area';
|
||||
import { renderer, type Renderer } from '@/utils/renderer';
|
||||
import {
|
||||
SidebarProvider,
|
||||
Sidebar,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
type SidebarProviderProps,
|
||||
type SidebarProps,
|
||||
} from './slots/sidebar';
|
||||
import { Container } from './slots/container';
|
||||
|
||||
export interface DocsSlots extends BaseSlots {
|
||||
container: FC<ComponentProps<'div'>>;
|
||||
tabDropdown: FC<TabDropdownProps>;
|
||||
sidebar: {
|
||||
provider: FC<SidebarProviderProps>;
|
||||
trigger: FC<ComponentProps<'button'>>;
|
||||
root: FC<SidebarProps>;
|
||||
useSidebar: () => { collapsed: boolean; open: boolean; setOpen: (v: boolean) => void };
|
||||
};
|
||||
}
|
||||
|
||||
export interface DocsLayoutProps extends BaseLayoutProps {
|
||||
tree: PageTree.Root;
|
||||
sidebar?: SidebarOptions;
|
||||
Container?: Renderer<ComponentProps<'div'>>;
|
||||
tabs?: LayoutTab[] | GetLayoutTabsOptions | false;
|
||||
slots?: Partial<DocsSlots>;
|
||||
renderNavigationPanel?: (props: NavigationPanelProps) => ReactNode;
|
||||
|
||||
/**
|
||||
* @deprecated use `Container` instead.
|
||||
*/
|
||||
containerProps?: ComponentProps<'div'>;
|
||||
}
|
||||
|
||||
interface SidebarOptions
|
||||
extends
|
||||
ComponentProps<'aside'>,
|
||||
Pick<ComponentProps<typeof Sidebar>, 'defaultOpenLevel' | 'prefetch'> {
|
||||
interface SidebarOptions extends SidebarProps, SidebarProviderProps {
|
||||
enabled?: boolean;
|
||||
component?: ReactNode;
|
||||
components?: Partial<SidebarPageTreeComponents>;
|
||||
|
||||
/**
|
||||
* Root Toggle options
|
||||
* @deprecated use layout-level `tabs` instead.
|
||||
*/
|
||||
tabs?: SidebarTabWithProps[] | GetSidebarTabsOptions | false;
|
||||
|
||||
banner?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
tabs?: LayoutTab[] | GetLayoutTabsOptions | false;
|
||||
}
|
||||
|
||||
export function DocsLayout(_: DocsLayoutProps) {
|
||||
const LayoutContext = createContext<{
|
||||
props: BaseSlotsProps;
|
||||
menuItems: LinkItemType[];
|
||||
navItems: LinkItemType[];
|
||||
slots: DocsSlots;
|
||||
} | null>(null);
|
||||
|
||||
export function useFluxLayout() {
|
||||
const context = use(LayoutContext);
|
||||
if (!context)
|
||||
throw new Error(
|
||||
'Please use Flux layout components under <DocsLayout /> (`@hanzo/docs-ui/layouts/flux`).',
|
||||
);
|
||||
return context;
|
||||
}
|
||||
|
||||
const { useProvider } = baseSlots({
|
||||
useProps() {
|
||||
return useFluxLayout().props;
|
||||
},
|
||||
});
|
||||
|
||||
export function DocsLayout(props: DocsLayoutProps) {
|
||||
const {
|
||||
tree,
|
||||
nav = {},
|
||||
sidebar: {
|
||||
enabled: sidebarEnabled = true,
|
||||
tabs: sidebarTabs,
|
||||
tabs: _tabs,
|
||||
defaultOpenLevel,
|
||||
prefetch,
|
||||
...sidebarProps
|
||||
} = {},
|
||||
tabs: defaultTabs = _tabs,
|
||||
children,
|
||||
containerProps,
|
||||
Container = containerProps ?? true,
|
||||
LanguageSwitch,
|
||||
SearchToggle: SearchToggleRenderer,
|
||||
ThemeSwitch,
|
||||
renderNavigationPanel = (props) => <NavigationPanel {...props} />,
|
||||
...props
|
||||
} = parseLayoutProps<DocsLayoutProps>(_);
|
||||
const { menuItems } = useLinkItems(props);
|
||||
const renderContainer = renderer(Container, 'div');
|
||||
const renderLanguageSwitch = renderer(LanguageSwitch, LanguageToggle);
|
||||
const renderThemeSwitch = renderer(ThemeSwitch, ThemeToggle);
|
||||
const renderSearchToggle = renderer(SearchToggleRenderer, SearchToggle);
|
||||
slots: defaultSlots = {},
|
||||
} = props;
|
||||
const linkItems = useLinkItems(props);
|
||||
const { baseSlots, baseProps } = useProvider(props);
|
||||
|
||||
const tabs = useMemo(() => {
|
||||
if (Array.isArray(sidebarTabs)) {
|
||||
return sidebarTabs;
|
||||
if (Array.isArray(defaultTabs)) {
|
||||
return defaultTabs;
|
||||
}
|
||||
if (typeof sidebarTabs === 'object') {
|
||||
return getSidebarTabs(tree, sidebarTabs);
|
||||
if (typeof defaultTabs === 'object') {
|
||||
return getLayoutTabs(tree, defaultTabs);
|
||||
}
|
||||
if (sidebarTabs !== false) {
|
||||
return getSidebarTabs(tree);
|
||||
if (defaultTabs !== false) {
|
||||
return getLayoutTabs(tree);
|
||||
}
|
||||
return [];
|
||||
}, [tree, sidebarTabs]);
|
||||
const iconLinks = menuItems.filter((item) => item.type === 'icon');
|
||||
|
||||
function sidebar() {
|
||||
const { footer, banner, component, components, ...rest } = sidebarProps;
|
||||
if (component) return component;
|
||||
|
||||
return (
|
||||
<SidebarContent {...rest}>
|
||||
<div className="flex flex-col gap-3 p-4 pb-2 empty:hidden">{banner}</div>
|
||||
<ScrollArea className="min-h-0 flex-1">
|
||||
<ScrollViewport
|
||||
className="p-4 overscroll-contain"
|
||||
style={
|
||||
{
|
||||
maskImage:
|
||||
'linear-gradient(to bottom, transparent, white 12px, white calc(100% - 12px), transparent)',
|
||||
} as object
|
||||
}
|
||||
>
|
||||
{menuItems
|
||||
.filter((v) => v.type !== 'icon')
|
||||
.map((item, i, list) => (
|
||||
<SidebarLinkItem
|
||||
key={i}
|
||||
item={item}
|
||||
className={cn(i === list.length - 1 && 'mb-4')}
|
||||
/>
|
||||
))}
|
||||
<SidebarPageTree {...components} />
|
||||
</ScrollViewport>
|
||||
</ScrollArea>
|
||||
{footer}
|
||||
</SidebarContent>
|
||||
);
|
||||
}
|
||||
}, [tree, defaultTabs]);
|
||||
const slots: DocsSlots = {
|
||||
...baseSlots,
|
||||
container: defaultSlots.container ?? Container,
|
||||
tabDropdown: defaultSlots.tabDropdown ?? TabDropdown,
|
||||
sidebar: defaultSlots.sidebar ?? {
|
||||
root: Sidebar,
|
||||
provider: SidebarProvider,
|
||||
trigger: SidebarTrigger,
|
||||
useSidebar,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<TreeContextProvider tree={tree}>
|
||||
<Sidebar defaultOpenLevel={defaultOpenLevel} prefetch={prefetch}>
|
||||
{renderContainer?.((t) => ({
|
||||
id: 'nd-flux-layout',
|
||||
children: (
|
||||
<LayoutContext
|
||||
value={{
|
||||
props: baseProps,
|
||||
slots,
|
||||
...linkItems,
|
||||
}}
|
||||
>
|
||||
<TreeContextProvider tree={tree}>
|
||||
<slots.sidebar.provider defaultOpenLevel={defaultOpenLevel} prefetch={prefetch}>
|
||||
<slots.container {...containerProps}>
|
||||
{sidebarEnabled && <slots.sidebar.root {...sidebarProps} />}
|
||||
{children}
|
||||
</slots.container>
|
||||
</slots.sidebar.provider>
|
||||
{renderNavigationPanel({
|
||||
head: (
|
||||
<>
|
||||
{sidebarEnabled && sidebar()}
|
||||
{children}
|
||||
{slots.navTitle && (
|
||||
<slots.navTitle className="inline-flex items-center gap-2.5 text-sm font-semibold" />
|
||||
)}
|
||||
{nav.children}
|
||||
</>
|
||||
),
|
||||
...t,
|
||||
className: cn('flex flex-col items-center pb-24 overflow-x-clip', t?.className),
|
||||
}))}
|
||||
{renderNavigationPanel({
|
||||
head: renderTitleNav(nav, {
|
||||
className: 'inline-flex items-center gap-2.5 text-sm font-semibold',
|
||||
}),
|
||||
tabDropdown: tabs.length > 0 && <SidebarTabsDropdown className="flex-1" options={tabs} />,
|
||||
tabDropdown: slots.tabDropdown && tabs.length > 0 && (
|
||||
<slots.tabDropdown className="flex-1" tabs={tabs} />
|
||||
),
|
||||
tool: (
|
||||
<>
|
||||
{renderLanguageSwitch?.((t) => ({
|
||||
children: <Languages className="size-4.5" />,
|
||||
...t,
|
||||
}))}
|
||||
{renderSearchToggle?.((t) => ({
|
||||
hideIfDisabled: true,
|
||||
...t,
|
||||
className: cn('rounded-lg', t?.className),
|
||||
}))}
|
||||
|
||||
<NavigationSidebarTrigger />
|
||||
{renderThemeSwitch?.((t) => ({
|
||||
...t,
|
||||
className: cn('p-1 h-full ms-1 rounded-xl bg-fd-muted *:rounded-lg', t?.className),
|
||||
}))}
|
||||
{slots.languageSelect && (
|
||||
<slots.languageSelect.root>
|
||||
<Languages className="size-4.5" />
|
||||
</slots.languageSelect.root>
|
||||
)}
|
||||
{slots.searchTrigger && (
|
||||
<slots.searchTrigger.sm hideIfDisabled className="rounded-lg" />
|
||||
)}
|
||||
{slots.sidebar && (
|
||||
<slots.sidebar.trigger
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: 'ghost',
|
||||
size: 'icon-sm',
|
||||
className: 'overflow-hidden',
|
||||
}),
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{slots.themeSwitch && (
|
||||
<slots.themeSwitch className="p-1 h-full ms-1 rounded-xl bg-fd-muted *:rounded-lg" />
|
||||
)}
|
||||
</>
|
||||
),
|
||||
link: iconLinks.map((item, i) => (
|
||||
<LinkItem
|
||||
key={i}
|
||||
item={item}
|
||||
className={cn(buttonVariants({ size: 'icon-sm', color: 'ghost' }))}
|
||||
aria-label={item.label}
|
||||
>
|
||||
{item.icon}
|
||||
</LinkItem>
|
||||
)),
|
||||
link: linkItems.menuItems
|
||||
.filter((item) => item.type === 'icon')
|
||||
.map((item, i) => (
|
||||
<LinkItem
|
||||
key={i}
|
||||
item={item}
|
||||
className={cn(buttonVariants({ size: 'icon-sm', color: 'ghost' }))}
|
||||
aria-label={item.label}
|
||||
>
|
||||
{item.icon}
|
||||
</LinkItem>
|
||||
)),
|
||||
})}
|
||||
</Sidebar>
|
||||
</TreeContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationSidebarTrigger() {
|
||||
const { open, setOpen } = useSidebar();
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: 'ghost',
|
||||
size: 'icon-sm',
|
||||
className: 'overflow-hidden',
|
||||
}),
|
||||
)}
|
||||
onClick={() => setOpen((prev) => !prev)}
|
||||
>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.span
|
||||
key={open ? 'open' : 'closed'}
|
||||
transition={{ duration: 0.2 }}
|
||||
initial={{
|
||||
y: '100%',
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
}}
|
||||
exit={{
|
||||
y: '100%',
|
||||
opacity: 0,
|
||||
}}
|
||||
>
|
||||
{open ? <XIcon /> : <SidebarIcon />}
|
||||
</motion.span>
|
||||
</AnimatePresence>
|
||||
</button>
|
||||
</TreeContextProvider>
|
||||
</LayoutContext>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
import type { ComponentProps, ReactNode } from 'react';
|
||||
'use client';
|
||||
import {
|
||||
type ComponentProps,
|
||||
createContext,
|
||||
type FC,
|
||||
type ReactNode,
|
||||
use,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { I18nLabel, useI18n } from '@/contexts/i18n';
|
||||
import { TOC, TOCProvider, type TOCProviderProps, type TOCProps } from './slots/toc';
|
||||
import { Footer, type FooterProps } from './slots/footer';
|
||||
import { Breadcrumb, type BreadcrumbProps } from './slots/breadcrumb';
|
||||
import { Container } from './slots/container';
|
||||
import type { TOCItemType } from '@hanzo/docs-core/toc';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { Edit } from 'lucide-react';
|
||||
<<<<<<< HEAD
|
||||
import { I18nLabel } from '@/contexts/i18n';
|
||||
import {
|
||||
type BreadcrumbProps,
|
||||
@@ -17,19 +33,11 @@ import * as TocDefault from '@/components/toc/default';
|
||||
import * as TocClerk from '@/components/toc/clerk';
|
||||
import { TOCProvider, TOCScrollArea } from '@/components/toc';
|
||||
import { ChildrenRenderer, renderer, type Renderer } from '@/utils/renderer';
|
||||
=======
|
||||
>>>>>>> dev
|
||||
|
||||
export interface DocsPageProps {
|
||||
export interface DocsPageProps extends ComponentProps<'article'> {
|
||||
toc?: TOCItemType[];
|
||||
/**
|
||||
* - `multiple` (default): Accept multiple active items
|
||||
* - `single`: Only accept one active item at most
|
||||
* */
|
||||
tocMode?: 'single' | 'multiple';
|
||||
TOC?: Renderer<TOCProps>;
|
||||
/** Footer navigation, located under the page body. */
|
||||
Footer?: Renderer<FooterProps>;
|
||||
Container?: Renderer<ComponentProps<'div'>>;
|
||||
Breadcrumb?: Renderer<BreadcrumbProps>;
|
||||
|
||||
/**
|
||||
* Extend the page to fill all available space
|
||||
@@ -38,107 +46,101 @@ export interface DocsPageProps {
|
||||
*/
|
||||
full?: boolean;
|
||||
children?: ReactNode;
|
||||
slots?: Partial<DocsPageSlots>;
|
||||
|
||||
/** @deprecated use `Footer` instead. */
|
||||
footer?: FooterOptions;
|
||||
/** @deprecated use `Container` instead. */
|
||||
className?: string;
|
||||
/** @deprecated use `Breadcrumb` instead. */
|
||||
breadcrumb?: BreadcrumbOptions;
|
||||
/** @deprecated use `TOC` instead, or `tocMode` for enabling `single`. */
|
||||
tableOfContent?: TableOfContentOptions;
|
||||
}
|
||||
|
||||
interface TableOfContentOptions extends Pick<AnchorProviderProps, 'single'>, TOCProps {
|
||||
interface TableOfContentOptions extends Pick<TOCProviderProps, 'single'>, TOCProps {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @deprecated use `slots.toc` instead.
|
||||
*/
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
interface BreadcrumbOptions extends BreadcrumbProps {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @deprecated use `slots.breadcrumb` instead.
|
||||
*/
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
interface FooterOptions extends FooterProps {
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @deprecated use `slots.footer` instead.
|
||||
*/
|
||||
component?: ReactNode;
|
||||
}
|
||||
|
||||
export interface TOCProps {
|
||||
container?: ComponentProps<typeof PageTOCPopover>;
|
||||
trigger?: ComponentProps<typeof PageTOCPopoverTrigger>;
|
||||
content?: ComponentProps<typeof PageTOCPopoverContent>;
|
||||
interface DocsPageSlots {
|
||||
toc: {
|
||||
provider: FC<TOCProviderProps>;
|
||||
main: FC<TOCProps>;
|
||||
};
|
||||
container: FC<ComponentProps<'article'>>;
|
||||
footer: FC<FooterProps>;
|
||||
breadcrumb: FC<BreadcrumbProps>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom content in TOC container, before the main TOC
|
||||
*/
|
||||
header?: ReactNode;
|
||||
type PageSlotsProps = Pick<DocsPageProps, 'full'>;
|
||||
|
||||
/**
|
||||
* Custom content in TOC container, after the main TOC
|
||||
*/
|
||||
footer?: ReactNode;
|
||||
const PageContext = createContext<{
|
||||
props: PageSlotsProps;
|
||||
slots: DocsPageSlots;
|
||||
} | null>(null);
|
||||
|
||||
/**
|
||||
* @defaultValue 'normal'
|
||||
*/
|
||||
style?: 'normal' | 'clerk';
|
||||
export function useDocsPage() {
|
||||
const context = use(PageContext);
|
||||
if (!context)
|
||||
throw new Error(
|
||||
'Please use page components under <DocsPage /> (`@hanzo/docs-ui/layouts/flux/page`).',
|
||||
);
|
||||
return context;
|
||||
}
|
||||
|
||||
export function DocsPage({
|
||||
tableOfContent: tocProps = {},
|
||||
TOC: TOCRenderer,
|
||||
footer = {},
|
||||
Footer = footer.enabled === false
|
||||
? false
|
||||
: footer.component
|
||||
? new ChildrenRenderer(footer.component)
|
||||
: footer,
|
||||
className,
|
||||
Container = { className },
|
||||
breadcrumb = {},
|
||||
Breadcrumb = breadcrumb.enabled === false
|
||||
? false
|
||||
: breadcrumb.component
|
||||
? new ChildrenRenderer(breadcrumb.component)
|
||||
: breadcrumb,
|
||||
tableOfContent: { enabled: tocEnabled, single, ...tocProps } = {},
|
||||
breadcrumb: { enabled: breadcrumbEnabled = true, ...breadcrumb } = {},
|
||||
footer: { enabled: footerEnabled = true, ...footer } = {},
|
||||
full = false,
|
||||
toc = [],
|
||||
tocMode = tocProps.single ? 'single' : 'multiple',
|
||||
slots: defaultSlots = {},
|
||||
children,
|
||||
...containerProps
|
||||
}: DocsPageProps) {
|
||||
if (tocProps.enabled ?? (toc.length > 0 || tocProps.header || tocProps.footer)) {
|
||||
TOCRenderer ??= tocProps.component ? new ChildrenRenderer(tocProps.component) : tocProps;
|
||||
} else {
|
||||
TOCRenderer = false;
|
||||
}
|
||||
tocEnabled ??= Boolean(toc.length > 0 || tocProps.header || tocProps.footer);
|
||||
|
||||
const renderBreadcrumb = renderer(Breadcrumb, PageBreadcrumb);
|
||||
const renderFooter = renderer(Footer, PageFooter);
|
||||
const renderContainer = renderer(Container, 'article');
|
||||
const renderToc = renderer(TOCRenderer, TOC);
|
||||
const slots: DocsPageSlots = {
|
||||
breadcrumb: defaultSlots.breadcrumb ?? Breadcrumb,
|
||||
footer: defaultSlots.footer ?? Footer,
|
||||
container: defaultSlots.container ?? Container,
|
||||
toc: defaultSlots.toc ?? {
|
||||
provider: TOCProvider,
|
||||
main: TOC,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<TOCProvider single={tocMode === 'single'} toc={renderToc ? toc : []}>
|
||||
{renderToc?.((t) => t ?? {})}
|
||||
{renderContainer?.((t) => ({
|
||||
id: 'nd-page',
|
||||
'data-full': full,
|
||||
children: (
|
||||
<>
|
||||
{renderBreadcrumb?.((t) => t ?? {})}
|
||||
{children}
|
||||
{renderFooter?.((t) => t ?? {})}
|
||||
</>
|
||||
),
|
||||
...t,
|
||||
className: cn(
|
||||
'flex flex-col w-full max-w-[900px] mx-auto [grid-area:main] px-4 py-6 gap-4 md:px-6 md:pt-8 xl:px-8 xl:pt-14',
|
||||
full ? 'max-w-[1200px]' : 'xl:layout:[--fd-toc-width:268px]',
|
||||
t?.className,
|
||||
),
|
||||
}))}
|
||||
</TOCProvider>
|
||||
<PageContext
|
||||
value={{
|
||||
props: { full },
|
||||
slots,
|
||||
}}
|
||||
>
|
||||
<slots.toc.provider single={single} toc={tocEnabled ? toc : []}>
|
||||
{tocEnabled && (tocProps.component ?? <slots.toc.main {...tocProps} />)}
|
||||
<slots.container {...containerProps}>
|
||||
{breadcrumbEnabled && (breadcrumb.component ?? <slots.breadcrumb {...breadcrumb} />)}
|
||||
{children}
|
||||
{footerEnabled && (footer.component ?? <slots.footer {...footer} />)}
|
||||
</slots.container>
|
||||
</slots.toc.provider>
|
||||
</PageContext>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -152,8 +154,8 @@ export function EditOnGitHub(props: ComponentProps<'a'>) {
|
||||
buttonVariants({
|
||||
color: 'secondary',
|
||||
size: 'sm',
|
||||
className: 'gap-1.5 not-prose',
|
||||
}),
|
||||
'gap-1.5 not-prose',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
@@ -197,21 +199,25 @@ export function DocsTitle({ children, className, ...props }: ComponentProps<'h1'
|
||||
);
|
||||
}
|
||||
|
||||
export function TOC({ container, trigger, content, header, footer, style }: TOCProps) {
|
||||
export function PageLastUpdate({
|
||||
date: value,
|
||||
...props
|
||||
}: Omit<ComponentProps<'p'>, 'children'> & { date: Date }) {
|
||||
const { text } = useI18n();
|
||||
const [date, setDate] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
// to the timezone of client
|
||||
setDate(value.toLocaleDateString());
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<PageTOCPopover {...container}>
|
||||
<PageTOCPopoverContent {...content}>
|
||||
{header}
|
||||
<TOCScrollArea>
|
||||
{style === 'clerk' ? <TocClerk.TOCItems /> : <TocDefault.TOCItems />}
|
||||
</TOCScrollArea>
|
||||
{footer}
|
||||
</PageTOCPopoverContent>
|
||||
<PageTOCPopoverTrigger {...trigger} />
|
||||
</PageTOCPopover>
|
||||
<p {...props} className={cn('text-sm text-fd-muted-foreground', props.className)}>
|
||||
{text.lastUpdate} {date}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export { PageLastUpdate, PageBreadcrumb, PageFooter } from './client';
|
||||
export type { FooterProps, BreadcrumbProps } from './client';
|
||||
export { type FooterProps, Footer as PageFooter } from './slots/footer';
|
||||
export { type BreadcrumbProps, Breadcrumb as PageBreadcrumb } from './slots/breadcrumb';
|
||||
export { MarkdownCopyButton, ViewOptionsPopover } from '@/layouts/shared/page-actions';
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
'use client';
|
||||
|
||||
import { useTreeContext, useTreePath } from '@/contexts/tree';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { type BreadcrumbOptions, getBreadcrumbItemsFromPath } from '@hanzo/docs-core/breadcrumb';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import Link from '@hanzo/docs-core/link';
|
||||
import { type ComponentProps, Fragment, useMemo } from 'react';
|
||||
|
||||
export type BreadcrumbProps = BreadcrumbOptions & ComponentProps<'div'>;
|
||||
|
||||
export function Breadcrumb({
|
||||
includeRoot,
|
||||
includeSeparator,
|
||||
includePage,
|
||||
...props
|
||||
}: BreadcrumbProps) {
|
||||
const path = useTreePath();
|
||||
const { root } = useTreeContext();
|
||||
const items = useMemo(() => {
|
||||
return getBreadcrumbItemsFromPath(root, path, {
|
||||
includePage,
|
||||
includeSeparator,
|
||||
includeRoot,
|
||||
});
|
||||
}, [includePage, includeRoot, includeSeparator, path, root]);
|
||||
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={cn('flex items-center gap-1.5 text-sm text-fd-muted-foreground', props.className)}
|
||||
>
|
||||
{items.map((item, i) => {
|
||||
const className = cn('truncate', i === items.length - 1 && 'text-fd-primary font-medium');
|
||||
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
{i !== 0 && <ChevronRight className="size-3.5 shrink-0" />}
|
||||
{item.url ? (
|
||||
<Link
|
||||
href={item.url}
|
||||
className={cn(className, 'transition-opacity hover:opacity-80')}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
) : (
|
||||
<span className={className}>{item.name}</span>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { useDocsPage } from '..';
|
||||
|
||||
export function Container(props: ComponentProps<'article'>) {
|
||||
const {
|
||||
props: { full },
|
||||
} = useDocsPage();
|
||||
|
||||
return (
|
||||
<article
|
||||
id="nd-page"
|
||||
data-full={full}
|
||||
{...props}
|
||||
className={cn(
|
||||
'flex flex-col w-full max-w-[900px] mx-auto [grid-area:main] px-4 py-6 gap-4 md:px-6 md:pt-8 xl:px-8 xl:pt-14',
|
||||
full ? 'max-w-[1200px]' : 'xl:layout:[--fd-toc-width:268px]',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
'use client';
|
||||
|
||||
import { useI18n } from '@/contexts/i18n';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { isActive } from '@/utils/urls';
|
||||
import { useFooterItems } from '@/utils/use-footer-items';
|
||||
import { usePathname } from '@hanzo/docs-core/framework';
|
||||
import type * as PageTree from '@hanzo/docs-core/page-tree';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import Link from '@hanzo/docs-core/link';
|
||||
import { type ComponentProps, useMemo } from 'react';
|
||||
|
||||
type Item = Pick<PageTree.Item, 'name' | 'description' | 'url'>;
|
||||
|
||||
export interface FooterProps extends ComponentProps<'div'> {
|
||||
/**
|
||||
* Items including information for the next and previous page
|
||||
*/
|
||||
items?: {
|
||||
previous?: Item;
|
||||
next?: Item;
|
||||
};
|
||||
}
|
||||
|
||||
export function Footer({ items, children, className, ...props }: FooterProps) {
|
||||
const footerList = useFooterItems();
|
||||
const pathname = usePathname();
|
||||
const { previous, next } = useMemo(() => {
|
||||
if (items) return items;
|
||||
|
||||
const idx = footerList.findIndex((item) => isActive(item.url, pathname));
|
||||
|
||||
if (idx === -1) return {};
|
||||
return {
|
||||
previous: footerList[idx - 1],
|
||||
next: footerList[idx + 1],
|
||||
};
|
||||
}, [footerList, items, pathname]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
'@container grid gap-4',
|
||||
previous && next ? 'grid-cols-2' : 'grid-cols-1',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{previous && <FooterItem item={previous} index={0} />}
|
||||
{next && <FooterItem item={next} index={1} />}
|
||||
</div>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function FooterItem({ item, index }: { item: Item; index: 0 | 1 }) {
|
||||
const { text } = useI18n();
|
||||
const Icon = index === 0 ? ChevronLeft : ChevronRight;
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={item.url}
|
||||
className={cn(
|
||||
'flex flex-col gap-2 rounded-lg border p-4 text-sm transition-colors hover:bg-fd-accent/80 hover:text-fd-accent-foreground @max-lg:col-span-full',
|
||||
index === 1 && 'text-end',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5 font-medium',
|
||||
index === 1 && 'flex-row-reverse',
|
||||
)}
|
||||
>
|
||||
<Icon className="-mx-1 size-4 shrink-0 rtl:rotate-180" />
|
||||
<p>{item.name}</p>
|
||||
</div>
|
||||
<p className="text-fd-muted-foreground truncate">
|
||||
{item.description ?? (index === 0 ? text.previousPage : text.nextPage)}
|
||||
</p>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
'use client';
|
||||
|
||||
import * as Base from '@/components/toc';
|
||||
import { useI18n } from '@/contexts/i18n';
|
||||
import { useTreePath } from '@/contexts/tree';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import {
|
||||
type ComponentProps,
|
||||
type ReactNode,
|
||||
createContext,
|
||||
use,
|
||||
useEffect,
|
||||
useEffectEvent,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import * as TocDefault from '@/components/toc/default';
|
||||
import * as TocClerk from '@/components/toc/clerk';
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
const TocPopoverContext = createContext<{
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
} | null>(null);
|
||||
|
||||
export type TOCProviderProps = Base.TOCProviderProps;
|
||||
|
||||
export const { TOCProvider } = Base;
|
||||
|
||||
export interface TOCProps {
|
||||
container?: ComponentProps<'div'>;
|
||||
trigger?: ComponentProps<'button'>;
|
||||
content?: ComponentProps<'div'>;
|
||||
|
||||
/**
|
||||
* Custom content in TOC container, before the main TOC
|
||||
*/
|
||||
header?: ReactNode;
|
||||
|
||||
/**
|
||||
* Custom content in TOC container, after the main TOC
|
||||
*/
|
||||
footer?: ReactNode;
|
||||
|
||||
/**
|
||||
* @defaultValue 'normal'
|
||||
*/
|
||||
style?: 'normal' | 'clerk';
|
||||
}
|
||||
|
||||
export function TOC({ container, trigger, content, header, footer, style }: TOCProps) {
|
||||
return (
|
||||
<PageTOCPopover {...container}>
|
||||
<PageTOCPopoverContent {...content}>
|
||||
{header}
|
||||
<Base.TOCScrollArea>
|
||||
{style === 'clerk' ? <TocClerk.TOCItems /> : <TocDefault.TOCItems />}
|
||||
</Base.TOCScrollArea>
|
||||
{footer}
|
||||
</PageTOCPopoverContent>
|
||||
<PageTOCPopoverTrigger {...trigger} />
|
||||
</PageTOCPopover>
|
||||
);
|
||||
}
|
||||
|
||||
function PageTOCPopover(props: ComponentProps<'div'>) {
|
||||
const [container, setContainer] = useState<HTMLElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const element = document.getElementById('flux-layout-slot');
|
||||
if (!element) return;
|
||||
setContainer(element);
|
||||
}, []);
|
||||
|
||||
if (!container) return;
|
||||
return createPortal(<PageTOCPopoverPhysical {...props} />, container);
|
||||
}
|
||||
|
||||
function PageTOCPopoverPhysical({ className, children, ...rest }: ComponentProps<'div'>) {
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const onClick = useEffectEvent((e: Event) => {
|
||||
if (!open) return;
|
||||
|
||||
if (ref.current && !ref.current.contains(e.target as HTMLElement)) setOpen(false);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('click', onClick);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('click', onClick);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<TocPopoverContext
|
||||
value={useMemo(
|
||||
() => ({
|
||||
open,
|
||||
setOpen,
|
||||
}),
|
||||
[setOpen, open],
|
||||
)}
|
||||
>
|
||||
<Collapsible
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
data-toc-popover=""
|
||||
className={cn('relative h-9 animate-fd-fade-in', className)}
|
||||
{...rest}
|
||||
>
|
||||
<header
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'absolute w-full bottom-0 border rounded-xl transition-colors bg-fd-secondary text-fd-secondary-foreground backdrop-blur-sm',
|
||||
open && 'shadow-lg bg-fd-popover/80 text-fd-popover-foreground',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</header>
|
||||
</Collapsible>
|
||||
</TocPopoverContext>
|
||||
);
|
||||
}
|
||||
|
||||
function PageTOCPopoverTrigger({ className, ...props }: ComponentProps<'button'>) {
|
||||
const { text } = useI18n();
|
||||
const { open } = use(TocPopoverContext)!;
|
||||
const items = Base.useItems();
|
||||
const selectedIdx = items.findIndex((item) => item.active);
|
||||
const path = useTreePath().at(-1);
|
||||
const spanProps = {
|
||||
transition: {
|
||||
duration: 0.1,
|
||||
},
|
||||
initial: {
|
||||
opacity: 0,
|
||||
y: 10,
|
||||
},
|
||||
animate: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
y: -10,
|
||||
},
|
||||
className: cn(open && 'text-fd-popover-foreground'),
|
||||
};
|
||||
|
||||
return (
|
||||
<CollapsibleTrigger
|
||||
className={cn(
|
||||
'flex w-full h-8.5 items-center text-sm text-fd-muted-foreground gap-2.5 px-2 text-start focus-visible:outline-none [&_svg]:size-4',
|
||||
className,
|
||||
)}
|
||||
data-toc-popover-trigger=""
|
||||
{...props}
|
||||
>
|
||||
<ProgressCircle
|
||||
value={(selectedIdx + 1) / Math.max(1, items.length)}
|
||||
max={1}
|
||||
className={cn('shrink-0', open && 'text-fd-primary')}
|
||||
/>
|
||||
<AnimatePresence mode="wait">
|
||||
{items[selectedIdx] && !open ? (
|
||||
<motion.span key={selectedIdx} {...spanProps}>
|
||||
{items[selectedIdx].original.title}
|
||||
</motion.span>
|
||||
) : path ? (
|
||||
<motion.span key={path.$id ?? ':pathId'} {...spanProps}>
|
||||
{path.name}
|
||||
</motion.span>
|
||||
) : (
|
||||
<motion.span key=":toc" {...spanProps}>
|
||||
{text.toc}
|
||||
</motion.span>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<ChevronDown className={cn('ms-auto shrink-0 transition-transform', open && 'rotate-180')} />
|
||||
</CollapsibleTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
interface ProgressCircleProps extends Omit<ComponentProps<'svg'>, 'strokeWidth'> {
|
||||
value: number;
|
||||
strokeWidth?: number;
|
||||
size?: number;
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
function clamp(input: number, min: number, max: number): number {
|
||||
if (input < min) return min;
|
||||
if (input > max) return max;
|
||||
return input;
|
||||
}
|
||||
|
||||
function ProgressCircle({
|
||||
value,
|
||||
strokeWidth = 2,
|
||||
size = 24,
|
||||
min = 0,
|
||||
max = 100,
|
||||
...restSvgProps
|
||||
}: ProgressCircleProps) {
|
||||
const normalizedValue = clamp(value, min, max);
|
||||
const radius = (size - strokeWidth) / 2;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const progress = (normalizedValue / max) * circumference;
|
||||
const circleProps = {
|
||||
cx: size / 2,
|
||||
cy: size / 2,
|
||||
r: radius,
|
||||
fill: 'none',
|
||||
strokeWidth,
|
||||
};
|
||||
|
||||
return (
|
||||
<svg
|
||||
role="progressbar"
|
||||
viewBox={`0 0 ${size} ${size}`}
|
||||
aria-valuenow={normalizedValue}
|
||||
aria-valuemin={min}
|
||||
aria-valuemax={max}
|
||||
{...restSvgProps}
|
||||
>
|
||||
<circle {...circleProps} className="stroke-current/25" />
|
||||
<circle
|
||||
{...circleProps}
|
||||
stroke="currentColor"
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={circumference - progress}
|
||||
strokeLinecap="round"
|
||||
transform={`rotate(-90 ${size / 2} ${size / 2})`}
|
||||
className="transition-all"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function PageTOCPopoverContent(props: ComponentProps<'div'>) {
|
||||
return (
|
||||
<CollapsibleContent data-toc-popover-content="" {...props}>
|
||||
<div className="flex flex-col px-2 max-h-[50vh]">{props.children}</div>
|
||||
</CollapsibleContent>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { cn } from '@/utils/cn';
|
||||
import type { ComponentProps } from 'react';
|
||||
|
||||
export function Container(props: ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
id="nd-flux-layout"
|
||||
{...props}
|
||||
className={cn('flex flex-col items-center pb-24 overflow-x-clip', props.className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+98
-36
@@ -1,13 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import * as Base from '@/components/sidebar/base';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { type ComponentProps, useEffect, useEffectEvent, useRef, useState } from 'react';
|
||||
import {
|
||||
type ComponentProps,
|
||||
type ReactNode,
|
||||
useEffect,
|
||||
useEffectEvent,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { createPageTreeRenderer } from '@/components/sidebar/page-tree';
|
||||
import {
|
||||
createPageTreeRenderer,
|
||||
type SidebarPageTreeComponents,
|
||||
} from '@/components/sidebar/page-tree';
|
||||
import { createLinkItemRenderer } from '@/components/sidebar/link-item';
|
||||
import { mergeRefs } from '@/utils/merge-refs';
|
||||
import { motion } from 'motion/react';
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
import { RemoveScroll } from 'react-remove-scroll';
|
||||
import { useFluxLayout } from '..';
|
||||
import { XIcon, SidebarIcon } from 'lucide-react';
|
||||
|
||||
const MotionSidebarItem = motion.create(Base.SidebarItem);
|
||||
const MotionSidebarFolderTrigger = motion.create(Base.SidebarFolderTrigger);
|
||||
@@ -34,35 +47,77 @@ function getItemOffset(depth: number) {
|
||||
return `calc(${2 + 3 * depth} * var(--spacing))`;
|
||||
}
|
||||
|
||||
export function Sidebar(props: ComponentProps<typeof Base.SidebarProvider>) {
|
||||
export interface SidebarProps extends ComponentProps<'aside'> {
|
||||
components?: Partial<SidebarPageTreeComponents>;
|
||||
banner?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
}
|
||||
|
||||
export type SidebarProviderProps = Base.SidebarProviderProps;
|
||||
|
||||
export const { useSidebar } = Base;
|
||||
|
||||
export function SidebarProvider(props: SidebarProviderProps) {
|
||||
return <Base.SidebarProvider {...props} />;
|
||||
}
|
||||
|
||||
export function SidebarFolder(props: ComponentProps<typeof Base.SidebarFolder>) {
|
||||
return <Base.SidebarFolder {...props} />;
|
||||
export function Sidebar({ footer, banner, components, ...rest }: SidebarProps) {
|
||||
const { menuItems } = useFluxLayout();
|
||||
|
||||
return (
|
||||
<SidebarContent {...rest}>
|
||||
<div className="flex flex-col gap-3 p-4 pb-2 empty:hidden">{banner}</div>
|
||||
<Base.SidebarViewport>
|
||||
<div className="flex flex-col">
|
||||
{menuItems
|
||||
.filter((v) => v.type !== 'icon')
|
||||
.map((item, i, list) => (
|
||||
<SidebarLinkItem
|
||||
key={i}
|
||||
item={item}
|
||||
className={cn(i === list.length - 1 && 'mb-4')}
|
||||
/>
|
||||
))}
|
||||
<SidebarPageTree {...components} />
|
||||
</div>
|
||||
</Base.SidebarViewport>
|
||||
{footer}
|
||||
</SidebarContent>
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarCollapseTrigger(props: ComponentProps<typeof Base.SidebarCollapseTrigger>) {
|
||||
return <Base.SidebarCollapseTrigger {...props} />;
|
||||
export function SidebarTrigger(props: ComponentProps<'button'>) {
|
||||
const { open, setOpen } = useSidebar();
|
||||
return (
|
||||
<button onClick={() => setOpen((prev) => !prev)} {...props}>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.span
|
||||
key={open ? 'open' : 'closed'}
|
||||
transition={{ duration: 0.2 }}
|
||||
initial={{
|
||||
y: '100%',
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
y: 0,
|
||||
opacity: 1,
|
||||
}}
|
||||
exit={{
|
||||
y: '100%',
|
||||
opacity: 0,
|
||||
}}
|
||||
>
|
||||
{open ? <XIcon /> : <SidebarIcon />}
|
||||
</motion.span>
|
||||
</AnimatePresence>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarViewport(props: ComponentProps<typeof Base.SidebarViewport>) {
|
||||
return <Base.SidebarViewport {...props} />;
|
||||
}
|
||||
|
||||
export function SidebarTrigger(props: ComponentProps<typeof Base.SidebarTrigger>) {
|
||||
return <Base.SidebarTrigger {...props} />;
|
||||
}
|
||||
|
||||
export function SidebarContent({
|
||||
ref: refProp,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ComponentProps<'aside'>) {
|
||||
function SidebarContent({ ref: refProp, className, children, ...props }: ComponentProps<'aside'>) {
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
const [blockScroll, setBlockScroll] = useState(false);
|
||||
const { open, setOpen } = Base.useSidebar();
|
||||
const { open, setOpen } = useSidebar();
|
||||
|
||||
const listener = useEffectEvent((e: KeyboardEvent) => {
|
||||
if (open && e.key === 'Escape') {
|
||||
@@ -136,12 +191,20 @@ export function SidebarContent({
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarSeparator({ className, style, children, ...props }: ComponentProps<'p'>) {
|
||||
function SidebarFolder(props: ComponentProps<typeof Base.SidebarFolder>) {
|
||||
return <Base.SidebarFolder {...props} />;
|
||||
}
|
||||
|
||||
function SidebarSeparator({ className, style, children, ...props }: ComponentProps<'p'>) {
|
||||
const depth = Base.useFolderDepth();
|
||||
|
||||
return (
|
||||
<Base.SidebarSeparator
|
||||
className={cn('[&_svg]:size-4 [&_svg]:shrink-0', className)}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-2 mb-1.5 px-2 mt-6 empty:mb-0 [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
depth === 0 && 'first:mt-0',
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
paddingInlineStart: getItemOffset(depth),
|
||||
...style,
|
||||
@@ -153,7 +216,7 @@ export function SidebarSeparator({ className, style, children, ...props }: Compo
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarItem({
|
||||
function SidebarItem({
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
@@ -175,7 +238,7 @@ export function SidebarItem({
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarFolderTrigger({
|
||||
function SidebarFolderTrigger({
|
||||
className,
|
||||
style,
|
||||
...props
|
||||
@@ -184,11 +247,11 @@ export function SidebarFolderTrigger({
|
||||
|
||||
return (
|
||||
<MotionSidebarFolderTrigger
|
||||
className={(s) =>
|
||||
className={(state) =>
|
||||
cn(
|
||||
itemVariants({ variant: collapsible ? 'button' : null }),
|
||||
'w-full',
|
||||
typeof className === 'function' ? className(s) : className,
|
||||
typeof className === 'function' ? className(state) : className,
|
||||
)
|
||||
}
|
||||
style={{
|
||||
@@ -202,7 +265,7 @@ export function SidebarFolderTrigger({
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarFolderLink({
|
||||
function SidebarFolderLink({
|
||||
className,
|
||||
style,
|
||||
...props
|
||||
@@ -223,7 +286,7 @@ export function SidebarFolderLink({
|
||||
);
|
||||
}
|
||||
|
||||
export function SidebarFolderContent({
|
||||
function SidebarFolderContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
@@ -233,13 +296,12 @@ export function SidebarFolderContent({
|
||||
|
||||
return (
|
||||
<MotionSidebarFolderContent
|
||||
className={(s) =>
|
||||
className={(state) =>
|
||||
cn(
|
||||
'relative',
|
||||
depth === 1 &&
|
||||
"before:content-[''] before:absolute before:w-px before:inset-y-1 before:bg-fd-border before:start-2.5",
|
||||
|
||||
typeof className === 'function' ? className(s) : className,
|
||||
typeof className === 'function' ? className(state) : className,
|
||||
)
|
||||
}
|
||||
{...(props as ComponentProps<typeof MotionSidebarFolderContent>)}
|
||||
@@ -263,7 +325,7 @@ export function SidebarFolderContent({
|
||||
);
|
||||
}
|
||||
|
||||
export const SidebarPageTree = createPageTreeRenderer({
|
||||
const SidebarPageTree = createPageTreeRenderer({
|
||||
SidebarFolder,
|
||||
SidebarFolderContent,
|
||||
SidebarSeparator,
|
||||
@@ -272,7 +334,7 @@ export const SidebarPageTree = createPageTreeRenderer({
|
||||
SidebarItem,
|
||||
});
|
||||
|
||||
export const SidebarLinkItem = createLinkItemRenderer({
|
||||
const SidebarLinkItem = createLinkItemRenderer({
|
||||
SidebarFolder,
|
||||
SidebarFolderContent,
|
||||
SidebarFolderLink,
|
||||
+9
-26
@@ -4,35 +4,24 @@ import { type ComponentProps, type ReactNode, useMemo, useState } from 'react';
|
||||
import Link from '@hanzo/docs-core/link';
|
||||
import { usePathname } from '@hanzo/docs-core/framework';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { isActive, normalize } from '@/utils/urls';
|
||||
import { useSidebar } from '@/components/sidebar/base';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import type { SidebarTab } from '@/components/sidebar/tabs';
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
import { isLayoutTabActive, type LayoutTab } from '../../shared';
|
||||
|
||||
export interface SidebarTabWithProps extends SidebarTab {
|
||||
props?: ComponentProps<'a'>;
|
||||
export interface TabDropdownProps extends ComponentProps<'button'> {
|
||||
placeholder?: ReactNode;
|
||||
tabs: LayoutTab[];
|
||||
}
|
||||
|
||||
export function SidebarTabsDropdown({
|
||||
options,
|
||||
placeholder,
|
||||
className,
|
||||
...props
|
||||
}: {
|
||||
placeholder?: ReactNode;
|
||||
options: SidebarTabWithProps[];
|
||||
} & ComponentProps<'button'>) {
|
||||
export function TabDropdown({ tabs, placeholder, className, ...props }: TabDropdownProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { closeOnRedirect } = useSidebar();
|
||||
const pathname = usePathname();
|
||||
const selectedIdx = useMemo(() => {
|
||||
return options.findLastIndex((item) => isTabActive(item, pathname));
|
||||
}, [options, pathname]);
|
||||
const selected = selectedIdx !== -1 ? options[selectedIdx] : undefined;
|
||||
return tabs.findLastIndex((item) => isLayoutTabActive(item, pathname));
|
||||
}, [tabs, pathname]);
|
||||
const selected = selectedIdx !== -1 ? tabs[selectedIdx] : undefined;
|
||||
|
||||
const onClick = () => {
|
||||
closeOnRedirect.current = false;
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
@@ -83,7 +72,7 @@ export function SidebarTabsDropdown({
|
||||
align="start"
|
||||
className="flex flex-col gap-1 max-w-svw p-1 fd-scroll-container"
|
||||
>
|
||||
{options.map((item, i) => {
|
||||
{tabs.map((item, i) => {
|
||||
const isActive = i === selectedIdx;
|
||||
if (!isActive && item.unlisted) return;
|
||||
|
||||
@@ -119,9 +108,3 @@ export function SidebarTabsDropdown({
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export function isTabActive(tab: SidebarTab, pathname: string) {
|
||||
if (tab.urls) return tab.urls.has(normalize(pathname));
|
||||
|
||||
return isActive(tab.url, pathname, true);
|
||||
}
|
||||
@@ -1,62 +1,87 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { type BaseLayoutProps, type NavOptions } from '@/layouts/shared';
|
||||
import { Header } from '@/layouts/home/client';
|
||||
import { renderer, type Renderer } from '@/utils/renderer';
|
||||
'use client';
|
||||
|
||||
export interface HomeLayoutProps extends BaseLayoutProps {
|
||||
Container?: Renderer<ComponentProps<'main'>>;
|
||||
nav?: Partial<
|
||||
NavOptions & {
|
||||
/**
|
||||
* Open mobile menu when hovering the trigger
|
||||
*/
|
||||
enableHoverToOpen?: boolean;
|
||||
}
|
||||
>;
|
||||
import type { BaseLayoutProps, NavOptions } from '@/layouts/shared';
|
||||
import { type ComponentProps, createContext, type FC, use } from 'react';
|
||||
import { baseSlots, useLinkItems, type BaseSlots, type BaseSlotsProps } from '@/layouts/shared';
|
||||
import type { LinkItemType } from '@/layouts/shared';
|
||||
import { Container } from './slots/container';
|
||||
import { Header } from './slots/header';
|
||||
|
||||
export interface HomeLayoutProps extends BaseLayoutProps, ComponentProps<'main'> {
|
||||
nav?: Nav;
|
||||
slots?: Partial<HomeSlots>;
|
||||
}
|
||||
|
||||
export function HomeLayout({
|
||||
nav = {},
|
||||
links,
|
||||
githubUrl,
|
||||
i18n,
|
||||
LanguageSwitch,
|
||||
ThemeSwitch,
|
||||
SearchToggle,
|
||||
LargeSearchToggle,
|
||||
themeSwitch = {},
|
||||
searchToggle,
|
||||
Container,
|
||||
children,
|
||||
...rest
|
||||
}: HomeLayoutProps & ComponentProps<'main'>) {
|
||||
const renderMain = renderer(Container ?? rest, 'main');
|
||||
interface Nav extends NavOptions {
|
||||
/**
|
||||
* Open mobile menu when hovering the trigger
|
||||
*/
|
||||
enableHoverToOpen?: boolean;
|
||||
}
|
||||
|
||||
return renderMain?.((t) => ({
|
||||
id: 'nd-home-layout',
|
||||
...t,
|
||||
...rest,
|
||||
className: cn('flex flex-1 flex-col [--fd-layout-width:1400px]', t?.className, rest.className),
|
||||
children: (
|
||||
<>
|
||||
{nav.enabled !== false &&
|
||||
(nav.component ?? (
|
||||
<Header
|
||||
links={links}
|
||||
nav={nav}
|
||||
themeSwitch={themeSwitch}
|
||||
searchToggle={searchToggle}
|
||||
i18n={i18n}
|
||||
LanguageSwitch={LanguageSwitch}
|
||||
ThemeSwitch={ThemeSwitch}
|
||||
SearchToggle={SearchToggle}
|
||||
LargeSearchToggle={LargeSearchToggle}
|
||||
githubUrl={githubUrl}
|
||||
/>
|
||||
))}
|
||||
export interface HomeSlots extends BaseSlots {
|
||||
header: FC<ComponentProps<'header'>>;
|
||||
container: FC<ComponentProps<'main'>>;
|
||||
}
|
||||
|
||||
const LayoutContext = createContext<{
|
||||
props: BaseSlotsProps<HomeLayoutProps>;
|
||||
navItems: LinkItemType[];
|
||||
menuItems: LinkItemType[];
|
||||
slots: HomeSlots;
|
||||
} | null>(null);
|
||||
|
||||
export function useHomeLayout() {
|
||||
const context = use(LayoutContext);
|
||||
if (!context)
|
||||
throw new Error('Please use this component under <HomeLayout /> (`@hanzo/docs-ui/layouts/home`).');
|
||||
return context;
|
||||
}
|
||||
|
||||
const { useProvider } = baseSlots({
|
||||
useProps() {
|
||||
return useHomeLayout().props;
|
||||
},
|
||||
});
|
||||
|
||||
export function HomeLayout(props: HomeLayoutProps) {
|
||||
const {
|
||||
nav: { enabled: navEnabled = true } = {},
|
||||
slots: defaultSlots,
|
||||
children,
|
||||
i18n: _i18n,
|
||||
githubUrl: _githubUrl,
|
||||
links: _links,
|
||||
themeSwitch: _themeSwitch,
|
||||
searchToggle: _searchToggle,
|
||||
...rest
|
||||
} = props;
|
||||
const { baseSlots, baseProps } = useProvider(props);
|
||||
const linkItems = useLinkItems(props);
|
||||
const slots: HomeSlots = {
|
||||
...baseSlots,
|
||||
header: defaultSlots?.header ?? InlineHeader,
|
||||
container: defaultSlots?.container ?? Container,
|
||||
};
|
||||
|
||||
return (
|
||||
<LayoutContext
|
||||
value={{
|
||||
props: baseProps,
|
||||
slots,
|
||||
...linkItems,
|
||||
}}
|
||||
>
|
||||
<slots.container {...rest}>
|
||||
{navEnabled && <slots.header />}
|
||||
{children}
|
||||
</>
|
||||
),
|
||||
}));
|
||||
</slots.container>
|
||||
</LayoutContext>
|
||||
);
|
||||
}
|
||||
|
||||
function InlineHeader(props: ComponentProps<'header'>) {
|
||||
const { nav } = useHomeLayout().props;
|
||||
if (nav?.component) return nav.component;
|
||||
return <Header {...props} />;
|
||||
}
|
||||
|
||||
@@ -9,35 +9,37 @@ import {
|
||||
NavigationMenuTrigger,
|
||||
type NavigationMenuTriggerProps,
|
||||
} from '@/components/ui/navigation-menu';
|
||||
import { navItemVariants } from './client';
|
||||
import { navItemVariants } from './slots/header';
|
||||
|
||||
export const NavbarMenu = NavigationMenuItem;
|
||||
|
||||
export function NavbarMenuContent({ className, ...props }: NavigationMenuContentProps) {
|
||||
export function NavbarMenuContent(props: NavigationMenuContentProps) {
|
||||
const { className, ...rest } = props;
|
||||
return (
|
||||
<NavigationMenuContent
|
||||
className={(s) =>
|
||||
{...rest}
|
||||
className={(state) =>
|
||||
cn(
|
||||
'grid grid-cols-1 gap-2 p-4 md:grid-cols-2 lg:grid-cols-3',
|
||||
typeof className === 'function' ? className(s) : className,
|
||||
'grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3',
|
||||
typeof className === 'function' ? className(state) : className,
|
||||
)
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
</NavigationMenuContent>
|
||||
);
|
||||
}
|
||||
|
||||
export function NavbarMenuTrigger({ className, ...props }: NavigationMenuTriggerProps) {
|
||||
export function NavbarMenuTrigger(props: NavigationMenuTriggerProps) {
|
||||
const { className, ...rest } = props;
|
||||
return (
|
||||
<NavigationMenuTrigger
|
||||
{...props}
|
||||
className={(s) =>
|
||||
{...rest}
|
||||
className={(state) =>
|
||||
cn(
|
||||
navItemVariants(),
|
||||
'text-sm rounded-md',
|
||||
typeof className === 'function' ? className(s) : className,
|
||||
typeof className === 'function' ? className(state) : className,
|
||||
)
|
||||
}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
export function Container(props: ComponentProps<'main'>) {
|
||||
return (
|
||||
<main
|
||||
id="nd-home-layout"
|
||||
{...props}
|
||||
className={cn('flex flex-1 flex-col [--fd-layout-width:1400px]', props.className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
'use client';
|
||||
|
||||
import { type ComponentProps, Fragment, useEffect, useEffectEvent, useRef, useState } from 'react';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import Link from '@hanzo/docs-core/link';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { type LinkItemType, LinkItem } from '@/layouts/shared';
|
||||
import {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
NavigationMenuViewport,
|
||||
} from '@/components/ui/navigation-menu';
|
||||
import { buttonVariants } from '@/components/ui/button';
|
||||
import { ChevronDown, Languages } from 'lucide-react';
|
||||
import { useIsScrollTop } from '@/utils/use-is-scroll-top';
|
||||
import { useHomeLayout } from '..';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||
import { mergeRefs } from '@/utils/merge-refs';
|
||||
|
||||
export const navItemVariants = cva('[&_svg]:size-4', {
|
||||
variants: {
|
||||
variant: {
|
||||
main: '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',
|
||||
button: buttonVariants({
|
||||
color: 'secondary',
|
||||
className: 'gap-1.5',
|
||||
}),
|
||||
icon: buttonVariants({
|
||||
color: 'ghost',
|
||||
size: 'icon',
|
||||
}),
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'main',
|
||||
},
|
||||
});
|
||||
|
||||
export function Header(props: ComponentProps<'header'>) {
|
||||
const {
|
||||
navItems,
|
||||
menuItems,
|
||||
slots,
|
||||
props: { nav },
|
||||
} = useHomeLayout();
|
||||
const headerRef = useRef<HTMLElement>(null);
|
||||
const listRef = useRef<HTMLUListElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const transparentMode = nav?.transparentMode ?? 'none';
|
||||
const isTop = useIsScrollTop({ enabled: transparentMode === 'top' }) ?? true;
|
||||
const isNavTransparent = transparentMode === 'top' ? isTop : transparentMode === 'always';
|
||||
|
||||
const onClick = useEffectEvent((e: Event) => {
|
||||
const element = headerRef.current;
|
||||
if (!open || !element) return;
|
||||
if (element !== e.target && !element.contains(e.target as HTMLElement)) {
|
||||
setOpen(false);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('click', onClick);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('click', onClick);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const list = (
|
||||
<NavigationMenuList
|
||||
ref={listRef}
|
||||
className="flex h-14 w-full mx-auto max-w-(--fd-layout-width) items-center px-4"
|
||||
>
|
||||
{slots.navTitle && (
|
||||
<slots.navTitle className="inline-flex items-center gap-2.5 font-semibold" />
|
||||
)}
|
||||
{nav?.children}
|
||||
<ul className="flex flex-row items-center gap-2 px-6 max-sm:hidden">
|
||||
{navItems
|
||||
.filter((item) => !isSecondary(item))
|
||||
.map((item, i) => (
|
||||
<NavigationMenuLinkItem key={i} item={item} className="text-sm" />
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex flex-row items-center justify-end gap-1.5 flex-1 max-lg:hidden">
|
||||
{slots.searchTrigger && (
|
||||
<slots.searchTrigger.full
|
||||
hideIfDisabled
|
||||
className="w-full rounded-full ps-2.5 max-w-[240px]"
|
||||
/>
|
||||
)}
|
||||
{slots.themeSwitch && <slots.themeSwitch />}
|
||||
{slots.languageSelect && (
|
||||
<slots.languageSelect.root>
|
||||
<Languages className="size-5" />
|
||||
</slots.languageSelect.root>
|
||||
)}
|
||||
<ul className="flex flex-row gap-2 items-center empty:hidden">
|
||||
{navItems.filter(isSecondary).map((item, i) => (
|
||||
<NavigationMenuLinkItem
|
||||
key={i}
|
||||
className={cn(item.type === 'icon' && '-mx-1 first:ms-0 last:me-0')}
|
||||
item={item}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex flex-row items-center ms-auto -me-1.5 lg:hidden">
|
||||
{slots.searchTrigger && <slots.searchTrigger.sm hideIfDisabled className="p-2" />}
|
||||
<CollapsibleTrigger
|
||||
aria-label="Toggle Menu"
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
size: 'icon',
|
||||
color: 'ghost',
|
||||
}),
|
||||
)}
|
||||
onPointerEnter={
|
||||
nav?.enableHoverToOpen
|
||||
? () => {
|
||||
setOpen(true);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<ChevronDown className={cn('transition-transform', open && 'rotate-180')} />
|
||||
</CollapsibleTrigger>
|
||||
</div>
|
||||
</NavigationMenuList>
|
||||
);
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
render={
|
||||
<header
|
||||
id="nd-nav"
|
||||
{...props}
|
||||
ref={mergeRefs(headerRef, props.ref)}
|
||||
className={cn('sticky h-14 top-0 z-40', props.className)}
|
||||
>
|
||||
<NavigationMenu
|
||||
className={(s) =>
|
||||
cn(
|
||||
'backdrop-blur-lg border-b transition-[box-shadow,background-color,border-radius]',
|
||||
open && 'max-lg:shadow-lg max-lg:rounded-b-2xl',
|
||||
(open || !isNavTransparent || s.open) && 'bg-fd-background/80',
|
||||
)
|
||||
}
|
||||
>
|
||||
{list}
|
||||
<CollapsibleContent className="mx-auto max-w-(--fd-layout-width) lg:hidden">
|
||||
<div className="flex flex-col pt-2 p-4 sm:flex-row sm:items-center sm:justify-end">
|
||||
{menuItems
|
||||
.filter((item) => !isSecondary(item))
|
||||
.map((item, i) => (
|
||||
<MobileNavigationMenuLinkItem key={i} item={item} className="sm:hidden" />
|
||||
))}
|
||||
<div className="-ms-1.5 flex flex-row items-center gap-2 max-sm:mt-2">
|
||||
{menuItems.filter(isSecondary).map((item, i) => (
|
||||
<MobileNavigationMenuLinkItem
|
||||
key={i}
|
||||
item={item}
|
||||
className={cn(item.type === 'icon' && '-mx-1 first:ms-0')}
|
||||
/>
|
||||
))}
|
||||
<div role="separator" className="flex-1" />
|
||||
{slots.languageSelect && (
|
||||
<slots.languageSelect.root>
|
||||
<Languages className="size-5" />
|
||||
{slots.languageSelect.text && <slots.languageSelect.text />}
|
||||
<ChevronDown className="size-3 text-fd-muted-foreground" />
|
||||
</slots.languageSelect.root>
|
||||
)}
|
||||
{slots.themeSwitch && <slots.themeSwitch />}
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
<NavigationMenuViewport side="bottom" anchor={listRef} />
|
||||
</NavigationMenu>
|
||||
</header>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function isSecondary(item: LinkItemType): boolean {
|
||||
if ('secondary' in item && item.secondary != null) return item.secondary;
|
||||
|
||||
return item.type === 'icon';
|
||||
}
|
||||
|
||||
function NavigationMenuLinkItem({ item, ...props }: { item: LinkItemType; className?: string }) {
|
||||
if (item.type === 'custom') return item.children;
|
||||
|
||||
if (item.type === 'menu') {
|
||||
const children = item.items.map((child, j) => {
|
||||
if (child.type === 'custom') {
|
||||
return <Fragment key={j}>{child.children}</Fragment>;
|
||||
}
|
||||
|
||||
const {
|
||||
banner = child.icon ? (
|
||||
<div className="w-fit rounded-md border bg-fd-muted p-1 [&_svg]:size-4">{child.icon}</div>
|
||||
) : null,
|
||||
...rest
|
||||
} = child.menu ?? {};
|
||||
|
||||
return (
|
||||
<NavigationMenuLink
|
||||
key={`${j}-${child.url}`}
|
||||
render={
|
||||
<Link
|
||||
href={child.url}
|
||||
external={child.external}
|
||||
{...rest}
|
||||
className={cn(
|
||||
'flex flex-col gap-2 rounded-lg border bg-fd-card p-3 transition-colors hover:bg-fd-accent/80 hover:text-fd-accent-foreground',
|
||||
rest.className,
|
||||
)}
|
||||
>
|
||||
{rest.children ?? (
|
||||
<>
|
||||
{banner}
|
||||
<p className="text-base font-medium">{child.text}</p>
|
||||
<p className="text-sm text-fd-muted-foreground empty:hidden">
|
||||
{child.description}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<NavigationMenuItem {...props}>
|
||||
<NavigationMenuTrigger className={cn(navItemVariants(), 'rounded-md')}>
|
||||
{item.url ? (
|
||||
<Link href={item.url} external={item.external}>
|
||||
{item.text}
|
||||
</Link>
|
||||
) : (
|
||||
item.text
|
||||
)}
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent className="grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-3">
|
||||
{children}
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationMenuItem {...props}>
|
||||
<NavigationMenuLink
|
||||
render={
|
||||
<LinkItem
|
||||
item={item}
|
||||
aria-label={item.type === 'icon' ? item.label : undefined}
|
||||
className={cn(navItemVariants({ variant: item.type }))}
|
||||
>
|
||||
{item.type === 'icon' ? item.icon : item.text}
|
||||
</LinkItem>
|
||||
}
|
||||
/>
|
||||
</NavigationMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileNavigationMenuLinkItem({
|
||||
item,
|
||||
...props
|
||||
}: {
|
||||
item: LinkItemType;
|
||||
className?: string;
|
||||
}) {
|
||||
if (item.type === 'custom')
|
||||
return <div className={cn('grid', props.className)}>{item.children}</div>;
|
||||
|
||||
if (item.type === 'menu') {
|
||||
const header = (
|
||||
<>
|
||||
{item.icon}
|
||||
{item.text}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={cn('mb-4 flex flex-col', props.className)}>
|
||||
<p className="mb-1 text-sm text-fd-muted-foreground">
|
||||
{item.url ? (
|
||||
<Link href={item.url} external={item.external}>
|
||||
{header}
|
||||
</Link>
|
||||
) : (
|
||||
header
|
||||
)}
|
||||
</p>
|
||||
{item.items.map((child, i) => (
|
||||
<MobileNavigationMenuLinkItem key={i} item={child} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkItem
|
||||
item={item}
|
||||
className={cn(
|
||||
{
|
||||
main: 'inline-flex items-center gap-2 py-1.5 transition-colors hover:text-fd-popover-foreground/50 data-[active=true]:font-medium data-[active=true]:text-fd-primary [&_svg]:size-4',
|
||||
icon: buttonVariants({
|
||||
size: 'icon',
|
||||
color: 'ghost',
|
||||
}),
|
||||
button: buttonVariants({
|
||||
color: 'secondary',
|
||||
className: 'gap-1.5 [&_svg]:size-4',
|
||||
}),
|
||||
}[item.type ?? 'main'],
|
||||
props.className,
|
||||
)}
|
||||
aria-label={item.type === 'icon' ? item.label : undefined}
|
||||
>
|
||||
{item.icon}
|
||||
{item.type === 'icon' ? undefined : item.text}
|
||||
</LinkItem>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
'use client';
|
||||
<<<<<<< HEAD
|
||||
import { cn } from '@/utils/cn';
|
||||
import {
|
||||
type ComponentProps,
|
||||
@@ -16,18 +17,63 @@ import { useSidebar } from '@/components/sidebar/base';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import Link from '@hanzo/docs-core/link';
|
||||
import { usePathname } from '@hanzo/docs-core/framework';
|
||||
=======
|
||||
import { type ComponentProps, createContext, type FC, use } from 'react';
|
||||
import { type DocsLayoutProps } from '.';
|
||||
>>>>>>> dev
|
||||
import { useIsScrollTop } from '@/utils/use-is-scroll-top';
|
||||
import { LinkItem, type LinkItemType, type MenuItemType } from '@/utils/link-item';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { isTabActive, type SidebarTabWithProps } from '@/components/sidebar/tabs/dropdown';
|
||||
import { renderer, type Renderer } from '@/utils/renderer';
|
||||
import { type LinkItemType } from '@/layouts/shared';
|
||||
import {
|
||||
baseSlots,
|
||||
type BaseSlots,
|
||||
type BaseSlotsProps,
|
||||
type LayoutTab,
|
||||
useLinkItems,
|
||||
} from '../shared';
|
||||
import { TreeContextProvider } from '@/contexts/tree';
|
||||
import { Container } from './slots/container';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarCollapseTrigger,
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
type SidebarProps,
|
||||
type SidebarProviderProps,
|
||||
} from './slots/sidebar';
|
||||
import { Header } from './slots/header';
|
||||
|
||||
const LayoutContext = createContext<
|
||||
| (LayoutInfo & {
|
||||
isNavTransparent: boolean;
|
||||
})
|
||||
| null
|
||||
>(null);
|
||||
export interface DocsSlots extends BaseSlots {
|
||||
container: FC<ComponentProps<'div'>>;
|
||||
header: FC<ComponentProps<'header'>>;
|
||||
sidebar: {
|
||||
provider: FC<SidebarProviderProps>;
|
||||
root: FC<SidebarProps>;
|
||||
trigger: FC<ComponentProps<'button'>>;
|
||||
collapseTrigger: FC<ComponentProps<'button'>>;
|
||||
useSidebar: () => { collapsed: boolean; open: boolean; setOpen: (V: boolean) => void };
|
||||
};
|
||||
}
|
||||
|
||||
const { useProvider } = baseSlots({
|
||||
useProps() {
|
||||
return useNotebookLayout().props;
|
||||
},
|
||||
});
|
||||
|
||||
interface SlotsProps extends BaseSlotsProps<DocsLayoutProps> {
|
||||
sidebar: SidebarProps;
|
||||
tabMode: NonNullable<DocsLayoutProps['tabMode']>;
|
||||
tabs: LayoutTab[];
|
||||
}
|
||||
|
||||
const LayoutContext = createContext<{
|
||||
props: SlotsProps;
|
||||
isNavTransparent: boolean;
|
||||
navItems: LinkItemType[];
|
||||
menuItems: LinkItemType[];
|
||||
slots: DocsSlots;
|
||||
} | null>(null);
|
||||
|
||||
export function useNotebookLayout() {
|
||||
const context = use(LayoutContext);
|
||||
@@ -38,230 +84,61 @@ export function useNotebookLayout() {
|
||||
return context;
|
||||
}
|
||||
|
||||
export interface LayoutInfo {
|
||||
tabMode: 'sidebar' | 'navbar';
|
||||
navMode: 'top' | 'auto';
|
||||
}
|
||||
|
||||
export function LayoutContextProvider({
|
||||
navTransparentMode = 'none',
|
||||
navMode,
|
||||
tabMode,
|
||||
children,
|
||||
}: LayoutInfo & {
|
||||
navTransparentMode?: 'always' | 'top' | 'none';
|
||||
children: ReactNode;
|
||||
}) {
|
||||
export function LayoutBody(
|
||||
props: Omit<DocsLayoutProps, 'tabs'> & {
|
||||
tabs: LayoutTab[];
|
||||
},
|
||||
) {
|
||||
const {
|
||||
nav: { enabled: navEnabled = true, transparentMode: navTransparentMode = 'none' } = {},
|
||||
sidebar: { defaultOpenLevel, prefetch, ...sidebarProps } = {},
|
||||
slots: defaultSlots,
|
||||
tabMode = 'sidebar',
|
||||
tabs,
|
||||
tree,
|
||||
containerProps,
|
||||
children,
|
||||
} = props;
|
||||
const isTop = useIsScrollTop({ enabled: navTransparentMode === 'top' }) ?? true;
|
||||
const isNavTransparent = navTransparentMode === 'top' ? isTop : navTransparentMode === 'always';
|
||||
const { baseSlots, baseProps } = useProvider(props);
|
||||
const linkItems = useLinkItems(props);
|
||||
const slots: DocsSlots = {
|
||||
...baseSlots,
|
||||
header: defaultSlots?.header ?? Header,
|
||||
container: defaultSlots?.container ?? Container,
|
||||
sidebar: defaultSlots?.sidebar ?? {
|
||||
provider: SidebarProvider,
|
||||
root: Sidebar,
|
||||
trigger: SidebarTrigger,
|
||||
collapseTrigger: SidebarCollapseTrigger,
|
||||
useSidebar,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<LayoutContext
|
||||
value={useMemo(
|
||||
() => ({
|
||||
<TreeContextProvider tree={tree}>
|
||||
<LayoutContext
|
||||
value={{
|
||||
props: {
|
||||
tabs,
|
||||
tabMode,
|
||||
sidebar: sidebarProps,
|
||||
...baseProps,
|
||||
},
|
||||
isNavTransparent,
|
||||
navMode,
|
||||
tabMode,
|
||||
}),
|
||||
[isNavTransparent, navMode, tabMode],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</LayoutContext>
|
||||
);
|
||||
}
|
||||
|
||||
export function LayoutHeader(props: ComponentProps<'header'>) {
|
||||
const { open } = useSidebar();
|
||||
const { isNavTransparent } = useNotebookLayout();
|
||||
|
||||
return (
|
||||
<header data-transparent={isNavTransparent && !open} {...props}>
|
||||
{props.children}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export function LayoutBody({
|
||||
_,
|
||||
children,
|
||||
}: {
|
||||
_: Renderer<ComponentProps<'div'>>;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const { navMode } = useNotebookLayout();
|
||||
const { collapsed } = useSidebar();
|
||||
const pageCol =
|
||||
'calc(var(--fd-layout-width,97rem) - var(--fd-sidebar-col) - var(--fd-toc-width))';
|
||||
const render = renderer(_, 'div');
|
||||
|
||||
return render?.(
|
||||
(t) =>
|
||||
({
|
||||
id: 'nd-notebook-layout',
|
||||
children,
|
||||
...t,
|
||||
className: cn(
|
||||
'grid overflow-x-clip min-h-(--fd-docs-height) transition-[grid-template-columns] auto-cols-auto auto-rows-auto [--fd-docs-height:100dvh] [--fd-header-height:0px] [--fd-toc-popover-height:0px] [--fd-sidebar-width:0px] [--fd-toc-width:0px]',
|
||||
t?.className,
|
||||
),
|
||||
style: {
|
||||
gridTemplate:
|
||||
navMode === 'top'
|
||||
? `". header header header ."
|
||||
"sidebar sidebar toc-popover toc-popover ."
|
||||
"sidebar sidebar main toc ." 1fr / minmax(min-content, 1fr) var(--fd-sidebar-col) minmax(0, ${pageCol}) var(--fd-toc-width) minmax(min-content, 1fr)`
|
||||
: `"sidebar sidebar header header ."
|
||||
"sidebar sidebar toc-popover toc-popover ."
|
||||
"sidebar sidebar main toc ." 1fr / minmax(min-content, 1fr) var(--fd-sidebar-col) minmax(0, ${pageCol}) var(--fd-toc-width) minmax(min-content, 1fr)`,
|
||||
'--fd-docs-row-1': 'var(--fd-banner-height, 0px)',
|
||||
'--fd-docs-row-2': 'calc(var(--fd-docs-row-1) + var(--fd-header-height))',
|
||||
'--fd-docs-row-3': 'calc(var(--fd-docs-row-2) + var(--fd-toc-popover-height))',
|
||||
'--fd-sidebar-col': collapsed ? '0px' : 'var(--fd-sidebar-width)',
|
||||
...t?.style,
|
||||
},
|
||||
}) as ComponentProps<'div'>,
|
||||
);
|
||||
}
|
||||
|
||||
export function LayoutHeaderTabs({
|
||||
options,
|
||||
className,
|
||||
...props
|
||||
}: ComponentProps<'div'> & {
|
||||
options: SidebarTabWithProps[];
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const selectedIdx = useMemo(() => {
|
||||
return options.findLastIndex((option) => isTabActive(option, pathname));
|
||||
}, [options, pathname]);
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-row items-end gap-6', className)} {...props}>
|
||||
{options.map((option, i) => {
|
||||
const { title, url, unlisted, props: { className, ...rest } = {} } = option;
|
||||
const isSelected = selectedIdx === i;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={i}
|
||||
href={url}
|
||||
className={cn(
|
||||
'inline-flex border-b-2 border-transparent transition-colors items-center pb-1.5 font-medium gap-2 text-fd-muted-foreground text-sm text-nowrap hover:text-fd-accent-foreground',
|
||||
unlisted && !isSelected && 'hidden',
|
||||
isSelected && 'border-fd-primary text-fd-primary',
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{title}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function NavbarLinkItem({
|
||||
item,
|
||||
className,
|
||||
...props
|
||||
}: { item: LinkItemType } & HTMLAttributes<HTMLElement>) {
|
||||
if (item.type === 'custom') return item.children;
|
||||
|
||||
if (item.type === 'menu') {
|
||||
return <NavbarLinkItemMenu item={item} className={className} {...props} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkItem
|
||||
item={item}
|
||||
className={cn(
|
||||
'text-sm text-fd-muted-foreground transition-colors hover:text-fd-accent-foreground data-[active=true]:text-fd-primary',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{item.text}
|
||||
</LinkItem>
|
||||
);
|
||||
}
|
||||
|
||||
function NavbarLinkItemMenu({
|
||||
item,
|
||||
hoverDelay = 50,
|
||||
className,
|
||||
...props
|
||||
}: { item: MenuItemType; hoverDelay?: number } & HTMLAttributes<HTMLElement>) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const timeoutRef = useRef<number>(null);
|
||||
const freezeUntil = useRef<number>(null);
|
||||
|
||||
const delaySetOpen = (value: boolean) => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
setOpen(value);
|
||||
freezeUntil.current = Date.now() + 300;
|
||||
}, hoverDelay);
|
||||
};
|
||||
const onPointerEnter = (e: PointerEvent) => {
|
||||
if (e.pointerType === 'touch') return;
|
||||
delaySetOpen(true);
|
||||
};
|
||||
const onPointerLeave = (e: PointerEvent) => {
|
||||
if (e.pointerType === 'touch') return;
|
||||
delaySetOpen(false);
|
||||
};
|
||||
function isTouchDevice() {
|
||||
return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={open}
|
||||
onOpenChange={(value) => {
|
||||
if (freezeUntil.current === null || Date.now() >= freezeUntil.current) setOpen(value);
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5 p-1 text-sm text-fd-muted-foreground transition-colors has-data-[active=true]:text-fd-primary data-[state=open]:text-fd-accent-foreground focus-visible:outline-none',
|
||||
className,
|
||||
)}
|
||||
onPointerEnter={onPointerEnter}
|
||||
onPointerLeave={onPointerLeave}
|
||||
{...props}
|
||||
slots,
|
||||
...linkItems,
|
||||
}}
|
||||
>
|
||||
{item.url ? <LinkItem item={item as never}>{item.text}</LinkItem> : item.text}
|
||||
<ChevronDown className="size-3" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="flex flex-col p-1 text-fd-muted-foreground text-start"
|
||||
onPointerEnter={onPointerEnter}
|
||||
onPointerLeave={onPointerLeave}
|
||||
>
|
||||
{item.items.map((child, i) => {
|
||||
if (child.type === 'custom') return <Fragment key={i}>{child.children}</Fragment>;
|
||||
|
||||
return (
|
||||
<LinkItem
|
||||
key={i}
|
||||
item={child}
|
||||
className="inline-flex items-center gap-2 rounded-md p-2 transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground data-[active=true]:text-fd-primary [&_svg]:size-4"
|
||||
onClick={() => {
|
||||
if (isTouchDevice()) setOpen(false);
|
||||
}}
|
||||
>
|
||||
{child.icon}
|
||||
{child.text}
|
||||
</LinkItem>
|
||||
);
|
||||
})}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<slots.sidebar.provider defaultOpenLevel={defaultOpenLevel} prefetch={prefetch}>
|
||||
<slots.container {...containerProps}>
|
||||
{navEnabled && <slots.header />}
|
||||
<slots.sidebar.root {...sidebarProps} />
|
||||
{children}
|
||||
</slots.container>
|
||||
</slots.sidebar.provider>
|
||||
</LayoutContext>
|
||||
</TreeContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user