UI: unify sidebar tabs matching

This commit is contained in:
Fuma Nama
2025-08-29 21:36:00 +08:00
parent 902a6128f1
commit dc6d8a0570
5 changed files with 54 additions and 55 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-ui': patch
---
unify sidebar tabs matching
@@ -4,26 +4,12 @@ import { type ComponentProps, type ReactNode, useMemo, useState } from 'react';
import Link from 'fumadocs-core/link';
import { usePathname } from 'fumadocs-core/framework';
import { cn } from '@/utils/cn';
import { isActive } from '@/utils/is-active';
import { isTabActive } from '@/utils/is-active';
import { useSidebar } from '@/contexts/sidebar';
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import type { SidebarTab } from '@/utils/get-sidebar-tabs';
export interface Option {
/**
* Redirect URL of the folder, usually the index page
*/
url: string;
icon?: ReactNode;
title: ReactNode;
description?: ReactNode;
unlisted?: boolean;
/**
* Detect from a list of urls
*/
urls?: Set<string>;
export interface Option extends SidebarTab {
props?: ComponentProps<'a'>;
}
@@ -40,13 +26,7 @@ export function RootToggle({
const pathname = usePathname();
const selected = useMemo(() => {
const lookup = pathname.endsWith('/') ? pathname.slice(0, -1) : pathname;
return options.findLast((item) => {
if (item.urls) return item.urls.has(lookup);
return isActive(item.url, pathname, true);
});
return options.findLast((item) => isTabActive(item, pathname));
}, [options, pathname]);
const onClick = () => {
+8 -14
View File
@@ -7,7 +7,7 @@ import { buttonVariants } from '@/components/ui/button';
import { Sidebar as SidebarIcon } from 'lucide-react';
import Link from 'fumadocs-core/link';
import { usePathname } from 'fumadocs-core/framework';
import { isActive } from '@/utils/is-active';
import { isTabActive } from '@/utils/is-active';
import type { Option } from '@/components/layout/root-toggle';
export function Navbar({
@@ -90,15 +90,7 @@ export function LayoutTabs({
}) {
const pathname = usePathname();
const selected = useMemo(() => {
const url = pathname.endsWith('/') ? pathname.slice(0, -1) : pathname;
return options.findLast((option) => {
if (option.urls) {
return option.urls.has(url);
}
return isActive(option.url, pathname, true);
});
return options.findLast((option) => isTabActive(option, pathname));
}, [options, pathname]);
return (
@@ -121,7 +113,7 @@ export function LayoutTabs({
}
function LayoutTab({
option,
option: { title, url, unlisted, props },
selected = false,
}: {
option: Option;
@@ -129,14 +121,16 @@ function LayoutTab({
}) {
return (
<Link
href={url}
{...props}
className={cn(
'inline-flex border-b-2 border-transparent transition-colors items-center pb-1.5 font-medium gap-2 text-fd-muted-foreground text-sm text-nowrap hover:text-fd-accent-foreground',
option.unlisted && !selected && 'hidden',
unlisted && !selected && 'hidden',
selected && 'border-fd-primary text-fd-primary',
props?.className,
)}
href={option.url}
>
{option.title}
{title}
</Link>
);
}
+29 -17
View File
@@ -1,8 +1,25 @@
import type { PageTree } from 'fumadocs-core/server';
import type { Option } from '@/components/layout/root-toggle';
import type { ReactNode } from 'react';
export interface SidebarTab {
/**
* Redirect URL of the folder, usually the index page
*/
url: string;
icon?: ReactNode;
title: ReactNode;
description?: ReactNode;
/**
* Detect from a list of urls
*/
urls?: Set<string>;
unlisted?: boolean;
}
export interface GetSidebarTabsOptions {
transform?: (option: Option, node: PageTree.Folder) => Option | null;
transform?: (option: SidebarTab, node: PageTree.Folder) => SidebarTab | null;
}
const defaultTransform: GetSidebarTabsOptions['transform'] = (option, node) => {
@@ -21,18 +38,18 @@ const defaultTransform: GetSidebarTabsOptions['transform'] = (option, node) => {
export function getSidebarTabs(
tree: PageTree.Root,
{ transform = defaultTransform }: GetSidebarTabsOptions = {},
): Option[] {
function findOptions(
): SidebarTab[] {
const results: SidebarTab[] = [];
function scanOptions(
node: PageTree.Root | PageTree.Folder,
unlisted?: boolean,
): Option[] {
const results: Option[] = [];
) {
if ('root' in node && node.root) {
const urls = getFolderUrls(node);
if (urls.size > 0) {
const option: Option = {
const option: SidebarTab = {
url: urls.values().next().value ?? '',
title: node.name,
icon: node.icon,
@@ -47,19 +64,14 @@ export function getSidebarTabs(
}
for (const child of node.children) {
if (child.type === 'folder')
results.push(...findOptions(child, unlisted));
if (child.type === 'folder') scanOptions(child, unlisted);
}
return results;
}
const options = findOptions(tree);
if (tree.fallback) {
options.push(...findOptions(tree.fallback, true));
}
scanOptions(tree);
if (tree.fallback) scanOptions(tree.fallback, true);
return options;
return results;
}
function getFolderUrls(
+8
View File
@@ -1,3 +1,5 @@
import type { SidebarTab } from '@/utils/get-sidebar-tabs';
function normalize(url: string) {
if (url.length > 1 && url.endsWith('/')) return url.slice(0, -1);
return url;
@@ -13,3 +15,9 @@ export function isActive(
return url === pathname || (nested && pathname.startsWith(`${url}/`));
}
export function isTabActive(tab: SidebarTab, pathname: string) {
if (tab.urls) return tab.urls.has(normalize(pathname));
return isActive(tab.url, pathname, true);
}