fix docs-menu, leftbar, sublink
This commit is contained in:
37
.gitignore
vendored
37
.gitignore
vendored
@@ -1 +1,36 @@
|
|||||||
node_modules
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
.yarn/install-state.gz
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|||||||
@@ -4,21 +4,42 @@ import { ROUTES } from "@/lib/routes-config";
|
|||||||
import SubLink from "./sublink";
|
import SubLink from "./sublink";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
export default function DocsMenu({ isSheet = false }) {
|
interface DocsMenuProps {
|
||||||
|
isSheet?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DocsMenu({ isSheet = false, className = "" }: DocsMenuProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
// Skip rendering if not on a docs page
|
||||||
if (!pathname.startsWith("/docs")) return null;
|
if (!pathname.startsWith("/docs")) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3.5 mt-5 pr-2 pb-6">
|
<nav
|
||||||
|
aria-label="Documentation navigation"
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<ul className="flex flex-col gap-3.5 mt-5 pr-2 pb-6">
|
||||||
{ROUTES.map((item, index) => {
|
{ROUTES.map((item, index) => {
|
||||||
|
// Normalize href - hapus leading/trailing slashes
|
||||||
|
const normalizedHref = `/${item.href.replace(/^\/+|\/+$/g, '')}`;
|
||||||
|
const itemHref = `/docs${normalizedHref}`;
|
||||||
|
|
||||||
const modifiedItems = {
|
const modifiedItems = {
|
||||||
...item,
|
...item,
|
||||||
href: `/docs${item.href}`,
|
href: itemHref,
|
||||||
level: 0,
|
level: 0,
|
||||||
isSheet,
|
isSheet,
|
||||||
};
|
};
|
||||||
return <SubLink key={item.title + index} {...modifiedItems} />;
|
|
||||||
|
return (
|
||||||
|
<li key={`${item.title}-${index}`}>
|
||||||
|
<SubLink {...modifiedItems} />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</ul>
|
||||||
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function Leftbar() {
|
|||||||
${collapsed ? "w-[48px]" : "w-[250px]"} flex flex-col pr-2`}
|
${collapsed ? "w-[48px]" : "w-[250px]"} flex flex-col pr-2`}
|
||||||
>
|
>
|
||||||
{/* Toggle Button */}
|
{/* Toggle Button */}
|
||||||
<div className="absolute top-0 right-0 py-2 px-0 ml-6 z-10">
|
<div className="absolute top-0 right-0 py-6 px-0 ml-6 z-10 -mt-4">
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -8,9 +8,15 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { SheetClose } from "@/components/ui/sheet";
|
import { SheetClose } from "@/components/ui/sheet";
|
||||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
|
interface SubLinkProps extends EachRoute {
|
||||||
|
level: number;
|
||||||
|
isSheet: boolean;
|
||||||
|
parentHref?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function SubLink({
|
export default function SubLink({
|
||||||
title,
|
title,
|
||||||
href,
|
href,
|
||||||
@@ -18,19 +24,45 @@ export default function SubLink({
|
|||||||
noLink,
|
noLink,
|
||||||
level,
|
level,
|
||||||
isSheet,
|
isSheet,
|
||||||
}: EachRoute & { level: number; isSheet: boolean }) {
|
parentHref = "",
|
||||||
|
}: SubLinkProps) {
|
||||||
const path = usePathname();
|
const path = usePathname();
|
||||||
const [isOpen, setIsOpen] = useState(level == 0);
|
const [isOpen, setIsOpen] = useState(level === 0);
|
||||||
|
|
||||||
|
// Full path including parent's href
|
||||||
|
const fullHref = `${parentHref}${href}`;
|
||||||
|
|
||||||
|
// Check if current path exactly matches this link's href
|
||||||
|
const isExactActive = useMemo(() => path === fullHref, [path, fullHref]);
|
||||||
|
|
||||||
|
// Check if any child is active (for parent items)
|
||||||
|
const hasActiveChild = useMemo(() => {
|
||||||
|
if (!items) return false;
|
||||||
|
return items.some(item => {
|
||||||
|
const childHref = `${fullHref}${item.href}`;
|
||||||
|
return path.startsWith(childHref) && path !== fullHref;
|
||||||
|
});
|
||||||
|
}, [items, path, fullHref]);
|
||||||
|
|
||||||
|
// Auto-expand if current path is a child of this item
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (path == href || path.includes(href)) setIsOpen(true);
|
if (items && (path.startsWith(fullHref) && path !== fullHref)) {
|
||||||
}, [href, path]);
|
setIsOpen(true);
|
||||||
|
}
|
||||||
|
}, [path, fullHref, items]);
|
||||||
|
|
||||||
const Comp = (
|
// Only apply active styles if it's an exact match and not a parent with active children
|
||||||
<Anchor activeClassName="text-primary font-medium" href={href}>
|
const Comp = useMemo(() => (
|
||||||
|
<Anchor
|
||||||
|
activeClassName={!hasActiveChild ? "text-primary font-medium" : ""}
|
||||||
|
href={fullHref}
|
||||||
|
className={cn(
|
||||||
|
hasActiveChild && "font-medium text-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
{title}
|
{title}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
);
|
), [title, fullHref, hasActiveChild]);
|
||||||
|
|
||||||
const titleOrLink = !noLink ? (
|
const titleOrLink = !noLink ? (
|
||||||
isSheet ? (
|
isSheet ? (
|
||||||
@@ -39,7 +71,12 @@ export default function SubLink({
|
|||||||
Comp
|
Comp
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<h4 className="font-medium sm:text-sm text-primary">{title}</h4>
|
<h4 className={cn(
|
||||||
|
"font-medium sm:text-sm",
|
||||||
|
hasActiveChild ? "text-foreground" : "text-primary"
|
||||||
|
)}>
|
||||||
|
{title}
|
||||||
|
</h4>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!items) {
|
if (!items) {
|
||||||
@@ -47,36 +84,47 @@ export default function SubLink({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1 w-full">
|
<div className={cn("flex flex-col gap-1 w-full")}>
|
||||||
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<CollapsibleTrigger className="w-full pr-5">
|
<CollapsibleTrigger
|
||||||
<div className="flex items-center justify-between cursor-pointer w-full">
|
className="w-full pr-5 text-left"
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
aria-controls={`collapsible-${fullHref.replace(/[^a-zA-Z0-9]/g, '-')}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between w-full">
|
||||||
{titleOrLink}
|
{titleOrLink}
|
||||||
<span>
|
<span className="ml-2">
|
||||||
{!isOpen ? (
|
{!isOpen ? (
|
||||||
<ChevronRight className="h-[0.9rem] w-[0.9rem]" />
|
<ChevronRight className="h-[0.9rem] w-[0.9rem]" aria-hidden="true" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronDown className="h-[0.9rem] w-[0.9rem]" />
|
<ChevronDown className="h-[0.9rem] w-[0.9rem]" aria-hidden="true" />
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
<CollapsibleContent>
|
<CollapsibleContent
|
||||||
|
id={`collapsible-${fullHref.replace(/[^a-zA-Z0-9]/g, '-')}`}
|
||||||
|
className={cn(
|
||||||
|
"overflow-hidden transition-all duration-200 ease-in-out",
|
||||||
|
isOpen ? "animate-collapsible-down" : "animate-collapsible-up"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col items-start sm:text-sm dark:text-stone-300/85 text-stone-800 ml-0.5 mt-2.5 gap-3",
|
"flex flex-col items-start sm:text-sm dark:text-stone-300/85 text-stone-800 ml-0.5 mt-2.5 gap-3",
|
||||||
level > 0 && "pl-4 border-l ml-1.5"
|
level > 0 && "pl-4 border-l ml-1.5"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{items?.map((innerLink) => {
|
{items?.map((innerLink) => (
|
||||||
const modifiedItems = {
|
<SubLink
|
||||||
...innerLink,
|
key={`${fullHref}${innerLink.href}`}
|
||||||
href: `${href + innerLink.href}`,
|
{...innerLink}
|
||||||
level: level + 1,
|
href={innerLink.href}
|
||||||
isSheet,
|
level={level + 1}
|
||||||
};
|
isSheet={isSheet}
|
||||||
return <SubLink key={modifiedItems.href} {...modifiedItems} />;
|
parentHref={fullHref}
|
||||||
})}
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|||||||
Reference in New Issue
Block a user