@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.
Files changed (129) hide show
  1. package/README.md +1 -3
  2. package/package.json +7 -7
  3. package/src/cli/program.js +32 -0
  4. package/src/cli/promptHandler.js +73 -0
  5. package/src/dist/LICENSE +21 -0
  6. package/src/dist/README.md +37 -0
  7. package/src/dist/app/docs/[[...slug]]/page.tsx +105 -0
  8. package/src/dist/app/docs/layout.tsx +16 -0
  9. package/src/dist/app/error.tsx +44 -0
  10. package/src/dist/app/layout.tsx +96 -0
  11. package/src/dist/app/not-found.tsx +19 -0
  12. package/src/dist/app/page.tsx +96 -0
  13. package/src/dist/components/GithubStart.tsx +44 -0
  14. package/src/dist/components/Sponsor.tsx +69 -0
  15. package/src/dist/components/anchor.tsx +84 -0
  16. package/src/dist/components/contexts/theme-provider.tsx +9 -0
  17. package/src/dist/components/docs-breadcrumb.tsx +47 -0
  18. package/src/dist/components/docs-menu.tsx +45 -0
  19. package/src/dist/components/edit-on-github.tsx +33 -0
  20. package/src/dist/components/footer.tsx +85 -0
  21. package/src/dist/components/leftbar.tsx +95 -0
  22. package/src/dist/components/markdown/AccordionMdx.tsx +47 -0
  23. package/src/dist/components/markdown/ButtonMdx.tsx +52 -0
  24. package/src/dist/components/markdown/CardGroupMdx.tsx +28 -0
  25. package/src/dist/components/markdown/CardMdx.tsx +41 -0
  26. package/src/dist/components/markdown/CopyMdx.tsx +33 -0
  27. package/src/dist/components/markdown/ImageMdx.tsx +25 -0
  28. package/src/dist/components/markdown/KeyboardMdx.tsx +102 -0
  29. package/src/dist/components/markdown/LinkMdx.tsx +14 -0
  30. package/src/dist/components/markdown/NoteMdx.tsx +52 -0
  31. package/src/dist/components/markdown/OutletMdx.tsx +29 -0
  32. package/src/dist/components/markdown/PreMdx.tsx +19 -0
  33. package/src/dist/components/markdown/ReleaseMdx.tsx +109 -0
  34. package/src/dist/components/markdown/StepperMdx.tsx +41 -0
  35. package/src/dist/components/markdown/TooltipsMdx.tsx +28 -0
  36. package/src/dist/components/markdown/YoutubeMdx.tsx +22 -0
  37. package/src/dist/components/markdown/mdx-provider.tsx +29 -0
  38. package/src/dist/components/mob-toc.tsx +128 -0
  39. package/src/dist/components/navbar.tsx +87 -0
  40. package/src/dist/components/pagination.tsx +49 -0
  41. package/src/dist/components/scroll-to-top.tsx +86 -0
  42. package/src/dist/components/search.tsx +214 -0
  43. package/src/dist/components/sublink.tsx +133 -0
  44. package/src/dist/components/theme-toggle.tsx +71 -0
  45. package/src/dist/components/toc-observer.tsx +264 -0
  46. package/src/dist/components/toc.tsx +27 -0
  47. package/src/dist/components/typography.tsx +9 -0
  48. package/src/dist/components/ui/accordion.tsx +58 -0
  49. package/src/dist/components/ui/animated-shiny-text.tsx +40 -0
  50. package/src/dist/components/ui/aurora.tsx +45 -0
  51. package/src/dist/components/ui/avatar.tsx +50 -0
  52. package/src/dist/components/ui/badge.tsx +37 -0
  53. package/src/dist/components/ui/breadcrumb.tsx +115 -0
  54. package/src/dist/components/ui/button.tsx +57 -0
  55. package/src/dist/components/ui/card.tsx +76 -0
  56. package/src/dist/components/ui/collapsible.tsx +11 -0
  57. package/src/dist/components/ui/command.tsx +153 -0
  58. package/src/dist/components/ui/dialog.tsx +124 -0
  59. package/src/dist/components/ui/dropdown-menu.tsx +200 -0
  60. package/src/dist/components/ui/icon-cloud.tsx +324 -0
  61. package/src/dist/components/ui/input.tsx +25 -0
  62. package/src/dist/components/ui/interactive-hover-button.tsx +35 -0
  63. package/src/dist/components/ui/popover.tsx +33 -0
  64. package/src/dist/components/ui/scroll-area.tsx +48 -0
  65. package/src/dist/components/ui/separator.tsx +30 -0
  66. package/src/dist/components/ui/sheet.tsx +140 -0
  67. package/src/dist/components/ui/shine-border.tsx +64 -0
  68. package/src/dist/components/ui/skeleton.tsx +15 -0
  69. package/src/dist/components/ui/sonner.tsx +31 -0
  70. package/src/dist/components/ui/table.tsx +117 -0
  71. package/src/dist/components/ui/tabs.tsx +55 -0
  72. package/src/dist/components/ui/toggle-group.tsx +61 -0
  73. package/src/dist/components/ui/toggle.tsx +46 -0
  74. package/src/dist/components.json +17 -0
  75. package/src/dist/contents/docs/getting-started/changelog/index.mdx +512 -0
  76. package/src/dist/contents/docs/getting-started/components/accordion/index.mdx +72 -0
  77. package/src/dist/contents/docs/getting-started/components/button/index.mdx +42 -0
  78. package/src/dist/contents/docs/getting-started/components/card/index.mdx +70 -0
  79. package/src/dist/contents/docs/getting-started/components/card-group/index.mdx +49 -0
  80. package/src/dist/contents/docs/getting-started/components/code-block/index.mdx +41 -0
  81. package/src/dist/contents/docs/getting-started/components/custom/index.mdx +38 -0
  82. package/src/dist/contents/docs/getting-started/components/image/index.mdx +37 -0
  83. package/src/dist/contents/docs/getting-started/components/index.mdx +9 -0
  84. package/src/dist/contents/docs/getting-started/components/keyboard/index.mdx +117 -0
  85. package/src/dist/contents/docs/getting-started/components/link/index.mdx +34 -0
  86. package/src/dist/contents/docs/getting-started/components/note/index.mdx +46 -0
  87. package/src/dist/contents/docs/getting-started/components/release-note/index.mdx +130 -0
  88. package/src/dist/contents/docs/getting-started/components/stepper/index.mdx +47 -0
  89. package/src/dist/contents/docs/getting-started/components/tabs/index.mdx +70 -0
  90. package/src/dist/contents/docs/getting-started/components/tooltips/index.mdx +22 -0
  91. package/src/dist/contents/docs/getting-started/components/youtube/index.mdx +21 -0
  92. package/src/dist/contents/docs/getting-started/customize/index.mdx +94 -0
  93. package/src/dist/contents/docs/getting-started/installation/index.mdx +84 -0
  94. package/src/dist/contents/docs/getting-started/introduction/index.mdx +50 -0
  95. package/src/dist/contents/docs/getting-started/project-structure/index.mdx +87 -0
  96. package/src/dist/contents/docs/getting-started/quick-start-guide/index.mdx +127 -0
  97. package/src/dist/docu.json +100 -0
  98. package/src/dist/hooks/index.ts +2 -0
  99. package/src/dist/hooks/useActiveSection.ts +68 -0
  100. package/src/dist/hooks/useScrollPosition.ts +28 -0
  101. package/src/dist/lib/markdown.ts +244 -0
  102. package/src/dist/lib/routes-config.ts +28 -0
  103. package/src/dist/lib/toc.ts +9 -0
  104. package/src/dist/lib/utils.ts +80 -0
  105. package/src/dist/next-env.d.ts +5 -0
  106. package/src/dist/next.config.mjs +14 -0
  107. package/src/dist/package.json +58 -0
  108. package/src/dist/postcss.config.js +6 -0
  109. package/src/dist/public/favicon.ico +0 -0
  110. package/src/dist/public/images/docu.svg +6 -0
  111. package/src/dist/public/images/example-img.png +0 -0
  112. package/src/dist/public/images/img-playground.png +0 -0
  113. package/src/dist/public/images/new-editor.png +0 -0
  114. package/src/dist/public/images/og-image.png +0 -0
  115. package/src/dist/public/images/release-note.png +0 -0
  116. package/src/dist/public/images/snippet.png +0 -0
  117. package/src/dist/public/images/vercel.png +0 -0
  118. package/src/dist/public/images/view-changelog.png +0 -0
  119. package/src/dist/styles/editor.css +57 -0
  120. package/src/dist/styles/globals.css +156 -0
  121. package/src/dist/styles/syntax.css +100 -0
  122. package/src/dist/tailwind.config.ts +112 -0
  123. package/src/dist/tsconfig.json +26 -0
  124. package/src/index.js +19 -0
  125. package/src/installer/projectInstaller.js +125 -0
  126. package/src/utils/display.js +83 -0
  127. package/src/utils/logger.js +11 -0
  128. package/src/utils/packageManager.js +54 -0
  129. 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
+ }