UI: Drop fumadocs-core/hide-if-empty usage

This commit is contained in:
Fuma Nama
2025-10-08 18:50:28 +08:00
parent 601e9175d1
commit 1494340703
7 changed files with 201 additions and 92 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-ui': patch
---
Drop `fumadocs-core/hide-if-empty` usage, prefer user-side component overrides for precise control
@@ -0,0 +1,35 @@
---
title: Hello World
description: |
Your first `document`
```ts
console.log("Hello World")
```
---
Hey there! fsd asfd sdfsfda
sfd
fdsa ff dsaf sdf sda fasd fsd
## Heading
Hello World dsasfdafsd
<Cards>
<Card title="Learn more about Next.js" href="https://nextjs.org/docs" />
<Card title="Learn more about Fumadocs" href="https://fumadocs.dev" />
</Cards>
### Heading
#### Heading
| Head | Description |
| ------------------------------- | ----------------------------------- |
| `hello` | Hello World |
| very **important** | Hey |
| _Surprisingly_ | Fumadocs |
| very long text that looks weird | hello world hello world hello world |
<include>./test.mdx</include>
@@ -0,0 +1,3 @@
{
"root": true
}
@@ -0,0 +1,24 @@
---
title: Test
description: A document to test Fumadocs
---
Hey there!
## Cards
<Cards>
<Card title="Learn more about Next.js" href="https://nextjs.org/docs" />
<Card title="Learn more about Fumadocs" href="https://fumadocs.dev" />
</Cards>
### CodeBlock
```js
console.log('Hello World');
```
#### List
- Hello
- World
+33 -12
View File
@@ -49,6 +49,28 @@ function isEmpty(node: HTMLElement) {
return true;
}
function disableAnimation() {
const css = document.createElement('style');
const nonce = document.currentScript?.nonce;
if (nonce) css.setAttribute('nonce', nonce);
css.appendChild(
document.createTextNode(
`*,*::before,*::after{-webkit-transition:none!important;-moz-transition:none!important;-o-transition:none!important;-ms-transition:none!important;transition:none!important}`,
),
);
document.head.appendChild(css);
return () => {
// Force restyle
(() => window.getComputedStyle(document.body))();
// Wait for next tick before removing
setTimeout(() => {
document.head.removeChild(css);
}, 1);
};
}
/**
* The built-in CSS `:empty` selector cannot detect if the children is hidden, classes such as `md:hidden` causes it to fail.
* This component is an enhancement to `empty:hidden` to fix the issue described above.
@@ -80,10 +102,10 @@ export function HideIfEmpty<Props extends HTMLAttributes<HTMLElement>>({
const init = (id: string) => {
const element = getElement(id);
if (element) element.hidden = isEmpty(element);
const script = document.currentScript;
if (script) script.parentNode?.removeChild(script);
if (element) {
disableAnimation();
element.hidden = isEmpty(element);
}
};
return (
@@ -93,14 +115,13 @@ export function HideIfEmpty<Props extends HTMLAttributes<HTMLElement>>({
data-fd-if-empty={id}
hidden={empty ?? false}
/>
{empty === undefined && (
<script
nonce={nonce}
dangerouslySetInnerHTML={{
__html: `{${getElement};${isEmpty};(${init})("${id}")}`,
}}
/>
)}
<script
suppressHydrationWarning
nonce={nonce}
dangerouslySetInnerHTML={{
__html: `{${getElement};${isEmpty};${disableAnimation};(${init})("${id}")}`,
}}
/>
</>
);
}
+34 -30
View File
@@ -31,7 +31,6 @@ import {
type BaseLayoutProps,
BaseLinkItem,
getLinks,
type IconItemType,
type LinkItemType,
} from '@/layouts/shared';
import {
@@ -52,7 +51,6 @@ import {
LargeSearchToggle,
SearchToggle,
} from '@/components/layout/search-toggle';
import { HideIfEmpty } from 'fumadocs-core/hide-if-empty';
import {
getSidebarTabs,
type GetSidebarTabsOptions,
@@ -138,9 +136,7 @@ export function DocsLayout({
} = sidebarProps;
if (component) return component;
const iconLinks = links.filter(
(item): item is IconItemType => item.type === 'icon',
);
const iconLinks = links.filter((item) => item.type === 'icon');
const viewport = (
<SidebarViewport>
@@ -244,32 +240,40 @@ export function DocsLayout({
{banner}
</SidebarHeader>
{viewport}
<HideIfEmpty as={SidebarFooter}>
<div className="flex text-fd-muted-foreground items-center empty:hidden">
{i18n ? (
<LanguageToggle>
<Languages className="size-4.5" />
</LanguageToggle>
) : null}
{iconLinks.map((item, i) => (
<BaseLinkItem
key={i}
item={item}
className={cn(
buttonVariants({ size: 'icon-sm', color: 'ghost' }),
)}
aria-label={item.label}
>
{item.icon}
</BaseLinkItem>
))}
{themeSwitch.enabled !== false &&
(themeSwitch.component ?? (
<ThemeToggle className="ms-auto p-0" mode={themeSwitch.mode} />
{(i18n ||
iconLinks.length > 0 ||
themeSwitch?.enabled !== false ||
footer) && (
<SidebarFooter>
<div className="flex text-fd-muted-foreground items-center empty:hidden">
{i18n && (
<LanguageToggle>
<Languages className="size-4.5" />
</LanguageToggle>
)}
{iconLinks.map((item, i) => (
<BaseLinkItem
key={i}
item={item}
className={cn(
buttonVariants({ size: 'icon-sm', color: 'ghost' }),
)}
aria-label={item.label}
>
{item.icon}
</BaseLinkItem>
))}
</div>
{footer}
</HideIfEmpty>
{themeSwitch.enabled !== false &&
(themeSwitch.component ?? (
<ThemeToggle
className="ms-auto p-0"
mode={themeSwitch.mode}
/>
))}
</div>
{footer}
</SidebarFooter>
)}
</SidebarContent>
);
+67 -50
View File
@@ -1,5 +1,6 @@
import {
type ComponentProps,
type FC,
Fragment,
type HTMLAttributes,
type ReactNode,
@@ -60,7 +61,6 @@ import {
LargeSearchToggle,
SearchToggle,
} from '@/components/layout/search-toggle';
import { HideIfEmpty } from 'fumadocs-core/hide-if-empty';
import {
getSidebarTabs,
type GetSidebarTabsOptions,
@@ -89,8 +89,8 @@ interface SidebarOptions
*/
tabs?: Option[] | GetSidebarTabsOptions | false;
banner?: ReactNode;
footer?: ReactNode;
banner?: ReactNode | FC<ComponentProps<typeof SidebarHeader>>;
footer?: ReactNode | FC<ComponentProps<typeof SidebarFooter>>;
/**
* Support collapsing the sidebar on desktop mode
@@ -137,6 +137,24 @@ export function DocsLayout(props: DocsLayoutProps) {
defaultOpenLevel,
...rest
} = sidebarProps;
const Header =
typeof banner === 'function'
? banner
: (props: ComponentProps<typeof SidebarHeader>) => (
<SidebarHeader {...props}>
{props.children}
{banner}
</SidebarHeader>
);
const Footer =
typeof footer === 'function'
? footer
: (props: ComponentProps<typeof SidebarFooter>) => (
<SidebarFooter {...props}>
{props.children}
{footer}
</SidebarFooter>
);
const iconLinks = links.filter((item) => item.type === 'icon');
const rootToggle = (
@@ -150,30 +168,6 @@ export function DocsLayout(props: DocsLayoutProps) {
</>
);
const sidebarNav = (
<div className="flex justify-between">
<Link
href={nav.url ?? '/'}
className="inline-flex items-center gap-2.5 font-medium"
>
{nav.title}
</Link>
{collapsible && (
<SidebarCollapseTrigger
className={cn(
buttonVariants({
color: 'ghost',
size: 'icon-sm',
className: 'mt-px mb-auto text-fd-muted-foreground',
}),
)}
>
<SidebarIcon />
</SidebarCollapseTrigger>
)}
</div>
);
const viewport = (
<SidebarViewport>
{links
@@ -200,16 +194,39 @@ export function DocsLayout(props: DocsLayoutProps) {
rest.className,
)}
>
<HideIfEmpty as={SidebarHeader}>
{navMode === 'auto' && sidebarNav}
<Header className="empty:hidden">
{navMode === 'auto' && (
<div className="flex justify-between">
<Link
href={nav.url ?? '/'}
className="inline-flex items-center gap-2.5 font-medium"
>
{nav.title}
</Link>
{collapsible && (
<SidebarCollapseTrigger
className={cn(
buttonVariants({
color: 'ghost',
size: 'icon-sm',
className: 'mt-px mb-auto text-fd-muted-foreground',
}),
)}
>
<SidebarIcon />
</SidebarCollapseTrigger>
)}
</div>
)}
{nav.children}
{banner}
{rootToggle}
</HideIfEmpty>
</Header>
{viewport}
<HideIfEmpty
as={SidebarFooter}
className="flex flex-row text-fd-muted-foreground items-center"
<Footer
className={cn(
'hidden flex-row text-fd-muted-foreground items-center',
iconLinks.length > 0 && 'max-lg:flex',
)}
>
{iconLinks.map((item, i) => (
<BaseLinkItem
@@ -227,14 +244,13 @@ export function DocsLayout(props: DocsLayoutProps) {
{item.icon}
</BaseLinkItem>
))}
{footer}
</HideIfEmpty>
</Footer>
</SidebarContent>
);
const mobile = (
<SidebarContentMobile {...rest}>
<SidebarHeader>
<Header>
<SidebarTrigger
className={cn(
buttonVariants({
@@ -246,13 +262,15 @@ export function DocsLayout(props: DocsLayoutProps) {
>
<X />
</SidebarTrigger>
{banner}
{rootToggle}
</SidebarHeader>
</Header>
{viewport}
<HideIfEmpty
as={SidebarFooter}
className="flex flex-row items-center justify-end"
<Footer
className={cn(
'hidden flex-row items-center justify-end',
(i18n || themeSwitch.enabled !== false) && 'flex',
iconLinks.length > 0 && 'max-lg:flex',
)}
>
{iconLinks.map((item, i) => (
<BaseLinkItem
@@ -271,17 +289,16 @@ export function DocsLayout(props: DocsLayoutProps) {
{item.icon}
</BaseLinkItem>
))}
{i18n ? (
{i18n && (
<LanguageToggle>
<Languages className="size-4.5 text-fd-muted-foreground" />
</LanguageToggle>
) : null}
)}
{themeSwitch.enabled !== false &&
(themeSwitch.component ?? (
<ThemeToggle mode={themeSwitch.mode ?? 'light-dark-system'} />
))}
{footer}
</HideIfEmpty>
</Footer>
</SidebarContentMobile>
);
@@ -321,16 +338,16 @@ export function DocsLayout(props: DocsLayoutProps) {
function DocsNavbar({
links,
tabs,
sidebar: { collapsible: sidebarCollapsible = true } = {},
searchToggle = {},
themeSwitch = {},
nav = {},
...props
i18n,
}: DocsLayoutProps & {
links: LinkItemType[];
tabs: Option[];
}) {
const navMode = nav.mode ?? 'auto';
const sidebarCollapsible = props.sidebar?.collapsible ?? true;
return (
<Navbar
@@ -438,11 +455,11 @@ function DocsNavbar({
</div>
<div className="flex items-center gap-2 max-md:hidden">
{props.i18n ? (
{i18n && (
<LanguageToggle>
<Languages className="size-4.5 text-fd-muted-foreground" />
</LanguageToggle>
) : null}
)}
{themeSwitch.enabled !== false &&
(themeSwitch.component ?? (
<ThemeToggle mode={themeSwitch.mode ?? 'light-dark-system'} />