UI: Redesign mobile sidebar

This commit is contained in:
Fuma Nama
2025-05-08 20:35:43 +08:00
parent 4de7fe7c60
commit 52b5ad8e1a
23 changed files with 388 additions and 372 deletions
+9
View File
@@ -0,0 +1,9 @@
---
'fumadocs-ui': minor
---
**Redesign mobile sidebar**
Mobile sidebar is now a separate component from the desktop one, with its own id `nd-sidebar-mobile`.
note to advanced use cases: Fumadocs UI now stopped using `fumadocs-core/sidebar`, avoid using the primitive directly as provider is not used.
+1 -1
View File
@@ -20,7 +20,7 @@ const docsOptions: DocsLayoutProps = {
...option,
icon: (
<div
className="rounded-md p-1 shadow-lg ring-2 [&_svg]:size-5"
className="rounded-md p-1 shadow-lg ring-2 [&_svg]:size-6.5 md:[&_svg]:size-5"
style={
{
color,
+10 -23
View File
@@ -1,16 +1,16 @@
'use client';
import {
createContext,
useContext,
useEffect,
useState,
type ElementType,
type ReactNode,
type ReactElement,
type ComponentPropsWithoutRef,
createContext,
type ElementType,
type ReactElement,
type ReactNode,
useContext,
useMemo,
useState,
} from 'react';
import { RemoveScroll } from 'react-remove-scroll';
import { useMediaQuery } from './utils/use-media-query';
const SidebarContext = createContext<SidebarContextType | null>(null);
@@ -105,27 +105,14 @@ export function SidebarList<T extends ElementType = 'aside'>({
...props
}: SidebarContentProps<T>): ReactElement {
const { open } = useSidebarContext();
const [isBlocking, setIsBlocking] = useState(false);
useEffect(() => {
if (!removeScrollOn) return;
const mediaQueryList = window.matchMedia(removeScrollOn);
const handleChange = (): void => {
setIsBlocking(mediaQueryList.matches);
};
handleChange();
mediaQueryList.addEventListener('change', handleChange);
return () => {
mediaQueryList.removeEventListener('change', handleChange);
};
}, [removeScrollOn]);
const isBlocking =
useMediaQuery(removeScrollOn ?? '', !removeScrollOn) ?? false;
return (
<RemoveScroll
as={as ?? 'aside'}
data-open={open}
enabled={Boolean(isBlocking && open)}
enabled={isBlocking && open}
{...props}
>
{props.children}
@@ -0,0 +1,21 @@
import { useEffect, useState } from 'react';
export function useMediaQuery(query: string, disabled = false): boolean | null {
const [isMatch, setMatch] = useState<boolean | null>(null);
useEffect(() => {
if (disabled) return;
const mediaQueryList = window.matchMedia(query);
const handleChange = () => {
setMatch(mediaQueryList.matches);
};
handleChange();
mediaQueryList.addEventListener('change', handleChange);
return () => {
mediaQueryList.removeEventListener('change', handleChange);
};
}, [disabled, query]);
return isMatch;
}
+1
View File
@@ -23,6 +23,7 @@ export default defineConfig({
'src/search/orama-cloud.ts',
'src/utils/use-on-change.ts',
'src/utils/use-effect-event.ts',
'src/utils/use-media-query.ts',
'src/i18n/index.ts',
'src/highlight/index.ts',
'src/highlight/client.tsx',
@@ -17,7 +17,7 @@ Open http://localhost:3000 with your browser to see the result.
In the project, you can see:
- `lib/source.ts`: Code for content source adapter, [`loader()`](/docs/headless/source-api) provides the interface to access your content.
- `lib/source.ts`: Code for content source adapter, [`loader()`](https://fumadocs.dev/docs/headless/source-api) provides the interface to access your content.
- `app/layout.config.tsx`: Shared options for layouts, optional but preferred to keep.
| Route | Description |
+1 -1
View File
@@ -17,7 +17,7 @@ Open http://localhost:3000 with your browser to see the result.
In the project, you can see:
- `lib/source.ts`: Code for content source adapter, [`loader()`](/docs/headless/source-api) provides the interface to access your content.
- `lib/source.ts`: Code for content source adapter, [`loader()`](https://fumadocs.dev/docs/headless/source-api) provides the interface to access your content.
- `app/layout.config.tsx`: Shared options for layouts, optional but preferred to keep.
| Route | Description |
-199
View File
@@ -1,199 +0,0 @@
@theme {
--animate-fd-fade-in: fd-fade-in 300ms ease;
--animate-fd-fade-out: fd-fade-out 300ms ease;
--animate-fd-dialog-in: fd-dialog-in 200ms cubic-bezier(0.32, 0.72, 0, 1);
--animate-fd-dialog-out: fd-dialog-out 300ms cubic-bezier(0.32, 0.72, 0, 1);
--animate-fd-popover-in: fd-popover-in 150ms ease;
--animate-fd-popover-out: fd-popover-out 150ms ease;
--animate-fd-collapsible-down: fd-collapsible-down 150ms ease-out;
--animate-fd-collapsible-up: fd-collapsible-up 150ms ease-out;
--animate-fd-accordion-down: fd-accordion-down 200ms ease-out;
--animate-fd-accordion-up: fd-accordion-up 200ms ease-out;
--animate-fd-nav-menu-in: fd-nav-menu-in 200ms ease;
--animate-fd-nav-menu-out: fd-nav-menu-out 200ms ease;
--animate-fd-enterFromLeft: fd-enterFromLeft 250ms ease;
--animate-fd-enterFromRight: fd-enterFromRight 250ms ease;
--animate-fd-exitToLeft: fd-exitToLeft 250ms ease;
--animate-fd-exitToRight: fd-exitToRight 250ms ease;
@keyframes fd-collapsible-down {
from {
height: 0;
opacity: 0;
}
to {
height: var(--radix-collapsible-content-height);
}
}
@keyframes fd-collapsible-up {
from {
height: var(--radix-collapsible-content-height);
}
to {
height: 0;
opacity: 0;
}
}
@keyframes fd-accordion-down {
from {
height: 0;
opacity: 0.5;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes fd-accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
opacity: 0.5;
}
}
@keyframes fd-dialog-in {
from {
transform: scale(0.95);
opacity: 0;
}
to {
transform: scale(1);
}
}
@keyframes fd-dialog-out {
from {
transform: scale(1);
}
to {
transform: scale(0.95);
opacity: 0;
}
}
@keyframes fd-popover-in {
from {
opacity: 0;
transform: scale(0.98) translateY(-4px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
@keyframes fd-popover-out {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-4px);
}
}
@keyframes fd-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fd-fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes fd-enterFromRight {
from {
opacity: 0;
transform: translateX(200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fd-enterFromLeft {
from {
opacity: 0;
transform: translateX(-200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fd-exitToRight {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(200px);
}
}
@keyframes fd-exitToLeft {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-200px);
}
}
@keyframes fd-nav-menu-in {
from {
opacity: 0;
height: 0px;
}
to {
opacity: 1;
height: var(--radix-navigation-menu-viewport-height);
}
}
@keyframes fd-nav-menu-out {
from {
opacity: 1;
height: var(--radix-navigation-menu-viewport-height);
}
to {
opacity: 0;
height: 0px;
}
}
}
+190 -1
View File
@@ -1,5 +1,4 @@
@import './shiki.css';
@import './animations.css';
@source '../dist/**/*.js';
@plugin '../dist/theme/typography/index.js';
@@ -17,6 +16,196 @@
--color-fd-diff-remove-symbol: rgb(230, 10, 100);
--color-fd-diff-add: rgba(14, 180, 100, 0.12);
--color-fd-diff-add-symbol: rgb(10, 200, 100);
--animate-fd-fade-in: fd-fade-in 300ms ease;
--animate-fd-fade-out: fd-fade-out 300ms ease;
--animate-fd-dialog-in: fd-dialog-in 200ms cubic-bezier(0.32, 0.72, 0, 1);
--animate-fd-dialog-out: fd-dialog-out 300ms cubic-bezier(0.32, 0.72, 0, 1);
--animate-fd-popover-in: fd-popover-in 150ms ease;
--animate-fd-popover-out: fd-popover-out 150ms ease;
--animate-fd-collapsible-down: fd-collapsible-down 150ms ease-out;
--animate-fd-collapsible-up: fd-collapsible-up 150ms ease-out;
--animate-fd-accordion-down: fd-accordion-down 200ms ease-out;
--animate-fd-accordion-up: fd-accordion-up 200ms ease-out;
--animate-fd-nav-menu-in: fd-nav-menu-in 200ms ease;
--animate-fd-nav-menu-out: fd-nav-menu-out 200ms ease;
--animate-fd-enterFromLeft: fd-enterFromLeft 250ms ease;
--animate-fd-enterFromRight: fd-enterFromRight 250ms ease;
--animate-fd-exitToLeft: fd-exitToLeft 250ms ease;
--animate-fd-exitToRight: fd-exitToRight 250ms ease;
@keyframes fd-collapsible-down {
from {
height: 0;
opacity: 0;
}
to {
height: var(--radix-collapsible-content-height);
}
}
@keyframes fd-collapsible-up {
from {
height: var(--radix-collapsible-content-height);
}
to {
height: 0;
opacity: 0;
}
}
@keyframes fd-accordion-down {
from {
height: 0;
opacity: 0.5;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes fd-accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
opacity: 0.5;
}
}
@keyframes fd-dialog-in {
from {
transform: scale(0.95);
opacity: 0;
}
to {
transform: scale(1);
}
}
@keyframes fd-dialog-out {
from {
transform: scale(1);
}
to {
transform: scale(0.95);
opacity: 0;
}
}
@keyframes fd-popover-in {
from {
opacity: 0;
transform: translateY(-12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fd-popover-out {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-12px);
}
}
@keyframes fd-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fd-fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes fd-enterFromRight {
from {
opacity: 0;
transform: translateX(200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fd-enterFromLeft {
from {
opacity: 0;
transform: translateX(-200px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fd-exitToRight {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(200px);
}
}
@keyframes fd-exitToLeft {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-200px);
}
}
@keyframes fd-nav-menu-in {
from {
opacity: 0;
height: 0px;
}
to {
opacity: 1;
height: var(--radix-navigation-menu-viewport-height);
}
}
@keyframes fd-nav-menu-out {
from {
opacity: 1;
height: var(--radix-navigation-menu-viewport-height);
}
to {
opacity: 0;
height: 0px;
}
}
}
@layer base {
+2
View File
@@ -88,6 +88,7 @@
"@radix-ui/react-direction": "^1.1.1",
"@radix-ui/react-navigation-menu": "^1.2.12",
"@radix-ui/react-popover": "^1.1.13",
"@radix-ui/react-presence": "^1.1.4",
"@radix-ui/react-scroll-area": "^1.2.8",
"@radix-ui/react-slot": "^1.2.2",
"@radix-ui/react-tabs": "^1.1.11",
@@ -97,6 +98,7 @@
"next-themes": "^0.4.6",
"postcss-selector-parser": "^7.1.0",
"react-medium-image-zoom": "^5.2.14",
"react-remove-scroll": "^2.6.3",
"tailwind-merge": "^3.2.0"
},
"devDependencies": {
@@ -1,13 +1,12 @@
'use client';
import type { PageTree } from 'fumadocs-core/server';
import { type ReactNode, type ComponentProps, useMemo } from 'react';
import { type ComponentProps, type ReactNode, useMemo } from 'react';
import { cn } from '@/utils/cn';
import { TreeContextProvider, useTreeContext } from 'fumadocs-ui/contexts/tree';
import Link from 'fumadocs-core/link';
import { useSearchContext } from 'fumadocs-ui/contexts/search';
import { useSidebar } from 'fumadocs-ui/contexts/sidebar';
import { cva } from 'class-variance-authority';
import * as SidebarPrimitive from 'fumadocs-core/sidebar';
import { usePathname } from 'fumadocs-core/framework';
export interface DocsLayoutProps {
@@ -85,8 +84,7 @@ function Sidebar() {
}, [root]);
return (
<SidebarPrimitive.SidebarList
removeScrollOn="(width < 768px)" // md
<aside
className={cn(
'fixed flex flex-col shrink-0 p-4 top-14 z-20 text-sm overflow-auto md:sticky md:h-[calc(100dvh-56px)] md:w-[300px]',
'max-md:inset-x-0 max-md:bottom-0 max-md:bg-fd-background',
@@ -94,7 +92,7 @@ function Sidebar() {
)}
>
{children}
</SidebarPrimitive.SidebarList>
</aside>
);
}
@@ -97,9 +97,9 @@ function Item(props: Option) {
<>
<>{props.icon}</>
<div className="flex-1 text-start">
<p className="text-sm font-medium">{props.title}</p>
<p className="text-[15px] font-medium md:text-sm">{props.title}</p>
{props.description ? (
<p className="text-xs text-fd-muted-foreground">
<p className="text-sm text-fd-muted-foreground md:text-xs">
{props.description}
</p>
) : null}
+102 -81
View File
@@ -1,9 +1,8 @@
'use client';
import { ChevronDown, ExternalLink } from 'lucide-react';
import * as Base from 'fumadocs-core/sidebar';
import { usePathname } from 'fumadocs-core/framework';
import {
type ButtonHTMLAttributes,
type ComponentProps,
createContext,
type FC,
Fragment,
@@ -33,8 +32,11 @@ import type {
} from '@radix-ui/react-collapsible';
import type { PageTree } from 'fumadocs-core/server';
import { useTreeContext, useTreePath } from '@/contexts/tree';
import { useMediaQuery } from 'fumadocs-core/utils/use-media-query';
import { RemoveScroll } from 'react-remove-scroll';
import { Presence } from '@radix-ui/react-presence';
export interface SidebarProps extends HTMLAttributes<HTMLElement> {
export interface SidebarProps extends ComponentProps<'aside'> {
/**
* Open folders by default if their level is lower or equal to a specific level
* (Starting from 1)
@@ -49,6 +51,13 @@ export interface SidebarProps extends HTMLAttributes<HTMLElement> {
* @defaultValue true
*/
prefetch?: boolean;
/**
* Support collapsing the sidebar on desktop mode
*
* @defaultValue true
*/
collapsible?: boolean;
}
interface InternalContext {
@@ -76,22 +85,91 @@ const FolderContext = createContext<{
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
} | null>(null);
export function CollapsibleSidebar(props: SidebarProps) {
const { collapsed } = useSidebar();
export function Sidebar({
defaultOpenLevel = 0,
prefetch = true,
collapsible = true,
...props
}: SidebarProps) {
const { open, setOpen, collapsed } = useSidebar();
const context = useMemo<InternalContext>(() => {
return {
defaultOpenLevel,
prefetch,
level: 1,
};
}, [defaultOpenLevel, prefetch]);
const [hover, setHover] = useState(false);
const timerRef = useRef(0);
const closeTimeRef = useRef(0);
// md
const isMobile = useMediaQuery('(width < 768px)') ?? false;
const state = open ? 'open' : 'closed';
useOnChange(collapsed, () => {
setHover(false);
closeTimeRef.current = Date.now() + 150;
});
if (isMobile) {
return (
<RemoveScroll enabled={open}>
<Presence present={open}>
<div
data-state={state}
className="fixed z-40 inset-0 bg-black/30 backdrop-blur-sm data-[state=open]:animate-fd-fade-in data-[state=closed]:animate-fd-fade-out"
onClick={() => setOpen(false)}
/>
</Presence>
<Presence present={open}>
<aside
id="nd-sidebar-mobile"
{...props}
data-state={state}
className={cn(
'fixed flex flex-col ps-1 border-e start-0 inset-y-0 w-[85%] max-w-[380px] z-40 bg-fd-background data-[state=open]:animate-fd-enterFromLeft data-[state=closed]:animate-fd-exitToLeft',
props.className,
)}
>
<Context.Provider value={context}>
{props.children}
</Context.Provider>
</aside>
</Presence>
</RemoveScroll>
);
}
return (
<Sidebar
<aside
id="nd-sidebar"
{...props}
data-collapsed={collapsed}
className={cn(
'sticky top-(--fd-sidebar-top) z-20 bg-fd-card text-sm h-(--fd-sidebar-height) max-md:hidden',
collapsible && [
'transition-all',
collapsed &&
'-me-(--fd-sidebar-width) -translate-x-(--fd-sidebar-offset) rtl:translate-x-(--fd-sidebar-offset)',
collapsed && hover && 'z-50 translate-x-0',
collapsed && !hover && 'opacity-0',
],
props.className,
)}
style={
{
'--fd-sidebar-offset': 'calc(var(--fd-sidebar-width) - 6px)',
'--fd-sidebar-top':
'calc(var(--fd-banner-height) + var(--fd-nav-height))',
'--fd-sidebar-height':
'calc(100dvh - var(--fd-banner-height) - var(--fd-nav-height))',
...props.style,
} as object
}
onPointerEnter={(e) => {
if (
!collapsible ||
!collapsed ||
e.pointerType === 'touch' ||
closeTimeRef.current > Date.now()
@@ -101,7 +179,7 @@ export function CollapsibleSidebar(props: SidebarProps) {
setHover(true);
}}
onPointerLeave={(e) => {
if (!collapsed || e.pointerType === 'touch') return;
if (!collapsible || !collapsed || e.pointerType === 'touch') return;
window.clearTimeout(timerRef.current);
timerRef.current = window.setTimeout(
@@ -114,89 +192,34 @@ export function CollapsibleSidebar(props: SidebarProps) {
: 500,
);
}}
data-collapsed={collapsed}
className={cn(
'md:transition-all',
collapsed &&
'md:-me-(--fd-sidebar-width) md:-translate-x-(--fd-sidebar-offset) rtl:md:translate-x-(--fd-sidebar-offset)',
collapsed && hover && 'z-50 md:translate-x-0',
collapsed && !hover && 'md:opacity-0',
props.className,
)}
style={
{
'--fd-sidebar-offset': 'calc(var(--fd-sidebar-width) - 6px)',
...props.style,
} as object
}
/>
>
<div className="flex w-(--fd-sidebar-width) h-full max-w-full flex-col ms-auto border-e">
<Context.Provider value={context}>{props.children}</Context.Provider>
</div>
</aside>
);
}
export function Sidebar({
defaultOpenLevel = 0,
prefetch = true,
inner,
...props
}: SidebarProps & { inner?: HTMLAttributes<HTMLDivElement> }) {
const context = useMemo<InternalContext>(() => {
return {
defaultOpenLevel,
prefetch,
level: 1,
};
}, [defaultOpenLevel, prefetch]);
return (
<Context.Provider value={context}>
<Base.SidebarList
id="nd-sidebar"
removeScrollOn="(width < 768px)" // md
{...props}
className={cn(
'fixed top-[calc(var(--fd-banner-height)+var(--fd-nav-height))] z-20 bg-fd-card text-sm md:sticky md:h-(--fd-sidebar-height)',
'max-md:inset-x-0 max-md:bottom-0 max-md:bg-fd-background/80 max-md:text-[15px] max-md:backdrop-blur-lg max-md:data-[open=false]:invisible',
props.className,
)}
style={
{
...props.style,
'--fd-sidebar-height':
'calc(100dvh - var(--fd-banner-height) - var(--fd-nav-height))',
} as object
}
>
<div
{...inner}
className={cn(
'flex size-full max-w-full flex-col pt-2 md:ms-auto md:w-(--fd-sidebar-width) md:border-e md:pt-4',
inner?.className,
)}
>
{props.children}
</div>
</Base.SidebarList>
</Context.Provider>
);
}
export function SidebarHeader(props: HTMLAttributes<HTMLDivElement>) {
export function SidebarHeader(props: ComponentProps<'div'>) {
return (
<div
{...props}
className={cn('flex flex-col gap-3 px-4 empty:hidden', props.className)}
className={cn(
'flex flex-col gap-3 px-4 max-md:pt-6 max-md:pb-4 empty:hidden',
props.className,
)}
>
{props.children}
</div>
);
}
export function SidebarFooter(props: HTMLAttributes<HTMLDivElement>) {
export function SidebarFooter(props: ComponentProps<'div'>) {
return (
<div
{...props}
className={cn(
'flex flex-col border-t px-4 py-3 empty:hidden',
'flex flex-col border-t px-4 py-3 empty:hidden max-md:pb-6',
props.className,
)}
>
@@ -220,14 +243,14 @@ export function SidebarViewport(props: ScrollAreaProps) {
);
}
export function SidebarSeparator(props: HTMLAttributes<HTMLParagraphElement>) {
export function SidebarSeparator(props: ComponentProps<'p'>) {
const { level } = useInternalContext();
return (
<p
{...props}
className={cn(
'inline-flex items-center gap-2 mb-2 px-2 text-sm font-medium empty:mb-0 [&_svg]:size-4 [&_svg]:shrink-0',
'inline-flex items-center gap-2 mb-2 px-2 font-medium empty:mb-0 [&_svg]:size-4 [&_svg]:shrink-0',
props.className,
)}
style={{
@@ -272,7 +295,7 @@ export function SidebarItem({
export function SidebarFolder({
defaultOpen = false,
...props
}: HTMLAttributes<HTMLDivElement> & {
}: ComponentProps<'div'> & {
defaultOpen?: boolean;
}) {
const [open, setOpen] = useState(defaultOpen);
@@ -375,9 +398,7 @@ export function SidebarFolderContent(props: CollapsibleContentProps) {
);
}
export function SidebarCollapseTrigger(
props: ButtonHTMLAttributes<HTMLButtonElement>,
) {
export function SidebarCollapseTrigger(props: ComponentProps<'button'>) {
const { collapsed, setCollapsed } = useSidebar();
return (
@@ -402,7 +423,7 @@ function useFolderContext() {
return ctx;
}
function useInternalContext(): InternalContext {
function useInternalContext() {
const ctx = useContext(Context);
if (!ctx) throw new Error('<Sidebar /> component required.');
+4 -7
View File
@@ -1,13 +1,12 @@
'use client';
import {
useState,
useMemo,
useRef,
type ReactNode,
type RefObject,
useMemo,
useRef,
useState,
} from 'react';
import { createContext, usePathname } from 'fumadocs-core/framework';
import { SidebarProvider as BaseProvider } from 'fumadocs-core/sidebar';
import { useOnChange } from 'fumadocs-core/utils/use-on-change';
interface SidebarContext {
@@ -59,9 +58,7 @@ export function SidebarProvider({
[open, collapsed],
)}
>
<BaseProvider open={open} onOpenChange={setOpen}>
{children}
</BaseProvider>
{children}
</SidebarContext.Provider>
);
}
+7 -7
View File
@@ -1,12 +1,11 @@
'use client';
import { Menu, Sidebar as SidebarIcon, X } from 'lucide-react';
import { Menu, Sidebar as SidebarIcon } from 'lucide-react';
import { type ButtonHTMLAttributes, type HTMLAttributes } from 'react';
import { cn } from '@/utils/cn';
import { buttonVariants } from '@/components/ui/button';
import { useSidebar } from '@/contexts/sidebar';
import { useNav } from '@/contexts/layout';
import { SidebarTrigger } from 'fumadocs-core/sidebar';
import { SidebarCollapseTrigger } from '@/components/layout/sidebar';
import { SearchToggle } from '@/components/layout/search-toggle';
@@ -19,7 +18,7 @@ export function Navbar(props: HTMLAttributes<HTMLElement>) {
id="nd-subnav"
{...props}
className={cn(
'sticky top-(--fd-banner-height) z-30 flex h-14 items-center px-4 border-b border-fd-foreground/10 transition-colors backdrop-blur-sm md:px-6',
'sticky top-(--fd-banner-height) z-30 flex h-14 items-center px-4 border-b transition-colors backdrop-blur-sm md:px-6',
(!isTransparent || open) && 'bg-fd-background/80',
props.className,
)}
@@ -32,10 +31,10 @@ export function Navbar(props: HTMLAttributes<HTMLElement>) {
export function NavbarSidebarTrigger(
props: ButtonHTMLAttributes<HTMLButtonElement>,
) {
const { open } = useSidebar();
const { setOpen } = useSidebar();
return (
<SidebarTrigger
<button
{...props}
className={cn(
buttonVariants({
@@ -44,9 +43,10 @@ export function NavbarSidebarTrigger(
}),
props.className,
)}
onClick={() => setOpen((prev) => !prev)}
>
{open ? <X /> : <Menu />}
</SidebarTrigger>
<Menu />
</button>
);
}
+3 -5
View File
@@ -4,7 +4,6 @@ import { Languages, Sidebar as SidebarIcon } from 'lucide-react';
import { cn } from '@/utils/cn';
import { buttonVariants } from '@/components/ui/button';
import {
CollapsibleSidebar,
Sidebar,
SidebarCollapseTrigger,
SidebarFooter,
@@ -183,13 +182,12 @@ export function DocsLayoutSidebar({
links?: LinkItemType[];
nav?: ReactNode;
}) {
const Aside = collapsible ? CollapsibleSidebar : Sidebar;
return (
<>
{collapsible ? <CollapsibleControl /> : null}
<Aside
<Sidebar
{...props}
collapsible={collapsible}
className={cn('md:ps-(--fd-layout-offset)', props.className)}
>
<SidebarHeader>
@@ -222,7 +220,7 @@ export function DocsLayoutSidebar({
<SidebarPageTree components={components} />
</SidebarViewport>
<SidebarFooter>{footer}</SidebarFooter>
</Aside>
</Sidebar>
</>
);
}
-1
View File
@@ -21,7 +21,6 @@ export const layoutVariables = {
};
export interface SidebarOptions extends SidebarProps {
collapsible?: boolean;
components?: Partial<SidebarComponents>;
/**
+1 -1
View File
@@ -34,7 +34,7 @@ export function Navbar(props: ComponentProps<'div'>) {
id="nd-nav"
{...props}
className={cn(
'fixed left-(--fd-nav-left) top-(--fd-banner-height) z-40 box-content w-full max-w-fd-container -translate-x-1/2 border-b border-fd-foreground/10 transition-colors lg:mt-2 lg:w-[calc(100%-1rem)] lg:rounded-2xl lg:border',
'fixed left-(--fd-nav-left) top-(--fd-banner-height) z-40 box-content w-full max-w-fd-container -translate-x-1/2 border-b transition-colors lg:mt-2 lg:w-[calc(100%-1rem)] lg:rounded-2xl lg:border',
value.length > 0 ? 'shadow-lg' : 'shadow-sm',
(!isTransparent || value.length > 0) &&
'bg-fd-background/80 backdrop-blur-lg',
+6 -6
View File
@@ -3,9 +3,8 @@ import { cn } from '@/utils/cn';
import { type ButtonHTMLAttributes, type HTMLAttributes } from 'react';
import { useSidebar } from '@/contexts/sidebar';
import { useNav } from '@/contexts/layout';
import { SidebarTrigger } from 'fumadocs-core/sidebar';
import { buttonVariants } from '@/components/ui/button';
import { Menu, X } from 'lucide-react';
import { Menu } from 'lucide-react';
import Link from 'fumadocs-core/link';
import { usePathname } from 'fumadocs-core/framework';
import { isActive } from '@/utils/is-active';
@@ -39,10 +38,10 @@ export function Navbar({
export function NavbarSidebarTrigger(
props: ButtonHTMLAttributes<HTMLButtonElement>,
) {
const { open } = useSidebar();
const { setOpen } = useSidebar();
return (
<SidebarTrigger
<button
{...props}
className={cn(
buttonVariants({
@@ -51,9 +50,10 @@ export function NavbarSidebarTrigger(
}),
props.className,
)}
onClick={() => setOpen((prev) => !prev)}
>
{open ? <X /> : <Menu />}
</SidebarTrigger>
<Menu />
</button>
);
}
+8 -13
View File
@@ -1,7 +1,6 @@
import { Fragment, type HTMLAttributes, useMemo } from 'react';
import { type BaseLayoutProps, getLinks, slot, slots } from '@/layouts/shared';
import {
CollapsibleSidebar,
Sidebar,
SidebarCollapseTrigger,
SidebarFooter,
@@ -65,7 +64,6 @@ export function DocsLayout(props: DocsLayoutProps) {
tabMode = 'sidebar',
nav: { transparentMode, ...nav } = {},
sidebar: {
collapsible: sidebarCollapsible = true,
tabs: tabOptions,
banner: sidebarBanner,
footer: sidebarFooter,
@@ -84,8 +82,6 @@ export function DocsLayout(props: DocsLayoutProps) {
[tabOptions, props.tree],
);
const Aside = sidebarCollapsible ? CollapsibleSidebar : Sidebar;
const variables = cn(
'[--fd-nav-height:calc(var(--spacing)*14)] [--fd-tocnav-height:36px] md:[--fd-sidebar-width:286px] xl:[--fd-toc-width:286px] xl:[--fd-tocnav-height:0px]',
tabs.length > 0 &&
@@ -115,16 +111,15 @@ export function DocsLayout(props: DocsLayoutProps) {
...props.containerProps?.style,
}}
>
<Aside
<Sidebar
{...sidebar}
className={cn(
'md:ps-(--fd-layout-offset)',
navMode === 'top' ? 'bg-transparent' : 'md:[--fd-nav-height:0px]',
navMode === 'top'
? 'md:bg-transparent md:*:pt-2.5'
: 'md:[--fd-nav-height:0px] md:*:pt-3.5',
sidebar.className,
)}
inner={{
className: cn(navMode === 'top' ? 'md:pt-2.5' : 'md:pt-3.5'),
}}
>
<SidebarHeader>
{navMode === 'auto' && (
@@ -135,14 +130,14 @@ export function DocsLayout(props: DocsLayoutProps) {
>
{nav.title}
</Link>
{sidebarCollapsible && (
{(sidebar.collapsible ?? true) && (
<SidebarCollapseTrigger
className={cn(
buttonVariants({
color: 'ghost',
size: 'icon-sm',
className: 'text-fd-muted-foreground mb-auto',
}),
'text-fd-muted-foreground mb-auto',
)}
>
<SidebarIcon />
@@ -195,7 +190,7 @@ export function DocsLayout(props: DocsLayoutProps) {
)}
{sidebarFooter}
</SidebarFooter>
</Aside>
</Sidebar>
<DocsNavbar
{...props}
links={links}
@@ -234,7 +229,7 @@ function DocsNavbar({
<Navbar mode={navMode}>
<div
className={cn(
'flex flex-row border-b border-fd-foreground/10 px-4 h-14',
'flex flex-row border-b px-4 h-14',
navMode === 'auto' && 'md:px-6',
)}
>
+6 -13
View File
@@ -14,7 +14,6 @@ import Link from 'fumadocs-core/link';
import { cn } from '@/utils/cn';
import { useI18n } from './contexts/i18n';
import { useTreeContext, useTreePath } from './contexts/tree';
import { useSidebar } from '@/contexts/sidebar';
import type { PageTree, TOCItemType } from 'fumadocs-core/server';
import { createContext, usePathname } from 'fumadocs-core/framework';
import {
@@ -48,7 +47,7 @@ export function TocPopoverTrigger({
[items, active],
);
const path = useTreePath().at(-1);
const showCurrent = selected !== -1 && !open;
const showItem = selected !== -1 && !open;
return (
<CollapsibleTrigger
@@ -63,16 +62,12 @@ export function TocPopoverTrigger({
max={1}
className={cn(open && 'text-fd-primary')}
/>
<span
className={cn(
'grid flex-1 *:row-start-1 *:col-start-1',
open && 'text-fd-foreground',
)}
>
<span className="grid flex-1 *:row-start-1 *:col-start-1">
<span
className={cn(
'truncate transition-all',
showCurrent && 'opacity-0 -translate-y-full pointer-events-none',
open && 'text-fd-foreground',
showItem && 'opacity-0 -translate-y-full pointer-events-none',
)}
>
{path?.name ?? text.toc}
@@ -80,7 +75,7 @@ export function TocPopoverTrigger({
<span
className={cn(
'truncate transition-all',
!showCurrent && 'opacity-0 translate-y-full pointer-events-none',
!showItem && 'opacity-0 translate-y-full pointer-events-none',
)}
>
{items[selected]?.title}
@@ -166,7 +161,6 @@ export function TocPopoverContent(props: ComponentProps<'div'>) {
export function TocPopover(props: HTMLAttributes<HTMLDivElement>) {
const ref = useRef<HTMLElement>(null);
const [open, setOpen] = useState(false);
const sidebar = useSidebar();
const { tocNav } = usePageStyles();
const { isTransparent } = useNav();
@@ -209,10 +203,9 @@ export function TocPopover(props: HTMLAttributes<HTMLDivElement>) {
id="nd-tocnav"
{...props}
className={cn(
'border-b border-fd-foreground/10 backdrop-blur-sm transition-colors',
'border-b backdrop-blur-sm transition-colors',
(!isTransparent || open) && 'bg-fd-background/80',
open && 'shadow-lg',
sidebar.open && 'max-md:hidden',
)}
>
{props.children}
+1 -2
View File
@@ -1,6 +1,5 @@
import type { TableOfContents } from 'fumadocs-core/server';
import {
type AnchorHTMLAttributes,
type ComponentProps,
forwardRef,
type HTMLAttributes,
@@ -46,7 +45,7 @@ type TableOfContentOptions = Omit<TOCProps, 'items' | 'children'> &
type TableOfContentPopoverOptions = Omit<TableOfContentOptions, 'single'>;
interface EditOnGitHubOptions
extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href' | 'children'> {
extends Omit<ComponentProps<'a'>, 'href' | 'children'> {
owner: string;
repo: string;
+9 -3
View File
@@ -276,7 +276,7 @@ importers:
version: 0.2.2(@content-collections/[email protected]([email protected]))([email protected])([email protected]([email protected]))([email protected])
'@content-collections/next':
specifier: ^0.2.6
version: 0.2.6(@content-collections/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]))
version: 0.2.6(@content-collections/[email protected]([email protected]))([email protected](@opentelemetry/[email protected])([email protected]([email protected]))([email protected]))
'@types/mdx':
specifier: ^2.0.13
version: 2.0.13
@@ -871,7 +871,7 @@ importers:
version: 0.2.2(@content-collections/[email protected]([email protected]))([email protected])([email protected]([email protected]))([email protected])
'@content-collections/next':
specifier: ^0.2.6
version: 0.2.6(@content-collections/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]))
version: 0.2.6(@content-collections/[email protected]([email protected]))([email protected](@opentelemetry/[email protected])([email protected]([email protected]))([email protected]))
'@react-router/dev':
specifier: ^7.5.3
version: 7.5.3(@react-router/[email protected]([email protected]([email protected]([email protected]))([email protected]))([email protected]))(@types/[email protected])([email protected])([email protected])([email protected])([email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected])([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])([email protected]))([email protected])
@@ -1426,6 +1426,9 @@ importers:
'@radix-ui/react-popover':
specifier: ^1.1.13
version: 1.1.13(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
'@radix-ui/react-presence':
specifier: ^1.1.4
version: 1.1.4(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
'@radix-ui/react-scroll-area':
specifier: ^1.2.8
version: 1.2.8(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
@@ -1459,6 +1462,9 @@ importers:
react-medium-image-zoom:
specifier: ^5.2.14
version: 5.2.14([email protected]([email protected]))([email protected])
react-remove-scroll:
specifier: ^2.6.3
version: 2.6.3(@types/[email protected])([email protected])
tailwind-merge:
specifier: ^3.2.0
version: 3.2.0
@@ -11052,7 +11058,7 @@ snapshots:
- acorn
- supports-color
'@content-collections/[email protected](@content-collections/[email protected]([email protected]))([email protected](@babel/[email protected])(@opentelemetry/[email protected])([email protected])([email protected]([email protected]))([email protected]))':
'@content-collections/[email protected](@content-collections/[email protected]([email protected]))([email protected](@opentelemetry/[email protected])([email protected]([email protected]))([email protected]))':
dependencies:
'@content-collections/core': 0.8.2([email protected])
'@content-collections/integrations': 0.2.1(@content-collections/[email protected]([email protected]))