docs: improve layout consistency (#1831)

* Fix sidebar tabs pre-rendering

* Improve sidebar consistency

* improve searchbar alignment

* improve animation
This commit is contained in:
Fuma Nama
2025-03-16 00:54:27 +08:00
committed by GitHub
parent 16e71d1c58
commit 00ad781427
9 changed files with 246 additions and 329 deletions

View File

@@ -2,8 +2,8 @@ import { DocsLayout } from "fumadocs-ui/layouts/docs";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { docsOptions } from "../layout.config"; import { docsOptions } from "../layout.config";
import ArticleLayout from "@/components/side-bar"; import ArticleLayout from "@/components/side-bar";
import { DocsNavBarMobile } from "@/components/nav-mobile";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
export default function Layout({ children }: { children: ReactNode }) { export default function Layout({ children }: { children: ReactNode }) {
return ( return (
<DocsLayout <DocsLayout
@@ -20,7 +20,6 @@ export default function Layout({ children }: { children: ReactNode }) {
), ),
}} }}
> >
<DocsNavBarMobile />
{children} {children}
</DocsLayout> </DocsLayout>
); );

View File

@@ -7,6 +7,8 @@
@source '../../node_modules/fumadocs-ui/dist/**/*.js'; @source '../../node_modules/fumadocs-ui/dist/**/*.js';
@source '../node_modules/fumadocs-ui/dist/**/*.js'; @source '../node_modules/fumadocs-ui/dist/**/*.js';
:root { :root {
--fd-nav-height: 57px;
--background: oklch(1 0 0); --background: oklch(1 0 0);
--foreground: oklch(0.147 0.004 49.25); --foreground: oklch(0.147 0.004 49.25);

View File

@@ -1,18 +1,9 @@
import {
DocsNavbarMobileBtn,
DocsNavbarMobileTitle,
} from "@/components/nav-mobile";
import { changelogs, source } from "@/lib/source"; import { changelogs, source } from "@/lib/source";
import { BaseLayoutProps } from "fumadocs-ui/layouts/shared"; import { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
export const baseOptions: BaseLayoutProps = { export const baseOptions: BaseLayoutProps = {
nav: { nav: {
component: ( enabled: false,
<div className="flex items-center justify-between py-4 px-2.5 md:hidden">
<DocsNavbarMobileTitle />
<DocsNavbarMobileBtn />
</div>
),
}, },
links: [ links: [
{ {

View File

@@ -7,10 +7,10 @@ import { Logo } from "./logo";
export const Navbar = () => { export const Navbar = () => {
return ( return (
<div className="flex flex-col sticky top-0 bg-background backdrop-blur-md z-30 "> <div className="flex flex-col sticky top-0 bg-background backdrop-blur-md z-30 ">
<nav className="md:grid grid-cols-12 md:border-b top-0 flex items-center justify-between "> <nav className="md:grid grid-cols-12 border-b top-0 flex items-center justify-between">
<Link <Link
href="/" href="/"
className="md:border-r md:px-5 px-2.5 py-4 text-foreground md:col-span-2 shrink-0 transition-colors md:w-[268px] lg:w-[286px]" className="md:border-r px-4 py-4 text-foreground md:col-span-2 shrink-0 transition-colors md:px-5 md:w-[268px] lg:w-[286px]"
> >
<div className="flex flex-col gap-2 w-full"> <div className="flex flex-col gap-2 w-full">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View File

@@ -1,5 +1,5 @@
"use client"; "use client";
import { Menu } from "lucide-react"; import { ChevronRight, Menu } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { Fragment, createContext, useContext, useState } from "react"; import { Fragment, createContext, useContext, useState } from "react";
import { import {
@@ -8,10 +8,9 @@ import {
AccordionItem, AccordionItem,
AccordionTrigger, AccordionTrigger,
} from "@/components/ui/accordion"; } from "@/components/ui/accordion";
import { AnimatePresence, FadeIn } from "@/components/ui/fade-in";
import { contents, examples } from "./sidebar-content"; import { contents, examples } from "./sidebar-content";
import { MobileThemeToggle } from "./theme-toggler";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { cn } from "@/lib/utils";
interface NavbarMobileContextProps { interface NavbarMobileContextProps {
isOpen: boolean; isOpen: boolean;
@@ -58,15 +57,14 @@ export const NavbarMobileBtn: React.FC = () => {
const { toggleNavbar } = useNavbarMobile(); const { toggleNavbar } = useNavbarMobile();
return ( return (
<div className="flex items-center "> <div className="flex items-center">
<MobileThemeToggle />
<button <button
className="text-muted-foreground overflow-hidden px-2.5 block md:hidden" className="overflow-hidden px-2.5 block md:hidden"
onClick={() => { onClick={() => {
toggleNavbar(); toggleNavbar();
}} }}
> >
<Menu /> <Menu className="size-5" />
</button> </button>
</div> </div>
); );
@@ -74,142 +72,125 @@ export const NavbarMobileBtn: React.FC = () => {
export const NavbarMobile = () => { export const NavbarMobile = () => {
const { isOpen, toggleNavbar } = useNavbarMobile(); const { isOpen, toggleNavbar } = useNavbarMobile();
const pathname = usePathname();
const isDocs = pathname.startsWith("/docs");
return ( return (
<div className="fixed top-[50px] left-0 px-4 mx-auto w-full h-auto md:hidden transform-gpu [border:1px_solid_rgba(255,255,255,.1)] z-[100] bg-background"> <div
<AnimatePresence> className={cn(
{isOpen && ( "fixed top-[50px] inset-x-0 transform-gpu z-[100] bg-background grid grid-rows-[0fr] duration-300 transition-all md:hidden",
<FadeIn isOpen &&
fromTopToBottom "shadow-lg border-b border-[rgba(255,255,255,.1)] grid-rows-[1fr]",
className="p-5 overflow-y-auto bg-transparent divide-y" )}
> >
{navMenu.map((menu, i) => ( <div
<Fragment key={menu.name}> className={cn(
{menu.child ? ( "px-9 min-h-0 overflow-y-auto max-h-[80vh] divide-y [mask-image:linear-gradient(to_top,transparent,white_40px)] transition-all duration-300",
<Accordion type="single" collapsible> isOpen ? "py-5" : "invisible",
<AccordionItem value={menu.name}> isDocs && "px-4",
<AccordionTrigger className="text-2xl font-normal text-foreground"> )}
{menu.name} >
</AccordionTrigger> {navMenu.map((menu) => (
<AccordionContent className="pl-5 divide-y"> <Fragment key={menu.name}>
{menu.child.map((child, j) => ( {menu.child ? (
<Link <Accordion type="single" collapsible>
href={child.path} <AccordionItem value={menu.name}>
key={child.name} <AccordionTrigger
className="block py-2 text-xl border-b first:pt-0 last:pb-0 last:border-0 text-muted-foreground" className={cn(
onClick={toggleNavbar} "font-normal text-foreground",
> !isDocs && "text-2xl",
{child.name} )}
</Link>
))}
</AccordionContent>
</AccordionItem>
</Accordion>
) : (
<Link
href={menu.path}
className="block py-4 text-2xl first:pt-0 last:pb-0"
onClick={toggleNavbar}
> >
{menu.name} {menu.name}
</Link> </AccordionTrigger>
<AccordionContent className="pl-5 divide-y">
{menu.child.map((child, j) => (
<Link
href={child.path}
key={child.name}
className={cn(
"block py-2 border-b first:pt-0 last:pb-0 last:border-0 text-muted-foreground",
!isDocs && "text-xl",
)}
onClick={toggleNavbar}
>
{child.name}
</Link>
))}
</AccordionContent>
</AccordionItem>
</Accordion>
) : (
<Link
href={menu.path}
className={cn(
"group flex items-center gap-2.5 first:pt-0 last:pb-0 text-2xl py-4",
isDocs && "text-base py-2",
)} )}
</Fragment> onClick={toggleNavbar}
))} >
</FadeIn> {isDocs && (
)} <ChevronRight className="ml-0.5 size-4 text-muted-foreground md:hidden" />
</AnimatePresence> )}
{menu.name}
</Link>
)}
</Fragment>
))}
<DocsNavBarContent />
</div>
</div> </div>
); );
}; };
export const DocsNavbarMobileBtn: React.FC = () => { function DocsNavBarContent() {
const { toggleDocsNavbar: toggleNavbar } = useNavbarMobile();
return (
<button
className="block ml-auto text-muted-foreground md:hidden"
onClick={() => {
toggleNavbar();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="1.4em"
height="1.4em"
viewBox="0 0 24 24"
>
<path
className="fill-foreground"
fillRule="evenodd"
d="M2.25 6A.75.75 0 0 1 3 5.25h18a.75.75 0 0 1 0 1.5H3A.75.75 0 0 1 2.25 6m0 4A.75.75 0 0 1 3 9.25h18a.75.75 0 0 1 0 1.5H3a.75.75 0 0 1-.75-.75m0 4a.75.75 0 0 1 .75-.75h7a.75.75 0 0 1 0 1.5H3a.75.75 0 0 1-.75-.75m0 4a.75.75 0 0 1 .75-.75h7a.75.75 0 0 1 0 1.5H3a.75.75 0 0 1-.75-.75"
clipRule="evenodd"
opacity=".5"
></path>
<path
fill="currentColor"
d="M13.43 14.512a.75.75 0 0 1 1.058-.081l3.012 2.581l3.012-2.581a.75.75 0 1 1 .976 1.139l-3.5 3a.75.75 0 0 1-.976 0l-3.5-3a.75.75 0 0 1-.081-1.058"
></path>
</svg>
</button>
);
};
export const DocsNavBarMobile = () => {
const { isDocsOpen: isOpen, toggleDocsNavbar: toggleNavbar } =
useNavbarMobile();
const pathname = usePathname(); const pathname = usePathname();
const { toggleNavbar } = useNavbarMobile();
if (!pathname.startsWith("/docs")) return null;
const content = pathname.startsWith("/docs/examples") ? examples : contents; const content = pathname.startsWith("/docs/examples") ? examples : contents;
return ( return (
<AnimatePresence> <>
{isOpen && ( {content.map((menu) => (
<FadeIn <Accordion type="single" collapsible key={menu.title}>
fromTopToBottom <AccordionItem value={menu.title}>
className="absolute top-[100px] left-0 bg-background h-[calc(100%-57px-27px)] w-full z-[1000] p-5 divide-y overflow-y-auto" <AccordionTrigger className="font-normal text-foreground">
> <div className="flex items-center gap-2">
{content.map((menu, i) => ( {!!menu.Icon && <menu.Icon className="w-5 h-5" />}
<Accordion type="single" collapsible key={menu.title}> {menu.title}
<AccordionItem value={menu.title}> </div>
<AccordionTrigger className="font-normal text-foreground"> </AccordionTrigger>
<div className="flex items-center gap-2"> <AccordionContent className="pl-5 divide-y">
{!!menu.Icon && <menu.Icon className="w-5 h-5" />} {menu.list.map((child) => (
{menu.title} <Link
</div> href={child.href}
</AccordionTrigger> key={child.title}
<AccordionContent className="pl-5 divide-y"> className="block py-2 text-sm border-b first:pt-0 last:pb-0 last:border-0 text-muted-foreground"
{menu.list.map((child, j) => ( onClick={toggleNavbar}
<Link >
href={child.href} {child.group ? (
key={child.title} <div className="flex flex-row items-center gap-2 ">
className="block py-2 text-sm border-b first:pt-0 last:pb-0 last:border-0 text-muted-foreground" <div className="flex-grow h-px bg-gradient-to-r from-stone-800/90 to-stone-800/60" />
onClick={toggleNavbar} <p className="text-sm text-transparent bg-gradient-to-tr dark:from-gray-100 dark:to-stone-200 bg-clip-text from-gray-900 to-stone-900">
> {child.title}
{child.group ? ( </p>
<div className="flex flex-row items-center gap-2 "> </div>
<div className="flex-grow h-px bg-gradient-to-r from-stone-800/90 to-stone-800/60" /> ) : (
<p className="text-sm text-transparent bg-gradient-to-tr dark:from-gray-100 dark:to-stone-200 bg-clip-text from-gray-900 to-stone-900"> <div className="flex items-center gap-2">
{child.title} <child.icon />
</p> {child.title}
</div> </div>
) : ( )}
<div className="flex items-center gap-2"> </Link>
<child.icon /> ))}
{child.title} </AccordionContent>
</div> </AccordionItem>
)} </Accordion>
</Link> ))}
))} </>
</AccordionContent>
</AccordionItem>
</Accordion>
))}
</FadeIn>
)}
</AnimatePresence>
); );
}; }
export const navMenu: { export const navMenu: {
name: string; name: string;
@@ -241,13 +222,3 @@ export const navMenu: {
path: "/community", path: "/community",
}, },
]; ];
export const DocsNavbarMobileTitle = () => {
const pathname = usePathname();
if (pathname.startsWith("/docs/examples")) {
return <p>Examples</p>;
} else {
return <p>Docs</p>;
}
};

View File

@@ -7,13 +7,7 @@ import { useSearchContext } from "fumadocs-ui/provider";
import { usePathname, useRouter } from "next/navigation"; import { usePathname, useRouter } from "next/navigation";
import { contents, examples } from "./sidebar-content"; import { contents, examples } from "./sidebar-content";
import { ChevronDownIcon, Search } from "lucide-react"; import { ChevronDownIcon, Search } from "lucide-react";
import { import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select";
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "./ui/select";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Badge } from "./ui/badge"; import { Badge } from "./ui/badge";
@@ -30,7 +24,6 @@ export default function ArticleLayout() {
return defaultValue === -1 ? 0 : defaultValue; return defaultValue === -1 ? 0 : defaultValue;
} }
const router = useRouter();
const [group, setGroup] = useState("docs"); const [group, setGroup] = useState("docs");
useEffect(() => { useEffect(() => {
@@ -46,100 +39,20 @@ export default function ArticleLayout() {
<aside <aside
className={cn( className={cn(
"md:transition-all", "md:transition-all",
"border-r border-lines md:flex hidden md:w-[268px] lg:w-[286px] overflow-y-auto absolute top-[58px] h-[92dvh] flex-col justify-between w-[var(--fd-sidebar-width)]", "border-r border-lines md:flex hidden md:w-[268px] lg:w-[286px] overflow-y-auto absolute top-[58px] h-[92dvh] flex-col justify-between w-[var(--fd-sidebar-width)]",
)} )}
> >
<div> <div>
<Select <SidebarTab group={group} setGroup={setGroup} />
defaultValue="docs" <button
value={group} className="flex w-full items-center gap-2 px-5 py-2.5 border-b text-muted-foreground bg-gradient-to-b dark:from-stone-900 dark:to-stone/10"
onValueChange={(val) => {
setGroup(val);
if (val === "docs") {
router.push("/docs");
} else {
router.push("/docs/examples");
}
}}
>
<SelectTrigger className="h-16 border border-b border-none rounded-none">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem
value="docs"
className="h-12 flex flex-col items-start gap-1"
>
<div className="flex items-center gap-1">
<svg
xmlns="http://www.w3.org/2000/svg"
width="1.4em"
height="1.4em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M4.727 2.733c.306-.308.734-.508 1.544-.618C7.105 2.002 8.209 2 9.793 2h4.414c1.584 0 2.688.002 3.522.115c.81.11 1.238.31 1.544.618c.305.308.504.74.613 1.557c.112.84.114 1.955.114 3.552V18H7.426c-1.084 0-1.462.006-1.753.068c-.513.11-.96.347-1.285.667c-.11.108-.164.161-.291.505A1.3 1.3 0 0 0 4 19.7V7.842c0-1.597.002-2.711.114-3.552c.109-.816.308-1.249.613-1.557"
opacity=".5"
></path>
<path
fill="currentColor"
d="M20 18H7.426c-1.084 0-1.462.006-1.753.068c-.513.11-.96.347-1.285.667c-.11.108-.164.161-.291.505s-.107.489-.066.78l.022.15c.11.653.31.998.616 1.244c.307.246.737.407 1.55.494c.837.09 1.946.092 3.536.092h4.43c1.59 0 2.7-.001 3.536-.092c.813-.087 1.243-.248 1.55-.494c.2-.16.354-.362.467-.664H8a.75.75 0 0 1 0-1.5h11.975c.018-.363.023-.776.025-1.25M7.25 7A.75.75 0 0 1 8 6.25h8a.75.75 0 0 1 0 1.5H8A.75.75 0 0 1 7.25 7M8 9.75a.75.75 0 0 0 0 1.5h5a.75.75 0 0 0 0-1.5z"
></path>
</svg>
Docs
</div>
<p className="text-xs text-muted-foreground">
get started, concepts, and plugins
</p>
</SelectItem>
<SelectItem
value="examples"
className="h-12 flex flex-col items-start gap-1"
>
<div className="flex items-center gap-1">
<svg
xmlns="http://www.w3.org/2000/svg"
width="1.4em"
height="1.4em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 2c4.714 0 7.071 0 8.535 1.464c1.08 1.08 1.364 2.647 1.439 5.286L22 9.5H2.026v-.75c.075-2.64.358-4.205 1.438-5.286C4.93 2 7.286 2 12 2"
opacity=".5"
></path>
<path
fill="currentColor"
d="M13 6a1 1 0 1 1-2 0a1 1 0 0 1 2 0m-3 0a1 1 0 1 1-2 0a1 1 0 0 1 2 0M7 6a1 1 0 1 1-2 0a1 1 0 0 1 2 0"
></path>
<path
fill="currentColor"
d="M2 12c0 4.714 0 7.071 1.464 8.535c1.01 1.01 2.446 1.324 4.786 1.421L9 22V9.5H2.026l-.023.75Q2 11.066 2 12"
opacity=".7"
></path>
<path
fill="currentColor"
d="M22 12c0 4.714 0 7.071-1.465 8.535C19.072 22 16.714 22 12 22c-.819 0-2.316 0-3-.008V9.5h13l-.003.75Q22 11.066 22 12"
></path>
</svg>
Examples
</div>
<p className="text-xs">examples and guides</p>
</SelectItem>
</SelectContent>
</Select>
<div
className="flex items-center gap-2 p-2 px-4 border-b bg-gradient-to-br dark:from-stone-900 dark:to-stone-950/80"
onClick={() => { onClick={() => {
setOpenSearch(true); setOpenSearch(true);
}} }}
> >
<Search className="w-4 h-4" /> <Search className="size-4 mx-0.5" />
<p className="text-sm text-transparent bg-gradient-to-tr from-gray-500 to-stone-400 bg-clip-text"> <p className="text-sm">Search documentation...</p>
Search documentation... </button>
</p>
</div>
<MotionConfig <MotionConfig
transition={{ duration: 0.4, type: "spring", bounce: 0 }} transition={{ duration: 0.4, type: "spring", bounce: 0 }}
@@ -157,7 +70,7 @@ export default function ArticleLayout() {
} }
}} }}
> >
<item.Icon className="w-5 h-5" /> <item.Icon className="size-5" />
<span className="grow">{item.title}</span> <span className="grow">{item.title}</span>
{item.isNew && <NewBadge />} {item.isNew && <NewBadge />}
<motion.div <motion.div
@@ -236,3 +149,112 @@ function NewBadge({ isSelected }: { isSelected?: boolean }) {
</div> </div>
); );
} }
const tabs = [
{
value: "docs",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1.4em"
height="1.4em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M4.727 2.733c.306-.308.734-.508 1.544-.618C7.105 2.002 8.209 2 9.793 2h4.414c1.584 0 2.688.002 3.522.115c.81.11 1.238.31 1.544.618c.305.308.504.74.613 1.557c.112.84.114 1.955.114 3.552V18H7.426c-1.084 0-1.462.006-1.753.068c-.513.11-.96.347-1.285.667c-.11.108-.164.161-.291.505A1.3 1.3 0 0 0 4 19.7V7.842c0-1.597.002-2.711.114-3.552c.109-.816.308-1.249.613-1.557"
opacity=".5"
></path>
<path
fill="currentColor"
d="M20 18H7.426c-1.084 0-1.462.006-1.753.068c-.513.11-.96.347-1.285.667c-.11.108-.164.161-.291.505s-.107.489-.066.78l.022.15c.11.653.31.998.616 1.244c.307.246.737.407 1.55.494c.837.09 1.946.092 3.536.092h4.43c1.59 0 2.7-.001 3.536-.092c.813-.087 1.243-.248 1.55-.494c.2-.16.354-.362.467-.664H8a.75.75 0 0 1 0-1.5h11.975c.018-.363.023-.776.025-1.25M7.25 7A.75.75 0 0 1 8 6.25h8a.75.75 0 0 1 0 1.5H8A.75.75 0 0 1 7.25 7M8 9.75a.75.75 0 0 0 0 1.5h5a.75.75 0 0 0 0-1.5z"
></path>
</svg>
),
title: "Docs",
description: "get started, concepts, and plugins",
},
{
value: "examples",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1.4em"
height="1.4em"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 2c4.714 0 7.071 0 8.535 1.464c1.08 1.08 1.364 2.647 1.439 5.286L22 9.5H2.026v-.75c.075-2.64.358-4.205 1.438-5.286C4.93 2 7.286 2 12 2"
opacity=".5"
></path>
<path
fill="currentColor"
d="M13 6a1 1 0 1 1-2 0a1 1 0 0 1 2 0m-3 0a1 1 0 1 1-2 0a1 1 0 0 1 2 0M7 6a1 1 0 1 1-2 0a1 1 0 0 1 2 0"
></path>
<path
fill="currentColor"
d="M2 12c0 4.714 0 7.071 1.464 8.535c1.01 1.01 2.446 1.324 4.786 1.421L9 22V9.5H2.026l-.023.75Q2 11.066 2 12"
opacity=".7"
></path>
<path
fill="currentColor"
d="M22 12c0 4.714 0 7.071-1.465 8.535C19.072 22 16.714 22 12 22c-.819 0-2.316 0-3-.008V9.5h13l-.003.75Q22 11.066 22 12"
></path>
</svg>
),
title: "Examples",
description: "examples and guides",
},
];
function SidebarTab({
group,
setGroup,
}: { group: string; setGroup: (group: string) => void }) {
const router = useRouter();
const selected = tabs.find((tab) => tab.value === group);
return (
<Select
value={group}
onValueChange={(val) => {
setGroup(val);
if (val === "docs") {
router.push("/docs");
} else {
router.push("/docs/examples");
}
}}
>
<SelectTrigger className="h-16 border border-b border-none rounded-none px-5">
{selected ? (
<div className="flex flex-col gap-1 items-start">
<div className="flex items-center gap-1 -ml-0.5">
{selected.icon}
{selected.title}
</div>
<p className="text-xs text-muted-foreground">
{selected.description}
</p>
</div>
) : null}
</SelectTrigger>
<SelectContent>
{tabs.map((tab) => (
<SelectItem
key={tab.value}
value={tab.value}
className="h-12 flex flex-col items-start gap-1"
>
<div className="flex items-center gap-1">
{tab.icon}
{tab.title}
</div>
<p className="text-xs text-muted-foreground">{tab.description}</p>
</SelectItem>
))}
</SelectContent>
</Select>
);
}

View File

@@ -9,8 +9,10 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import type { ComponentProps } from "react";
import { cn } from "@/lib/utils";
export function ThemeToggle() { export function ThemeToggle(props: ComponentProps<typeof Button>) {
const { setTheme } = useTheme(); const { setTheme } = useTheme();
return ( return (
@@ -19,11 +21,15 @@ export function ThemeToggle() {
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="border-l ring-0 rounded-none h-14 w-[3.56rem] hidden md:flex shrink-0" aria-label="Toggle Theme"
{...props}
className={cn(
"flex ring-0 shrink-0 md:w-[3.56rem] md:h-14 md:border-l md:text-muted-foreground max-md:-mr-1.5 max-md:hover:bg-transparent",
props.className,
)}
> >
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" /> <Sun className="size-4 fill-current dark:hidden md:size-5" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" /> <Moon className="absolute fill-current size-4 hidden dark:block md:size-5" />
<span className="sr-only">Toggle theme</span>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="rounded-none" align="end"> <DropdownMenuContent className="rounded-none" align="end">
@@ -49,19 +55,3 @@ export function ThemeToggle() {
</DropdownMenu> </DropdownMenu>
); );
} }
export function MobileThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<div className="block md:hidden">
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
>
<Sun className="h-4 w-4 dark:hidden" color="#000" />
<Moon className="hidden h-4 w-4 dark:block" />
<span className="sr-only">Toggle theme</span>
</Button>
</div>
);
}

View File

@@ -1,64 +1,6 @@
"use client"; "use client";
import { import { AnimatePresence as PrimitiveAnimatePresence } from "framer-motion";
AnimatePresence as PrimitiveAnimatePresence,
motion,
useReducedMotion,
} from "framer-motion";
import { createContext, useContext } from "react";
const FadeInStaggerContext = createContext(false);
const viewport = { once: true, margin: "0px 0px -200px" };
export const FadeIn = (
props: React.ComponentPropsWithoutRef<typeof motion.div> & {
fromTopToBottom?: boolean;
},
) => {
const shouldReduceMotion = useReducedMotion();
const isInStaggerGroup = useContext(FadeInStaggerContext);
return (
<motion.div
variants={{
hidden: {
opacity: 0,
y: shouldReduceMotion ? 0 : props.fromTopToBottom ? -24 : 2,
},
visible: { opacity: 1, y: 0 },
}}
transition={{ duration: 0.3 }}
{...(isInStaggerGroup
? {}
: {
initial: "hidden",
whileInView: "visible",
viewport,
})}
{...props}
/>
);
};
export const FadeInStagger = ({
faster = false,
...props
}: React.ComponentPropsWithoutRef<typeof motion.div> & {
faster?: boolean;
}) => {
return (
<FadeInStaggerContext.Provider value={true}>
<motion.div
initial="hidden"
whileInView="visible"
viewport={viewport}
transition={{ staggerChildren: faster ? 0.08 : 0.2 }}
{...props}
/>
</FadeInStaggerContext.Provider>
);
};
export const AnimatePresence = ( export const AnimatePresence = (
props: React.ComponentPropsWithoutRef<typeof PrimitiveAnimatePresence>, props: React.ComponentPropsWithoutRef<typeof PrimitiveAnimatePresence>,

View File

@@ -26,7 +26,7 @@ const SelectTrigger = React.forwardRef<
> >
{children} {children}
<SelectPrimitive.Icon asChild> <SelectPrimitive.Icon asChild>
<ChevronsUpDown className="h-4 w-4 opacity-50" /> <ChevronsUpDown className="size-4 opacity-50" />
</SelectPrimitive.Icon> </SelectPrimitive.Icon>
</SelectPrimitive.Trigger> </SelectPrimitive.Trigger>
)); ));