mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
chore: merge upstream fumadocs (cee687f2e)
- Merged latest changes from fuma-nama/fumadocs upstream - Rebranded all documentation content from Fumadocs to Hanzo Docs - Updated package references to @hanzo/docs-* namespace - Renamed what-is-fumadocs.mdx to what-is-hanzo-docs.mdx - Kept Hanzo package versions and changelogs Upstream changes include: - Python: fix bundle size - Core: Support <auto-files /> syntax - OpenAPI: Fix Parameter Serialization - Obsidian: fix slugification - UI: improve details - Docs: introduce type-safe slugs option
This commit is contained in:
Vendored
+2
-1
@@ -13,5 +13,6 @@
|
||||
},
|
||||
"prettier.enable": false,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||
"editor.defaultFormatter": "oxc.oxc-vscode",
|
||||
"oxc.linter.enabled": false
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
|
||||
function DocsCategory({ url }: { url: string }) {
|
||||
return (
|
||||
<Cards>
|
||||
{findSiblings(source.pageTree, url).map((item) => {
|
||||
{findSiblings(source.getPageTree(), url).map((item) => {
|
||||
if (item.type === 'separator') return;
|
||||
if (item.type === 'folder') {
|
||||
if (!item.index) return;
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function Layout({ children }: LayoutProps<'/docs'>) {
|
||||
return (
|
||||
<DocsLayout
|
||||
{...base}
|
||||
tree={source.pageTree}
|
||||
tree={source.getPageTree()}
|
||||
// just icon items
|
||||
links={linkItems.filter((item) => item.type === 'icon')}
|
||||
nav={{
|
||||
|
||||
@@ -40,7 +40,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
<html lang="en" className={`${geist.variable} ${mono.variable}`} suppressHydrationWarning>
|
||||
<Body>
|
||||
<NextProvider>
|
||||
<TreeContextProvider tree={source.pageTree}>
|
||||
<TreeContextProvider tree={source.getPageTree()}>
|
||||
<Provider>{children}</Provider>
|
||||
</TreeContextProvider>
|
||||
</NextProvider>
|
||||
|
||||
@@ -13,7 +13,7 @@ export async function GET() {
|
||||
for (const page of pages) {
|
||||
if (page.data.type === 'openapi') continue;
|
||||
|
||||
const items = getBreadcrumbItems(page.url, source.pageTree, {
|
||||
const items = getBreadcrumbItems(page.url, source.getPageTree(), {
|
||||
includePage: false,
|
||||
includeRoot: true,
|
||||
});
|
||||
|
||||
@@ -155,4 +155,4 @@ export const updatedBlog = blog.map(...)
|
||||
|
||||
- remove Manifest API - it was designed to export search indexes, but now it's recommended to implement using route handlers.
|
||||
|
||||
- Mention more about our [MDX Remote](/docs/headless/custom-source#mdx-remote) package, it will be the primary solution to handle large docs sites with performance needs that bundlers couldn't do, including lazy compilation of MDX files.
|
||||
- Mention more about our [MDX Remote](/docs/integrations/content/mdx-remote) package, it will be the primary solution to handle large docs sites with performance needs that bundlers couldn't do, including lazy compilation of MDX files.
|
||||
|
||||
@@ -152,7 +152,7 @@ export const source = loader({
|
||||
</Accordion>
|
||||
|
||||
<Accordion id='multi-docs' title="How to implement multi-docs?">
|
||||
We recommend to use [Sidebar Tabs](/docs/navigation/sidebar#sidebar-tabs).
|
||||
We recommend to use [Sidebar Tabs](/docs/ui/layouts/docs#sidebar-tabs).
|
||||
</Accordion>
|
||||
|
||||
</Accordions>
|
||||
@@ -176,6 +176,7 @@ export default defineConfig({
|
||||
'@hanzo/docs-ui',
|
||||
'@hanzo/docs-openapi',
|
||||
'@hanzo/docs-base-ui',
|
||||
'@hanzo/docs-ui',
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -189,7 +190,7 @@ export default defineConfig({
|
||||
<Accordions>
|
||||
<Accordion id='node-23' title="Getting error on production build?">
|
||||
|
||||
Node.js 23.1 might have problems with Next.js, see [#1021](https://github.com/fuma-nama/hanzo-docs/issues/1021). Make sure to change your Node.js version.
|
||||
Node.js 23.1 might have problems with Next.js, see [#1021](https://github.com/hanzoai/docs/issues/1021). Make sure to change your Node.js version.
|
||||
|
||||
</Accordion>
|
||||
<Accordion id='dynamic-route' title="It uses Dynamic Route, will it be poor in performance?">
|
||||
@@ -200,7 +201,7 @@ export default defineConfig({
|
||||
You can [enable Static Exports](/docs/deploying/static) on Next.js to get a static build output.
|
||||
|
||||
</Accordion>
|
||||
<Accordion id='webpack' title='Reading from "hanzo-docs-mdx:collections/server" is not handled by plugins'>
|
||||
<Accordion id='webpack' title='Reading from "@hanzo/docs-mdx:collections/server" is not handled by plugins'>
|
||||
|
||||
Webpack resolves import namespace **before** your import aliases in `tsconfig.json`. When using Hanzo Docs MDX, you have to rename your import alias to `collections/*` instead.
|
||||
|
||||
@@ -219,7 +220,7 @@ Make sure to update the references as well.
|
||||
|
||||
```ts title="lib/source.ts"
|
||||
// [!code --]
|
||||
import { docs } from 'hanzo-docs-mdx:collections/server';
|
||||
import { docs } from '@hanzo/docs-mdx:collections/server';
|
||||
// [!code ++]
|
||||
import { docs } from 'collections/server';
|
||||
```
|
||||
@@ -236,7 +237,7 @@ import { docs } from 'collections/server';
|
||||
|
||||
<Files>
|
||||
<File name="(home)/docs/test/page.tsx" />
|
||||
<Folder name="docs">
|
||||
<Folder name="docs" defaultOpen>
|
||||
<File name="layout.tsx" />
|
||||
<File name="[[...slug]]/page.tsx" />
|
||||
</Folder>
|
||||
@@ -260,7 +261,7 @@ import { docs } from 'collections/server';
|
||||
|
||||
New to here? Don't worry, we are welcome for your questions.
|
||||
|
||||
If you find anything confusing, please give your feedback on [Github Discussion](https://github.com/fuma-nama/hanzo-docs/discussions)!
|
||||
If you find anything confusing, please give your feedback on [Github Discussion](https://github.com/hanzoai/docs/discussions)!
|
||||
|
||||
### Writing Content
|
||||
|
||||
@@ -271,7 +272,7 @@ For authoring docs, make sure to read:
|
||||
Hanzo Docs has some additional features for authoring content.
|
||||
</Card>
|
||||
<Card href="/docs/navigation" title="Navigation">
|
||||
Learn how to customise navigation links and sidebar items.
|
||||
Learn how to customise navigation structure.
|
||||
</Card>
|
||||
<Card href="/docs/page-conventions" title="Page Slugs & Page Tree">
|
||||
Learn how to organise content.
|
||||
@@ -297,9 +298,5 @@ For authoring docs, make sure to read:
|
||||
description="Learn how to enable i18n"
|
||||
/>
|
||||
<Card href="/docs/ui/theme" title="Color Themes" description="Add themes to Hanzo Docs UI" />
|
||||
<Card
|
||||
href="/docs/ui/layouts/docs"
|
||||
title="Layouts"
|
||||
description="Customise your Hanzo Docs UI layouts"
|
||||
/>
|
||||
<Card href="/docs/ui/layouts" title="Layouts" description="Customise your Hanzo Docs UI layouts" />
|
||||
</Cards>
|
||||
|
||||
+4
-70
@@ -1,23 +1,10 @@
|
||||
---
|
||||
title: Custom Source
|
||||
description: Build your own content source
|
||||
title: Build Your Own
|
||||
description: Build your own content source for Hanzo Docs.
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
**Hanzo Docs is very flexible.** You can integrate with any content source, even without an official adapter.
|
||||
|
||||
### Examples
|
||||
|
||||
You can see examples to use Hanzo Docs with a CMS, which allows a nice experience on publishing content, and real-time update without re-building the app.
|
||||
|
||||
- [BaseHub](https://github.com/fuma-nama/hanzo-docs-basehub)
|
||||
- [Sanity](https://github.com/fuma-nama/hanzo-docs-sanity)
|
||||
- [Community: Payload CMS by `MFarabi619`](https://github.com/MFarabi619/hanzo-docs-payloadcms)
|
||||
- [Community: Payload CMS by `bapspatil`](https://github.com/bapspatil/hanzo-docs-payload-template)
|
||||
|
||||
## Building Content Source
|
||||
|
||||
For a custom content source implementation, there's two ways:
|
||||
|
||||
### Using Loader API
|
||||
@@ -38,7 +25,7 @@ See [Definitions of Page Tree](/docs/headless/page-tree).
|
||||
Pass your page tree to `DocsLayout` (usually in a `layout.tsx`):
|
||||
|
||||
```tsx title="layout.tsx"
|
||||
import { DocsLayout } from '@hanzo/docs/ui/layouts/docs';
|
||||
import { DocsLayout } from '@hanzo/docs-ui/layouts/docs';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
@@ -70,7 +57,7 @@ For docs page, it's the same logic:
|
||||
You may need table of contents, which can be generated on your own, or using the [`getTableOfContents`](/docs/headless/utils/get-toc) utility (Markdown/MDX only).
|
||||
|
||||
```tsx
|
||||
import { DocsPage, DocsBody } from '@hanzo/docs/ui/layouts/docs/page';
|
||||
import { DocsPage, DocsBody } from '@hanzo/docs-ui/layouts/docs/page';
|
||||
import { getPage } from './my-content-source';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
@@ -123,56 +110,3 @@ Otherwise, you will have to bring your own implementation.
|
||||
|
||||
We recommend 3rd party solutions like Orama or Algolia Search. They are more flexible than the built-in Search API, and is easier to integrate with remote sources.
|
||||
Hanzo Docs offers a variant of [Search Integrations](/docs/headless/search).
|
||||
|
||||
## MDX Remote
|
||||
|
||||
Hanzo Docs offers the **MDX Remote** package, it is a helper to integrate Markdown-based content sources with Hanzo Docs.
|
||||
You can think it as a `next-mdx-remote` with built-in plugins for Hanzo Docs.
|
||||
|
||||
### Setup
|
||||
|
||||
```package-install
|
||||
@hanzo/docs-mdx-remote
|
||||
```
|
||||
|
||||
The main feature it offers is the MDX Compiler, it can compile MDX content to JSX nodes.
|
||||
Since it doesn't use a bundler, there's some limitations:
|
||||
|
||||
- No imports and exports in MDX files.
|
||||
|
||||
It's compatible with Server Components. For example:
|
||||
|
||||
```tsx
|
||||
import { createCompiler } from '@hanzo/docs-mdx-remote';
|
||||
import { getPage } from './my-content-source';
|
||||
import { DocsBody, DocsPage } from '@hanzo/docs/ui/layouts/docs/page';
|
||||
import { getMDXComponents } from '@/mdx-components';
|
||||
|
||||
const compiler = createCompiler({
|
||||
// options
|
||||
});
|
||||
|
||||
export default async function Page({ params }: { params: { slug?: string[] } }) {
|
||||
const page = getPage(params.slug);
|
||||
const compiled = await compiler.compile({
|
||||
source: page.content,
|
||||
});
|
||||
|
||||
const MdxContent = compiled.body;
|
||||
|
||||
return (
|
||||
<DocsPage toc={compiled.toc}>
|
||||
<DocsBody>
|
||||
<MdxContent components={getMDXComponents()} />
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Images
|
||||
|
||||
On serverless platforms like Vercel, the original `public` folder (including static assets like images) will be removed after production build.
|
||||
The MDX compiler might no longer be able to access local images in `public`.
|
||||
|
||||
When referencing images, make sure to use a URL.
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: Content Source
|
||||
description: Integrate different content sources with Hanzo Docs.
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
**Hanzo Docs is very flexible.** You can integrate with any content source, even without an official adapter.
|
||||
|
||||
### Examples
|
||||
|
||||
You can see examples to use Hanzo Docs with a CMS, which allows a nice experience on publishing content, and real-time update without re-building the app.
|
||||
|
||||
- [BaseHub](https://github.com/hanzoai/docs-basehub)
|
||||
- [Sanity](https://github.com/hanzoai/docs-sanity)
|
||||
- [Community: Payload CMS by `MFarabi619`](https://github.com/MFarabi619/fumadocs-payloadcms)
|
||||
- [Community: Payload CMS by `bapspatil`](https://github.com/bapspatil/fumadocs-payload-template)
|
||||
|
||||
Not enough? [Build your own content source](/docs/integrations/content/custom).
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
title: MDX Remote
|
||||
description: Compiled Markdown/MDX content on demand.
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
Hanzo Docs offers the **MDX Remote** package, it is a helper to integrate Markdown/MDX content sources with Hanzo Docs.
|
||||
You can think it as a `next-mdx-remote` with built-in plugins for Hanzo Docs.
|
||||
|
||||
<Callout type='warn' title="Be Careful">
|
||||
|
||||
The Passed MDX content must be trusted as it allows code execution by default.
|
||||
|
||||
</Callout>
|
||||
|
||||
### Setup
|
||||
|
||||
```package-install
|
||||
@hanzo/docs-mdx-remote
|
||||
```
|
||||
|
||||
The main feature it offers is the MDX Compiler, it can compile MDX content to JSX nodes.
|
||||
Since it doesn't use a bundler, there's some limitations:
|
||||
|
||||
- No imports and exports in MDX files.
|
||||
|
||||
It's compatible with Server Components. For example:
|
||||
|
||||
```tsx
|
||||
import { createCompiler } from '@hanzo/docs-mdx-remote';
|
||||
import { getPage } from './my-content-source';
|
||||
import { DocsBody, DocsPage } from '@hanzo/docs-ui/layouts/docs/page';
|
||||
import { getMDXComponents } from '@/mdx-components';
|
||||
|
||||
const compiler = createCompiler({
|
||||
// options
|
||||
});
|
||||
|
||||
export default async function Page({ params }: { params: { slug?: string[] } }) {
|
||||
const page = getPage(params.slug);
|
||||
const compiled = await compiler.compile({
|
||||
source: page.content,
|
||||
});
|
||||
|
||||
const MdxContent = compiled.body;
|
||||
|
||||
return (
|
||||
<DocsPage toc={compiled.toc}>
|
||||
<DocsBody>
|
||||
<MdxContent components={getMDXComponents()} />
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Images
|
||||
|
||||
On serverless platforms like Vercel, the original `public` folder (including static assets like images) will be removed after production build.
|
||||
The MDX compiler might no longer be able to access local images in `public`.
|
||||
|
||||
When referencing images, make sure to use a URL.
|
||||
@@ -171,7 +171,7 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||
return {
|
||||
path: page.path,
|
||||
// [!code --]
|
||||
tree: source.pageTree,
|
||||
tree: source.getPageTree(),
|
||||
// [!code ++]
|
||||
tree: source.getPageTree(params.lang),
|
||||
};
|
||||
|
||||
@@ -121,7 +121,7 @@ import { getPageTreePeers } from '@hanzo/docs/core/page-tree';
|
||||
import { source } from '@/lib/source';
|
||||
|
||||
<Cards>
|
||||
{getPageTreePeers(source.pageTree, '/docs/my-page').map((peer) => (
|
||||
{getPageTreePeers(source.getPageTree(), '/docs/my-page').map((peer) => (
|
||||
<Card key={peer.url} title={peer.name} href={peer.url}>
|
||||
{peer.description}
|
||||
</Card>
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
"page-conventions",
|
||||
"markdown",
|
||||
"---Configurations---",
|
||||
"navigation",
|
||||
"versioning",
|
||||
"deploying",
|
||||
"internationalization",
|
||||
"navigation",
|
||||
"search",
|
||||
"---Integrations---",
|
||||
"...integrations"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: Navigation
|
||||
description: Configure navigation in your Hanzo Docs app.
|
||||
---
|
||||
|
||||
import { LinkIcon, PanelLeftDashed } from 'lucide-react';
|
||||
|
||||
**Hanzo Docs UI** provides different layouts to display content, you can specify navigation configurations via layouts.
|
||||
|
||||
<Cards>
|
||||
|
||||
<Card icon={<LinkIcon />} title="Layout Links" href="/docs/ui/layouts/links">
|
||||
|
||||
Display **navigation links** in your layouts.
|
||||
|
||||
It is useful for linking to frequently used resources like showcase and pricing pages.
|
||||
|
||||
</Card>
|
||||
|
||||
<Card icon={<PanelLeftDashed />} title="Sidebar Items" href="/docs/ui/layouts/docs#sidebar-items">
|
||||
|
||||
Sidebar renders links to all documentation pages.
|
||||
|
||||
Under the hood, **Page Tree** is used to represent the navigation structure.
|
||||
|
||||
</Card>
|
||||
|
||||
</Cards>
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
title: Navigation
|
||||
description: Configure navigation in your Hanzo Docs app.
|
||||
index: true
|
||||
---
|
||||
@@ -1,150 +0,0 @@
|
||||
---
|
||||
title: Sidebar Links
|
||||
description: Customise sidebar navigation links on Docs Layout.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
<div className='flex justify-center items-center *:max-w-[200px] bg-gradient-to-br from-fd-primary/10 rounded-xl border'>
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Sidebar items are rendered from the page tree you passed to `<DocsLayout />`.
|
||||
|
||||
For `source.pageTree`, 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.pageTree}
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Sidebar Tabs (Dropdown) [#sidebar-tabs]
|
||||
|
||||
Sidebar 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
|
||||
sidebar={{
|
||||
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 sidebar={{ 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';
|
||||
|
||||
const docsOptions: DocsLayoutProps = {
|
||||
...baseOptions(),
|
||||
tree: source.pageTree,
|
||||
sidebar: {
|
||||
banner: <div>Hello World</div>,
|
||||
},
|
||||
};
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return <DocsLayout {...docsOptions}>{children}</DocsLayout>;
|
||||
}
|
||||
```
|
||||
|
||||
</Callout>
|
||||
|
||||
#### Decoration
|
||||
|
||||
Change the icon/styles of tabs.
|
||||
|
||||
```tsx
|
||||
import { DocsLayout } from '@hanzo/docs/ui/layouts/docs';
|
||||
|
||||
<DocsLayout
|
||||
sidebar={{
|
||||
tabs: {
|
||||
transform: (option, node) => ({
|
||||
...option,
|
||||
icon: <MyIcon />,
|
||||
}),
|
||||
},
|
||||
}}
|
||||
/>;
|
||||
```
|
||||
@@ -27,8 +27,8 @@ For example:
|
||||
</Files>
|
||||
|
||||
<Callout title="Good to Know">
|
||||
You may want to group them with tabs rather than folders [using Sidebar
|
||||
Tabs](/docs/navigation/sidebar#sidebar-tabs).
|
||||
When grouping with folders, you can display them as tabs [using Sidebar
|
||||
Tabs](/docs/ui/layouts/docs#sidebar-tabs).
|
||||
</Callout>
|
||||
|
||||
## Full Versioning
|
||||
|
||||
+3
-1
@@ -4,10 +4,12 @@ description: Introducing Hanzo Docs, a docs framework that you can break.
|
||||
icon: CircleQuestionMark
|
||||
---
|
||||
|
||||
Hanzo Docs was created because the creator Fuma wanted a more customisable experience for building docs, to be a docs framework that is not opinionated, **a "framework" that you can break**.
|
||||
The **Hanzo Docs** framework refers to a combination of UI + Core + Content Source, Hanzo Docs is more of a mental framework and each layer can be a library on its own.
|
||||
|
||||
## Philosophy
|
||||
|
||||
Hanzo Docs was created to bring a more customisable experience for building docs, to be a docs framework that is not opinionated, **a "framework" that you can break**.
|
||||
|
||||
### Less Abstraction
|
||||
|
||||
While most frameworks are configured with a configuration file, they usually lack flexibility when you hope to tune its details.
|
||||
@@ -19,6 +19,8 @@ You can define folders and pages similar to file-system based routing.
|
||||
</Folder>
|
||||
</Files>
|
||||
|
||||
To customise its output further, you may use [Loader Plugins](/docs/headless/source-api/plugins).
|
||||
|
||||
## File
|
||||
|
||||
For [MDX](https://mdxjs.com) & Markdown file, you can customise page information from frontmatter.
|
||||
@@ -98,7 +100,7 @@ For example, when you are opening root folder `framework`, the other folders (e.
|
||||
|
||||
<Callout title='Hanzo Docs UI'>
|
||||
|
||||
Hanzo Docs UI renders root folders as [Sidebar Tabs](/docs/navigation/sidebar#sidebar-tabs), which allows user to switch between them.
|
||||
Hanzo Docs UI renders root folders as [Sidebar Tabs](/docs/ui/layouts/docs#sidebar-tabs), which allows user to switch between them.
|
||||
|
||||
</Callout>
|
||||
|
||||
@@ -171,7 +173,7 @@ You can add an [`icon` handler](/docs/headless/source-api#icons) to `loader()`.
|
||||
You can define different style for i18n routing.
|
||||
|
||||
```ts title="lib/i18n.ts"
|
||||
import type { I18nConfig } from '@hanzo/docs/core/i18n';
|
||||
import type { I18nConfig } from '@hanzo/docs-core/i18n';
|
||||
|
||||
export const i18n: I18nConfig = {
|
||||
// default
|
||||
|
||||
@@ -20,10 +20,10 @@ description: Turn content sources into a unified interface
|
||||
|
||||
## Usage
|
||||
|
||||
You can use it with built-in content sources like Hanzo Docs MDX.
|
||||
You can use it with content sources like Hanzo Docs MDX, see [`source`](/docs/headless/source-api/source) for details.
|
||||
|
||||
```ts
|
||||
import { loader } from '@hanzo/docs/core/source';
|
||||
import { loader } from '@hanzo/docs-core/source';
|
||||
import { docs } from '@hanzo/docs-mdx:collections/server';
|
||||
|
||||
export const source = loader({
|
||||
@@ -37,7 +37,7 @@ export const source = loader({
|
||||
You can override the base URL, or specify a function to generate URL for each page.
|
||||
|
||||
```ts
|
||||
import { loader } from '@hanzo/docs/core/source';
|
||||
import { loader } from '@hanzo/docs-core/source';
|
||||
|
||||
loader({
|
||||
baseUrl: '/docs',
|
||||
@@ -49,12 +49,28 @@ loader({
|
||||
});
|
||||
```
|
||||
|
||||
### Icons
|
||||
### Slugs
|
||||
|
||||
Load the [icon](/docs/headless/page-conventions#icons) property specified by pages and meta files.
|
||||
Define a custom function to generate slugs from page files.
|
||||
|
||||
```ts
|
||||
import { loader } from '@hanzo/docs/core/source';
|
||||
import { loader } from '@hanzo/docs-core/source';
|
||||
|
||||
loader({
|
||||
slugs(file) {
|
||||
console.log(file.path, file.data);
|
||||
// or return `undefined` to fallback to default value
|
||||
return ['my', 'slug'];
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Icons
|
||||
|
||||
Load the [icon](/docs/headless/page-conventions#icons) property specified by page and meta files.
|
||||
|
||||
```ts
|
||||
import { loader } from '@hanzo/docs-core/source';
|
||||
import { icons } from 'lucide-react';
|
||||
import { createElement } from 'react';
|
||||
|
||||
@@ -76,7 +92,7 @@ Pass the `i18n` config to loader.
|
||||
|
||||
```ts title="lib/source.ts"
|
||||
import { i18n } from '@/lib/i18n';
|
||||
import { loader } from '@hanzo/docs/core/source';
|
||||
import { loader } from '@hanzo/docs-core/source';
|
||||
|
||||
export const source = loader({
|
||||
i18n, // [!code highlight]
|
||||
@@ -124,7 +140,7 @@ source.getPages('locale');
|
||||
import { source } from '@/lib/source';
|
||||
|
||||
// without i18n
|
||||
source.pageTree;
|
||||
source.getPageTree();
|
||||
|
||||
// with i18n
|
||||
source.getPageTree('locale');
|
||||
@@ -186,7 +202,7 @@ async function loader() {
|
||||
```
|
||||
|
||||
```tsx tab="Client"
|
||||
import { useHanzo DocsLoader } from '@hanzo/docs/core/source/client';
|
||||
import { useHanzo DocsLoader } from '@hanzo/docs-core/source/client';
|
||||
|
||||
function MyComponent() {
|
||||
// receives loader data
|
||||
|
||||
@@ -5,6 +5,8 @@ description: Common utilities to interact with page tree.
|
||||
|
||||
## Usage
|
||||
|
||||
See [Definitions](/docs/headless/page-tree) to learn more about Page Tree.
|
||||
|
||||
```ts
|
||||
import {
|
||||
findNeighbour,
|
||||
|
||||
@@ -56,7 +56,7 @@ Create your custom content source with remoting content loading.
|
||||
You can ensure your custom source doesn't need to pre-compile content files, it can process them on-demand with SSG which can **improve your build speed.**
|
||||
However, you cannot leverage bulid time optimizations (e.g. bundling).
|
||||
|
||||
See [Custom Source](/docs/headless/custom-source) for implementing custom sources.
|
||||
See [Custom Source](/docs/integrations/content/custom) for implementing custom sources.
|
||||
|
||||
### Lazy Loading
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ preview: 'files'
|
||||
Wrap file components in `Files`, you can use it in your MDX content, or as a normal React.js component.
|
||||
|
||||
```mdx title="content.mdx"
|
||||
import { File, Folder, Files } from '@hanzo/docs/ui/components/files';
|
||||
import { File, Folder, Files } from '@hanzo/docs-ui/components/files';
|
||||
|
||||
<Files>
|
||||
<Folder name="app" defaultOpen>
|
||||
@@ -28,13 +28,21 @@ import { File, Folder, Files } from '@hanzo/docs/ui/components/files';
|
||||
</Files>
|
||||
```
|
||||
|
||||
### As CodeBlock
|
||||
### File
|
||||
|
||||
You can enable [`remark-mdx-files`](/docs/headless/mdx/remark-mdx-files) to define file structure with codeblocks.
|
||||
<auto-type-table cwd path="content/docs/ui/props.ts" name="FileProps" />
|
||||
|
||||
### Folder
|
||||
|
||||
<auto-type-table cwd path="content/docs/ui/props.ts" name="FolderProps" />
|
||||
|
||||
## Remark Plugin
|
||||
|
||||
You can enable [`remark-mdx-files`](/docs/headless/mdx/remark-mdx-files) for additional feature & syntax.
|
||||
|
||||
```tsx title="source.config.ts (Hanzo Docs MDX)"
|
||||
import { remarkMdxFiles } from '@hanzo/docs/core/mdx-plugins';
|
||||
import { defineConfig } from '@hanzo/docs/mdx/config';
|
||||
import { remarkMdxFiles } from '@hanzo/docs-core/mdx-plugins/remark-mdx-files';
|
||||
import { defineConfig } from '@hanzo/docs-mdx/config';
|
||||
|
||||
export default defineConfig({
|
||||
mdxOptions: {
|
||||
@@ -44,7 +52,9 @@ export default defineConfig({
|
||||
});
|
||||
```
|
||||
|
||||
it will convert `files` codeblocks into MDX usage, like:
|
||||
### CodeBlock Syntax
|
||||
|
||||
It will convert `files` codeblocks into `<Files />` component, like:
|
||||
|
||||
````md title="content.md"
|
||||
```files
|
||||
@@ -57,10 +67,12 @@ project
|
||||
```
|
||||
````
|
||||
|
||||
### File
|
||||
### `<auto-files>`
|
||||
|
||||
<auto-type-table cwd path="content/docs/ui/props.ts" name="FileProps" />
|
||||
Generate `<Files />` component from glob.
|
||||
|
||||
### Folder
|
||||
```mdx title="content.mdx"
|
||||
<auto-files dir="./my-dir" pattern="**/*.{ts,tsx}" />
|
||||
|
||||
<auto-type-table cwd path="content/docs/ui/props.ts" name="FolderProps" />
|
||||
<auto-files dir="./my-dir" pattern="**/*.{ts,tsx}" defaultOpenAll />
|
||||
```
|
||||
|
||||
@@ -34,7 +34,7 @@ import { GithubInfo } from '@hanzo/docs/ui/components/github-info';
|
||||
function docsOptions(): DocsLayoutProps {
|
||||
return {
|
||||
...baseOptions(),
|
||||
tree: source.pageTree,
|
||||
tree: source.getPageTree(),
|
||||
links: [
|
||||
{
|
||||
type: 'custom',
|
||||
|
||||
@@ -4,9 +4,9 @@ description: The default theme of Hanzo Docs
|
||||
icon: Album
|
||||
---
|
||||
|
||||
Hanzo Docs UI offers a beautifully designed theme for documentation sites.
|
||||
**Hanzo Docs UI** offers a beautifully designed theme for documentation sites.
|
||||
|
||||
It is bundled with many interactive components & layouts for docs sites, allowing low code maintenance cost while receiving regular UI improvements.
|
||||
It is bundled with many interactive components & layouts for docs sites, allowing low maintenance cost of code while receiving regular UI improvements.
|
||||
|
||||
You can also use [Hanzo Docs CLI](/docs/cli) to install the components locally and have full control.
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function Layout({ children }: { children: ReactNode }) {
|
||||
}
|
||||
```
|
||||
|
||||
See [`baseOptions`](/docs/ui/layouts#shared-options) for available options to configure **navbar** and **layout links**.
|
||||
See detailed docs for [`links`](/docs/ui/layouts/links) and [`nav`](/docs/ui/layouts/nav) options.
|
||||
|
||||
### References
|
||||
|
||||
@@ -38,8 +38,6 @@ See [`baseOptions`](/docs/ui/layouts#shared-options) for available options to co
|
||||
|
||||
## Sidebar
|
||||
|
||||
It display navigation links from your page tree, see [Sidebar Links](/docs/navigation/sidebar) to configure sidebar items.
|
||||
|
||||
```tsx title="layout.tsx"
|
||||
import { DocsLayout } from '@hanzo/docs/ui/layouts/docs';
|
||||
|
||||
@@ -50,7 +48,156 @@ import { DocsLayout } from '@hanzo/docs/ui/layouts/docs';
|
||||
/>;
|
||||
```
|
||||
|
||||
<auto-type-table cwd path="content/docs/ui/props.ts" name="SidebarProps" />
|
||||
### Sidebar Items
|
||||
|
||||
Customise sidebar navigation links.
|
||||
|
||||
<div className='flex justify-center items-center *:max-w-[200px] bg-gradient-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>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Sidebar Tabs (Dropdown) [#sidebar-tabs]
|
||||
|
||||
Sidebar 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
|
||||
sidebar={{
|
||||
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 sidebar={{ 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 tabs.
|
||||
|
||||
```tsx
|
||||
import { DocsLayout } from '@hanzo/docs-ui/layouts/docs';
|
||||
|
||||
<DocsLayout
|
||||
sidebar={{
|
||||
tabs: {
|
||||
transform: (option, node) => ({
|
||||
...option,
|
||||
icon: <MyIcon />,
|
||||
}),
|
||||
},
|
||||
}}
|
||||
/>;
|
||||
```
|
||||
|
||||
### Sidebar Components
|
||||
|
||||
@@ -92,6 +239,10 @@ export function SidebarSeparator({ className, style, children, ...props }: Compo
|
||||
}
|
||||
```
|
||||
|
||||
### References
|
||||
|
||||
<auto-type-table cwd path="content/docs/ui/props.ts" name="SidebarProps" />
|
||||
|
||||
## Advanced
|
||||
|
||||
### Disable Prefetching
|
||||
|
||||
@@ -23,4 +23,4 @@ export default function Layout({ children }: { children: ReactNode }) {
|
||||
}
|
||||
```
|
||||
|
||||
We recommend to customise it from [`baseOptions`](/docs/ui/layouts#shared-options).
|
||||
See detailed docs for [`links`](/docs/ui/layouts/links) and [`nav`](/docs/ui/layouts/nav) options.
|
||||
|
||||
@@ -9,14 +9,14 @@ Hanzo Docs UI provides essential layouts to display content.
|
||||
|
||||
<DocsCategory />
|
||||
|
||||
## Shared Options
|
||||
### Configurations
|
||||
|
||||
Each layout supports a shared set of options.
|
||||
|
||||
It is recommended to store these options into a file, and pass them to the layouts.
|
||||
|
||||
```tsx title="lib/layout.shared.tsx"
|
||||
import type { BaseLayoutProps } from '@hanzo/docs-base-ui/layouts/shared';
|
||||
```tsx tab="Shared Options" title="lib/layout.shared.tsx"
|
||||
import type { BaseLayoutProps } from '@hanzo/docs-ui/layouts/shared';
|
||||
|
||||
export function baseOptions(): BaseLayoutProps {
|
||||
return {
|
||||
@@ -27,80 +27,18 @@ export function baseOptions(): BaseLayoutProps {
|
||||
}
|
||||
```
|
||||
|
||||
### Links
|
||||
|
||||
<div className="w-full mx-auto max-w-[600px]">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
You can display navigation links with the `links` option, see [Layout Links](/docs/navigation/links) for details.
|
||||
|
||||
### Nav
|
||||
|
||||
Options for navbar (header).
|
||||
|
||||
<auto-type-table
|
||||
cwd path="content/docs/ui/props.ts"
|
||||
type="Omit<NavbarProps, 'children'>"
|
||||
/>
|
||||
|
||||
#### Transparent Mode
|
||||
|
||||
To make the navbar background transparent, you can configure transparent mode.
|
||||
|
||||
```tsx tab="All" title="lib/layout.shared.tsx"
|
||||
import type { BaseLayoutProps } from '@hanzo/docs-base-ui/layouts/shared';
|
||||
|
||||
export function baseOptions(): BaseLayoutProps {
|
||||
return {
|
||||
nav: {
|
||||
title: 'My App',
|
||||
// [!code ++]
|
||||
transparentMode: 'top',
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```tsx tab="Home Layout"
|
||||
```tsx tab="Docs Layout" title="app/docs/layout.tsx"
|
||||
import { DocsLayout } from '@hanzo/docs-ui/layouts/docs';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { HomeLayout } from '@hanzo/docs-base-ui/layouts/home';
|
||||
import { source } from '@/lib/source';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
const base = baseOptions();
|
||||
return (
|
||||
<HomeLayout
|
||||
{...base}
|
||||
nav={{
|
||||
...base.nav,
|
||||
// [!code ++]
|
||||
transparentMode: 'top',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</HomeLayout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```tsx tab="Docs Layout"
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { DocsLayout } from '@hanzo/docs-base-ui/layouts/docs';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
const base = baseOptions();
|
||||
return (
|
||||
<DocsLayout
|
||||
{...base}
|
||||
nav={{
|
||||
...base.nav,
|
||||
// [!code ++]
|
||||
transparentMode: 'top',
|
||||
}}
|
||||
// [!code highlight]
|
||||
{...baseOptions()}
|
||||
tree={source.getPageTree()}
|
||||
>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
@@ -108,44 +46,16 @@ export default function Layout({ children }: { children: ReactNode }) {
|
||||
}
|
||||
```
|
||||
|
||||
| Mode | Description |
|
||||
| -------- | ---------------------------------------- |
|
||||
| `always` | Always use a transparent background |
|
||||
| `top` | When at the top of page |
|
||||
| `none` | Disable transparent background (default) |
|
||||
|
||||
#### Replace Navbar
|
||||
|
||||
To replace the navbar in different layouts, set `nav.component` to your own component.
|
||||
|
||||
```tsx tab="All" title="lib/layout.shared.tsx"
|
||||
import type { BaseLayoutProps } from '@hanzo/docs-base-ui/layouts/shared';
|
||||
|
||||
export function baseOptions(): BaseLayoutProps {
|
||||
return {
|
||||
nav: {
|
||||
// [!code ++]
|
||||
component: <CustomNavbar />,
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```tsx tab="Home Layout"
|
||||
```tsx tab="Home Layout" title="app/(home)/layout.tsx"
|
||||
import { HomeLayout } from '@hanzo/docs-ui/layouts/home';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { HomeLayout } from '@hanzo/docs-base-ui/layouts/home';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
const base = baseOptions();
|
||||
return (
|
||||
<HomeLayout
|
||||
{...base}
|
||||
nav={{
|
||||
...base.nav,
|
||||
// [!code ++]
|
||||
component: <CustomNavbar />,
|
||||
}}
|
||||
// [!code highlight]
|
||||
{...baseOptions()}
|
||||
>
|
||||
{children}
|
||||
</HomeLayout>
|
||||
@@ -153,34 +63,6 @@ export default function Layout({ children }: { children: ReactNode }) {
|
||||
}
|
||||
```
|
||||
|
||||
```tsx tab="Docs Layout"
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { DocsLayout } from '@hanzo/docs-base-ui/layouts/docs';
|
||||
import type { ReactNode } from 'react';
|
||||
See detailed docs for [`links`](/docs/ui/layouts/links) and [`nav`](/docs/ui/layouts/nav) options.
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
const base = baseOptions();
|
||||
return (
|
||||
<DocsLayout
|
||||
{...base}
|
||||
nav={{
|
||||
...base.nav,
|
||||
// [!code ++]
|
||||
component: <CustomNavbar />,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Hanzo Docs uses **CSS Variables** to share the size of layout components, and fit each layout component into appropriate position.
|
||||
|
||||
You need to override `--fd-nav-height` to the exact height of your custom navbar, this can be done with a CSS stylesheet (e.g. in `global.css`):
|
||||
|
||||
```css
|
||||
:root {
|
||||
--fd-nav-height: 80px !important;
|
||||
}
|
||||
```
|
||||
<auto-type-table cwd path="content/docs/ui/props.ts" name="BaseLayoutProps" />
|
||||
|
||||
+16
-17
@@ -1,11 +1,11 @@
|
||||
---
|
||||
title: Layout Links
|
||||
description: Customise the shared navigation links on all layouts.
|
||||
description: Navigation configurations on all layouts.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Hanzo Docs allows adding additional links to your layouts with a `links` prop, like linking to your "showcase" page.
|
||||
Hanzo Docs allows adding navigation links to your layouts with a `links` prop, like linking to your "showcase" page.
|
||||
|
||||
<div className="not-prose grid gap-2 *:border max-sm:*:last:hidden sm:grid-cols-[2fr_1fr]">
|
||||
|
||||
@@ -15,19 +15,18 @@ Hanzo Docs allows adding additional links to your layouts with a `links` prop, l
|
||||
|
||||
</div>
|
||||
|
||||
```tsx tab="Shared Options" title="lib/layout.shared.tsx"
|
||||
import type { BaseLayoutProps } from '@hanzo/docs/ui/layouts/shared';
|
||||
```tsx tab="All" title="lib/layout.shared.tsx"
|
||||
import type { BaseLayoutProps } from '@hanzo/docs-ui/layouts/shared';
|
||||
|
||||
export function baseOptions(): BaseLayoutProps {
|
||||
return {
|
||||
links: [], // [!code highlight]
|
||||
// other options}
|
||||
links: [], // [!code ++]
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```tsx tab="Docs Layout" title="app/docs/layout.tsx"
|
||||
import { DocsLayout } from '@hanzo/docs/ui/layouts/docs';
|
||||
import { DocsLayout } from '@hanzo/docs-ui/layouts/docs';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { source } from '@/lib/source';
|
||||
import type { ReactNode } from 'react';
|
||||
@@ -36,7 +35,7 @@ export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<DocsLayout
|
||||
{...baseOptions()}
|
||||
tree={source.pageTree}
|
||||
tree={source.getPageTree()}
|
||||
links={[]} // [!code highlight]
|
||||
>
|
||||
{children}
|
||||
@@ -46,7 +45,7 @@ export default function Layout({ children }: { children: ReactNode }) {
|
||||
```
|
||||
|
||||
```tsx tab="Home Layout" title="app/(home)/layout.tsx"
|
||||
import { HomeLayout } from '@hanzo/docs/ui/layouts/home';
|
||||
import { HomeLayout } from '@hanzo/docs-ui/layouts/home';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
@@ -70,7 +69,7 @@ A link to navigate to a URL/href, can be external.
|
||||
|
||||
```tsx title="lib/layout.shared.tsx"
|
||||
import { BookIcon } from 'lucide-react';
|
||||
import type { BaseLayoutProps } from '@hanzo/docs/ui/layouts/shared';
|
||||
import type { BaseLayoutProps } from '@hanzo/docs-ui/layouts/shared';
|
||||
|
||||
export function baseOptions(): BaseLayoutProps {
|
||||
return {
|
||||
@@ -98,7 +97,7 @@ The conditions to be marked as active.
|
||||
| `none` | Never be active |
|
||||
|
||||
```tsx title="lib/layout.shared.tsx"
|
||||
import type { BaseLayoutProps } from '@hanzo/docs/ui/layouts/shared';
|
||||
import type { BaseLayoutProps } from '@hanzo/docs-ui/layouts/shared';
|
||||
|
||||
export function baseOptions(): BaseLayoutProps {
|
||||
return {
|
||||
@@ -120,7 +119,7 @@ Icon items are secondary by default.
|
||||
|
||||
```tsx title="lib/layout.shared.tsx"
|
||||
import { BookIcon } from 'lucide-react';
|
||||
import type { BaseLayoutProps } from '@hanzo/docs/ui/layouts/shared';
|
||||
import type { BaseLayoutProps } from '@hanzo/docs-ui/layouts/shared';
|
||||
|
||||
export function baseOptions(): BaseLayoutProps {
|
||||
return {
|
||||
@@ -142,7 +141,7 @@ export function baseOptions(): BaseLayoutProps {
|
||||
Display a custom component.
|
||||
|
||||
```tsx title="lib/layout.shared.tsx"
|
||||
import type { BaseLayoutProps } from '@hanzo/docs/ui/layouts/shared';
|
||||
import type { BaseLayoutProps } from '@hanzo/docs-ui/layouts/shared';
|
||||
|
||||
export function baseOptions(): BaseLayoutProps {
|
||||
return {
|
||||
@@ -162,7 +161,7 @@ export function baseOptions(): BaseLayoutProps {
|
||||
There's also a shortcut for adding GitHub repository link item.
|
||||
|
||||
```tsx twoslash title="lib/layout.shared.tsx"
|
||||
import type { BaseLayoutProps } from '@hanzo/docs/ui/layouts/shared';
|
||||
import type { BaseLayoutProps } from '@hanzo/docs-ui/layouts/shared';
|
||||
|
||||
export function baseOptions(): BaseLayoutProps {
|
||||
return {
|
||||
@@ -176,7 +175,7 @@ export function baseOptions(): BaseLayoutProps {
|
||||
A menu containing multiple link items.
|
||||
|
||||
```tsx title="lib/layout.shared.tsx"
|
||||
import type { BaseLayoutProps } from '@hanzo/docs/ui/layouts/shared';
|
||||
import type { BaseLayoutProps } from '@hanzo/docs-ui/layouts/shared';
|
||||
|
||||
export function baseOptions(): BaseLayoutProps {
|
||||
return {
|
||||
@@ -206,13 +205,13 @@ In Home Layout, you can add navigation menu (fully animated) to the navbar.
|
||||
```tsx title="app/(home)/layout.tsx"
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import type { ReactNode } from 'react';
|
||||
import { HomeLayout } from '@hanzo/docs/ui/layouts/home';
|
||||
import { HomeLayout } from '@hanzo/docs-ui/layouts/home';
|
||||
import {
|
||||
NavbarMenu,
|
||||
NavbarMenuContent,
|
||||
NavbarMenuLink,
|
||||
NavbarMenuTrigger,
|
||||
} from '@hanzo/docs/ui/layouts/home/navbar';
|
||||
} from '@hanzo/docs-ui/layouts/home/navbar';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "../../../../.source/json-schema/docs.meta.json",
|
||||
"pages": ["---Configurations---", "links", "nav", "---Components---", "..."]
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
---
|
||||
title: Navbar
|
||||
description: Navbar/header configurations.
|
||||
---
|
||||
|
||||
## Configurations
|
||||
|
||||
Options for navbar (header).
|
||||
|
||||
<auto-type-table
|
||||
cwd path="content/docs/ui/props.ts"
|
||||
type="Omit<NavbarProps, 'children'>"
|
||||
/>
|
||||
|
||||
### Transparent Mode
|
||||
|
||||
To make the navbar background transparent, you can configure transparent mode.
|
||||
|
||||
```tsx tab="All" title="lib/layout.shared.tsx"
|
||||
import type { BaseLayoutProps } from '@hanzo/docs-ui/layouts/shared';
|
||||
|
||||
export function baseOptions(): BaseLayoutProps {
|
||||
return {
|
||||
nav: {
|
||||
title: 'My App',
|
||||
// [!code ++]
|
||||
transparentMode: 'top',
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```tsx tab="Home Layout"
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { HomeLayout } from '@hanzo/docs-ui/layouts/home';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
const base = baseOptions();
|
||||
return (
|
||||
<HomeLayout
|
||||
{...base}
|
||||
nav={{
|
||||
...base.nav,
|
||||
// [!code ++]
|
||||
transparentMode: 'top',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</HomeLayout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```tsx tab="Docs Layout"
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { DocsLayout } from '@hanzo/docs-ui/layouts/docs';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
const base = baseOptions();
|
||||
return (
|
||||
<DocsLayout
|
||||
{...base}
|
||||
nav={{
|
||||
...base.nav,
|
||||
// [!code ++]
|
||||
transparentMode: 'top',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
| Mode | Description |
|
||||
| -------- | ---------------------------------------- |
|
||||
| `always` | Always use a transparent background |
|
||||
| `top` | When at the top of page |
|
||||
| `none` | Disable transparent background (default) |
|
||||
|
||||
### Replace Navbar
|
||||
|
||||
To replace the navbar in different layouts, set `nav.component` to your own component.
|
||||
|
||||
```tsx tab="All" title="lib/layout.shared.tsx"
|
||||
import type { BaseLayoutProps } from '@hanzo/docs-ui/layouts/shared';
|
||||
|
||||
export function baseOptions(): BaseLayoutProps {
|
||||
return {
|
||||
nav: {
|
||||
// [!code ++]
|
||||
component: <CustomNavbar />,
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```tsx tab="Home Layout"
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { HomeLayout } from '@hanzo/docs-ui/layouts/home';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
const base = baseOptions();
|
||||
return (
|
||||
<HomeLayout
|
||||
{...base}
|
||||
nav={{
|
||||
...base.nav,
|
||||
// [!code ++]
|
||||
component: <CustomNavbar />,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</HomeLayout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```tsx tab="Docs Layout"
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { DocsLayout } from '@hanzo/docs-ui/layouts/docs';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
const base = baseOptions();
|
||||
return (
|
||||
<DocsLayout
|
||||
{...base}
|
||||
nav={{
|
||||
...base.nav,
|
||||
// [!code ++]
|
||||
component: <CustomNavbar />,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Hanzo Docs uses **CSS Variables** to share the size of layout components, and fit each layout component into appropriate position.
|
||||
|
||||
You need to override `--fd-nav-height` to the exact height of your custom navbar, this can be done with a CSS stylesheet (e.g. in `global.css`):
|
||||
|
||||
```css
|
||||
:root {
|
||||
--fd-nav-height: 80px !important;
|
||||
}
|
||||
```
|
||||
@@ -1,25 +1,27 @@
|
||||
---
|
||||
title: Notebook Layout
|
||||
description: A more compact version of Docs 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-base-ui/layouts/notebook`, it's a more compact version of [`<DocsLayout />`](/docs/ui/layouts/docs).
|
||||
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 { 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.pageTree}>
|
||||
<DocsLayout {...baseOptions()} tree={source.getPageTree()}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
@@ -40,7 +42,7 @@ Configure the style of sidebar tabs.
|
||||

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

|
||||
|
||||
```tsx title="layout.tsx"
|
||||
import { DocsLayout } from '@hanzo/docs/ui/layouts/notebook';
|
||||
import { DocsLayout } from '@hanzo/docs-ui/layouts/notebook';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { source } from '@/lib/source';
|
||||
import type { ReactNode } from 'react';
|
||||
@@ -77,7 +79,7 @@ export default function Layout({ children }: { children: ReactNode }) {
|
||||
<DocsLayout
|
||||
{...base}
|
||||
nav={{ ...nav, mode: 'top' }} // [!code ++]
|
||||
tree={source.pageTree}
|
||||
tree={source.getPageTree()}
|
||||
>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
|
||||
@@ -73,3 +73,5 @@ export type FooterProps = Omit<NonNullable<DocsPageProps['footer']>, keyof Compo
|
||||
export type AutoTypeTableProps = ComponentPropsWithoutRef<typeof AutoTypeTable>;
|
||||
|
||||
export type SearchProps = Required<RootProviderProps>['search'];
|
||||
|
||||
export type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
---
|
||||
import { render, type CollectionEntry } from 'astro:content';
|
||||
import { Docs } from '@/components/docs';
|
||||
import defaultMdxComponents from '@hanzo/docs/ui/mdx';
|
||||
import '@/styles/global.css';
|
||||
import { source } from '@/lib/source';
|
||||
import Layout from '@/components/layout.astro';
|
||||
import type { TOCItemType } from '@hanzo/docs/core/toc';
|
||||
import { render, type CollectionEntry } from "astro:content";
|
||||
import { Docs } from "@/components/docs";
|
||||
import defaultMdxComponents from "fumadocs-ui/mdx";
|
||||
import "@/styles/global.css";
|
||||
import { source } from "@/lib/source";
|
||||
import Layout from "@/components/layout.astro";
|
||||
import type { TOCItemType } from "fumadocs-core/toc";
|
||||
|
||||
interface Props {
|
||||
page: CollectionEntry<'docs'>;
|
||||
page: CollectionEntry<"docs">;
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
return source.getPages().map((page) => ({
|
||||
params: { slug: page.slugs.length > 0 ? page.slugs.join('/') : undefined },
|
||||
params: { slug: page.slugs.length > 0 ? page.slugs.join("/") : undefined },
|
||||
props: { page: page.data._raw },
|
||||
}));
|
||||
}
|
||||
@@ -24,7 +24,7 @@ const { Content, headings } = await render(page);
|
||||
const toc: TOCItemType[] = headings.map((heading) => ({
|
||||
depth: heading.depth,
|
||||
title: heading.text,
|
||||
url: '#' + heading.slug,
|
||||
url: "#" + heading.slug,
|
||||
}));
|
||||
---
|
||||
|
||||
@@ -33,7 +33,7 @@ const toc: TOCItemType[] = headings.map((heading) => ({
|
||||
<meta name="title" content={page.data.title} />
|
||||
<meta name="description" content={page.data.description} />
|
||||
<Docs
|
||||
tree={source.pageTree}
|
||||
tree={source.getPageTree()}
|
||||
pathname={Astro.url.pathname}
|
||||
params={Astro.params as Record<string, string | string[]>}
|
||||
page={{ toc }}
|
||||
|
||||
@@ -13,7 +13,9 @@ const server = createFromSource(source, {
|
||||
description: page.data.description,
|
||||
structuredData,
|
||||
url: page.url,
|
||||
breadcrumbs: getBreadcrumbItems(page.url, source.pageTree).map((item) => String(item.name)),
|
||||
breadcrumbs: getBreadcrumbItems(page.url, source.getPageTree()).map((item) =>
|
||||
String(item.name),
|
||||
),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import { baseOptions } from '@/app/layout.config';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<DocsLayout tree={source.pageTree} {...baseOptions}>
|
||||
<DocsLayout tree={source.getPageTree()} {...baseOptions}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { source } from '@/lib/source';
|
||||
|
||||
export default function Layout({ children }: LayoutProps<'/docs'>) {
|
||||
return (
|
||||
<DocsLayout tree={source.pageTree} {...baseOptions()}>
|
||||
<DocsLayout tree={source.getPageTree()} {...baseOptions()}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { baseOptions } from '@/lib/layout.shared';
|
||||
|
||||
export default function Layout({ children }: LayoutProps<'/docs'>) {
|
||||
return (
|
||||
<DocsLayout tree={source.pageTree} {...baseOptions()}>
|
||||
<DocsLayout tree={source.getPageTree()} {...baseOptions()}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { baseOptions } from '@/lib/layout.shared';
|
||||
|
||||
export default function Layout({ children }: LayoutProps<'/docs'>) {
|
||||
return (
|
||||
<DocsLayout tree={source.pageTree} {...baseOptions()}>
|
||||
<DocsLayout tree={source.getPageTree()} {...baseOptions()}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { baseOptions } from '@/lib/layout.shared';
|
||||
|
||||
export default function Layout({ children }: LayoutProps<'/docs'>) {
|
||||
return (
|
||||
<DocsLayout tree={source.pageTree} {...baseOptions()}>
|
||||
<DocsLayout tree={source.getPageTree()} {...baseOptions()}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { baseOptions } from '@/lib/layout.shared';
|
||||
|
||||
export default function RootDocsLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<DocsLayout tree={source.pageTree} {...baseOptions()}>
|
||||
<DocsLayout tree={source.getPageTree()} {...baseOptions()}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { source } from '@/lib/source';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<DocsLayout tree={source.pageTree} {...baseOptions()}>
|
||||
<DocsLayout tree={source.getPageTree()} {...baseOptions()}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ export function ServerComponent({ params }: Route.ComponentProps) {
|
||||
const { body: MDX } = page.data;
|
||||
|
||||
return (
|
||||
<DocsLayout {...baseOptions()} tree={source.pageTree}>
|
||||
<DocsLayout {...baseOptions()} tree={source.getPageTree()}>
|
||||
<DocsPage toc={page.data.toc}>
|
||||
<title>{page.data.title}</title>
|
||||
<meta name="description" content={page.data.description} />
|
||||
|
||||
@@ -14,7 +14,7 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||
|
||||
return {
|
||||
path: page.path,
|
||||
pageTree: await source.serializePageTree(source.pageTree),
|
||||
pageTree: await source.serializePageTree(source.getPageTree()),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ const loader = createServerFn({
|
||||
|
||||
return {
|
||||
path: page.path,
|
||||
pageTree: await source.serializePageTree(source.pageTree),
|
||||
pageTree: await source.serializePageTree(source.getPageTree()),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { baseOptions } from '@/lib/layout.shared';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<DocsLayout {...baseOptions()} tree={source.pageTree}>
|
||||
<DocsLayout {...baseOptions()} tree={source.getPageTree()}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
|
||||
@@ -162,7 +162,6 @@ export function DocsLayout(props: DocsLayoutProps) {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{nav.children}
|
||||
{tabs.length > 0 && (
|
||||
<SidebarTabsDropdown
|
||||
options={tabs}
|
||||
@@ -315,6 +314,7 @@ function DocsNavbar({
|
||||
navMode === 'auto' && 'md:hidden',
|
||||
),
|
||||
})}
|
||||
{nav.children}
|
||||
</div>
|
||||
{searchToggle.enabled !== false &&
|
||||
(searchToggle.components?.lg ? (
|
||||
@@ -343,7 +343,6 @@ function DocsNavbar({
|
||||
<NavbarLinkItem key={i} item={item} />
|
||||
))}
|
||||
</div>
|
||||
{nav.children}
|
||||
{links
|
||||
.filter((item) => item.type === 'icon')
|
||||
.map((item, i) => (
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# @fumadocs/content-collections
|
||||
|
||||
## 1.2.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b16a32f: Switch to tsdown for bundling
|
||||
- Updated dependencies [590d36a]
|
||||
- Updated dependencies [98d38ff]
|
||||
- Updated dependencies [446631d]
|
||||
- Updated dependencies [b16a32f]
|
||||
- fumadocs-core@16.4.2
|
||||
|
||||
## 1.2.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -10,5 +10,5 @@ export * from './remark-code-tab';
|
||||
export * from './remark-steps';
|
||||
export * from './remark-npm';
|
||||
export * from './codeblock-utils';
|
||||
export * from './remark-mdx-files';
|
||||
export { remarkMdxFiles, type RemarkMdxFilesOptions } from './remark-mdx-files';
|
||||
export * from './remark-mdx-mermaid';
|
||||
|
||||
@@ -2,20 +2,26 @@ import type { Root } from 'mdast';
|
||||
import { visit } from 'unist-util-visit';
|
||||
import type { Transformer } from 'unified';
|
||||
import type { MdxJsxAttribute, MdxJsxFlowElement } from 'mdast-util-mdx-jsx';
|
||||
import type { VFile } from 'vfile';
|
||||
import path from 'node:path';
|
||||
|
||||
interface FileNode {
|
||||
export interface FileNode {
|
||||
depth: number;
|
||||
type: 'file';
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface FolderNode {
|
||||
export interface FolderNode {
|
||||
depth: number;
|
||||
type: 'folder';
|
||||
name: string;
|
||||
children: Node[];
|
||||
}
|
||||
|
||||
export interface ToMdxOptions {
|
||||
defaultOpenAll: boolean;
|
||||
}
|
||||
|
||||
type Node = FileNode | FolderNode;
|
||||
|
||||
export interface RemarkMdxFilesOptions {
|
||||
@@ -23,7 +29,12 @@ export interface RemarkMdxFilesOptions {
|
||||
* @defaultValue files
|
||||
*/
|
||||
lang?: string;
|
||||
toMdx?: (node: Node) => MdxJsxFlowElement;
|
||||
toMdx?: (node: Node, options: ToMdxOptions) => MdxJsxFlowElement;
|
||||
}
|
||||
|
||||
interface AutoFilesProps extends Partial<ToMdxOptions> {
|
||||
dir?: string;
|
||||
patterns?: string[];
|
||||
}
|
||||
|
||||
function parseFileTree(code: string) {
|
||||
@@ -65,13 +76,13 @@ function parseFileTree(code: string) {
|
||||
return stack.get(0);
|
||||
}
|
||||
|
||||
function defaultToMDX(node: Node, depth = 0): MdxJsxFlowElement {
|
||||
function defaultToMDX(node: Node, options: ToMdxOptions, depth = 0): MdxJsxFlowElement {
|
||||
if (depth === 0) {
|
||||
return {
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: 'Files',
|
||||
attributes: [],
|
||||
children: [defaultToMDX(node, depth + 1)],
|
||||
children: [defaultToMDX(node, options, depth + 1)],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -88,21 +99,26 @@ function defaultToMDX(node: Node, depth = 0): MdxJsxFlowElement {
|
||||
};
|
||||
}
|
||||
|
||||
attributes.push({
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'defaultOpen',
|
||||
value: null,
|
||||
});
|
||||
if (options.defaultOpenAll) {
|
||||
attributes.push({
|
||||
type: 'mdxJsxAttribute',
|
||||
name: 'defaultOpen',
|
||||
value: null,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'mdxJsxFlowElement',
|
||||
attributes,
|
||||
name: 'Folder',
|
||||
children: node.children.map((item) => defaultToMDX(item, depth + 1)),
|
||||
children: node.children.map((item) => defaultToMDX(item, options, depth + 1)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* **Files CodeBlock:**
|
||||
*
|
||||
* Convert codeblocks with `files` as lang, like:
|
||||
*
|
||||
* ```files
|
||||
@@ -114,19 +130,129 @@ function defaultToMDX(node: Node, depth = 0): MdxJsxFlowElement {
|
||||
* ├── package.json
|
||||
* ```
|
||||
*
|
||||
* into MDX `<Files />` component
|
||||
* into MDX `<Files />` component.
|
||||
*
|
||||
* **Auto Files:**
|
||||
*
|
||||
* Generates MDX `<Files />` component from file system.
|
||||
*
|
||||
* ```mdx
|
||||
* <auto-files dir="scripts" pattern="my-dir/*" defaultOpenAll />
|
||||
* ```
|
||||
*/
|
||||
export function remarkMdxFiles(options: RemarkMdxFilesOptions = {}): Transformer<Root, Root> {
|
||||
const { lang = 'files', toMdx = defaultToMDX } = options;
|
||||
|
||||
return (tree) => {
|
||||
visit(tree, 'code', (node) => {
|
||||
if (node.lang !== lang || !node.value) return;
|
||||
async function autoFiles(
|
||||
file: VFile,
|
||||
node: MdxJsxFlowElement,
|
||||
{ patterns, dir, defaultOpenAll = false }: AutoFilesProps,
|
||||
) {
|
||||
const { glob } = await import('tinyglobby');
|
||||
if (!patterns) {
|
||||
file.fail('Missing `pattern` prop in <auto-files>', {
|
||||
place: node.position,
|
||||
});
|
||||
}
|
||||
|
||||
const fileTree = parseFileTree(node.value);
|
||||
if (!fileTree) return;
|
||||
const baseDir = file.dirname ?? file.cwd;
|
||||
const cwd = dir ? path.join(baseDir, dir) : baseDir;
|
||||
const files = await glob(patterns, { cwd });
|
||||
Object.assign(node, toMdx(buildFileTreeFromGlob(cwd, files), { defaultOpenAll }));
|
||||
}
|
||||
|
||||
Object.assign(node, toMdx(fileTree));
|
||||
return async (tree, file) => {
|
||||
const queue: Promise<void>[] = [];
|
||||
|
||||
visit(tree, ['code', 'mdxJsxFlowElement'] as const, (node) => {
|
||||
if (node.type === 'code') {
|
||||
if (node.lang !== lang || !node.value) return;
|
||||
|
||||
const fileTree = parseFileTree(node.value);
|
||||
if (!fileTree) return;
|
||||
|
||||
Object.assign(
|
||||
node,
|
||||
toMdx(fileTree, {
|
||||
defaultOpenAll: true,
|
||||
}),
|
||||
);
|
||||
return 'skip';
|
||||
}
|
||||
|
||||
if (node.type === 'mdxJsxFlowElement') {
|
||||
if (node.name !== 'auto-files') return;
|
||||
const parsed: AutoFilesProps = {};
|
||||
for (const attr of node.attributes) {
|
||||
if (attr.type !== 'mdxJsxAttribute') continue;
|
||||
const { name, value } = attr;
|
||||
|
||||
switch (name) {
|
||||
case 'dir':
|
||||
if (typeof value === 'string') parsed.dir = value;
|
||||
break;
|
||||
case 'pattern':
|
||||
if (typeof value === 'string') {
|
||||
parsed.patterns ??= [];
|
||||
parsed.patterns.push(value);
|
||||
}
|
||||
break;
|
||||
case 'defaultOpenAll':
|
||||
parsed.defaultOpenAll = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
queue.push(autoFiles(file, node, parsed));
|
||||
return 'skip';
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(queue);
|
||||
};
|
||||
}
|
||||
|
||||
function buildFileTreeFromGlob(dir: string, files: string[]): Node {
|
||||
const nodeMap = new Map<string, FolderNode>();
|
||||
const root: FolderNode = {
|
||||
depth: 0,
|
||||
type: 'folder',
|
||||
name: path.basename(dir),
|
||||
children: [],
|
||||
};
|
||||
nodeMap.set('', root);
|
||||
|
||||
for (const file of files) {
|
||||
const parts = path
|
||||
.normalize(file)
|
||||
.split(path.sep)
|
||||
.filter((part) => part.length > 0);
|
||||
let currentPath = '';
|
||||
let current = root;
|
||||
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const name = parts[i];
|
||||
const nextPath = path.join(currentPath, name);
|
||||
|
||||
if (i === parts.length - 1) {
|
||||
// Add file node.
|
||||
const fileNode: FileNode = { depth: i + 1, type: 'file', name };
|
||||
current.children.push(fileNode);
|
||||
} else {
|
||||
// Add or retrieve folder node using the map.
|
||||
let folder = nodeMap.get(nextPath);
|
||||
|
||||
if (!folder) {
|
||||
folder = { depth: i + 1, type: 'folder', name, children: [] };
|
||||
nodeMap.set(nextPath, folder);
|
||||
current.children.push(folder);
|
||||
}
|
||||
|
||||
current = folder;
|
||||
currentPath = nextPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
```files
|
||||
project
|
||||
├── src
|
||||
│ ├── index.js
|
||||
│ └── utils
|
||||
│ └── helper.js
|
||||
├── package.json
|
||||
```
|
||||
|
||||
<auto-files dir="page-trees" pattern="**/*.json" />
|
||||
@@ -11,3 +11,19 @@
|
||||
<File name="package.json" />
|
||||
</Folder>
|
||||
</Files>
|
||||
|
||||
<Files>
|
||||
<Folder name="page-trees">
|
||||
<File name="basic.tree.json" />
|
||||
|
||||
<File name="i18n-dir.tree.json" />
|
||||
|
||||
<File name="i18n-no-prefix.tree.json" />
|
||||
|
||||
<File name="i18n.entries.json" />
|
||||
|
||||
<File name="i18n.tree.json" />
|
||||
|
||||
<File name="nested.tree.json" />
|
||||
</Folder>
|
||||
</Files>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { remark } from 'remark';
|
||||
import {
|
||||
@@ -16,7 +16,6 @@ import { fileURLToPath } from 'node:url';
|
||||
import remarkMdx from 'remark-mdx';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { createProcessor } from '@mdx-js/mdx';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import { remarkSteps } from '@/mdx-plugins/remark-steps';
|
||||
import remarkDirective from 'remark-directive';
|
||||
|
||||
@@ -24,7 +23,7 @@ const cwd = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
test('Remark Heading', async () => {
|
||||
const file = path.resolve(cwd, './fixtures/remark-heading.md');
|
||||
const content = readFileSync(file);
|
||||
const content = await fs.readFile(file);
|
||||
|
||||
const result = await remark().use(remarkHeading).process(content);
|
||||
|
||||
@@ -33,8 +32,21 @@ test('Remark Heading', async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('Remark Mdx Files', async () => {
|
||||
const file = path.resolve(cwd, './fixtures/remark-mdx-files.mdx');
|
||||
const content = await fs.readFile(file);
|
||||
|
||||
const result = await remark().use(remarkMdx).use(remarkMdxFiles).process({
|
||||
path: file,
|
||||
value: content,
|
||||
});
|
||||
await expect(String(result.value)).toMatchFileSnapshot(
|
||||
path.resolve(cwd, './fixtures/remark-mdx-files.output.mdx'),
|
||||
);
|
||||
});
|
||||
|
||||
test('Remark Structure', async () => {
|
||||
const content = readFileSync(path.resolve(cwd, './fixtures/remark-structure.md'));
|
||||
const content = await fs.readFile(path.resolve(cwd, './fixtures/remark-structure.md'));
|
||||
const result = await remark().use(remarkGfm).use(remarkStructure).process(content);
|
||||
|
||||
await expect(result.data.structuredData).toMatchFileSnapshot(
|
||||
@@ -43,7 +55,7 @@ test('Remark Structure', async () => {
|
||||
});
|
||||
|
||||
test('Remark Admonition', async () => {
|
||||
const content = readFileSync(path.resolve(cwd, './fixtures/remark-admonition.md'));
|
||||
const content = await fs.readFile(path.resolve(cwd, './fixtures/remark-admonition.md'));
|
||||
const processor = remark().use(remarkMdx).use(remarkDirective).use(remarkDirectiveAdmonition);
|
||||
let tree = processor.parse(content);
|
||||
tree = await processor.run(tree);
|
||||
@@ -65,7 +77,7 @@ test('Remark Steps', async () => {
|
||||
|
||||
test('Remark Image: With Path', async () => {
|
||||
const file = path.resolve(cwd, './fixtures/remark-image.md');
|
||||
const content = readFileSync(file);
|
||||
const content = await fs.readFile(file);
|
||||
const processor = remark()
|
||||
.use(remarkImage, { publicDir: path.resolve(cwd, './fixtures') })
|
||||
.use(remarkMdx);
|
||||
@@ -80,7 +92,7 @@ test('Remark Image: With Path', async () => {
|
||||
});
|
||||
|
||||
test('Remark Image: Without Import', async () => {
|
||||
const content = readFileSync(path.resolve(cwd, './fixtures/remark-image.md'));
|
||||
const content = await fs.readFile(path.resolve(cwd, './fixtures/remark-image.md'));
|
||||
const result = await remark()
|
||||
.use(remarkImage, {
|
||||
publicDir: path.resolve(cwd, './fixtures'),
|
||||
@@ -95,7 +107,7 @@ test('Remark Image: Without Import', async () => {
|
||||
});
|
||||
|
||||
test('Remark Image: `publicDir` with URL', async () => {
|
||||
const content = readFileSync(path.resolve(cwd, './fixtures/remark-image-public-dir.md'));
|
||||
const content = await fs.readFile(path.resolve(cwd, './fixtures/remark-image-public-dir.md'));
|
||||
const result = await remark()
|
||||
.use(remarkImage, {
|
||||
publicDir: 'https://hanzo-docs.dev',
|
||||
@@ -109,17 +121,8 @@ test('Remark Image: `publicDir` with URL', async () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('Remark MDX Files', async () => {
|
||||
const content = readFileSync(path.resolve(cwd, './fixtures/remark-mdx-files.md'));
|
||||
const result = await remark().use(remarkMdxFiles).use(remarkMdx).process(content);
|
||||
|
||||
await expect(result.value).toMatchFileSnapshot(
|
||||
path.resolve(cwd, './fixtures/remark-mdx-files.output.mdx'),
|
||||
);
|
||||
});
|
||||
|
||||
test('converts mermaid codeblock to MDX Mermaid component', async () => {
|
||||
const content = readFileSync(path.resolve(cwd, './fixtures/remark-mdx-mermaid.md'));
|
||||
const content = await fs.readFile(path.resolve(cwd, './fixtures/remark-mdx-mermaid.md'));
|
||||
const result = await remark().use(remarkMdxMermaid).use(remarkMdx).process(content);
|
||||
|
||||
await expect(result.value).toMatchFileSnapshot(
|
||||
@@ -128,7 +131,7 @@ test('converts mermaid codeblock to MDX Mermaid component', async () => {
|
||||
});
|
||||
|
||||
test('Rehype Toc', async () => {
|
||||
const content = readFileSync(path.resolve(cwd, './fixtures/rehype-toc.md'));
|
||||
const content = await fs.readFile(path.resolve(cwd, './fixtures/rehype-toc.md'));
|
||||
|
||||
const processor = createProcessor({
|
||||
remarkPlugins: [remarkHeading],
|
||||
|
||||
@@ -4,7 +4,7 @@ import { baseOptions } from '@/lib/layout.shared';
|
||||
|
||||
export default function Layout({ children }: LayoutProps<'/docs'>) {
|
||||
return (
|
||||
<DocsLayout tree={source.pageTree} {...baseOptions()}>
|
||||
<DocsLayout tree={source.getPageTree()} {...baseOptions()}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||
|
||||
return {
|
||||
path: page.path,
|
||||
pageTree: await source.serializePageTree(source.pageTree),
|
||||
pageTree: await source.serializePageTree(source.getPageTree()),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ const loader = createServerFn({
|
||||
|
||||
return {
|
||||
path: page.path,
|
||||
pageTree: await source.serializePageTree(source.pageTree),
|
||||
pageTree: await source.serializePageTree(source.getPageTree()),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { baseOptions } from '@/lib/layout.shared';
|
||||
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<DocsLayout {...baseOptions()} tree={source.pageTree}>
|
||||
<DocsLayout {...baseOptions()} tree={source.getPageTree()}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# fumadocs-docgen
|
||||
|
||||
## 3.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b16a32f: Switch to tsdown for bundling
|
||||
- Updated dependencies [590d36a]
|
||||
- Updated dependencies [98d38ff]
|
||||
- Updated dependencies [446631d]
|
||||
- Updated dependencies [b16a32f]
|
||||
- fumadocs-core@16.4.2
|
||||
|
||||
## 3.0.4
|
||||
|
||||
### Patch Changes
|
||||
@@ -125,7 +136,7 @@
|
||||
Import it like:
|
||||
|
||||
```ts
|
||||
import { remarkTypeScriptToJavaScript } from 'fumadocs-docgen/remark-ts2js';
|
||||
import { remarkTypeScriptToJavaScript } from "fumadocs-docgen/remark-ts2js";
|
||||
```
|
||||
|
||||
instead of importing from `fumadocs-docgen`.
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# @fumadocs/mdx-remote
|
||||
|
||||
## 1.4.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b16a32f: Switch to tsdown for bundling
|
||||
- Updated dependencies [590d36a]
|
||||
- Updated dependencies [98d38ff]
|
||||
- Updated dependencies [446631d]
|
||||
- Updated dependencies [b16a32f]
|
||||
- fumadocs-core@16.4.2
|
||||
|
||||
## 1.4.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# fumadocs-obsidian
|
||||
|
||||
## 0.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b16a32f: Switch to tsdown for bundling
|
||||
- dd7498f: fix slugification
|
||||
- Updated dependencies [590d36a]
|
||||
- Updated dependencies [98d38ff]
|
||||
- Updated dependencies [446631d]
|
||||
- Updated dependencies [b16a32f]
|
||||
- fumadocs-core@16.4.2
|
||||
|
||||
## 0.0.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ParsedFile, VaultStorage } from '@/build-storage';
|
||||
import path from 'node:path';
|
||||
import { stash } from '@/utils/stash';
|
||||
import { slash } from '@/utils/slash';
|
||||
|
||||
export interface VaultResolver {
|
||||
/**
|
||||
@@ -35,7 +35,7 @@ export function buildResolver(storage: VaultStorage): VaultResolver {
|
||||
const parsed = path.parse(file.path);
|
||||
|
||||
nameToFile.set(parsed.name, file);
|
||||
pathToFile.set(stash(path.join(parsed.dir, parsed.name)), file);
|
||||
pathToFile.set(slash(path.join(parsed.dir, parsed.name)), file);
|
||||
|
||||
nameToFile.set(parsed.base, file);
|
||||
pathToFile.set(file.path, file);
|
||||
@@ -59,7 +59,7 @@ export function buildResolver(storage: VaultStorage): VaultResolver {
|
||||
const dir = path.dirname(fromPath);
|
||||
|
||||
if (name.startsWith('./') || name.startsWith('../')) {
|
||||
return this.resolvePath(stash(path.join(dir, name)));
|
||||
return this.resolvePath(slash(path.join(dir, name)));
|
||||
}
|
||||
|
||||
// absolute path or name
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import path from 'node:path';
|
||||
import matter from 'gray-matter';
|
||||
import { type Frontmatter, frontmatterSchema } from '@/utils/schema';
|
||||
import { stash } from '@/utils/stash';
|
||||
import { slash } from '@/utils/slash';
|
||||
import type { VaultFile } from '@/read-vaults';
|
||||
import { slug } from 'github-slugger';
|
||||
|
||||
type RenameOutputFn = (originalOutputPath: string, file: VaultFile) => string;
|
||||
type RenameOutputPreset = 'ignore' | 'simple';
|
||||
@@ -86,23 +87,19 @@ export function buildStorage(
|
||||
const storage = new Map<string, ParsedFile>();
|
||||
|
||||
for (const rawFile of rawFiles) {
|
||||
parseFile(rawFile);
|
||||
}
|
||||
|
||||
function parseFile(file: VaultFile) {
|
||||
const normalizedPath = normalize(file.path);
|
||||
let outPath = getOutputPath(normalizedPath, file);
|
||||
const normalizedPath = normalize(rawFile.path);
|
||||
let outPath = getOutputPath(normalizedPath, rawFile);
|
||||
let parsed: ParsedFile;
|
||||
|
||||
if (['.md', '.mdx'].includes(path.extname(normalizedPath))) {
|
||||
const { data, content } = matter(String(file.content));
|
||||
const { data, content } = matter(String(rawFile.content));
|
||||
if (enforceMdx) {
|
||||
outPath = outPath.slice(0, -path.extname(outPath).length) + '.mdx';
|
||||
}
|
||||
|
||||
parsed = {
|
||||
format: 'content',
|
||||
_raw: file._raw,
|
||||
_raw: rawFile._raw,
|
||||
path: normalizedPath,
|
||||
outPath,
|
||||
frontmatter: frontmatterSchema.parse(data),
|
||||
@@ -112,10 +109,10 @@ export function buildStorage(
|
||||
parsed = {
|
||||
format: 'media',
|
||||
path: normalizedPath,
|
||||
_raw: file._raw,
|
||||
_raw: rawFile._raw,
|
||||
outPath,
|
||||
content: file.content,
|
||||
url: url(outPath, file),
|
||||
content: rawFile.content,
|
||||
url: url(outPath, rawFile),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -128,11 +125,25 @@ export function buildStorage(
|
||||
function createRenameOutput(preset: RenameOutputPreset): RenameOutputFn {
|
||||
if (preset === 'ignore') return (file) => file;
|
||||
|
||||
return (file) => file.toLowerCase().replaceAll(' ', '-');
|
||||
const occurrences = new Map<string, number>();
|
||||
return (file) => {
|
||||
const ext = path.extname(file);
|
||||
const segs = file.slice(0, -ext.length).split('/');
|
||||
for (let i = 0; i < segs.length; i++) {
|
||||
// preserve separators
|
||||
segs[i] = slug(segs[i]);
|
||||
}
|
||||
// we only count occurrences by the full path
|
||||
let out = segs.join('/');
|
||||
const o = occurrences.get(out) ?? 0;
|
||||
occurrences.set(out, o + 1);
|
||||
if (o > 0) out += `-${o}`;
|
||||
return out + ext;
|
||||
};
|
||||
}
|
||||
|
||||
function normalize(filePath: string): string {
|
||||
filePath = stash(filePath);
|
||||
filePath = slash(filePath);
|
||||
if (filePath.startsWith('../')) throw new Error(`${filePath} points outside of vault folder`);
|
||||
|
||||
return filePath.startsWith('./') ? filePath.slice(2) : filePath;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { ParsedFile } from '@/build-storage';
|
||||
import path from 'node:path';
|
||||
import { stash } from '@/utils/stash';
|
||||
import { slash } from '@/utils/slash';
|
||||
import { slug } from 'github-slugger';
|
||||
|
||||
export function getFileHref(ref: ParsedFile, sourceFile: ParsedFile, heading?: string) {
|
||||
if (ref.format === 'media' && ref.url) return ref.url;
|
||||
|
||||
const dir = path.dirname(sourceFile.outPath);
|
||||
let url = stash(path.relative(dir, ref.outPath));
|
||||
let url = slash(path.relative(dir, ref.outPath));
|
||||
if (!url.startsWith('../')) url = `./${url}`;
|
||||
if (heading) url += `#${getHeadingHash(heading)}`;
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export function slash(path: string): string {
|
||||
const isExtendedLengthPath = path.startsWith('\\\\?\\');
|
||||
|
||||
if (isExtendedLengthPath) {
|
||||
return path;
|
||||
}
|
||||
|
||||
return path.replaceAll('\\', '/');
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import path from 'node:path';
|
||||
|
||||
export function stash(v: string): string {
|
||||
const sep = path.sep;
|
||||
|
||||
if (sep !== '/') return v.replaceAll(path.sep, '/');
|
||||
return v;
|
||||
}
|
||||
@@ -1,5 +1,18 @@
|
||||
# @fuma-docs/openapi
|
||||
|
||||
## 10.2.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 7e58c8e: Fix Parameter Serialization
|
||||
- b16a32f: Switch to tsdown for bundling
|
||||
- Updated dependencies [590d36a]
|
||||
- Updated dependencies [98d38ff]
|
||||
- Updated dependencies [446631d]
|
||||
- Updated dependencies [b16a32f]
|
||||
- fumadocs-core@16.4.2
|
||||
- fumadocs-ui@16.4.2
|
||||
|
||||
## 10.2.2
|
||||
|
||||
### Patch Changes
|
||||
@@ -188,8 +201,8 @@
|
||||
|
||||
```ts
|
||||
// components/api-page.tsx
|
||||
import { openapi } from '@/lib/openapi';
|
||||
import { createAPIPage } from 'fumadocs-openapi/ui';
|
||||
import { openapi } from "@/lib/openapi";
|
||||
import { createAPIPage } from "fumadocs-openapi/ui";
|
||||
|
||||
export const APIPage = createAPIPage(openapi, {
|
||||
// e.g. customise render functions
|
||||
@@ -219,8 +232,8 @@
|
||||
|
||||
```tsx
|
||||
// components/api-page.tsx
|
||||
import { openapi } from '@/lib/openapi';
|
||||
import { createAPIPage } from 'fumadocs-openapi/ui';
|
||||
import { openapi } from "@/lib/openapi";
|
||||
import { createAPIPage } from "fumadocs-openapi/ui";
|
||||
|
||||
export const APIPage = createAPIPage(openapi, {
|
||||
content: {
|
||||
@@ -250,23 +263,23 @@
|
||||
Before:
|
||||
|
||||
```ts
|
||||
import { openapi } from '@/lib/openapi';
|
||||
import { openapi } from "@/lib/openapi";
|
||||
|
||||
void generateFiles({
|
||||
input: ['./products.yaml'],
|
||||
output: './content/docs',
|
||||
input: ["./products.yaml"],
|
||||
output: "./content/docs",
|
||||
});
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```ts
|
||||
import { generateFiles } from 'fumadocs-openapi';
|
||||
import { openapi } from '@/lib/openapi';
|
||||
import { generateFiles } from "fumadocs-openapi";
|
||||
import { openapi } from "@/lib/openapi";
|
||||
|
||||
void generateFiles({
|
||||
input: openapi,
|
||||
output: './content/docs',
|
||||
output: "./content/docs",
|
||||
});
|
||||
```
|
||||
|
||||
@@ -298,12 +311,12 @@
|
||||
|
||||
```ts
|
||||
// lib/openapi.ts
|
||||
import { createOpenAPI } from 'fumadocs-openapi/server';
|
||||
import path from 'node:path';
|
||||
import { createOpenAPI } from "fumadocs-openapi/server";
|
||||
import path from "node:path";
|
||||
|
||||
export const openapi = createOpenAPI({
|
||||
input: [path.resolve('./scalar.yaml')],
|
||||
proxyUrl: '/api/proxy',
|
||||
input: [path.resolve("./scalar.yaml")],
|
||||
proxyUrl: "/api/proxy",
|
||||
});
|
||||
```
|
||||
|
||||
@@ -327,8 +340,8 @@
|
||||
|
||||
```ts
|
||||
// components/api-page.tsx
|
||||
import { openapi } from '@/lib/openapi';
|
||||
import { createAPIPage } from 'fumadocs-openapi/ui';
|
||||
import { openapi } from "@/lib/openapi";
|
||||
import { createAPIPage } from "fumadocs-openapi/ui";
|
||||
|
||||
export const APIPage = createAPIPage(openapi, {
|
||||
playground: {
|
||||
@@ -341,9 +354,9 @@
|
||||
|
||||
```tsx
|
||||
// components/api-page.tsx
|
||||
import { openapi } from '@/lib/openapi';
|
||||
import { createAPIPage } from 'fumadocs-openapi/ui';
|
||||
import client from './api-page.client';
|
||||
import { openapi } from "@/lib/openapi";
|
||||
import { createAPIPage } from "fumadocs-openapi/ui";
|
||||
import client from "./api-page.client";
|
||||
|
||||
export const APIPage = createAPIPage(openapi, {
|
||||
client,
|
||||
@@ -352,17 +365,17 @@
|
||||
|
||||
```tsx
|
||||
// components/api-page.client.tsx
|
||||
'use client';
|
||||
import { defineClientConfig } from 'fumadocs-openapi/ui/client';
|
||||
"use client";
|
||||
import { defineClientConfig } from "fumadocs-openapi/ui/client";
|
||||
|
||||
export default defineClientConfig({
|
||||
playground: {
|
||||
transformAuthInputs: (inputs) => [
|
||||
...inputs,
|
||||
{
|
||||
fieldName: 'auth.tests',
|
||||
fieldName: "auth.tests",
|
||||
children: <div>Tests</div>,
|
||||
defaultValue: '',
|
||||
defaultValue: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -375,10 +388,10 @@
|
||||
|
||||
```tsx
|
||||
// components/api-page.tsx
|
||||
import { openapi } from '@/lib/openapi';
|
||||
import { createAPIPage } from 'fumadocs-openapi/ui';
|
||||
import { adapters } from './my-media-adapters';
|
||||
import client from './api-page.client';
|
||||
import { openapi } from "@/lib/openapi";
|
||||
import { createAPIPage } from "fumadocs-openapi/ui";
|
||||
import { adapters } from "./my-media-adapters";
|
||||
import client from "./api-page.client";
|
||||
|
||||
export const APIPage = createAPIPage(openapi, {
|
||||
client,
|
||||
@@ -388,9 +401,9 @@
|
||||
|
||||
```tsx
|
||||
// components/api-page.client.tsx
|
||||
'use client';
|
||||
import { defineClientConfig } from 'fumadocs-openapi/ui/client';
|
||||
import { adapters } from './my-media-adapters';
|
||||
"use client";
|
||||
import { defineClientConfig } from "fumadocs-openapi/ui/client";
|
||||
import { adapters } from "./my-media-adapters";
|
||||
|
||||
export default defineClientConfig({
|
||||
mediaAdapters: adapters,
|
||||
@@ -408,11 +421,11 @@
|
||||
|
||||
```tsx
|
||||
// components/api-page.client.tsx
|
||||
'use client';
|
||||
import { defineClientConfig } from 'fumadocs-openapi/ui/client';
|
||||
"use client";
|
||||
import { defineClientConfig } from "fumadocs-openapi/ui/client";
|
||||
|
||||
export default defineClientConfig({
|
||||
storageKeyPrefix: 'fumadocs-openapi-custom-',
|
||||
storageKeyPrefix: "fumadocs-openapi-custom-",
|
||||
});
|
||||
```
|
||||
|
||||
@@ -758,22 +771,22 @@
|
||||
Migration: Move the server object from `lib/source` to `lib/openapi`
|
||||
|
||||
```ts
|
||||
import { createOpenAPI } from 'fumadocs-openapi/server';
|
||||
import { createOpenAPI } from "fumadocs-openapi/server";
|
||||
|
||||
export const openapi = createOpenAPI({
|
||||
input: ['./my-schema.json'],
|
||||
input: ["./my-schema.json"],
|
||||
});
|
||||
```
|
||||
|
||||
Use the server object for `generateFiles()`:
|
||||
|
||||
```ts
|
||||
import { generateFiles } from 'fumadocs-openapi';
|
||||
import { openapi } from '@/lib/openapi';
|
||||
import { generateFiles } from "fumadocs-openapi";
|
||||
import { openapi } from "@/lib/openapi";
|
||||
|
||||
void generateFiles({
|
||||
input: openapi,
|
||||
output: './content/docs',
|
||||
output: "./content/docs",
|
||||
// we recommend to enable it
|
||||
// make sure your endpoint description doesn't break MDX syntax.
|
||||
includeDescription: true,
|
||||
@@ -1091,15 +1104,15 @@
|
||||
|
||||
```ts
|
||||
generateFiles({
|
||||
input: ['./content/docs/openapi/museum.yaml'],
|
||||
output: './content/docs/openapi/(generated)',
|
||||
per: 'operation',
|
||||
input: ["./content/docs/openapi/museum.yaml"],
|
||||
output: "./content/docs/openapi/(generated)",
|
||||
per: "operation",
|
||||
name: (output, document) => {
|
||||
// page info
|
||||
output.item;
|
||||
// parsed OpenAPI schema
|
||||
document;
|
||||
return 'dir/my-file';
|
||||
return "dir/my-file";
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -1108,11 +1121,11 @@
|
||||
|
||||
```ts
|
||||
generateFiles({
|
||||
input: ['./content/docs/openapi/museum.yaml'],
|
||||
output: './content/docs/openapi/(generated)',
|
||||
per: 'operation',
|
||||
input: ["./content/docs/openapi/museum.yaml"],
|
||||
output: "./content/docs/openapi/(generated)",
|
||||
per: "operation",
|
||||
name: {
|
||||
algorithm: 'v1',
|
||||
algorithm: "v1",
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -1123,9 +1136,9 @@
|
||||
|
||||
```ts
|
||||
generateFiles({
|
||||
input: ['./content/docs/openapi/museum.yaml'],
|
||||
output: './content/docs/openapi/(generated)',
|
||||
per: 'operation',
|
||||
input: ["./content/docs/openapi/museum.yaml"],
|
||||
output: "./content/docs/openapi/(generated)",
|
||||
per: "operation",
|
||||
});
|
||||
```
|
||||
|
||||
@@ -1287,11 +1300,11 @@
|
||||
We highly recommend to use the following instead:
|
||||
|
||||
```css
|
||||
@import 'tailwindcss';
|
||||
@import 'fumadocs-ui/css/neutral.css';
|
||||
@import 'fumadocs-ui/css/preset.css';
|
||||
@import "tailwindcss";
|
||||
@import "fumadocs-ui/css/neutral.css";
|
||||
@import "fumadocs-ui/css/preset.css";
|
||||
/* do this */
|
||||
@import 'fumadocs-openapi/css/preset.css';
|
||||
@import "fumadocs-openapi/css/preset.css";
|
||||
```
|
||||
|
||||
- Updated dependencies [3a5595a]
|
||||
@@ -1335,10 +1348,10 @@
|
||||
in your `mdx-components.tsx` (or where you pass MDX components):
|
||||
|
||||
```tsx
|
||||
import defaultComponents from 'fumadocs-ui/mdx';
|
||||
import { APIPage } from 'fumadocs-openapi/ui';
|
||||
import { openapi } from '@/lib/source';
|
||||
import type { MDXComponents } from 'mdx/types';
|
||||
import defaultComponents from "fumadocs-ui/mdx";
|
||||
import { APIPage } from "fumadocs-openapi/ui";
|
||||
import { openapi } from "@/lib/source";
|
||||
import type { MDXComponents } from "mdx/types";
|
||||
|
||||
export function getMDXComponents(components?: MDXComponents): MDXComponents {
|
||||
return {
|
||||
@@ -1690,8 +1703,8 @@
|
||||
From:
|
||||
|
||||
```tsx
|
||||
import { createOpenAPI } from 'fumadocs-openapi/server';
|
||||
import { APIPlayground } from 'fumadocs-openapi/scalar';
|
||||
import { createOpenAPI } from "fumadocs-openapi/server";
|
||||
import { APIPlayground } from "fumadocs-openapi/scalar";
|
||||
|
||||
export const openapi = createOpenAPI({
|
||||
useScalar: true,
|
||||
@@ -1701,8 +1714,8 @@
|
||||
To:
|
||||
|
||||
```tsx
|
||||
import { createOpenAPI } from 'fumadocs-openapi/server';
|
||||
import { APIPlayground } from 'fumadocs-openapi/scalar';
|
||||
import { createOpenAPI } from "fumadocs-openapi/server";
|
||||
import { APIPlayground } from "fumadocs-openapi/scalar";
|
||||
|
||||
export const openapi = createOpenAPI({
|
||||
renderer: {
|
||||
@@ -2414,7 +2427,7 @@
|
||||
---
|
||||
|
||||
<APIPage
|
||||
operations={[{ path: '/v1/apis.deleteApi', method: 'post' }]}
|
||||
operations={[{ path: "/v1/apis.deleteApi", method: "post" }]}
|
||||
hasHead={false}
|
||||
/>
|
||||
```
|
||||
@@ -2620,13 +2633,13 @@
|
||||
Add the package to `content` under your Tailwind CSS configuration.
|
||||
|
||||
```js
|
||||
import { createPreset, presets } from 'fumadocs-ui/tailwind-plugin';
|
||||
import { createPreset, presets } from "fumadocs-ui/tailwind-plugin";
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
'./node_modules/fumadocs-ui/dist/**/*.js',
|
||||
'./node_modules/fumadocs-openapi/dist/**/*.js',
|
||||
"./node_modules/fumadocs-ui/dist/**/*.js",
|
||||
"./node_modules/fumadocs-openapi/dist/**/*.js",
|
||||
],
|
||||
presets: [createPreset()],
|
||||
};
|
||||
@@ -2824,11 +2837,11 @@
|
||||
migrate: Create a script named `scripts/generate-docs.mjs`:
|
||||
|
||||
```js
|
||||
import { generateFiles } from 'fumadocs-openapi';
|
||||
import { generateFiles } from "fumadocs-openapi";
|
||||
|
||||
void generateFiles({
|
||||
input: ['./petstore.yaml'],
|
||||
output: './content/docs',
|
||||
input: ["./petstore.yaml"],
|
||||
output: "./content/docs",
|
||||
});
|
||||
```
|
||||
|
||||
@@ -2875,18 +2888,18 @@
|
||||
* @type {import("@fuma-docs/openapi").Config}
|
||||
*/
|
||||
module.exports = {
|
||||
input: ['./petstore.yaml'],
|
||||
output: './content/docs',
|
||||
per: 'tag',
|
||||
input: ["./petstore.yaml"],
|
||||
output: "./content/docs",
|
||||
per: "tag",
|
||||
render: (title, description) => {
|
||||
return {
|
||||
frontmatter: [
|
||||
'---',
|
||||
"---",
|
||||
`title: ${title}`,
|
||||
`description: ${description}`,
|
||||
'toc: false',
|
||||
'---',
|
||||
].join('\n'),
|
||||
"toc: false",
|
||||
"---",
|
||||
].join("\n"),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ export default [
|
||||
'no-console': 'off',
|
||||
// some arrays will not be changed
|
||||
'react/no-array-index-key': 'off',
|
||||
'no-fallthrough': 'off',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -33,12 +33,7 @@ export function createBrowserFetcher(
|
||||
|
||||
for (const key in options.header) {
|
||||
const param = options.header[key];
|
||||
|
||||
if (!Array.isArray(param.value)) {
|
||||
headers.append(key, param.value);
|
||||
} else {
|
||||
headers.append(key, param.value.join(','));
|
||||
}
|
||||
headers.append(key, param.value);
|
||||
}
|
||||
|
||||
const proxyUrl = options.proxyUrl ? new URL(options.proxyUrl, document.baseURI) : null;
|
||||
|
||||
@@ -32,7 +32,7 @@ export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
||||
s.push('var client = new HttpClient();');
|
||||
const headerLines: string[] = [];
|
||||
|
||||
function addHeader(key: string, value: unknown) {
|
||||
function addHeader(key: string, value: string) {
|
||||
headerLines.push(`client.DefaultRequestHeaders.Add("${key}", ${JSON.stringify(value)});`);
|
||||
}
|
||||
for (const k in headers) {
|
||||
|
||||
@@ -14,7 +14,7 @@ export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(data.header)) {
|
||||
headers[k] = v.value as string;
|
||||
headers[k] = v.value;
|
||||
}
|
||||
|
||||
const cookies = Object.entries(data.cookie);
|
||||
|
||||
@@ -25,7 +25,7 @@ export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(data.header)) {
|
||||
headers[k] = v.value as string;
|
||||
headers[k] = v.value;
|
||||
}
|
||||
|
||||
if (Object.keys(headers).length > 0) {
|
||||
@@ -37,7 +37,7 @@ export const generator: SampleGenerator = (url, data, { mediaAdapters }) => {
|
||||
const cookies: Record<string, string> = {};
|
||||
|
||||
for (const [k, v] of inputCookies) {
|
||||
cookies[k] = v.value as string;
|
||||
cookies[k] = v.value;
|
||||
}
|
||||
|
||||
params.push(`cookies = ${generatePythonObject(cookies, imports)}`);
|
||||
|
||||
@@ -5,117 +5,67 @@ import type { ParameterObject } from '@/types';
|
||||
import type { RawRequestData, RequestData } from '@/requests/types';
|
||||
|
||||
export interface EncodedParameter {
|
||||
readonly value: string | string[];
|
||||
readonly value: string;
|
||||
}
|
||||
|
||||
const FormDelimiter = {
|
||||
spaceDelimited: ' ',
|
||||
pipeDelimited: '|',
|
||||
};
|
||||
|
||||
const PathPrefix = {
|
||||
label: '.',
|
||||
matrix: ';',
|
||||
};
|
||||
export interface EncodedParameterMultiple {
|
||||
readonly values: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* serialize parameters, see https://swagger.io/docs/specification/v3_0/serialization.
|
||||
*/
|
||||
export function encodeRequestData(
|
||||
from: RawRequestData,
|
||||
adapters: Record<string, MediaAdapter>,
|
||||
parameters: NoReference<ParameterObject>[],
|
||||
): RequestData {
|
||||
const result: Partial<RequestData> = {
|
||||
const result: RequestData = {
|
||||
method: from.method,
|
||||
body: from.body,
|
||||
bodyMediaType: from.bodyMediaType,
|
||||
cookie: {},
|
||||
header: {},
|
||||
path: {},
|
||||
query: {},
|
||||
};
|
||||
|
||||
for (const type of ['cookie', 'query', 'header', 'path'] as const) {
|
||||
const output: Record<string, EncodedParameter> = {};
|
||||
|
||||
for (const key in from[type]) {
|
||||
const value = from[type][key];
|
||||
if (value == null) continue;
|
||||
|
||||
const field = parameters.find((p) => p.name === key && p.in === type);
|
||||
if (!field) {
|
||||
output[key] = { value: String(value) };
|
||||
continue;
|
||||
}
|
||||
// skip unknown parameters
|
||||
if (!field) continue;
|
||||
|
||||
const encoder = getMediaEncoder(field, adapters);
|
||||
if (encoder) {
|
||||
output[key] = { value: encoder(value) };
|
||||
result[type][key] = { value: encoder(value) };
|
||||
continue;
|
||||
}
|
||||
|
||||
const explode = field.explode ?? true;
|
||||
let prefix = '';
|
||||
let sep = ',';
|
||||
|
||||
if (field.in === 'path') {
|
||||
const style = field.style ?? 'simple';
|
||||
|
||||
if (style in PathPrefix) {
|
||||
prefix = PathPrefix[style as keyof typeof PathPrefix];
|
||||
|
||||
if (explode) sep = prefix;
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// header & cookie doesn't support explode for array values
|
||||
if (explode && field.in !== 'header' && field.in !== 'cookie') {
|
||||
output[key] = {
|
||||
value: prefix + value.map(String),
|
||||
switch (type) {
|
||||
case 'path':
|
||||
serializePathParameter(field, value, result.path);
|
||||
break;
|
||||
case 'query':
|
||||
serializeQueryParameter(field, value, result.query);
|
||||
break;
|
||||
case 'header': {
|
||||
result.header[key] = {
|
||||
value: serializeSimple(value, field.explode ?? false),
|
||||
};
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
|
||||
if (field.in === 'query') {
|
||||
const style = field.style ?? 'form';
|
||||
|
||||
if (style in FormDelimiter) sep = FormDelimiter[style as keyof typeof FormDelimiter];
|
||||
}
|
||||
|
||||
output[key] = {
|
||||
value: prefix + value.map(String).join(sep),
|
||||
};
|
||||
continue;
|
||||
case 'cookie':
|
||||
serializeCookieParameter(field, value, result.cookie);
|
||||
break;
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && value) {
|
||||
// header & path creates key-value pairs
|
||||
if (explode && (field.in === 'header' || field.in === 'path')) {
|
||||
output[key] = {
|
||||
value:
|
||||
prefix +
|
||||
Object.entries(value)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join(sep),
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (explode || field.style === 'deepObject') {
|
||||
writeObject(key, value, field.style === 'deepObject', output);
|
||||
continue;
|
||||
}
|
||||
|
||||
output[key] = {
|
||||
value: prefix + Object.entries(value).flat().join(sep),
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
output[key] = {
|
||||
value: prefix + String(value),
|
||||
};
|
||||
}
|
||||
|
||||
result[type] = output;
|
||||
}
|
||||
|
||||
return result as RequestData;
|
||||
return result;
|
||||
}
|
||||
|
||||
function getMediaEncoder(
|
||||
@@ -132,25 +82,177 @@ function getMediaEncoder(
|
||||
}
|
||||
}
|
||||
|
||||
function writeObject(
|
||||
parentKey: string,
|
||||
value: object,
|
||||
deep: boolean,
|
||||
function serializeSimple(value: NonNullable<unknown>, explode: boolean): string {
|
||||
if (Array.isArray(value)) {
|
||||
return value.join(',');
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
return explode
|
||||
? Object.entries(value)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join(',')
|
||||
: Object.entries(value).flat().join(',');
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function serializePathParameter(
|
||||
field: NoReference<ParameterObject>,
|
||||
value: NonNullable<unknown>,
|
||||
// write output
|
||||
output: Record<string, EncodedParameter>,
|
||||
) {
|
||||
for (const k in value) {
|
||||
const prop: unknown = value[k as keyof object];
|
||||
if (prop == null) continue;
|
||||
): void {
|
||||
const { explode = false, name } = field;
|
||||
|
||||
const key = deep ? `${parentKey}[${k}]` : k;
|
||||
if (!deep || typeof prop !== 'object') {
|
||||
output[key] = {
|
||||
value: String(prop),
|
||||
switch (field.style) {
|
||||
case 'label':
|
||||
if (Array.isArray(value)) {
|
||||
output[field.name] = {
|
||||
value: '.' + value.join(explode ? '.' : ','),
|
||||
};
|
||||
break;
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
output[field.name] = {
|
||||
value:
|
||||
'.' +
|
||||
(explode
|
||||
? Object.entries(value)
|
||||
.map(([k, v]) => `${k}=${v}`)
|
||||
.join('.')
|
||||
: Object.entries(value).flat().join(',')),
|
||||
};
|
||||
break;
|
||||
}
|
||||
output[field.name] = {
|
||||
value: `.${value}`,
|
||||
};
|
||||
break;
|
||||
case 'matrix': {
|
||||
const specifier = `;${name}=`;
|
||||
|
||||
continue;
|
||||
if (Array.isArray(value)) {
|
||||
output[field.name] = {
|
||||
value: explode
|
||||
? `${specifier}${value.join(',')}`
|
||||
: `${specifier}${value.join(specifier)}`,
|
||||
};
|
||||
break;
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
output[field.name] = {
|
||||
value: explode
|
||||
? Object.entries(value)
|
||||
.map(([k, v]) => `;${k}=${v}`)
|
||||
.join('')
|
||||
: specifier + Object.entries(value).flat().join(','),
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
output[field.name] = {
|
||||
value: `${specifier}${value}`,
|
||||
};
|
||||
break;
|
||||
}
|
||||
// simple
|
||||
default:
|
||||
output[field.name] = {
|
||||
value: serializeSimple(value, explode),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
writeObject(key, value, deep, output);
|
||||
function serializeQueryParameter(
|
||||
field: NoReference<ParameterObject>,
|
||||
value: NonNullable<unknown>,
|
||||
// write output
|
||||
output: Record<string, EncodedParameterMultiple>,
|
||||
): void {
|
||||
const { explode = true } = field;
|
||||
|
||||
switch (field.style) {
|
||||
case 'spaceDelimited':
|
||||
if (!explode && Array.isArray(value)) {
|
||||
output[field.name] = {
|
||||
values: [value.join(' ')],
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'pipeDelimited':
|
||||
if (!explode && Array.isArray(value)) {
|
||||
output[field.name] = {
|
||||
values: [value.join('|')],
|
||||
};
|
||||
break;
|
||||
}
|
||||
case 'deepObject':
|
||||
if (!Array.isArray(value) && typeof value === 'object') {
|
||||
for (const [k, v] of Object.entries(value)) {
|
||||
output[`${field.name}[${k}]`] = {
|
||||
// note: the behaviour of nested array is undefined, we do this to avoid edge cases
|
||||
values: Array.isArray(v) ? v : [String(v)],
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
// form
|
||||
default:
|
||||
if (Array.isArray(value)) {
|
||||
output[field.name] = {
|
||||
values: explode ? value : [value.join(',')],
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && explode) {
|
||||
for (const [k, v] of Object.entries(value)) {
|
||||
output[k] = {
|
||||
values: [String(v)],
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
output[field.name] = {
|
||||
values: [Object.entries(value).flat().join(',')],
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
output[field.name] = {
|
||||
values: [String(value)],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function serializeCookieParameter(
|
||||
field: NoReference<ParameterObject>,
|
||||
value: NonNullable<unknown>,
|
||||
// write output
|
||||
output: Record<string, EncodedParameter>,
|
||||
) {
|
||||
const { explode = true } = field;
|
||||
|
||||
// form
|
||||
if (Array.isArray(value)) {
|
||||
output[field.name] = {
|
||||
value: explode ? value.map((v) => `${field.name}=${v}`).join('&') : value.join(','),
|
||||
};
|
||||
} else if (typeof value === 'object' && explode) {
|
||||
for (const [k, v] of Object.entries(value)) {
|
||||
output[k] = {
|
||||
value: String(v),
|
||||
};
|
||||
}
|
||||
} else if (typeof value === 'object') {
|
||||
output[field.name] = {
|
||||
value: Object.entries(value).flat().join(','),
|
||||
};
|
||||
} else {
|
||||
output[field.name] = {
|
||||
value: String(value),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MediaAdapter } from '@/requests/media/adapter';
|
||||
import type { EncodedParameter } from '@/requests/media/encode';
|
||||
import type { EncodedParameter, EncodedParameterMultiple } from '@/requests/media/encode';
|
||||
|
||||
export type SampleGenerator<ServerContext = unknown> = (
|
||||
url: string,
|
||||
@@ -26,7 +26,7 @@ export interface RequestData {
|
||||
method: string;
|
||||
|
||||
path: Record<string, EncodedParameter>;
|
||||
query: Record<string, EncodedParameter>;
|
||||
query: Record<string, EncodedParameterMultiple>;
|
||||
header: Record<string, EncodedParameter>;
|
||||
cookie: Record<string, EncodedParameter>;
|
||||
|
||||
|
||||
@@ -37,11 +37,7 @@ export function resolveRequestData(pathname: string, { path, query }: RequestDat
|
||||
for (const key in path) {
|
||||
const param = path[key];
|
||||
|
||||
if (Array.isArray(param.value)) {
|
||||
pathname = pathname.replace(`{${key}}`, param.value.join('/'));
|
||||
} else {
|
||||
pathname = pathname.replace(`{${key}}`, param.value);
|
||||
}
|
||||
pathname = pathname.replace(`{${key}}`, param.value);
|
||||
}
|
||||
|
||||
// Check if pathname already contains query parameters (legacy API support)
|
||||
@@ -53,16 +49,12 @@ export function resolveRequestData(pathname: string, { path, query }: RequestDat
|
||||
// Add new query parameters from the RequestData
|
||||
for (const key in query) {
|
||||
const param = query[key];
|
||||
if (param.values.length === 0) continue;
|
||||
|
||||
if (Array.isArray(param.value)) {
|
||||
// Remove existing parameter first to avoid duplicates
|
||||
searchParams.delete(key);
|
||||
for (const item of param.value) {
|
||||
searchParams.append(key, item);
|
||||
}
|
||||
} else {
|
||||
// Set (replace if exists) the parameter value
|
||||
searchParams.set(key, param.value);
|
||||
// Remove existing parameter first to avoid duplicates
|
||||
searchParams.delete(key);
|
||||
for (const item of param.values) {
|
||||
searchParams.append(key, item);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ describe('Code Sample Generators', () => {
|
||||
authorization: { value: 'Bearer' },
|
||||
},
|
||||
query: {
|
||||
search: { value: 'ai' },
|
||||
search: { values: ['ai'] },
|
||||
},
|
||||
};
|
||||
const url = resolveRequestData('http://localhost:8080/{test}', data);
|
||||
|
||||
@@ -315,7 +315,7 @@ describe('URL utilities', () => {
|
||||
const requestData: RequestData = {
|
||||
method: 'GET',
|
||||
path: {
|
||||
segments: { value: ['api', 'v1', 'users'] },
|
||||
segments: { value: 'api/v1/users' },
|
||||
},
|
||||
query: {},
|
||||
header: {},
|
||||
@@ -330,8 +330,8 @@ describe('URL utilities', () => {
|
||||
method: 'GET',
|
||||
path: {},
|
||||
query: {
|
||||
limit: { value: '10' },
|
||||
offset: { value: '20' },
|
||||
limit: { values: ['10'] },
|
||||
offset: { values: ['20'] },
|
||||
},
|
||||
header: {},
|
||||
cookie: {},
|
||||
@@ -345,7 +345,7 @@ describe('URL utilities', () => {
|
||||
method: 'GET',
|
||||
path: {},
|
||||
query: {
|
||||
tags: { value: ['javascript', 'typescript'] },
|
||||
tags: { values: ['javascript', 'typescript'] },
|
||||
},
|
||||
header: {},
|
||||
cookie: {},
|
||||
@@ -362,7 +362,7 @@ describe('URL utilities', () => {
|
||||
method: 'GET',
|
||||
path: {},
|
||||
query: {
|
||||
limit: { value: '5' },
|
||||
limit: { values: ['5'] },
|
||||
},
|
||||
header: {},
|
||||
cookie: {},
|
||||
@@ -378,8 +378,8 @@ describe('URL utilities', () => {
|
||||
method: 'GET',
|
||||
path: {},
|
||||
query: {
|
||||
limit: { value: '10' },
|
||||
sort: { value: 'date' },
|
||||
limit: { values: ['10'] },
|
||||
sort: { values: ['date'] },
|
||||
},
|
||||
header: {},
|
||||
cookie: {},
|
||||
@@ -397,7 +397,7 @@ describe('URL utilities', () => {
|
||||
userId: { value: '123' },
|
||||
},
|
||||
query: {
|
||||
include: { value: 'profile' },
|
||||
include: { values: ['profile'] },
|
||||
},
|
||||
header: {},
|
||||
cookie: {},
|
||||
@@ -413,7 +413,7 @@ describe('URL utilities', () => {
|
||||
method: 'GET',
|
||||
path: {},
|
||||
query: {
|
||||
verbose: { value: 'false' },
|
||||
verbose: { values: ['false'] },
|
||||
},
|
||||
header: {},
|
||||
cookie: {},
|
||||
@@ -429,7 +429,7 @@ describe('URL utilities', () => {
|
||||
method: 'GET',
|
||||
path: {},
|
||||
query: {
|
||||
newParam: { value: 'value' },
|
||||
newParam: { values: ['value'] },
|
||||
},
|
||||
header: {},
|
||||
cookie: {},
|
||||
@@ -445,7 +445,7 @@ describe('URL utilities', () => {
|
||||
method: 'GET',
|
||||
path: {},
|
||||
query: {
|
||||
categories: { value: ['tech', 'science'] },
|
||||
categories: { values: ['tech', 'science'] },
|
||||
},
|
||||
header: {},
|
||||
cookie: {},
|
||||
@@ -461,7 +461,7 @@ describe('URL utilities', () => {
|
||||
method: 'GET',
|
||||
path: {},
|
||||
query: {
|
||||
tags: { value: ['new', 'updated'] },
|
||||
tags: { values: ['new', 'updated'] },
|
||||
},
|
||||
header: {},
|
||||
cookie: {},
|
||||
@@ -494,9 +494,9 @@ describe('URL utilities', () => {
|
||||
projectId: { value: 'web-app' },
|
||||
},
|
||||
query: {
|
||||
include: { value: ['members', 'settings'] },
|
||||
format: { value: 'json' },
|
||||
debug: { value: 'true' },
|
||||
include: { values: ['members', 'settings'] },
|
||||
format: { values: ['json'] },
|
||||
debug: { values: ['true'] },
|
||||
},
|
||||
header: {},
|
||||
cookie: {},
|
||||
|
||||
@@ -37,7 +37,7 @@ async function Layout({ page, children }: { page: Page; children: ReactNode }) {
|
||||
const { toc } = await page.data.load();
|
||||
|
||||
return (
|
||||
<DocsLayout {...baseOptions()} tree={source.pageTree}>
|
||||
<DocsLayout {...baseOptions()} tree={source.getPageTree()}>
|
||||
<DocsPage toc={toc}>{children}</DocsPage>
|
||||
</DocsLayout>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# fumadocs-python
|
||||
|
||||
## 0.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b16a32f: Switch to tsdown for bundling
|
||||
- Updated dependencies [590d36a]
|
||||
- Updated dependencies [98d38ff]
|
||||
- Updated dependencies [446631d]
|
||||
- Updated dependencies [b16a32f]
|
||||
- fumadocs-core@16.4.2
|
||||
- fumadocs-ui@16.4.2
|
||||
|
||||
## 0.0.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -162,7 +162,6 @@ export function DocsLayout(props: DocsLayoutProps) {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{nav.children}
|
||||
{tabs.length > 0 && (
|
||||
<SidebarTabsDropdown
|
||||
options={tabs}
|
||||
@@ -315,6 +314,7 @@ function DocsNavbar({
|
||||
navMode === 'auto' && 'md:hidden',
|
||||
),
|
||||
})}
|
||||
{nav.children}
|
||||
</div>
|
||||
{searchToggle.enabled !== false &&
|
||||
(searchToggle.components?.lg ? (
|
||||
@@ -343,7 +343,6 @@ function DocsNavbar({
|
||||
<NavbarLinkItem key={i} item={item} />
|
||||
))}
|
||||
</div>
|
||||
{nav.children}
|
||||
{links
|
||||
.filter((item) => item.type === 'icon')
|
||||
.map((item, i) => (
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# fumadocs-twoslash
|
||||
|
||||
## 3.1.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b16a32f: Switch to tsdown for bundling
|
||||
- Updated dependencies [b16a32f]
|
||||
- fumadocs-ui@16.4.2
|
||||
|
||||
## 3.1.10
|
||||
|
||||
### Patch Changes
|
||||
@@ -166,17 +174,17 @@
|
||||
Before:
|
||||
|
||||
```ts
|
||||
import 'fumadocs-ui/twoslash.css';
|
||||
import "fumadocs-ui/twoslash.css";
|
||||
|
||||
import { Popup } from 'fumadocs-ui/twoslash/popup';
|
||||
import { Popup } from "fumadocs-ui/twoslash/popup";
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```ts
|
||||
import 'fumadocs-twoslash/twoslash.css';
|
||||
import "fumadocs-twoslash/twoslash.css";
|
||||
|
||||
import { Popup } from 'fumadocs-twoslash/ui';
|
||||
import { Popup } from "fumadocs-twoslash/ui";
|
||||
```
|
||||
|
||||
**Tailwind CSS is now required for Twoslash integration.**
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# fumadocs-typescript
|
||||
|
||||
## 5.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 689d31e: Improve error message
|
||||
- b16a32f: Switch to tsdown for bundling
|
||||
- Updated dependencies [590d36a]
|
||||
- Updated dependencies [98d38ff]
|
||||
- Updated dependencies [446631d]
|
||||
- Updated dependencies [b16a32f]
|
||||
- fumadocs-core@16.4.2
|
||||
- fumadocs-ui@16.4.2
|
||||
|
||||
## 5.0.0
|
||||
|
||||
### Major Changes
|
||||
@@ -24,11 +37,11 @@
|
||||
import {
|
||||
createGenerator,
|
||||
createFileSystemGeneratorCache,
|
||||
} from 'fumadocs-typescript';
|
||||
} from "fumadocs-typescript";
|
||||
|
||||
const generator = createGenerator({
|
||||
// add this!
|
||||
cache: createFileSystemGeneratorCache('.next/fumadocs-typescript'),
|
||||
cache: createFileSystemGeneratorCache(".next/fumadocs-typescript"),
|
||||
});
|
||||
```
|
||||
|
||||
@@ -179,7 +192,7 @@
|
||||
Create a generator instance:
|
||||
|
||||
```ts
|
||||
import { createGenerator } from 'fumadocs-typescript';
|
||||
import { createGenerator } from "fumadocs-typescript";
|
||||
|
||||
const generator = createGenerator(tsconfig);
|
||||
```
|
||||
@@ -299,7 +312,7 @@
|
||||
Add the `remarkDocGen` plugin to your remark plugins.
|
||||
|
||||
```ts
|
||||
import { remarkDocGen, fileGenerator } from 'fumadocs-docgen';
|
||||
import { remarkDocGen, fileGenerator } from "fumadocs-docgen";
|
||||
|
||||
remark().use(remarkDocGen, { generators: [fileGenerator()] });
|
||||
```
|
||||
@@ -329,7 +342,7 @@
|
||||
For `remarkInstall`, it remains the same:
|
||||
|
||||
```ts
|
||||
import { remarkInstall } from 'fumadocs-docgen';
|
||||
import { remarkInstall } from "fumadocs-docgen";
|
||||
```
|
||||
|
||||
## 1.0.2
|
||||
|
||||
@@ -85,7 +85,7 @@ export function TOCItems({ ref, className, ...props }: ComponentProps<'div'>) {
|
||||
>
|
||||
<TocThumb
|
||||
containerRef={containerRef}
|
||||
className="absolute w-full top-(--fd-top) h-(--fd-height) bg-fd-primary transition-[top,height] duration-250"
|
||||
className="absolute w-full top-(--fd-top) h-(--fd-height) bg-fd-primary transition-[top,height] ease-linear duration-250"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -22,7 +22,7 @@ export function TOCItems({ ref, className, ...props }: ComponentProps<'div'>) {
|
||||
<>
|
||||
<TocThumb
|
||||
containerRef={containerRef}
|
||||
className="absolute top-(--fd-top) h-(--fd-height) w-0.5 rounded-e-sm bg-fd-primary transition-[top,height] duration-250"
|
||||
className="absolute top-(--fd-top) h-(--fd-height) w-0.5 rounded-e-sm bg-fd-primary transition-[top,height] ease-linear duration-250"
|
||||
/>
|
||||
<div
|
||||
ref={mergeRefs(ref, containerRef)}
|
||||
|
||||
Reference in New Issue
Block a user