@docubook/create 1.9.0 → 1.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -3
- package/package.json +7 -7
- package/src/cli/program.js +32 -0
- package/src/cli/promptHandler.js +73 -0
- package/src/dist/LICENSE +21 -0
- package/src/dist/README.md +37 -0
- package/src/dist/app/docs/[[...slug]]/page.tsx +105 -0
- package/src/dist/app/docs/layout.tsx +16 -0
- package/src/dist/app/error.tsx +44 -0
- package/src/dist/app/layout.tsx +96 -0
- package/src/dist/app/not-found.tsx +19 -0
- package/src/dist/app/page.tsx +96 -0
- package/src/dist/components/GithubStart.tsx +44 -0
- package/src/dist/components/Sponsor.tsx +69 -0
- package/src/dist/components/anchor.tsx +84 -0
- package/src/dist/components/contexts/theme-provider.tsx +9 -0
- package/src/dist/components/docs-breadcrumb.tsx +47 -0
- package/src/dist/components/docs-menu.tsx +45 -0
- package/src/dist/components/edit-on-github.tsx +33 -0
- package/src/dist/components/footer.tsx +85 -0
- package/src/dist/components/leftbar.tsx +95 -0
- package/src/dist/components/markdown/AccordionMdx.tsx +47 -0
- package/src/dist/components/markdown/ButtonMdx.tsx +52 -0
- package/src/dist/components/markdown/CardGroupMdx.tsx +28 -0
- package/src/dist/components/markdown/CardMdx.tsx +41 -0
- package/src/dist/components/markdown/CopyMdx.tsx +33 -0
- package/src/dist/components/markdown/ImageMdx.tsx +25 -0
- package/src/dist/components/markdown/KeyboardMdx.tsx +102 -0
- package/src/dist/components/markdown/LinkMdx.tsx +14 -0
- package/src/dist/components/markdown/NoteMdx.tsx +52 -0
- package/src/dist/components/markdown/OutletMdx.tsx +29 -0
- package/src/dist/components/markdown/PreMdx.tsx +19 -0
- package/src/dist/components/markdown/ReleaseMdx.tsx +109 -0
- package/src/dist/components/markdown/StepperMdx.tsx +41 -0
- package/src/dist/components/markdown/TooltipsMdx.tsx +28 -0
- package/src/dist/components/markdown/YoutubeMdx.tsx +22 -0
- package/src/dist/components/markdown/mdx-provider.tsx +29 -0
- package/src/dist/components/mob-toc.tsx +128 -0
- package/src/dist/components/navbar.tsx +87 -0
- package/src/dist/components/pagination.tsx +49 -0
- package/src/dist/components/scroll-to-top.tsx +86 -0
- package/src/dist/components/search.tsx +214 -0
- package/src/dist/components/sublink.tsx +133 -0
- package/src/dist/components/theme-toggle.tsx +71 -0
- package/src/dist/components/toc-observer.tsx +264 -0
- package/src/dist/components/toc.tsx +27 -0
- package/src/dist/components/typography.tsx +9 -0
- package/src/dist/components/ui/accordion.tsx +58 -0
- package/src/dist/components/ui/animated-shiny-text.tsx +40 -0
- package/src/dist/components/ui/aurora.tsx +45 -0
- package/src/dist/components/ui/avatar.tsx +50 -0
- package/src/dist/components/ui/badge.tsx +37 -0
- package/src/dist/components/ui/breadcrumb.tsx +115 -0
- package/src/dist/components/ui/button.tsx +57 -0
- package/src/dist/components/ui/card.tsx +76 -0
- package/src/dist/components/ui/collapsible.tsx +11 -0
- package/src/dist/components/ui/command.tsx +153 -0
- package/src/dist/components/ui/dialog.tsx +124 -0
- package/src/dist/components/ui/dropdown-menu.tsx +200 -0
- package/src/dist/components/ui/icon-cloud.tsx +324 -0
- package/src/dist/components/ui/input.tsx +25 -0
- package/src/dist/components/ui/interactive-hover-button.tsx +35 -0
- package/src/dist/components/ui/popover.tsx +33 -0
- package/src/dist/components/ui/scroll-area.tsx +48 -0
- package/src/dist/components/ui/separator.tsx +30 -0
- package/src/dist/components/ui/sheet.tsx +140 -0
- package/src/dist/components/ui/shine-border.tsx +64 -0
- package/src/dist/components/ui/skeleton.tsx +15 -0
- package/src/dist/components/ui/sonner.tsx +31 -0
- package/src/dist/components/ui/table.tsx +117 -0
- package/src/dist/components/ui/tabs.tsx +55 -0
- package/src/dist/components/ui/toggle-group.tsx +61 -0
- package/src/dist/components/ui/toggle.tsx +46 -0
- package/src/dist/components.json +17 -0
- package/src/dist/contents/docs/getting-started/changelog/index.mdx +512 -0
- package/src/dist/contents/docs/getting-started/components/accordion/index.mdx +72 -0
- package/src/dist/contents/docs/getting-started/components/button/index.mdx +42 -0
- package/src/dist/contents/docs/getting-started/components/card/index.mdx +70 -0
- package/src/dist/contents/docs/getting-started/components/card-group/index.mdx +49 -0
- package/src/dist/contents/docs/getting-started/components/code-block/index.mdx +41 -0
- package/src/dist/contents/docs/getting-started/components/custom/index.mdx +38 -0
- package/src/dist/contents/docs/getting-started/components/image/index.mdx +37 -0
- package/src/dist/contents/docs/getting-started/components/index.mdx +9 -0
- package/src/dist/contents/docs/getting-started/components/keyboard/index.mdx +117 -0
- package/src/dist/contents/docs/getting-started/components/link/index.mdx +34 -0
- package/src/dist/contents/docs/getting-started/components/note/index.mdx +46 -0
- package/src/dist/contents/docs/getting-started/components/release-note/index.mdx +130 -0
- package/src/dist/contents/docs/getting-started/components/stepper/index.mdx +47 -0
- package/src/dist/contents/docs/getting-started/components/tabs/index.mdx +70 -0
- package/src/dist/contents/docs/getting-started/components/tooltips/index.mdx +22 -0
- package/src/dist/contents/docs/getting-started/components/youtube/index.mdx +21 -0
- package/src/dist/contents/docs/getting-started/customize/index.mdx +94 -0
- package/src/dist/contents/docs/getting-started/installation/index.mdx +84 -0
- package/src/dist/contents/docs/getting-started/introduction/index.mdx +50 -0
- package/src/dist/contents/docs/getting-started/project-structure/index.mdx +87 -0
- package/src/dist/contents/docs/getting-started/quick-start-guide/index.mdx +127 -0
- package/src/dist/docu.json +100 -0
- package/src/dist/hooks/index.ts +2 -0
- package/src/dist/hooks/useActiveSection.ts +68 -0
- package/src/dist/hooks/useScrollPosition.ts +28 -0
- package/src/dist/lib/markdown.ts +244 -0
- package/src/dist/lib/routes-config.ts +28 -0
- package/src/dist/lib/toc.ts +9 -0
- package/src/dist/lib/utils.ts +80 -0
- package/src/dist/next-env.d.ts +5 -0
- package/src/dist/next.config.mjs +14 -0
- package/src/dist/package.json +58 -0
- package/src/dist/postcss.config.js +6 -0
- package/src/dist/public/favicon.ico +0 -0
- package/src/dist/public/images/docu.svg +6 -0
- package/src/dist/public/images/example-img.png +0 -0
- package/src/dist/public/images/img-playground.png +0 -0
- package/src/dist/public/images/new-editor.png +0 -0
- package/src/dist/public/images/og-image.png +0 -0
- package/src/dist/public/images/release-note.png +0 -0
- package/src/dist/public/images/snippet.png +0 -0
- package/src/dist/public/images/vercel.png +0 -0
- package/src/dist/public/images/view-changelog.png +0 -0
- package/src/dist/styles/editor.css +57 -0
- package/src/dist/styles/globals.css +156 -0
- package/src/dist/styles/syntax.css +100 -0
- package/src/dist/tailwind.config.ts +112 -0
- package/src/dist/tsconfig.json +26 -0
- package/src/index.js +19 -0
- package/src/installer/projectInstaller.js +125 -0
- package/src/utils/display.js +83 -0
- package/src/utils/logger.js +11 -0
- package/src/utils/packageManager.js +54 -0
- package/create.js +0 -223
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { ArrowUpRight } from "lucide-react";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import Image from "next/image";
|
|
4
|
+
import Search from "@/components/search";
|
|
5
|
+
import Anchor from "@/components/anchor";
|
|
6
|
+
import { SheetLeftbar } from "@/components/leftbar";
|
|
7
|
+
import { SheetClose } from "@/components/ui/sheet";
|
|
8
|
+
import { Separator } from "@/components/ui/separator";
|
|
9
|
+
import docuConfig from "@/docu.json"; // Import JSON
|
|
10
|
+
|
|
11
|
+
export function Navbar() {
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<nav className="sticky top-0 z-50 w-full h-16 border-b bg-background">
|
|
15
|
+
<div className="sm:container mx-auto w-[95vw] h-full flex items-center justify-between md:gap-2">
|
|
16
|
+
<div className="flex items-center gap-5">
|
|
17
|
+
<SheetLeftbar />
|
|
18
|
+
<div className="flex items-center gap-6">
|
|
19
|
+
<div className="hidden lg:flex">
|
|
20
|
+
<Logo />
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
<div className="flex items-center gap-2">
|
|
25
|
+
<div className="items-center hidden gap-4 text-sm font-medium lg:flex text-muted-foreground">
|
|
26
|
+
<NavMenu />
|
|
27
|
+
</div>
|
|
28
|
+
<Separator className="hidden lg:flex my-4 h-9" orientation="vertical" />
|
|
29
|
+
<Search />
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</nav>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function Logo() {
|
|
37
|
+
const { navbar } = docuConfig; // Extract navbar from JSON
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Link href="/" className="flex items-center gap-1.5">
|
|
41
|
+
<div className="relative w-8 h-8">
|
|
42
|
+
<Image
|
|
43
|
+
src={navbar.logo.src}
|
|
44
|
+
alt={navbar.logo.alt}
|
|
45
|
+
fill
|
|
46
|
+
sizes="32px"
|
|
47
|
+
className="object-contain"
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
<h2 className="font-bold font-code text-md">{navbar.logoText}</h2>
|
|
51
|
+
</Link>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function NavMenu({ isSheet = false }) {
|
|
56
|
+
const { navbar } = docuConfig; // Extract navbar from JSON
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<>
|
|
60
|
+
{navbar?.menu?.map((item) => {
|
|
61
|
+
const isExternal = item.href.startsWith("http");
|
|
62
|
+
|
|
63
|
+
const Comp = (
|
|
64
|
+
<Anchor
|
|
65
|
+
key={`${item.title}-${item.href}`}
|
|
66
|
+
activeClassName="!text-primary md:font-semibold font-medium"
|
|
67
|
+
absolute
|
|
68
|
+
className="flex items-center gap-1 dark:text-stone-300/85 text-stone-800"
|
|
69
|
+
href={item.href}
|
|
70
|
+
target={isExternal ? "_blank" : undefined}
|
|
71
|
+
rel={isExternal ? "noopener noreferrer" : undefined}
|
|
72
|
+
>
|
|
73
|
+
{item.title}
|
|
74
|
+
{isExternal && <ArrowUpRight className="w-4 h-4 text-muted-foreground" />}
|
|
75
|
+
</Anchor>
|
|
76
|
+
);
|
|
77
|
+
return isSheet ? (
|
|
78
|
+
<SheetClose key={item.title + item.href} asChild>
|
|
79
|
+
{Comp}
|
|
80
|
+
</SheetClose>
|
|
81
|
+
) : (
|
|
82
|
+
Comp
|
|
83
|
+
);
|
|
84
|
+
})}
|
|
85
|
+
</>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { getPreviousNext } from "@/lib/markdown";
|
|
2
|
+
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { buttonVariants } from "@/components/ui/button";
|
|
5
|
+
|
|
6
|
+
export default function Pagination({ pathname }: { pathname: string }) {
|
|
7
|
+
const res = getPreviousNext(pathname);
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 flex-grow sm:py-10 py-7 gap-3">
|
|
11
|
+
<div>
|
|
12
|
+
{res.prev && (
|
|
13
|
+
<Link
|
|
14
|
+
className={buttonVariants({
|
|
15
|
+
variant: "outline",
|
|
16
|
+
className:
|
|
17
|
+
"no-underline w-full flex flex-col pl-3 !py-8 !items-start",
|
|
18
|
+
})}
|
|
19
|
+
href={`/docs${res.prev.href}`}
|
|
20
|
+
>
|
|
21
|
+
<span className="flex items-center text-xs">
|
|
22
|
+
<ChevronLeftIcon className="w-[1rem] h-[1rem] mr-1" />
|
|
23
|
+
Previous
|
|
24
|
+
</span>
|
|
25
|
+
<span className="mt-1 ml-1">{res.prev.title}</span>
|
|
26
|
+
</Link>
|
|
27
|
+
)}
|
|
28
|
+
</div>
|
|
29
|
+
<div>
|
|
30
|
+
{res.next && (
|
|
31
|
+
<Link
|
|
32
|
+
className={buttonVariants({
|
|
33
|
+
variant: "outline",
|
|
34
|
+
className:
|
|
35
|
+
"no-underline w-full flex flex-col pr-3 !py-8 !items-end",
|
|
36
|
+
})}
|
|
37
|
+
href={`/docs${res.next.href}`}
|
|
38
|
+
>
|
|
39
|
+
<span className="flex items-center text-xs">
|
|
40
|
+
Next
|
|
41
|
+
<ChevronRightIcon className="w-[1rem] h-[1rem] ml-1" />
|
|
42
|
+
</span>
|
|
43
|
+
<span className="mt-1 mr-1">{res.next.title}</span>
|
|
44
|
+
</Link>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ArrowUpIcon } from "lucide-react";
|
|
4
|
+
import { useEffect, useState, useCallback } from "react";
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
interface ScrollToTopProps {
|
|
9
|
+
className?: string;
|
|
10
|
+
showIcon?: boolean;
|
|
11
|
+
offset?: number; // Optional offset in pixels from the trigger point
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ScrollToTop({
|
|
15
|
+
className,
|
|
16
|
+
showIcon = true,
|
|
17
|
+
offset = 0
|
|
18
|
+
}: ScrollToTopProps) {
|
|
19
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
20
|
+
|
|
21
|
+
const checkScroll = useCallback(() => {
|
|
22
|
+
// Calculate 50% of viewport height
|
|
23
|
+
const halfViewportHeight = window.innerHeight * 0.5;
|
|
24
|
+
// Check if scrolled past half viewport height (plus any offset)
|
|
25
|
+
const scrolledPastHalfViewport = window.scrollY > (halfViewportHeight + offset);
|
|
26
|
+
|
|
27
|
+
// Only update state if it changes to prevent unnecessary re-renders
|
|
28
|
+
if (scrolledPastHalfViewport !== isVisible) {
|
|
29
|
+
setIsVisible(scrolledPastHalfViewport);
|
|
30
|
+
}
|
|
31
|
+
}, [isVisible, offset]);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
// Initial check
|
|
35
|
+
checkScroll();
|
|
36
|
+
|
|
37
|
+
// Set up scroll listener with debounce for better performance
|
|
38
|
+
let timeoutId: NodeJS.Timeout;
|
|
39
|
+
const handleScroll = () => {
|
|
40
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
41
|
+
timeoutId = setTimeout(checkScroll, 100);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
45
|
+
|
|
46
|
+
// Cleanup
|
|
47
|
+
return () => {
|
|
48
|
+
window.removeEventListener('scroll', handleScroll);
|
|
49
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
50
|
+
};
|
|
51
|
+
}, [checkScroll]);
|
|
52
|
+
|
|
53
|
+
const scrollToTop = useCallback((e: React.MouseEvent) => {
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
window.scrollTo({
|
|
56
|
+
top: 0,
|
|
57
|
+
behavior: 'smooth'
|
|
58
|
+
});
|
|
59
|
+
}, []);
|
|
60
|
+
|
|
61
|
+
if (!isVisible) return null;
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div
|
|
65
|
+
className={cn(
|
|
66
|
+
"mt-4 pt-4 border-t border-stone-200 dark:border-stone-800",
|
|
67
|
+
"transition-opacity duration-300",
|
|
68
|
+
isVisible ? 'opacity-100' : 'opacity-0',
|
|
69
|
+
className
|
|
70
|
+
)}
|
|
71
|
+
>
|
|
72
|
+
<Link
|
|
73
|
+
href="#"
|
|
74
|
+
onClick={scrollToTop}
|
|
75
|
+
className={cn(
|
|
76
|
+
"inline-flex items-center text-sm text-muted-foreground hover:text-foreground",
|
|
77
|
+
"transition-all duration-200 hover:translate-y-[-1px]"
|
|
78
|
+
)}
|
|
79
|
+
aria-label="Scroll to top"
|
|
80
|
+
>
|
|
81
|
+
{showIcon && <ArrowUpIcon className="mr-1 h-3.5 w-3.5 flex-shrink-0" />}
|
|
82
|
+
<span>Scroll to Top</span>
|
|
83
|
+
</Link>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import { useEffect, useMemo, useState, useRef } from "react";
|
|
5
|
+
import { ArrowUpIcon, ArrowDownIcon, CommandIcon, FileTextIcon, SearchIcon, CornerDownLeftIcon } from "lucide-react";
|
|
6
|
+
import { Input } from "@/components/ui/input";
|
|
7
|
+
import {
|
|
8
|
+
Dialog,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogFooter,
|
|
12
|
+
DialogTrigger,
|
|
13
|
+
DialogClose,
|
|
14
|
+
DialogTitle,
|
|
15
|
+
DialogDescription,
|
|
16
|
+
} from "@/components/ui/dialog";
|
|
17
|
+
import Anchor from "./anchor";
|
|
18
|
+
import { advanceSearch, cn } from "@/lib/utils";
|
|
19
|
+
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
20
|
+
|
|
21
|
+
export default function Search() {
|
|
22
|
+
const router = useRouter();
|
|
23
|
+
const [searchedInput, setSearchedInput] = useState("");
|
|
24
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
25
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
26
|
+
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
30
|
+
if ((event.ctrlKey || event.metaKey) && event.key === "k") {
|
|
31
|
+
event.preventDefault();
|
|
32
|
+
setIsOpen(true);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
37
|
+
return () => {
|
|
38
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
39
|
+
};
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
const filteredResults = useMemo(
|
|
43
|
+
() => advanceSearch(searchedInput.trim()),
|
|
44
|
+
[searchedInput]
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
setSelectedIndex(0);
|
|
49
|
+
}, [filteredResults]);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const handleNavigation = (event: KeyboardEvent) => {
|
|
53
|
+
if (!isOpen || filteredResults.length === 0) return;
|
|
54
|
+
|
|
55
|
+
if (event.key === "ArrowDown") {
|
|
56
|
+
event.preventDefault();
|
|
57
|
+
setSelectedIndex((prev) => (prev + 1) % filteredResults.length);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (event.key === "ArrowUp") {
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
setSelectedIndex((prev) => (prev - 1 + filteredResults.length) % filteredResults.length);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (event.key === "Enter") {
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
const selectedItem = filteredResults[selectedIndex];
|
|
68
|
+
if (selectedItem) {
|
|
69
|
+
router.push(`/docs${selectedItem.href}`);
|
|
70
|
+
setIsOpen(false);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
window.addEventListener("keydown", handleNavigation);
|
|
76
|
+
return () => {
|
|
77
|
+
window.removeEventListener("keydown", handleNavigation);
|
|
78
|
+
};
|
|
79
|
+
}, [isOpen, filteredResults, selectedIndex, router]);
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (itemRefs.current[selectedIndex]) {
|
|
83
|
+
itemRefs.current[selectedIndex]?.scrollIntoView({
|
|
84
|
+
behavior: "smooth",
|
|
85
|
+
block: "nearest",
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}, [selectedIndex]);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div>
|
|
92
|
+
<Dialog
|
|
93
|
+
open={isOpen}
|
|
94
|
+
onOpenChange={(open) => {
|
|
95
|
+
if (!open) setSearchedInput("");
|
|
96
|
+
setIsOpen(open);
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
<DialogTrigger asChild>
|
|
100
|
+
<div className="relative flex-1 cursor-pointer max-w-[140px]">
|
|
101
|
+
<div className="flex items-center">
|
|
102
|
+
<div className="md:hidden p-2 -ml-2">
|
|
103
|
+
<SearchIcon className="h-5 w-5 text-stone-500 dark:text-stone-400" />
|
|
104
|
+
</div>
|
|
105
|
+
<div className="hidden md:block w-full">
|
|
106
|
+
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-stone-500 dark:text-stone-400" />
|
|
107
|
+
<Input
|
|
108
|
+
className="w-full rounded-full dark:bg-background/95 bg-background border h-9 pl-10 pr-0 sm:pr-4 text-sm shadow-sm overflow-ellipsis"
|
|
109
|
+
placeholder="Search"
|
|
110
|
+
type="search"
|
|
111
|
+
/>
|
|
112
|
+
<div className="flex absolute top-1/2 -translate-y-1/2 right-2 text-xs font-medium font-mono items-center gap-0.5 dark:bg-accent bg-accent text-white px-2 py-0.5 rounded-full">
|
|
113
|
+
<CommandIcon className="w-3 h-3" />
|
|
114
|
+
<span>K</span>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</DialogTrigger>
|
|
120
|
+
<DialogContent className="p-0 max-w-[650px] sm:top-[38%] top-[45%] !rounded-md">
|
|
121
|
+
<DialogHeader>
|
|
122
|
+
<DialogTitle className="sr-only">Search Documentation</DialogTitle>
|
|
123
|
+
</DialogHeader>
|
|
124
|
+
<DialogDescription className="sr-only">
|
|
125
|
+
Search through the documentation
|
|
126
|
+
</DialogDescription>
|
|
127
|
+
<input
|
|
128
|
+
value={searchedInput}
|
|
129
|
+
onChange={(e) => setSearchedInput(e.target.value)}
|
|
130
|
+
placeholder="Type something to search..."
|
|
131
|
+
autoFocus
|
|
132
|
+
className="h-14 px-6 bg-transparent border-b text-[14px] outline-none w-full"
|
|
133
|
+
aria-label="Search documentation"
|
|
134
|
+
/>
|
|
135
|
+
{filteredResults.length == 0 && searchedInput && (
|
|
136
|
+
<p className="text-muted-foreground mx-auto mt-2 text-sm">
|
|
137
|
+
No results found for{" "}
|
|
138
|
+
<span className="text-primary">{`"${searchedInput}"`}</span>
|
|
139
|
+
</p>
|
|
140
|
+
)}
|
|
141
|
+
<ScrollArea className="max-h-[400px] overflow-y-auto">
|
|
142
|
+
<div className="flex flex-col items-start overflow-y-auto sm:px-2 px-1 pb-4">
|
|
143
|
+
{filteredResults.map((item, index) => {
|
|
144
|
+
const level = (item.href.split("/").slice(1).length - 1) as keyof typeof paddingMap;
|
|
145
|
+
const paddingClass = paddingMap[level];
|
|
146
|
+
const isActive = index === selectedIndex;
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<DialogClose key={item.href} asChild>
|
|
150
|
+
<Anchor
|
|
151
|
+
ref={(el) => {
|
|
152
|
+
itemRefs.current[index] = el as HTMLDivElement | null;
|
|
153
|
+
}}
|
|
154
|
+
className={cn(
|
|
155
|
+
"dark:hover:bg-accent/15 hover:bg-accent/10 w-full px-3 rounded-sm text-sm flex items-center gap-2.5",
|
|
156
|
+
isActive && "bg-primary/20 dark:bg-primary/30",
|
|
157
|
+
paddingClass
|
|
158
|
+
)}
|
|
159
|
+
href={`/docs${item.href}`}
|
|
160
|
+
tabIndex={0}
|
|
161
|
+
>
|
|
162
|
+
<div
|
|
163
|
+
className={cn(
|
|
164
|
+
"flex items-center w-full h-full py-3 gap-1.5 px-2 justify-between",
|
|
165
|
+
level > 1 && "border-l pl-4"
|
|
166
|
+
)}
|
|
167
|
+
>
|
|
168
|
+
<div className="flex items-center">
|
|
169
|
+
<FileTextIcon className="h-[1.1rem] w-[1.1rem] mr-1" />
|
|
170
|
+
<span>{item.title}</span>
|
|
171
|
+
</div>
|
|
172
|
+
{isActive && (
|
|
173
|
+
<div className="hidden md:flex items-center text-xs text-muted-foreground">
|
|
174
|
+
<span>Return</span>
|
|
175
|
+
<CornerDownLeftIcon className="h-3 w-3 ml-1" />
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
</Anchor>
|
|
180
|
+
</DialogClose>
|
|
181
|
+
);
|
|
182
|
+
})}
|
|
183
|
+
</div>
|
|
184
|
+
</ScrollArea>
|
|
185
|
+
<DialogFooter className="md:flex md:justify-start hidden h-14 px-6 bg-transparent border-t text-[14px] outline-none">
|
|
186
|
+
<div className="flex items-center gap-2">
|
|
187
|
+
<span className="dark:bg-accent/15 bg-slate-200 border rounded p-2">
|
|
188
|
+
<ArrowUpIcon className="w-3 h-3"/>
|
|
189
|
+
</span>
|
|
190
|
+
<span className="dark:bg-accent/15 bg-slate-200 border rounded p-2">
|
|
191
|
+
<ArrowDownIcon className="w-3 h-3"/>
|
|
192
|
+
</span>
|
|
193
|
+
<p className="text-muted-foreground">to navigate</p>
|
|
194
|
+
<span className="dark:bg-accent/15 bg-slate-200 border rounded p-2">
|
|
195
|
+
<CornerDownLeftIcon className="w-3 h-3"/>
|
|
196
|
+
</span>
|
|
197
|
+
<p className="text-muted-foreground">to select</p>
|
|
198
|
+
<span className="dark:bg-accent/15 bg-slate-200 border rounded px-2 py-1">
|
|
199
|
+
esc
|
|
200
|
+
</span>
|
|
201
|
+
<p className="text-muted-foreground">to close</p>
|
|
202
|
+
</div>
|
|
203
|
+
</DialogFooter>
|
|
204
|
+
</DialogContent>
|
|
205
|
+
</Dialog>
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const paddingMap = {
|
|
211
|
+
1: "pl-2",
|
|
212
|
+
2: "pl-4",
|
|
213
|
+
3: "pl-10",
|
|
214
|
+
} as const;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { EachRoute } from "@/lib/routes-config";
|
|
2
|
+
import Anchor from "./anchor";
|
|
3
|
+
import {
|
|
4
|
+
Collapsible,
|
|
5
|
+
CollapsibleContent,
|
|
6
|
+
CollapsibleTrigger,
|
|
7
|
+
} from "@/components/ui/collapsible";
|
|
8
|
+
import { cn } from "@/lib/utils";
|
|
9
|
+
import { SheetClose } from "@/components/ui/sheet";
|
|
10
|
+
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
11
|
+
import { useEffect, useState, useMemo } from "react";
|
|
12
|
+
import { usePathname } from "next/navigation";
|
|
13
|
+
|
|
14
|
+
interface SubLinkProps extends EachRoute {
|
|
15
|
+
level: number;
|
|
16
|
+
isSheet: boolean;
|
|
17
|
+
parentHref?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function SubLink({
|
|
21
|
+
title,
|
|
22
|
+
href,
|
|
23
|
+
items,
|
|
24
|
+
noLink,
|
|
25
|
+
level,
|
|
26
|
+
isSheet,
|
|
27
|
+
parentHref = "",
|
|
28
|
+
}: SubLinkProps) {
|
|
29
|
+
const path = usePathname();
|
|
30
|
+
const [isOpen, setIsOpen] = useState(level === 0);
|
|
31
|
+
|
|
32
|
+
// Full path including parent's href
|
|
33
|
+
const fullHref = `${parentHref}${href}`;
|
|
34
|
+
|
|
35
|
+
// Check if current path exactly matches this link's href
|
|
36
|
+
const isExactActive = useMemo(() => path === fullHref, [path, fullHref]);
|
|
37
|
+
|
|
38
|
+
// Check if any child is active (for parent items)
|
|
39
|
+
const hasActiveChild = useMemo(() => {
|
|
40
|
+
if (!items) return false;
|
|
41
|
+
return items.some(item => {
|
|
42
|
+
const childHref = `${fullHref}${item.href}`;
|
|
43
|
+
return path.startsWith(childHref) && path !== fullHref;
|
|
44
|
+
});
|
|
45
|
+
}, [items, path, fullHref]);
|
|
46
|
+
|
|
47
|
+
// Auto-expand if current path is a child of this item
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (items && (path.startsWith(fullHref) && path !== fullHref)) {
|
|
50
|
+
setIsOpen(true);
|
|
51
|
+
}
|
|
52
|
+
}, [path, fullHref, items]);
|
|
53
|
+
|
|
54
|
+
// Only apply active styles if it's an exact match and not a parent with active children
|
|
55
|
+
const Comp = useMemo(() => (
|
|
56
|
+
<Anchor
|
|
57
|
+
activeClassName={!hasActiveChild ? "text-primary font-medium" : ""}
|
|
58
|
+
href={fullHref}
|
|
59
|
+
className={cn(
|
|
60
|
+
hasActiveChild && "font-medium text-foreground"
|
|
61
|
+
)}
|
|
62
|
+
>
|
|
63
|
+
{title}
|
|
64
|
+
</Anchor>
|
|
65
|
+
), [title, fullHref, hasActiveChild]);
|
|
66
|
+
|
|
67
|
+
const titleOrLink = !noLink ? (
|
|
68
|
+
isSheet ? (
|
|
69
|
+
<SheetClose asChild>{Comp}</SheetClose>
|
|
70
|
+
) : (
|
|
71
|
+
Comp
|
|
72
|
+
)
|
|
73
|
+
) : (
|
|
74
|
+
<h4 className={cn(
|
|
75
|
+
"font-medium sm:text-sm",
|
|
76
|
+
hasActiveChild ? "text-foreground" : "text-primary"
|
|
77
|
+
)}>
|
|
78
|
+
{title}
|
|
79
|
+
</h4>
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (!items) {
|
|
83
|
+
return <div className="flex flex-col">{titleOrLink}</div>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div className={cn("flex flex-col gap-1 w-full")}>
|
|
88
|
+
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
|
89
|
+
<CollapsibleTrigger
|
|
90
|
+
className="w-full pr-5 text-left"
|
|
91
|
+
aria-expanded={isOpen}
|
|
92
|
+
aria-controls={`collapsible-${fullHref.replace(/[^a-zA-Z0-9]/g, '-')}`}
|
|
93
|
+
>
|
|
94
|
+
<div className="flex items-center justify-between w-full">
|
|
95
|
+
{titleOrLink}
|
|
96
|
+
<span className="ml-2">
|
|
97
|
+
{!isOpen ? (
|
|
98
|
+
<ChevronRight className="h-[0.9rem] w-[0.9rem]" aria-hidden="true" />
|
|
99
|
+
) : (
|
|
100
|
+
<ChevronDown className="h-[0.9rem] w-[0.9rem]" aria-hidden="true" />
|
|
101
|
+
)}
|
|
102
|
+
</span>
|
|
103
|
+
</div>
|
|
104
|
+
</CollapsibleTrigger>
|
|
105
|
+
<CollapsibleContent
|
|
106
|
+
id={`collapsible-${fullHref.replace(/[^a-zA-Z0-9]/g, '-')}`}
|
|
107
|
+
className={cn(
|
|
108
|
+
"overflow-hidden transition-all duration-200 ease-in-out",
|
|
109
|
+
isOpen ? "animate-collapsible-down" : "animate-collapsible-up"
|
|
110
|
+
)}
|
|
111
|
+
>
|
|
112
|
+
<div
|
|
113
|
+
className={cn(
|
|
114
|
+
"flex flex-col items-start sm:text-sm dark:text-stone-300/85 text-stone-800 ml-0.5 mt-2.5 gap-3",
|
|
115
|
+
level > 0 && "pl-4 border-l ml-1.5"
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
118
|
+
{items?.map((innerLink) => (
|
|
119
|
+
<SubLink
|
|
120
|
+
key={`${fullHref}${innerLink.href}`}
|
|
121
|
+
{...innerLink}
|
|
122
|
+
href={innerLink.href}
|
|
123
|
+
level={level + 1}
|
|
124
|
+
isSheet={isSheet}
|
|
125
|
+
parentHref={fullHref}
|
|
126
|
+
/>
|
|
127
|
+
))}
|
|
128
|
+
</div>
|
|
129
|
+
</CollapsibleContent>
|
|
130
|
+
</Collapsible>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Moon, Sun, Monitor } from "lucide-react";
|
|
5
|
+
import { useTheme } from "next-themes";
|
|
6
|
+
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
|
7
|
+
|
|
8
|
+
export function ModeToggle() {
|
|
9
|
+
const { theme, setTheme } = useTheme();
|
|
10
|
+
const [selectedTheme, setSelectedTheme] = React.useState<string>("system");
|
|
11
|
+
|
|
12
|
+
// Pastikan toggle tetap di posisi yang benar setelah reload
|
|
13
|
+
React.useEffect(() => {
|
|
14
|
+
if (theme) {
|
|
15
|
+
setSelectedTheme(theme);
|
|
16
|
+
} else {
|
|
17
|
+
setSelectedTheme("system"); // Default ke system jika undefined
|
|
18
|
+
}
|
|
19
|
+
}, [theme]);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<ToggleGroup
|
|
23
|
+
type="single"
|
|
24
|
+
value={selectedTheme}
|
|
25
|
+
onValueChange={(value) => {
|
|
26
|
+
if (value) {
|
|
27
|
+
setTheme(value);
|
|
28
|
+
setSelectedTheme(value);
|
|
29
|
+
}
|
|
30
|
+
}}
|
|
31
|
+
className="flex items-center gap-1 rounded-full border border-gray-300 dark:border-gray-700 p-1 transition-all"
|
|
32
|
+
>
|
|
33
|
+
<ToggleGroupItem
|
|
34
|
+
value="light"
|
|
35
|
+
size="sm"
|
|
36
|
+
aria-label="Light Mode"
|
|
37
|
+
className={`rounded-full p-1 transition-all ${
|
|
38
|
+
selectedTheme === "light"
|
|
39
|
+
? "bg-blue-500 text-white"
|
|
40
|
+
: "bg-transparent"
|
|
41
|
+
}`}
|
|
42
|
+
>
|
|
43
|
+
<Sun className={`h-4 w-4 ${selectedTheme === "light" ? "text-white" : "text-gray-600 dark:text-gray-300"}`} />
|
|
44
|
+
</ToggleGroupItem>
|
|
45
|
+
<ToggleGroupItem
|
|
46
|
+
value="system"
|
|
47
|
+
size="sm"
|
|
48
|
+
aria-label="System Mode"
|
|
49
|
+
className={`rounded-full p-1 transition-all ${
|
|
50
|
+
selectedTheme === "system"
|
|
51
|
+
? "bg-blue-500 text-white"
|
|
52
|
+
: "bg-transparent"
|
|
53
|
+
}`}
|
|
54
|
+
>
|
|
55
|
+
<Monitor className={`h-4 w-4 ${selectedTheme === "system" ? "text-white" : "text-gray-600 dark:text-gray-300"}`} />
|
|
56
|
+
</ToggleGroupItem>
|
|
57
|
+
<ToggleGroupItem
|
|
58
|
+
value="dark"
|
|
59
|
+
size="sm"
|
|
60
|
+
aria-label="Dark Mode"
|
|
61
|
+
className={`rounded-full p-1 transition-all ${
|
|
62
|
+
selectedTheme === "dark"
|
|
63
|
+
? "bg-blue-500 text-white"
|
|
64
|
+
: "bg-transparent"
|
|
65
|
+
}`}
|
|
66
|
+
>
|
|
67
|
+
<Moon className={`h-4 w-4 ${selectedTheme === "dark" ? "text-white" : "text-gray-600 dark:text-gray-300"}`} />
|
|
68
|
+
</ToggleGroupItem>
|
|
69
|
+
</ToggleGroup>
|
|
70
|
+
);
|
|
71
|
+
}
|