mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 06:31:49 +00:00
Core: Support referencing original page/meta from page tree nodes
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-ui': patch
|
||||
---
|
||||
|
||||
Improve Root Toggle component
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-core': minor
|
||||
---
|
||||
|
||||
Support referencing original page/meta from page tree nodes
|
||||
@@ -16,7 +16,7 @@ import { TypeTable } from 'fumadocs-ui/components/type-table';
|
||||
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
||||
import Preview from '@/components/preview';
|
||||
import { createMetadata, metadataImage } from '@/utils/metadata';
|
||||
import { openapi, utils } from '@/app/source';
|
||||
import { openapi, source } from '@/app/source';
|
||||
import { Wrapper } from '@/components/preview/wrapper';
|
||||
|
||||
interface Param {
|
||||
@@ -28,7 +28,7 @@ export default function Page({
|
||||
}: {
|
||||
params: Param;
|
||||
}): React.ReactElement {
|
||||
const page = utils.getPage(params.slug);
|
||||
const page = source.getPage(params.slug);
|
||||
|
||||
if (!page) notFound();
|
||||
|
||||
@@ -75,7 +75,7 @@ export default function Page({
|
||||
}}
|
||||
/>
|
||||
{page.data.index ? (
|
||||
<DocsCategory page={page} pages={utils.getPages()} />
|
||||
<DocsCategory page={page} pages={source.getPages()} />
|
||||
) : null}
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
@@ -83,7 +83,7 @@ export default function Page({
|
||||
}
|
||||
|
||||
export function generateMetadata({ params }: { params: Param }): Metadata {
|
||||
const page = utils.getPage(params.slug);
|
||||
const page = source.getPage(params.slug);
|
||||
|
||||
if (!page) notFound();
|
||||
|
||||
@@ -102,5 +102,5 @@ export function generateMetadata({ params }: { params: Param }): Metadata {
|
||||
}
|
||||
|
||||
export function generateStaticParams(): Param[] {
|
||||
return utils.generateParams();
|
||||
return source.generateParams();
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { type DocsLayoutProps } from 'fumadocs-ui/layouts/docs';
|
||||
import { AlbumIcon, BookIcon, Heart, LayoutTemplateIcon } from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import { RootToggle } from 'fumadocs-ui/components/layout/root-toggle';
|
||||
import {
|
||||
type Option,
|
||||
RootToggle,
|
||||
} from 'fumadocs-ui/components/layout/root-toggle';
|
||||
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
import { type PageTree } from 'fumadocs-core/server';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { FumadocsIcon } from '@/app/layout.client';
|
||||
import Logo from '@/public/logo.png';
|
||||
import { utils } from '@/app/source';
|
||||
import { source } from '@/app/source';
|
||||
import { modes } from '@/utils/modes';
|
||||
|
||||
export const baseOptions: BaseLayoutProps = {
|
||||
@@ -58,9 +63,42 @@ export const baseOptions: BaseLayoutProps = {
|
||||
],
|
||||
};
|
||||
|
||||
function getItems(): Option[] {
|
||||
const options: Option[] = [];
|
||||
|
||||
function traverse(node: PageTree.Node): void {
|
||||
if (node.type === 'folder' && node.root && node.index) {
|
||||
const meta = source.getNodeMeta(node);
|
||||
if (!meta) return;
|
||||
|
||||
options.push({
|
||||
url: node.index.url,
|
||||
icon: (
|
||||
<Slot
|
||||
className="mb-auto bg-gradient-to-t from-fd-background/80 p-1 [&_svg]:size-5"
|
||||
style={{
|
||||
color: `hsl(var(--${meta.file.dirname}-color))`,
|
||||
backgroundColor: `hsl(var(--${meta.file.dirname}-color)/.3)`,
|
||||
}}
|
||||
>
|
||||
{node.icon}
|
||||
</Slot>
|
||||
),
|
||||
description: meta.data.description,
|
||||
title: node.name,
|
||||
});
|
||||
}
|
||||
|
||||
if (node.type === 'folder') node.children.forEach(traverse);
|
||||
}
|
||||
|
||||
source.pageTree.children.forEach(traverse);
|
||||
return options;
|
||||
}
|
||||
|
||||
export const docsOptions: DocsLayoutProps = {
|
||||
...baseOptions,
|
||||
tree: utils.pageTree,
|
||||
tree: source.pageTree,
|
||||
nav: {
|
||||
...baseOptions.nav,
|
||||
transparentMode: 'none',
|
||||
@@ -70,20 +108,23 @@ export const docsOptions: DocsLayoutProps = {
|
||||
sidebar: {
|
||||
banner: (
|
||||
<RootToggle
|
||||
options={modes.map((mode) => ({
|
||||
url: `/docs/${mode.param}`,
|
||||
icon: (
|
||||
<mode.icon
|
||||
className="size-9 shrink-0 rounded-md bg-gradient-to-t from-fd-background/80 p-1.5"
|
||||
style={{
|
||||
backgroundColor: `hsl(var(--${mode.param}-color)/.3)`,
|
||||
color: `hsl(var(--${mode.param}-color))`,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
title: mode.name,
|
||||
description: mode.description,
|
||||
}))}
|
||||
options={
|
||||
getItems() ||
|
||||
modes.map((mode) => ({
|
||||
url: `/docs/${mode.param}`,
|
||||
icon: (
|
||||
<mode.icon
|
||||
className="size-9 shrink-0 rounded-md bg-gradient-to-t from-fd-background/80 p-1.5"
|
||||
style={{
|
||||
backgroundColor: `hsl(var(--${mode.param}-color)/.3)`,
|
||||
color: `hsl(var(--${mode.param}-color))`,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
title: mode.name,
|
||||
description: mode.description,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MetadataRoute } from 'next';
|
||||
import { baseUrl } from '@/utils/metadata';
|
||||
import { utils } from '@/app/source';
|
||||
import { source } from '@/app/source';
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const url = (path: string): string => new URL(path, baseUrl).toString();
|
||||
@@ -21,7 +21,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.8,
|
||||
},
|
||||
...utils.getPages().map<MetadataRoute.Sitemap[number]>((page) => ({
|
||||
...source.getPages().map<MetadataRoute.Sitemap[number]>((page) => ({
|
||||
url: url(page.url),
|
||||
lastModified: page.data.lastModified
|
||||
? new Date(page.data.lastModified)
|
||||
|
||||
@@ -3,14 +3,17 @@ import type { InferMetaType, InferPageType } from 'fumadocs-core/source';
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
import { icons } from 'lucide-react';
|
||||
import { attachFile, createOpenAPI } from 'fumadocs-openapi/server';
|
||||
import { create } from '@/components/ui/icon';
|
||||
import { createElement } from 'react';
|
||||
import { IconContainer } from '@/components/ui/icon';
|
||||
import { meta, docs, blog as blogPosts } from '@/.source';
|
||||
|
||||
export const utils = loader({
|
||||
export const source = loader({
|
||||
baseUrl: '/docs',
|
||||
icon(icon) {
|
||||
if (icon && icon in icons)
|
||||
return create({ icon: icons[icon as keyof typeof icons] });
|
||||
return createElement(IconContainer, {
|
||||
icon: icons[icon as keyof typeof icons],
|
||||
});
|
||||
},
|
||||
source: createMDXSource(docs, meta),
|
||||
pageTree: {
|
||||
@@ -23,7 +26,7 @@ export const blog = loader({
|
||||
source: createMDXSource(blogPosts, []),
|
||||
});
|
||||
|
||||
export const openapi = createOpenAPI({});
|
||||
export const openapi = createOpenAPI();
|
||||
|
||||
export type Page = InferPageType<typeof utils>;
|
||||
export type Meta = InferMetaType<typeof utils>;
|
||||
export type Page = InferPageType<typeof source>;
|
||||
export type Meta = InferMetaType<typeof source>;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { type DocumentRecord } from 'fumadocs-core/search/algolia';
|
||||
import { utils } from '@/app/source';
|
||||
import { source } from '@/app/source';
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
export function GET(): Response {
|
||||
const results: DocumentRecord[] = [];
|
||||
const pages = utils.getPages();
|
||||
const pages = source.getPages();
|
||||
|
||||
for (const page of pages) {
|
||||
results.push({
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { TerminalIcon } from 'lucide-react';
|
||||
import { type HTMLAttributes } from 'react';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
export function create({
|
||||
export function IconContainer({
|
||||
icon: Icon,
|
||||
}: {
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement> & {
|
||||
icon?: LucideIcon;
|
||||
}): React.ReactElement {
|
||||
return (
|
||||
<div className="rounded-md border bg-gradient-to-b from-fd-secondary p-1 shadow-sm">
|
||||
<div
|
||||
{...props}
|
||||
className={cn(
|
||||
'rounded-md border bg-gradient-to-b from-fd-secondary p-1 shadow-sm',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{Icon ? <Icon /> : <TerminalIcon />}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
title: User Guide
|
||||
description: The CLI tool that automates setups and installing components.
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
The CLI doesn't need to install any extra packages, but It needs Fumadocs Core for minimal setup.
|
||||
|
||||
You can initialize a config for CLI.
|
||||
|
||||
```package-install
|
||||
npx fumadocs config
|
||||
```
|
||||
|
||||
It defines the output paths of components.
|
||||
|
||||
### Components
|
||||
|
||||
Select and install components.
|
||||
|
||||
```package-install
|
||||
npx fumadocs add
|
||||
```
|
||||
|
||||
You can pass component names.
|
||||
|
||||
```package-install
|
||||
npx fumadocs add banner files
|
||||
```
|
||||
|
||||
#### How the magic works?
|
||||
|
||||
The CLI fetches the latest version of component from the GitHub repository of Fumadocs.
|
||||
When you install the component, it is guaranteed to be up-to-date.
|
||||
|
||||
In addition, it also transforms import paths.
|
||||
Make sure to use the latest version of CLI
|
||||
|
||||
> This is highly Inspired by Shadcn UI.
|
||||
|
||||
### Tree
|
||||
|
||||
Generate files tree for Fumadocs UI `Files` component, using the `tree` command from your terminal.
|
||||
|
||||
```package-install
|
||||
npx fumadocs tree ./my-dir ./output.tsx
|
||||
```
|
||||
|
||||
You can output MDX file too:
|
||||
|
||||
```package-install
|
||||
npx fumadocs tree ./my-dir ./output.mdx
|
||||
```
|
||||
|
||||
See help for further details:
|
||||
|
||||
```package-install
|
||||
npx fumadocs tree -h
|
||||
```
|
||||
|
||||
#### Example Output
|
||||
|
||||
```tsx title="output.tsx"
|
||||
import { File, Folder, Files } from 'fumadocs-ui/components/files';
|
||||
|
||||
export default (
|
||||
<Files>
|
||||
<Folder name="app">
|
||||
<File name="layout.tsx" />
|
||||
<File name="page.tsx" />
|
||||
<File name="global.css" />
|
||||
</Folder>
|
||||
<Folder name="components">
|
||||
<File name="button.tsx" />
|
||||
<File name="tabs.tsx" />
|
||||
<File name="dialog.tsx" />
|
||||
</Folder>
|
||||
<File name="package.json" />
|
||||
</Files>
|
||||
);
|
||||
```
|
||||
|
||||
### Initialize Features
|
||||
|
||||
Some features of Fumadocs require copying code to get started, it is similar to `codemod` but for configuring features automatically.
|
||||
|
||||
```package-install
|
||||
npx fumadocs init
|
||||
```
|
||||
|
||||
Please note that some features may change your existing code, make sure to commit your changes to Git before running it.
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"root": true,
|
||||
"title": "Fumadocs CLI",
|
||||
"description": "The CLI tool for docs automation",
|
||||
"icon": "Terminal"
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"title": "headless",
|
||||
"title": "Fumadocs Core",
|
||||
"description": "The headless library",
|
||||
"icon": "Library",
|
||||
"root": true,
|
||||
"pages": [
|
||||
"---Guide---",
|
||||
|
||||
@@ -16,7 +16,7 @@ export default defineConfig({
|
||||
```
|
||||
|
||||
During a production build (`next build`), Fumadocs MDX will output a manifest file in `.source/manifest.json`.
|
||||
You can use it to collect build infos.
|
||||
You can use it to collect build information.
|
||||
|
||||
### What is It?
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"title": "mdx",
|
||||
"title": "Fumadocs MDX",
|
||||
"description": "The official content source",
|
||||
"icon": "Pencil",
|
||||
"root": true,
|
||||
"pages": [
|
||||
"---Guide---",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"pages": ["ui", "headless", "mdx", "cli"]
|
||||
}
|
||||
@@ -55,6 +55,8 @@ source.getPage(params.slug, params.lang);
|
||||
source.getPages(params.lang);
|
||||
```
|
||||
|
||||
See [Source API](/docs/headless/source-api) for other usages.
|
||||
|
||||
### Middleware
|
||||
|
||||
Create a middleware that redirects users to appropriate locale.
|
||||
@@ -117,7 +119,7 @@ Make sure to update references to `source` and configure Static Site Generation
|
||||
|
||||
### Writing Documents
|
||||
|
||||
see [Page Conventions](/docs/ui/page-conventions#internationalization) to learn how to organize your documents.
|
||||
see [Page Conventions](/docs/ui/page-conventions#internationalization) to learn how to organize your documents.
|
||||
|
||||
### Search
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"title": "ui",
|
||||
"title": "Framework",
|
||||
"description": "The docs framework",
|
||||
"icon": "Building2",
|
||||
"root": true,
|
||||
"pages": [
|
||||
"---Guide---",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"dependencies": {
|
||||
"@next/env": "^14.2.11",
|
||||
"@radix-ui/react-popover": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"@shikijs/transformers": "^1.17.5",
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
defineDocs,
|
||||
defineCollections,
|
||||
frontmatterSchema,
|
||||
metaSchema,
|
||||
} from 'fumadocs-mdx/config';
|
||||
import { rehypeCodeDefaultOptions } from 'fumadocs-core/mdx-plugins';
|
||||
import { transformerTwoslash } from 'fumadocs-twoslash';
|
||||
@@ -28,6 +29,11 @@ export const { docs, meta } = defineDocs({
|
||||
method: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
meta: {
|
||||
schema: metaSchema.extend({
|
||||
description: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export const blog = defineCollections({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Metadata } from 'next/types';
|
||||
import { createMetadataImage } from 'fumadocs-core/server';
|
||||
import { utils } from '@/app/source';
|
||||
import { source } from '@/app/source';
|
||||
|
||||
export function createMetadata(override: Metadata): Metadata {
|
||||
return {
|
||||
@@ -25,11 +25,11 @@ export function createMetadata(override: Metadata): Metadata {
|
||||
}
|
||||
|
||||
export const baseUrl =
|
||||
process.env.NODE_ENV === 'development'
|
||||
process.env.NODE_ENV === 'development' || !process.env.VERCEL_URL
|
||||
? new URL('http://localhost:3000')
|
||||
: new URL(`https://${process.env.VERCEL_URL!}`);
|
||||
: new URL(`https://${process.env.VERCEL_URL}`);
|
||||
|
||||
export const metadataImage = createMetadataImage({
|
||||
source: utils,
|
||||
source,
|
||||
imageRoute: 'og',
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { Command } from 'commander';
|
||||
import picocolors from 'picocolors';
|
||||
import { isCancel, multiselect, outro } from '@clack/prompts';
|
||||
import { isCancel, multiselect, outro, select } from '@clack/prompts';
|
||||
import { init } from '@/commands/init';
|
||||
import { add } from '@/commands/add';
|
||||
import { initConfig, loadConfig } from '@/config';
|
||||
@@ -34,15 +34,33 @@ program
|
||||
program
|
||||
.command('init')
|
||||
.description('init a new plugin to your docs')
|
||||
.argument('<string>', 'plugin name')
|
||||
.argument('[string]', 'plugin name')
|
||||
.option('--config <string>')
|
||||
.action(async (str: string, { config }) => {
|
||||
.action(async (str: string | undefined, { config }) => {
|
||||
const loadedConfig = await loadConfig(config as string | undefined);
|
||||
const plugin = str in plugins ? plugins[str] : undefined;
|
||||
|
||||
if (!plugin) throw new Error(`Plugin not found: ${str}`);
|
||||
if (str) {
|
||||
const plugin = str in plugins ? plugins[str] : undefined;
|
||||
if (!plugin) throw new Error(`Plugin not found: ${str}`);
|
||||
|
||||
await init(plugin, loadedConfig);
|
||||
await init(plugin, loadedConfig);
|
||||
return;
|
||||
}
|
||||
|
||||
const value = await select({
|
||||
message: 'Select components to install',
|
||||
options: Object.keys(plugins).map((c) => ({
|
||||
label: c,
|
||||
value: c,
|
||||
})),
|
||||
});
|
||||
|
||||
if (isCancel(value)) {
|
||||
outro('Ended');
|
||||
return;
|
||||
}
|
||||
|
||||
await init(plugins[value as keyof typeof plugins], loadedConfig);
|
||||
});
|
||||
|
||||
program
|
||||
|
||||
@@ -13,6 +13,10 @@ export interface Item {
|
||||
url: string;
|
||||
external?: boolean;
|
||||
icon?: ReactElement;
|
||||
|
||||
$ref?: {
|
||||
file: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Separator {
|
||||
@@ -23,9 +27,15 @@ export interface Separator {
|
||||
export interface Folder {
|
||||
/**
|
||||
* Optional id to be attached to folders
|
||||
*
|
||||
* @deprecated use `$ref`
|
||||
*/
|
||||
id?: string;
|
||||
|
||||
$ref?: {
|
||||
metaFile?: string;
|
||||
};
|
||||
|
||||
type: 'folder';
|
||||
name: ReactNode;
|
||||
root?: boolean;
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { MetaData, PageData, UrlFn } from './types';
|
||||
import type { BuildPageTreeOptions } from './page-tree-builder';
|
||||
import { createPageTreeBuilder } from './page-tree-builder';
|
||||
import { type FileInfo } from './path';
|
||||
import type { File, PageFile, Storage } from './file-system';
|
||||
import type { MetaFile, PageFile, Storage } from './file-system';
|
||||
|
||||
export interface LoaderConfig {
|
||||
source: SourceConfig;
|
||||
@@ -80,9 +80,8 @@ export interface LoaderOutput<Config extends LoaderConfig> {
|
||||
pageTree: Config['i18n'] extends true
|
||||
? Record<string, PageTree.Root>
|
||||
: PageTree.Root;
|
||||
_i18n?: I18nConfig;
|
||||
|
||||
files: File[];
|
||||
_i18n?: I18nConfig;
|
||||
|
||||
/**
|
||||
* Get list of pages from language, empty if language hasn't specified
|
||||
@@ -101,6 +100,14 @@ export interface LoaderOutput<Config extends LoaderConfig> {
|
||||
language?: string,
|
||||
) => Page<Config['source']['pageData']> | undefined;
|
||||
|
||||
getNodePage: (
|
||||
node: PageTree.Item,
|
||||
) => Page<Config['source']['pageData']> | undefined;
|
||||
|
||||
getNodeMeta: (
|
||||
node: PageTree.Folder,
|
||||
) => Meta<Config['source']['metaData']> | undefined;
|
||||
|
||||
/**
|
||||
* generate static params for Next.js SSG
|
||||
*/
|
||||
@@ -113,35 +120,53 @@ export interface LoaderOutput<Config extends LoaderConfig> {
|
||||
) => (Record<TSlug, string[]> & Record<TLang, string>)[];
|
||||
}
|
||||
|
||||
function buildPageMap(
|
||||
function indexPages(
|
||||
storage: Storage,
|
||||
getUrl: UrlFn,
|
||||
languages: string[] = [],
|
||||
): Map<string, Map<string, Page>> {
|
||||
const map = new Map<string, Map<string, Page>>();
|
||||
): {
|
||||
// locale -> (slugs -> page[])
|
||||
i18n: Map<string, Map<string, Page>>;
|
||||
|
||||
pathToPage: Map<string, Page>;
|
||||
pathToMeta: Map<string, Meta>;
|
||||
} {
|
||||
const i18n = new Map<string, Map<string, Page>>();
|
||||
const pages = new Map<string, Page>();
|
||||
const metas = new Map<string, Meta>();
|
||||
|
||||
const defaultMap = new Map<string, Page>();
|
||||
|
||||
map.set('', defaultMap);
|
||||
i18n.set('', defaultMap);
|
||||
for (const file of storage.list()) {
|
||||
if (file.format !== 'page' || file.file.locale) continue;
|
||||
const page = fileToPage(file, getUrl);
|
||||
if (file.format === 'meta') metas.set(file.file.path, fileToMeta(file));
|
||||
|
||||
defaultMap.set(page.slugs.join('/'), page);
|
||||
if (file.format === 'page') {
|
||||
const page = fileToPage(file, getUrl, file.file.locale);
|
||||
pages.set(file.file.path, page);
|
||||
|
||||
for (const lang of languages) {
|
||||
const langMap = map.get(lang) ?? new Map<string, Page>();
|
||||
if (file.file.locale) continue;
|
||||
defaultMap.set(page.slugs.join('/'), page);
|
||||
|
||||
const localized = storage.read(
|
||||
`${file.file.flattenedPath}.${lang}`,
|
||||
'page',
|
||||
);
|
||||
const localizedPage = fileToPage(localized ?? file, getUrl, lang);
|
||||
langMap.set(localizedPage.slugs.join('/'), localizedPage);
|
||||
map.set(lang, langMap);
|
||||
for (const lang of languages) {
|
||||
const langMap = i18n.get(lang) ?? new Map<string, Page>();
|
||||
|
||||
const localized = storage.read(
|
||||
`${file.file.flattenedPath}.${lang}`,
|
||||
'page',
|
||||
);
|
||||
const localizedPage = fileToPage(localized ?? file, getUrl, lang);
|
||||
langMap.set(localizedPage.slugs.join('/'), localizedPage);
|
||||
i18n.set(lang, langMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
return {
|
||||
i18n,
|
||||
pathToPage: pages,
|
||||
pathToMeta: metas,
|
||||
};
|
||||
}
|
||||
|
||||
export function createGetUrl(baseUrl: string): UrlFn {
|
||||
@@ -195,7 +220,7 @@ function createOutput({
|
||||
getSlugs: slugsFn,
|
||||
},
|
||||
);
|
||||
const i18nMap = buildPageMap(storage, getUrl, i18n?.languages);
|
||||
const walker = indexPages(storage, getUrl, i18n?.languages);
|
||||
const builder = createPageTreeBuilder();
|
||||
const pageTree =
|
||||
i18n === undefined
|
||||
@@ -216,14 +241,13 @@ function createOutput({
|
||||
return {
|
||||
_i18n: i18n,
|
||||
pageTree: pageTree as LoaderOutput<LoaderConfig>['pageTree'],
|
||||
files: storage.list(),
|
||||
getPages(language = i18n?.defaultLanguage ?? '') {
|
||||
return Array.from(i18nMap.get(language)?.values() ?? []);
|
||||
return Array.from(walker.i18n.get(language)?.values() ?? []);
|
||||
},
|
||||
getLanguages() {
|
||||
const list: LanguageEntry[] = [];
|
||||
|
||||
for (const [language, pages] of i18nMap) {
|
||||
for (const [language, pages] of walker.i18n) {
|
||||
if (language === '') continue;
|
||||
|
||||
list.push({
|
||||
@@ -235,7 +259,17 @@ function createOutput({
|
||||
return list;
|
||||
},
|
||||
getPage(slugs = [], language = i18n?.defaultLanguage ?? '') {
|
||||
return i18nMap.get(language)?.get(slugs.join('/'));
|
||||
return walker.i18n.get(language)?.get(slugs.join('/'));
|
||||
},
|
||||
getNodeMeta(node) {
|
||||
if (!node.$ref?.metaFile) return;
|
||||
|
||||
return walker.pathToMeta.get(node.$ref.metaFile);
|
||||
},
|
||||
getNodePage(node) {
|
||||
if (!node.$ref?.file) return;
|
||||
|
||||
return walker.pathToPage.get(node.$ref.file);
|
||||
},
|
||||
// @ts-expect-error -- ignore this
|
||||
generateParams(slug, lang) {
|
||||
@@ -248,13 +282,20 @@ function createOutput({
|
||||
);
|
||||
}
|
||||
|
||||
return Array.from(i18nMap.get('')?.values() ?? []).map((page) => ({
|
||||
return Array.from(walker.i18n.get('')?.values() ?? []).map((page) => ({
|
||||
[slug ?? 'slug']: page.slugs,
|
||||
}));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function fileToMeta<Data = MetaData>(file: MetaFile): Meta<Data> {
|
||||
return {
|
||||
file: file.file,
|
||||
data: file.data.data as Data,
|
||||
};
|
||||
}
|
||||
|
||||
function fileToPage<Data = PageData>(
|
||||
file: PageFile,
|
||||
getUrl: UrlFn,
|
||||
|
||||
@@ -18,11 +18,11 @@ interface PageTreeBuilderContext {
|
||||
|
||||
export interface BuildPageTreeOptions {
|
||||
/**
|
||||
* Attach the `folder.id` property
|
||||
* Remove references to the file path of original nodes (`$ref`)
|
||||
*
|
||||
* @defaultValue false
|
||||
*/
|
||||
attachFolderIds?: boolean;
|
||||
noRef?: boolean;
|
||||
|
||||
attachFile?: (node: PageTree.Item, file?: PageFile) => PageTree.Item;
|
||||
attachFolder?: (
|
||||
@@ -211,12 +211,13 @@ function buildFolderNode(
|
||||
defaultOpen: metadata?.defaultOpen,
|
||||
index,
|
||||
children,
|
||||
$ref: !ctx.options.noRef
|
||||
? {
|
||||
metaFile: meta?.file.path,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
if (ctx.options.attachFolderIds) {
|
||||
node.id = folder.file.flattenedPath;
|
||||
}
|
||||
|
||||
return removeUndefined(
|
||||
ctx.options.attachFolder?.(node, folder, meta) ?? node,
|
||||
);
|
||||
@@ -245,6 +246,11 @@ function buildFileNode(
|
||||
name: localized.data.data.title,
|
||||
icon: ctx.options.resolveIcon?.(localized.data.data.icon),
|
||||
url: ctx.options.getUrl(localized.data.slugs, urlLocale),
|
||||
$ref: !ctx.options.noRef
|
||||
? {
|
||||
file: localized.file.path,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
return removeUndefined(ctx.options.attachFile?.(item, file) ?? item);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { isActive } from '@/utils/shared';
|
||||
import { useSidebar } from '@/contexts/sidebar';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
|
||||
|
||||
interface Option {
|
||||
export interface Option {
|
||||
/**
|
||||
* Redirect URL of the folder, usually the index page
|
||||
*/
|
||||
@@ -30,8 +30,7 @@ export function RootToggle({
|
||||
const [open, setOpen] = useState(false);
|
||||
const { closeOnRedirect } = useSidebar();
|
||||
const pathname = usePathname();
|
||||
const selected =
|
||||
options.find((item) => isActive(item.url, pathname, true)) ?? options[0];
|
||||
const selected = options.find((item) => isActive(item.url, pathname, true));
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
closeOnRedirect.current = false;
|
||||
@@ -42,12 +41,15 @@ export function RootToggle({
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger
|
||||
{...props}
|
||||
{...selected?.props}
|
||||
className={cn(
|
||||
'-mx-2 flex flex-row items-center gap-2.5 rounded-lg p-2 hover:bg-fd-accent/50 hover:text-fd-accent-foreground',
|
||||
'-mx-1 flex flex-row items-center gap-2.5 rounded-lg p-1 hover:bg-fd-accent/50 hover:text-fd-accent-foreground',
|
||||
props.className,
|
||||
selected?.props?.className,
|
||||
)}
|
||||
>
|
||||
<Item {...selected} />
|
||||
{selected ? <Item {...selected} /> : null}
|
||||
|
||||
<ChevronDown className="size-4 text-fd-muted-foreground md:me-1.5" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[var(--radix-popover-trigger-width)] overflow-hidden p-0">
|
||||
|
||||
Generated
+3
@@ -56,6 +56,9 @@ importers:
|
||||
'@radix-ui/react-popover':
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
'@radix-ui/react-slot':
|
||||
specifier: ^1.1.0
|
||||
version: 1.1.0(@types/[email protected])([email protected])
|
||||
'@radix-ui/react-tooltip':
|
||||
specifier: ^1.1.2
|
||||
version: 1.1.2(@types/[email protected])(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
|
||||
Reference in New Issue
Block a user