UI: DocsCategory support page tree

This commit is contained in:
fuma-nama
2024-10-03 19:05:59 +08:00
parent 7c6cf07163
commit be53a0e205
18 changed files with 139 additions and 40 deletions
+19
View File
@@ -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} />
```
+1 -3
View File
@@ -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>
);
+27
View File
@@ -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`.
+29 -9
View File
@@ -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
+1 -1
View File
@@ -22,7 +22,7 @@
}
],
"devDependencies": {
"lucide-react": "^0.446.0"
"lucide-react": "^0.447.0"
},
"dependencies": {
"@radix-ui/react-accordion": "^1.2.1",
+1 -1
View File
@@ -16,7 +16,7 @@
}
],
"devDependencies": {
"lucide-react": "^0.446.0"
"lucide-react": "^0.447.0"
},
"dependencies": {
"react": "",
+1 -1
View File
@@ -15,7 +15,7 @@
}
],
"devDependencies": {
"lucide-react": "^0.446.0"
"lucide-react": "^0.447.0"
},
"dependencies": {
"react": "",
+1 -1
View File
@@ -30,7 +30,7 @@
}
],
"devDependencies": {
"lucide-react": "^0.446.0"
"lucide-react": "^0.447.0"
},
"dependencies": {
"react": "",
+1 -1
View File
@@ -23,7 +23,7 @@
}
],
"devDependencies": {
"lucide-react": "^0.446.0"
"lucide-react": "^0.447.0"
},
"dependencies": {
"class-variance-authority": "^0.7.0",
+1 -1
View File
@@ -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": "",
+1 -1
View File
@@ -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": "",
+3
View File
@@ -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,
};
}
+3
View File
@@ -1,6 +1,9 @@
import slash from '@/utils/slash';
export interface FileInfo {
/**
* The locale extension of file
*/
locale?: string;
/**
+43 -14
View File
@@ -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}