mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
UI: DocsCategory support page tree
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
---
|
||||
'fumadocs-ui': major
|
||||
---
|
||||
|
||||
**`DocsCategory` now accept `from` prop instead of `pages` prop.**
|
||||
|
||||
**why:** This allows sharing the order of items with page tree.
|
||||
**migrate:**
|
||||
|
||||
The component now takes `from` prop which is the Source API object.
|
||||
|
||||
```tsx
|
||||
import { source } from '@/lib/source';
|
||||
import { DocsCategory } from 'fumadocs-ui/page';
|
||||
|
||||
const page = source.getPage(params.slug);
|
||||
|
||||
<DocsCategory page={page} from={source} />
|
||||
```
|
||||
@@ -69,9 +69,7 @@ export default async function Page(props: {
|
||||
UIOnly: params.slug[0] === 'ui' ? Fragment : () => undefined,
|
||||
}}
|
||||
/>
|
||||
{page.data.index ? (
|
||||
<DocsCategory page={page} pages={source.getPages()} />
|
||||
) : null}
|
||||
{page.data.index ? <DocsCategory page={page} from={source} /> : null}
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
);
|
||||
|
||||
@@ -162,6 +162,33 @@ The input/source of your content, it can be a CMS, or local data layers like [Co
|
||||
</Card>
|
||||
```
|
||||
|
||||
### Better Category Component
|
||||
|
||||
The original `DocsCategory` component was copied from our official docs, which is a very simple implementation that doesn't take page tree into account.
|
||||
|
||||
Now it accepts the source object via `from` prop.
|
||||
|
||||
```tsx title="page.tsx"
|
||||
import { source } from '@/lib/source';
|
||||
import { DocsCategory } from 'fumadocs-ui/page';
|
||||
|
||||
const page = source.getPage(params.slug);
|
||||
|
||||
<DocsCategory page={page} from={source} />;
|
||||
```
|
||||
|
||||
By default, it takes the `locale` property from `page` to find the corresponding page tree to traverse.
|
||||
You can also force a page tree.
|
||||
|
||||
```tsx title="page.tsx (i18n enabled)"
|
||||
import { source } from '@/lib/source';
|
||||
import { DocsCategory } from 'fumadocs-ui/page';
|
||||
|
||||
const page = source.getPage(params.slug, params.lang);
|
||||
|
||||
<DocsCategory page={page} from={source} tree={source.pageTree['en']} />;
|
||||
```
|
||||
|
||||
### OpenAPI Tag Display Name
|
||||
|
||||
Change the display name with `x-displayName`.
|
||||
|
||||
@@ -23,17 +23,24 @@ import {
|
||||
</DocsPage>;
|
||||
```
|
||||
|
||||
<Callout type='info' title='Good to know'>
|
||||
|
||||
Instead of rendering the title with `DocsTitle` or your custom component in `page.tsx`, you can put the title into MDX file.
|
||||
This will render the title in the MDX body.
|
||||
|
||||
</Callout>
|
||||
|
||||
### Page
|
||||
|
||||
The main layout of docs pages.
|
||||
|
||||
### Title
|
||||
|
||||
Page title.
|
||||
Optional, display the page title.
|
||||
|
||||
### Description
|
||||
|
||||
Page description.
|
||||
Optional, display the page description.
|
||||
|
||||
### Body
|
||||
|
||||
@@ -51,17 +58,30 @@ import { DocsBody } from 'fumadocs-ui/page';
|
||||
|
||||
### Category
|
||||
|
||||
Optional, display the other pages in its folder.
|
||||
Optional, display the other pages in its folder in page tree.
|
||||
|
||||
> You can use this component without `<DocsPage />`.
|
||||
|
||||
```tsx
|
||||
import { DocsPage, DocsCategory } from 'fumadocs-ui/page';
|
||||
import { getPages } from '@/lib/source';
|
||||
```tsx title="page.tsx"
|
||||
import { source } from '@/lib/source';
|
||||
import { DocsCategory } from 'fumadocs-ui/page';
|
||||
|
||||
<DocsPage>
|
||||
<DocsCategory page={page} pages={getPages()} />
|
||||
</DocsPage>;
|
||||
const page = source.getPage(['...']);
|
||||
|
||||
<DocsCategory page={page} from={source} />;
|
||||
```
|
||||
|
||||
By default, it takes the `locale` property from `page` to find the corresponding page tree to traverse.
|
||||
You can also specify it.
|
||||
|
||||
```tsx title="page.tsx (i18n enabled)"
|
||||
import { source } from '@/lib/source';
|
||||
import { DocsCategory } from 'fumadocs-ui/page';
|
||||
|
||||
const page = source.getPage(['...'], 'cn');
|
||||
|
||||
// force to use English page tree
|
||||
<DocsCategory page={page} from={source} tree={source.pageTree['en']} />;
|
||||
```
|
||||
|
||||
## Configurations
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
}
|
||||
],
|
||||
"devDependencies": {
|
||||
"lucide-react": "^0.446.0"
|
||||
"lucide-react": "^0.447.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-accordion": "^1.2.1",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
}
|
||||
],
|
||||
"devDependencies": {
|
||||
"lucide-react": "^0.446.0"
|
||||
"lucide-react": "^0.447.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
}
|
||||
],
|
||||
"devDependencies": {
|
||||
"lucide-react": "^0.446.0"
|
||||
"lucide-react": "^0.447.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
}
|
||||
],
|
||||
"devDependencies": {
|
||||
"lucide-react": "^0.446.0"
|
||||
"lucide-react": "^0.447.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
}
|
||||
],
|
||||
"devDependencies": {
|
||||
"lucide-react": "^0.446.0"
|
||||
"lucide-react": "^0.447.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"class-variance-authority": "^0.7.0",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
}
|
||||
],
|
||||
"devDependencies": {
|
||||
"lucide-react": "^0.446.0"
|
||||
"lucide-react": "^0.447.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "",
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
"files": [
|
||||
{
|
||||
"imports": {},
|
||||
"content": "'use client';\n\nimport Image, { type ImageProps } from 'next/image';\nimport { type ImgHTMLAttributes } from 'react';\nimport './image-zoom.css';\nimport Zoom, { type UncontrolledProps } from 'react-medium-image-zoom';\n\nexport type ImageZoomProps = ImageProps & {\n /**\n * Image props when zoom in\n */\n zoomInProps?: ImgHTMLAttributes<HTMLImageElement>;\n\n /**\n * Props for `react-medium-image-zoom`\n */\n rmiz?: UncontrolledProps;\n};\n\nfunction getImageSrc(src: ImageProps['src']): string {\n if (typeof src === 'string') return src;\n if ('default' in src) return src.default.src;\n return src.src;\n}\n\nexport function ImageZoom({\n zoomInProps,\n children,\n rmiz,\n ...props\n}: ImageZoomProps): React.ReactElement {\n return (\n <Zoom\n zoomMargin={20}\n wrapElement=\"span\"\n {...rmiz}\n zoomImg={{\n src: getImageSrc(props.src),\n sizes: undefined,\n ...zoomInProps,\n }}\n >\n {children ?? (\n <Image\n sizes=\"(max-width: 768px) 100vw, (max-width: 1200px) 70vw, 900px\"\n {...props}\n />\n )}\n </Zoom>\n );\n}\n",
|
||||
"path": "components:image-zoom.tsx"
|
||||
"content": "[data-rmiz] {\n display: block;\n position: relative;\n}\n\n[data-rmiz-ghost] {\n pointer-events: none;\n position: absolute;\n}\n\n[data-rmiz-btn-zoom],\n[data-rmiz-btn-unzoom] {\n display: none;\n}\n\n[data-rmiz-content='found'] img {\n cursor: zoom-in;\n}\n\n[data-rmiz-modal][open] {\n width: 100vw /* fallback */;\n width: 100dvw;\n\n height: 100vh /* fallback */;\n height: 100dvh;\n\n background-color: transparent;\n max-width: none;\n max-height: none;\n margin: 0;\n padding: 0;\n position: fixed;\n overflow: hidden;\n}\n\n[data-rmiz-modal]:focus-visible {\n outline: none;\n}\n\n[data-rmiz-modal-overlay] {\n transition: background-color 0.3s;\n position: absolute;\n inset: 0;\n}\n\n[data-rmiz-modal-overlay='hidden'] {\n background-color: transparent;\n}\n\n[data-rmiz-modal-overlay='visible'] {\n @apply bg-fd-background;\n}\n\n[data-rmiz-modal-content] {\n width: 100%;\n height: 100%;\n position: relative;\n}\n\n[data-rmiz-modal]::backdrop {\n display: none;\n}\n\n[data-rmiz-modal-img] {\n cursor: zoom-out;\n image-rendering: high-quality;\n transform-origin: 0 0;\n transition: transform 0.3s;\n position: absolute;\n}\n\n@media (prefers-reduced-motion: reduce) {\n [data-rmiz-modal-overlay],\n [data-rmiz-modal-img] {\n transition-duration: 0.01ms !important;\n }\n}\n",
|
||||
"path": "components:image-zoom.css"
|
||||
},
|
||||
{
|
||||
"imports": {},
|
||||
"content": "[data-rmiz] {\n display: block;\n position: relative;\n}\n\n[data-rmiz-ghost] {\n pointer-events: none;\n position: absolute;\n}\n\n[data-rmiz-btn-zoom],\n[data-rmiz-btn-unzoom] {\n display: none;\n}\n\n[data-rmiz-content='found'] img {\n cursor: zoom-in;\n}\n\n[data-rmiz-modal][open] {\n width: 100vw /* fallback */;\n width: 100dvw;\n\n height: 100vh /* fallback */;\n height: 100dvh;\n\n background-color: transparent;\n max-width: none;\n max-height: none;\n margin: 0;\n padding: 0;\n position: fixed;\n overflow: hidden;\n}\n\n[data-rmiz-modal]:focus-visible {\n outline: none;\n}\n\n[data-rmiz-modal-overlay] {\n transition: background-color 0.3s;\n position: absolute;\n inset: 0;\n}\n\n[data-rmiz-modal-overlay='hidden'] {\n background-color: transparent;\n}\n\n[data-rmiz-modal-overlay='visible'] {\n @apply bg-fd-background;\n}\n\n[data-rmiz-modal-content] {\n width: 100%;\n height: 100%;\n position: relative;\n}\n\n[data-rmiz-modal]::backdrop {\n display: none;\n}\n\n[data-rmiz-modal-img] {\n cursor: zoom-out;\n image-rendering: high-quality;\n transform-origin: 0 0;\n transition: transform 0.3s;\n position: absolute;\n}\n\n@media (prefers-reduced-motion: reduce) {\n [data-rmiz-modal-overlay],\n [data-rmiz-modal-img] {\n transition-duration: 0.01ms !important;\n }\n}\n",
|
||||
"path": "components:image-zoom.css"
|
||||
"content": "'use client';\n\nimport Image, { type ImageProps } from 'next/image';\nimport { type ImgHTMLAttributes } from 'react';\nimport './image-zoom.css';\nimport Zoom, { type UncontrolledProps } from 'react-medium-image-zoom';\n\nexport type ImageZoomProps = ImageProps & {\n /**\n * Image props when zoom in\n */\n zoomInProps?: ImgHTMLAttributes<HTMLImageElement>;\n\n /**\n * Props for `react-medium-image-zoom`\n */\n rmiz?: UncontrolledProps;\n};\n\nfunction getImageSrc(src: ImageProps['src']): string {\n if (typeof src === 'string') return src;\n if ('default' in src) return src.default.src;\n return src.src;\n}\n\nexport function ImageZoom({\n zoomInProps,\n children,\n rmiz,\n ...props\n}: ImageZoomProps): React.ReactElement {\n return (\n <Zoom\n zoomMargin={20}\n wrapElement=\"span\"\n {...rmiz}\n zoomImg={{\n src: getImageSrc(props.src),\n sizes: undefined,\n ...zoomInProps,\n }}\n >\n {children ?? (\n <Image\n sizes=\"(max-width: 768px) 100vw, (max-width: 1200px) 70vw, 900px\"\n {...props}\n />\n )}\n </Zoom>\n );\n}\n",
|
||||
"path": "components:image-zoom.tsx"
|
||||
}
|
||||
],
|
||||
"devDependencies": {},
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
}
|
||||
],
|
||||
"devDependencies": {
|
||||
"lucide-react": "^0.446.0"
|
||||
"lucide-react": "^0.447.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"fumadocs-core": "",
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
}
|
||||
],
|
||||
"devDependencies": {
|
||||
"lucide-react": "^0.446.0"
|
||||
"lucide-react": "^0.447.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "",
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"@oramacloud/client": "^1.3.16",
|
||||
"react": "",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"lucide-react": "^0.446.0",
|
||||
"lucide-react": "^0.447.0",
|
||||
"fumadocs-ui": "",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"remark": "^15.0.1",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
}
|
||||
],
|
||||
"devDependencies": {
|
||||
"lucide-react": "^0.446.0"
|
||||
"lucide-react": "^0.447.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "",
|
||||
|
||||
@@ -64,6 +64,8 @@ export interface Page<Data = PageData> {
|
||||
slugs: string[];
|
||||
url: string;
|
||||
data: Data;
|
||||
|
||||
locale?: string | undefined;
|
||||
}
|
||||
|
||||
export interface Meta<Data = MetaData> {
|
||||
@@ -315,5 +317,6 @@ function fileToPage<Data = PageData>(
|
||||
url: getUrl(file.data.slugs, locale),
|
||||
slugs: file.data.slugs,
|
||||
data: file.data.data as Data,
|
||||
locale,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import slash from '@/utils/slash';
|
||||
|
||||
export interface FileInfo {
|
||||
/**
|
||||
* The locale extension of file
|
||||
*/
|
||||
locale?: string;
|
||||
|
||||
/**
|
||||
|
||||
+43
-14
@@ -1,7 +1,7 @@
|
||||
import { type TableOfContents } from 'fumadocs-core/server';
|
||||
import { type PageTree, type TableOfContents } from 'fumadocs-core/server';
|
||||
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import type { Page } from 'fumadocs-core/source';
|
||||
import type { LoaderConfig, LoaderOutput, Page } from 'fumadocs-core/source';
|
||||
import { type AnchorProviderProps, AnchorProvider } from 'fumadocs-core/toc';
|
||||
import { Card, Cards } from '@/components/card';
|
||||
import type { EditOnGitHubOptions } from '@/components/layout/edit-on-github';
|
||||
@@ -214,24 +214,53 @@ DocsTitle.displayName = 'DocsTitle';
|
||||
|
||||
export function DocsCategory({
|
||||
page,
|
||||
pages,
|
||||
from,
|
||||
tree: forcedTree,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement> & {
|
||||
page: Page;
|
||||
pages: Page[];
|
||||
}): React.ReactElement {
|
||||
const filtered = pages.filter(
|
||||
(item) =>
|
||||
(item.file.dirname === page.file.dirname &&
|
||||
item.file.name !== page.file.name) ||
|
||||
(item.file.name === 'index' &&
|
||||
item.file.dirname.split('/').slice(0, -1).join('/') ===
|
||||
page.file.dirname),
|
||||
);
|
||||
from: LoaderOutput<LoaderConfig>;
|
||||
tree?: PageTree.Root;
|
||||
}): React.ReactNode {
|
||||
const tree =
|
||||
forcedTree ??
|
||||
(from._i18n
|
||||
? (from as LoaderOutput<LoaderConfig & { i18n: true }>).pageTree[
|
||||
page.locale ?? from._i18n.defaultLanguage
|
||||
]
|
||||
: from.pageTree);
|
||||
|
||||
function findParent(
|
||||
node: PageTree.Root | PageTree.Folder,
|
||||
): PageTree.Root | PageTree.Folder | undefined {
|
||||
if ('index' in node && node.index?.$ref?.file === page.file.path) {
|
||||
return node;
|
||||
}
|
||||
|
||||
for (const child of node.children) {
|
||||
if (child.type === 'folder') {
|
||||
const parent = findParent(child);
|
||||
if (parent) return parent;
|
||||
}
|
||||
|
||||
if (child.type === 'page' && child.$ref?.file === page.file.path) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const parent = findParent(tree);
|
||||
if (!parent) return null;
|
||||
|
||||
const items = parent.children.flatMap<Page>((item) => {
|
||||
if (item.type !== 'page' || item.url === page.url) return [];
|
||||
|
||||
return from.getNodePage(item) ?? [];
|
||||
});
|
||||
|
||||
return (
|
||||
<Cards {...props}>
|
||||
{filtered.map((item) => (
|
||||
{items.map((item) => (
|
||||
<Card
|
||||
key={item.url}
|
||||
title={item.data.title}
|
||||
|
||||
Reference in New Issue
Block a user