mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-09 20:27:44 +00:00
docs: improve layout consistency (#1831)
* Fix sidebar tabs pre-rendering * Improve sidebar consistency * improve searchbar alignment * improve animation
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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>
|
||||||
));
|
));
|
||||||
|
|||||||
Reference in New Issue
Block a user