@docubook/create 2.5.0 → 2.5.1
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/package.json +1 -1
- package/src/dist/app/docs/[[...slug]]/page.tsx +3 -3
- package/src/dist/components/DocSearch.tsx +16 -15
- package/src/dist/components/DocsSidebar.tsx +23 -12
- package/src/dist/components/SearchBox.tsx +11 -9
- package/src/dist/components/SearchModal.tsx +77 -79
- package/src/dist/components/SearchTrigger.tsx +20 -15
- package/src/dist/components/leftbar.tsx +2 -2
- package/src/dist/components/markdown/AccordionContext.tsx +21 -0
- package/src/dist/components/markdown/AccordionGroupMdx.tsx +11 -22
- package/src/dist/components/markdown/AccordionMdx.tsx +58 -59
- package/src/dist/components/markdown/PreMdx.tsx +2 -2
- package/src/dist/components/navbar.tsx +3 -3
- package/src/dist/components/typography.tsx +1 -1
- package/src/dist/docs/components/accordion-group.mdx +13 -12
- package/src/dist/docs/components/accordion.mdx +11 -14
- package/src/dist/docu.json +3 -0
- package/src/dist/hooks/useActiveSection.ts +14 -4
- package/src/dist/lib/search/algolia.ts +5 -0
- package/src/dist/lib/search/built-in.ts +43 -0
- package/src/dist/lib/search/config.ts +7 -0
- package/src/dist/lib/toc.ts +1 -0
- package/src/dist/lib/utils.ts +13 -54
- package/src/dist/package.json +1 -1
- package/src/dist/styles/algolia.css +3 -28
- package/src/dist/lib/accordion-context.ts +0 -4
package/package.json
CHANGED
|
@@ -79,10 +79,10 @@ export default async function DocsPage(props: PageProps) {
|
|
|
79
79
|
const tocs = await getDocsTocs(pathName)
|
|
80
80
|
|
|
81
81
|
return (
|
|
82
|
-
<div className="flex w-full flex-1 px-0 pb-4 lg:px-8 lg:pb-8 h-[calc(100vh-4rem)]">
|
|
83
|
-
<div id="scroll-container" className="max-lg:scroll-p-
|
|
82
|
+
<div className="flex w-full flex-1 px-0 pb-4 lg:px-8 lg:pb-8 lg:h-[calc(100vh-4rem)]">
|
|
83
|
+
<div id="scroll-container" className="max-lg:scroll-p-54 bg-card dark:bg-card/20 border-muted-foreground/20 flex w-full flex-col items-start lg:h-full lg:rounded-xl rounded-b-3xl border shadow-md backdrop-blur-sm lg:flex-row lg:overflow-y-auto relative">
|
|
84
84
|
<div className="flex-7 w-full min-w-0 px-4 py-4 lg:px-8 lg:py-8">
|
|
85
|
-
<MobToc tocs={tocs} />
|
|
85
|
+
<MobToc tocs={tocs} title={title} />
|
|
86
86
|
<DocsBreadcrumb paths={slug} />
|
|
87
87
|
<Typography>
|
|
88
88
|
<h1 className="-mt-0.5! text-3xl">{title}</h1>
|
|
@@ -1,26 +1,27 @@
|
|
|
1
|
-
"use client"
|
|
1
|
+
"use client"
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
3
|
+
import { DocSearch } from "@docsearch/react"
|
|
4
|
+
import { algoliaConfig } from "@/lib/search/algolia"
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
interface AlgoliaSearchProps {
|
|
8
|
+
className?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function AlgoliaSearch({ className }: AlgoliaSearchProps) {
|
|
12
|
+
const { appId, apiKey, indexName } = algoliaConfig
|
|
10
13
|
|
|
11
14
|
if (!appId || !apiKey || !indexName) {
|
|
12
|
-
console.error(
|
|
13
|
-
"DocSearch credentials are not set in the environment variables."
|
|
14
|
-
);
|
|
15
|
+
console.error("DocSearch credentials are not set in the environment variables.")
|
|
15
16
|
return (
|
|
16
|
-
<button className="text-
|
|
17
|
+
<button className="text-muted-foreground text-sm" disabled>
|
|
17
18
|
Search... (misconfigured)
|
|
18
19
|
</button>
|
|
19
|
-
)
|
|
20
|
+
)
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
return (
|
|
23
|
-
<div className="docsearch">
|
|
24
|
+
<div className={cn("docsearch", className)}>
|
|
24
25
|
<DocSearch
|
|
25
26
|
appId={appId}
|
|
26
27
|
apiKey={apiKey}
|
|
@@ -28,5 +29,5 @@ export default function DocSearchComponent() {
|
|
|
28
29
|
placeholder="Type something to search..."
|
|
29
30
|
/>
|
|
30
31
|
</div>
|
|
31
|
-
)
|
|
32
|
-
}
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
|
|
26
26
|
interface MobTocProps {
|
|
27
27
|
tocs: TocItem[]
|
|
28
|
+
title?: string
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
const useClickOutside = (ref: React.RefObject<HTMLElement | null>, callback: () => void) => {
|
|
@@ -45,7 +46,7 @@ const useClickOutside = (ref: React.RefObject<HTMLElement | null>, callback: ()
|
|
|
45
46
|
}, [handleClick])
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
export default function MobToc({ tocs }: MobTocProps) {
|
|
49
|
+
export default function MobToc({ tocs, title }: MobTocProps) {
|
|
49
50
|
const pathname = usePathname()
|
|
50
51
|
const [isExpanded, setIsExpanded] = React.useState(false)
|
|
51
52
|
const tocRef = useRef<HTMLDivElement>(null)
|
|
@@ -57,8 +58,12 @@ export default function MobToc({ tocs }: MobTocProps) {
|
|
|
57
58
|
// Only show on /docs pages
|
|
58
59
|
const isDocsPage = useMemo(() => pathname?.startsWith("/docs"), [pathname])
|
|
59
60
|
|
|
60
|
-
// Get title from
|
|
61
|
-
const
|
|
61
|
+
// Get title from active section if available, otherwise document title
|
|
62
|
+
const activeSection = useMemo(() => {
|
|
63
|
+
return tocs.find((toc) => toc.href.slice(1) === activeId)
|
|
64
|
+
}, [tocs, activeId])
|
|
65
|
+
|
|
66
|
+
const displayTitle = activeSection?.text || title || "On this page"
|
|
62
67
|
|
|
63
68
|
const [mounted, setMounted] = React.useState(false)
|
|
64
69
|
|
|
@@ -111,16 +116,24 @@ export default function MobToc({ tocs }: MobTocProps) {
|
|
|
111
116
|
exit={{ y: -100, opacity: 0 }}
|
|
112
117
|
transition={{ duration: 0.2, ease: "easeInOut" }}
|
|
113
118
|
>
|
|
114
|
-
<div className="bg-background/95 w-full border-b
|
|
119
|
+
<div className="bg-background/95 border-muted dark:border-foreground/10 dark:bg-background w-full border-b shadow-sm backdrop-blur-sm">
|
|
115
120
|
<div className="p-2">
|
|
116
121
|
<div className="flex items-center gap-2">
|
|
117
122
|
<DropdownMenu>
|
|
118
123
|
<DropdownMenuTrigger asChild>
|
|
119
|
-
<Button
|
|
120
|
-
|
|
124
|
+
<Button
|
|
125
|
+
variant="ghost"
|
|
126
|
+
size="icon"
|
|
127
|
+
className="h-8 w-8 shrink-0"
|
|
128
|
+
aria-label="Navigation menu"
|
|
129
|
+
>
|
|
130
|
+
<MoreVertical className="text-muted-foreground h-5 w-5" />
|
|
121
131
|
</Button>
|
|
122
132
|
</DropdownMenuTrigger>
|
|
123
|
-
<DropdownMenuContent
|
|
133
|
+
<DropdownMenuContent
|
|
134
|
+
align="start"
|
|
135
|
+
className="flex min-w-[160px] flex-col gap-1 p-2"
|
|
136
|
+
>
|
|
124
137
|
<NavMenu />
|
|
125
138
|
</DropdownMenuContent>
|
|
126
139
|
</DropdownMenu>
|
|
@@ -132,9 +145,7 @@ export default function MobToc({ tocs }: MobTocProps) {
|
|
|
132
145
|
aria-label={isExpanded ? "Collapse table of contents" : "Expand table of contents"}
|
|
133
146
|
>
|
|
134
147
|
<div className="flex items-center gap-2">
|
|
135
|
-
<span className="text-sm font-medium capitalize">
|
|
136
|
-
{pageTitle || "On this page"}
|
|
137
|
-
</span>
|
|
148
|
+
<span className="text-sm font-medium capitalize">{displayTitle}</span>
|
|
138
149
|
</div>
|
|
139
150
|
{chevronIcon}
|
|
140
151
|
</Button>
|
|
@@ -142,10 +153,10 @@ export default function MobToc({ tocs }: MobTocProps) {
|
|
|
142
153
|
<Sheet>
|
|
143
154
|
<SheetTrigger asChild>
|
|
144
155
|
<Button variant="ghost" size="icon" className="hidden max-lg:flex">
|
|
145
|
-
<PanelRight className="h-6 w-6 shrink-0
|
|
156
|
+
<PanelRight className="text-muted-foreground h-6 w-6 shrink-0" />
|
|
146
157
|
</Button>
|
|
147
158
|
</SheetTrigger>
|
|
148
|
-
<SheetContent className="w-full flex
|
|
159
|
+
<SheetContent className="flex w-full flex-col gap-4 px-0 lg:w-auto" side="right">
|
|
149
160
|
<DialogTitle className="sr-only">Navigation Menu</DialogTitle>
|
|
150
161
|
<DialogDescription className="sr-only">
|
|
151
162
|
Main navigation menu with links to different sections
|
|
@@ -3,32 +3,34 @@
|
|
|
3
3
|
import { Dialog } from "@/components/ui/dialog"
|
|
4
4
|
import { SearchTrigger } from "@/components/SearchTrigger"
|
|
5
5
|
import { SearchModal } from "@/components/SearchModal"
|
|
6
|
-
import
|
|
6
|
+
import AlgoliaSearch from "@/components/DocSearch"
|
|
7
7
|
import { useSearch } from "./SearchContext"
|
|
8
8
|
import { DialogTrigger } from "@/components/ui/dialog"
|
|
9
|
+
import { searchConfig } from "@/lib/search/config"
|
|
9
10
|
|
|
10
11
|
interface SearchProps {
|
|
11
12
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
* Override the search type from config.
|
|
14
|
+
* If not provided, uses the config value.
|
|
14
15
|
*/
|
|
15
16
|
type?: "default" | "algolia"
|
|
17
|
+
className?: string
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
export default function Search({ type
|
|
20
|
+
export default function Search({ type, className }: SearchProps) {
|
|
19
21
|
const { isOpen, setIsOpen } = useSearch()
|
|
22
|
+
const searchType = type ?? searchConfig.type
|
|
20
23
|
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
return <DocSearchComponent />
|
|
24
|
+
if (searchType === "algolia") {
|
|
25
|
+
return <AlgoliaSearch className={className} />
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
// Logic for 'default' search
|
|
27
29
|
return (
|
|
28
|
-
<div>
|
|
30
|
+
<div className={className}>
|
|
29
31
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
30
32
|
<DialogTrigger asChild>
|
|
31
|
-
<SearchTrigger />
|
|
33
|
+
<SearchTrigger className={className} />
|
|
32
34
|
</DialogTrigger>
|
|
33
35
|
<SearchModal isOpen={isOpen} setIsOpen={setIsOpen} />
|
|
34
36
|
</Dialog>
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { useRouter } from "next/navigation"
|
|
4
|
-
import { useEffect, useMemo, useState, useRef } from "react"
|
|
5
|
-
import { ArrowUpIcon, ArrowDownIcon, CornerDownLeftIcon, FileTextIcon } from "lucide-react"
|
|
6
|
-
import Anchor from "./anchor"
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useRouter } from "next/navigation"
|
|
4
|
+
import { useEffect, useMemo, useState, useRef } from "react"
|
|
5
|
+
import { ArrowUpIcon, ArrowDownIcon, CornerDownLeftIcon, FileTextIcon } from "lucide-react"
|
|
6
|
+
import Anchor from "./anchor"
|
|
7
|
+
import { cn } from "@/lib/utils"
|
|
8
|
+
import { advanceSearch } from "@/lib/search/built-in"
|
|
9
|
+
import { ScrollArea } from "@/components/ui/scroll-area"
|
|
10
|
+
import { page_routes } from "@/lib/routes"
|
|
10
11
|
import {
|
|
11
12
|
DialogContent,
|
|
12
13
|
DialogHeader,
|
|
@@ -14,63 +15,63 @@ import {
|
|
|
14
15
|
DialogClose,
|
|
15
16
|
DialogTitle,
|
|
16
17
|
DialogDescription,
|
|
17
|
-
} from "@/components/ui/dialog"
|
|
18
|
+
} from "@/components/ui/dialog"
|
|
18
19
|
|
|
19
20
|
type ContextInfo = {
|
|
20
|
-
icon: string
|
|
21
|
-
description: string
|
|
22
|
-
title?: string
|
|
23
|
-
}
|
|
21
|
+
icon: string
|
|
22
|
+
description: string
|
|
23
|
+
title?: string
|
|
24
|
+
}
|
|
24
25
|
|
|
25
26
|
type SearchResult = {
|
|
26
|
-
title: string
|
|
27
|
-
href: string
|
|
28
|
-
noLink?: boolean
|
|
29
|
-
items?: undefined
|
|
30
|
-
score?: number
|
|
31
|
-
context?: ContextInfo
|
|
32
|
-
}
|
|
27
|
+
title: string
|
|
28
|
+
href: string
|
|
29
|
+
noLink?: boolean
|
|
30
|
+
items?: undefined
|
|
31
|
+
score?: number
|
|
32
|
+
context?: ContextInfo
|
|
33
|
+
}
|
|
33
34
|
|
|
34
35
|
const paddingMap = {
|
|
35
36
|
1: "pl-2",
|
|
36
37
|
2: "pl-4",
|
|
37
38
|
3: "pl-10",
|
|
38
|
-
} as const
|
|
39
|
+
} as const
|
|
39
40
|
|
|
40
41
|
interface SearchModalProps {
|
|
41
|
-
isOpen: boolean
|
|
42
|
-
setIsOpen: (open: boolean) => void
|
|
42
|
+
isOpen: boolean
|
|
43
|
+
setIsOpen: (open: boolean) => void
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
|
|
46
|
-
const router = useRouter()
|
|
47
|
-
const [searchedInput, setSearchedInput] = useState("")
|
|
48
|
-
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
49
|
-
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
|
|
47
|
+
const router = useRouter()
|
|
48
|
+
const [searchedInput, setSearchedInput] = useState("")
|
|
49
|
+
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
50
|
+
const itemRefs = useRef<(HTMLDivElement | null)[]>([])
|
|
50
51
|
|
|
51
52
|
useEffect(() => {
|
|
52
53
|
if (!isOpen) {
|
|
53
54
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
54
|
-
setSearchedInput("")
|
|
55
|
+
setSearchedInput("")
|
|
55
56
|
}
|
|
56
|
-
}, [isOpen])
|
|
57
|
+
}, [isOpen])
|
|
57
58
|
|
|
58
59
|
const filteredResults = useMemo<SearchResult[]>(() => {
|
|
59
|
-
const trimmedInput = searchedInput.trim()
|
|
60
|
+
const trimmedInput = searchedInput.trim()
|
|
60
61
|
|
|
61
62
|
if (trimmedInput.length < 3) {
|
|
62
63
|
return page_routes
|
|
63
|
-
.filter((route) => !route.href.endsWith(
|
|
64
|
+
.filter((route) => !route.href.endsWith("/"))
|
|
64
65
|
.slice(0, 6)
|
|
65
66
|
.map((route: { title: string; href: string; noLink?: boolean; context?: ContextInfo }) => ({
|
|
66
67
|
title: route.title,
|
|
67
68
|
href: route.href,
|
|
68
69
|
noLink: route.noLink,
|
|
69
70
|
context: route.context,
|
|
70
|
-
}))
|
|
71
|
+
}))
|
|
71
72
|
}
|
|
72
|
-
return advanceSearch(trimmedInput) as unknown as SearchResult[]
|
|
73
|
-
}, [searchedInput])
|
|
73
|
+
return advanceSearch(trimmedInput) as unknown as SearchResult[]
|
|
74
|
+
}, [searchedInput])
|
|
74
75
|
|
|
75
76
|
// useEffect(() => {
|
|
76
77
|
// setSelectedIndex(0);
|
|
@@ -78,39 +79,39 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
|
|
|
78
79
|
|
|
79
80
|
useEffect(() => {
|
|
80
81
|
const handleNavigation = (event: KeyboardEvent) => {
|
|
81
|
-
if (!isOpen || filteredResults.length === 0) return
|
|
82
|
+
if (!isOpen || filteredResults.length === 0) return
|
|
82
83
|
|
|
83
84
|
if (event.key === "ArrowDown") {
|
|
84
|
-
event.preventDefault()
|
|
85
|
-
setSelectedIndex((prev) => (prev + 1) % filteredResults.length)
|
|
85
|
+
event.preventDefault()
|
|
86
|
+
setSelectedIndex((prev) => (prev + 1) % filteredResults.length)
|
|
86
87
|
} else if (event.key === "ArrowUp") {
|
|
87
|
-
event.preventDefault()
|
|
88
|
-
setSelectedIndex((prev) => (prev - 1 + filteredResults.length) % filteredResults.length)
|
|
88
|
+
event.preventDefault()
|
|
89
|
+
setSelectedIndex((prev) => (prev - 1 + filteredResults.length) % filteredResults.length)
|
|
89
90
|
} else if (event.key === "Enter") {
|
|
90
|
-
event.preventDefault()
|
|
91
|
-
const selectedItem = filteredResults[selectedIndex]
|
|
91
|
+
event.preventDefault()
|
|
92
|
+
const selectedItem = filteredResults[selectedIndex]
|
|
92
93
|
if (selectedItem) {
|
|
93
|
-
router.push(`/docs${selectedItem.href}`)
|
|
94
|
-
setIsOpen(false)
|
|
94
|
+
router.push(`/docs${selectedItem.href}`)
|
|
95
|
+
setIsOpen(false)
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
|
-
}
|
|
98
|
+
}
|
|
98
99
|
|
|
99
|
-
window.addEventListener("keydown", handleNavigation)
|
|
100
|
-
return () => window.removeEventListener("keydown", handleNavigation)
|
|
101
|
-
}, [isOpen, filteredResults, selectedIndex, router, setIsOpen])
|
|
100
|
+
window.addEventListener("keydown", handleNavigation)
|
|
101
|
+
return () => window.removeEventListener("keydown", handleNavigation)
|
|
102
|
+
}, [isOpen, filteredResults, selectedIndex, router, setIsOpen])
|
|
102
103
|
|
|
103
104
|
useEffect(() => {
|
|
104
105
|
if (itemRefs.current[selectedIndex]) {
|
|
105
106
|
itemRefs.current[selectedIndex]?.scrollIntoView({
|
|
106
107
|
behavior: "smooth",
|
|
107
108
|
block: "nearest",
|
|
108
|
-
})
|
|
109
|
+
})
|
|
109
110
|
}
|
|
110
|
-
}, [selectedIndex])
|
|
111
|
+
}, [selectedIndex])
|
|
111
112
|
|
|
112
113
|
return (
|
|
113
|
-
<DialogContent className="
|
|
114
|
+
<DialogContent className="rounded-md! top-[45%] max-w-[650px] p-0 sm:top-[38%]">
|
|
114
115
|
<DialogHeader>
|
|
115
116
|
<DialogTitle className="sr-only">Search Documentation</DialogTitle>
|
|
116
117
|
<DialogDescription className="sr-only">Search through the documentation</DialogDescription>
|
|
@@ -119,36 +120,35 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
|
|
|
119
120
|
<input
|
|
120
121
|
value={searchedInput}
|
|
121
122
|
onChange={(e) => {
|
|
122
|
-
setSearchedInput(e.target.value)
|
|
123
|
-
setSelectedIndex(0)
|
|
123
|
+
setSearchedInput(e.target.value)
|
|
124
|
+
setSelectedIndex(0)
|
|
124
125
|
}}
|
|
125
126
|
placeholder="Type something to search..."
|
|
126
127
|
autoFocus
|
|
127
|
-
className="h-14
|
|
128
|
+
className="h-14 w-full border-b bg-transparent px-6 text-[14px] outline-none"
|
|
128
129
|
aria-label="Search documentation"
|
|
129
130
|
/>
|
|
130
131
|
|
|
131
132
|
{filteredResults.length == 0 && searchedInput && (
|
|
132
133
|
<p className="text-muted-foreground mx-auto mt-2 text-sm">
|
|
133
|
-
No results found for{"
|
|
134
|
-
<span className="text-primary">{`"${searchedInput}"`}</span>
|
|
134
|
+
No results found for <span className="text-primary">{`"${searchedInput}"`}</span>
|
|
135
135
|
</p>
|
|
136
136
|
)}
|
|
137
137
|
<ScrollArea className="max-h-[400px] overflow-y-auto">
|
|
138
|
-
<div className="flex flex-col items-start overflow-y-auto
|
|
138
|
+
<div className="flex flex-col items-start overflow-y-auto px-1 pb-4 sm:px-2">
|
|
139
139
|
{filteredResults.map((item, index) => {
|
|
140
|
-
const level = (item.href.split("/").slice(1).length - 1) as keyof typeof paddingMap
|
|
141
|
-
const paddingClass = paddingMap[level] ||
|
|
142
|
-
const isActive = index === selectedIndex
|
|
140
|
+
const level = (item.href.split("/").slice(1).length - 1) as keyof typeof paddingMap
|
|
141
|
+
const paddingClass = paddingMap[level] || "pl-2"
|
|
142
|
+
const isActive = index === selectedIndex
|
|
143
143
|
|
|
144
144
|
return (
|
|
145
145
|
<DialogClose key={item.href} asChild>
|
|
146
146
|
<Anchor
|
|
147
147
|
ref={(el) => {
|
|
148
|
-
itemRefs.current[index] = el as HTMLDivElement | null
|
|
148
|
+
itemRefs.current[index] = el as HTMLDivElement | null
|
|
149
149
|
}}
|
|
150
150
|
className={cn(
|
|
151
|
-
"dark:hover:bg-accent/15 hover:bg-accent/10 w-full
|
|
151
|
+
"dark:hover:bg-accent/15 hover:bg-accent/10 flex w-full items-center gap-2.5 rounded-sm px-3 text-sm",
|
|
152
152
|
isActive && "bg-primary/20 dark:bg-primary/30",
|
|
153
153
|
paddingClass
|
|
154
154
|
)}
|
|
@@ -157,46 +157,44 @@ export function SearchModal({ isOpen, setIsOpen }: SearchModalProps) {
|
|
|
157
157
|
>
|
|
158
158
|
<div
|
|
159
159
|
className={cn(
|
|
160
|
-
"flex
|
|
160
|
+
"flex h-full w-full items-center justify-between gap-1.5 px-2 py-3",
|
|
161
161
|
level > 1 && "border-l pl-4"
|
|
162
162
|
)}
|
|
163
163
|
>
|
|
164
164
|
<div className="flex items-center">
|
|
165
|
-
<FileTextIcon className="h-[1.1rem] w-[1.1rem]
|
|
165
|
+
<FileTextIcon className="mr-1 h-[1.1rem] w-[1.1rem]" />
|
|
166
166
|
<span>{item.title}</span>
|
|
167
167
|
</div>
|
|
168
168
|
{isActive && (
|
|
169
|
-
<div className="hidden
|
|
169
|
+
<div className="text-muted-foreground hidden items-center text-xs md:flex">
|
|
170
170
|
<span>Return</span>
|
|
171
|
-
<CornerDownLeftIcon className="h-3 w-3
|
|
171
|
+
<CornerDownLeftIcon className="ml-1 h-3 w-3" />
|
|
172
172
|
</div>
|
|
173
173
|
)}
|
|
174
174
|
</div>
|
|
175
175
|
</Anchor>
|
|
176
176
|
</DialogClose>
|
|
177
|
-
)
|
|
177
|
+
)
|
|
178
178
|
})}
|
|
179
179
|
</div>
|
|
180
180
|
</ScrollArea>
|
|
181
|
-
<DialogFooter className="
|
|
181
|
+
<DialogFooter className="hidden h-14 border-t bg-transparent px-6 text-[14px] outline-none md:flex md:justify-start">
|
|
182
182
|
<div className="flex items-center gap-2">
|
|
183
|
-
<span className="dark:bg-accent/15 bg-slate-200
|
|
184
|
-
<ArrowUpIcon className="
|
|
183
|
+
<span className="dark:bg-accent/15 rounded border bg-slate-200 p-2">
|
|
184
|
+
<ArrowUpIcon className="h-3 w-3" />
|
|
185
185
|
</span>
|
|
186
|
-
<span className="dark:bg-accent/15 bg-slate-200
|
|
187
|
-
<ArrowDownIcon className="
|
|
186
|
+
<span className="dark:bg-accent/15 rounded border bg-slate-200 p-2">
|
|
187
|
+
<ArrowDownIcon className="h-3 w-3" />
|
|
188
188
|
</span>
|
|
189
189
|
<p className="text-muted-foreground">to navigate</p>
|
|
190
|
-
<span className="dark:bg-accent/15 bg-slate-200
|
|
191
|
-
<CornerDownLeftIcon className="
|
|
190
|
+
<span className="dark:bg-accent/15 rounded border bg-slate-200 p-2">
|
|
191
|
+
<CornerDownLeftIcon className="h-3 w-3" />
|
|
192
192
|
</span>
|
|
193
193
|
<p className="text-muted-foreground">to select</p>
|
|
194
|
-
<span className="dark:bg-accent/15 bg-slate-200
|
|
195
|
-
esc
|
|
196
|
-
</span>
|
|
194
|
+
<span className="dark:bg-accent/15 rounded border bg-slate-200 px-2 py-1">esc</span>
|
|
197
195
|
<p className="text-muted-foreground">to close</p>
|
|
198
196
|
</div>
|
|
199
197
|
</DialogFooter>
|
|
200
198
|
</DialogContent>
|
|
201
|
-
)
|
|
202
|
-
}
|
|
199
|
+
)
|
|
200
|
+
}
|
|
@@ -1,31 +1,36 @@
|
|
|
1
|
-
"use client"
|
|
1
|
+
"use client"
|
|
2
2
|
|
|
3
|
-
import { CommandIcon, SearchIcon } from "lucide-react"
|
|
4
|
-
import { DialogTrigger } from "@/components/ui/dialog"
|
|
5
|
-
import { Input } from "@/components/ui/input"
|
|
3
|
+
import { CommandIcon, SearchIcon } from "lucide-react"
|
|
4
|
+
import { DialogTrigger } from "@/components/ui/dialog"
|
|
5
|
+
import { Input } from "@/components/ui/input"
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
interface SearchTriggerProps {
|
|
9
|
+
className?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function SearchTrigger({ className }: SearchTriggerProps) {
|
|
8
13
|
return (
|
|
9
14
|
<DialogTrigger asChild>
|
|
10
|
-
<div className="relative flex-1 cursor-pointer">
|
|
15
|
+
<div className={cn("relative flex-1 cursor-pointer", className)}>
|
|
11
16
|
<div className="flex items-center">
|
|
12
|
-
<div className="
|
|
13
|
-
<SearchIcon className="h-6 w-6
|
|
17
|
+
<div className="-ml-2 block p-2 lg:hidden">
|
|
18
|
+
<SearchIcon className="text-muted-foreground h-6 w-6" />
|
|
14
19
|
</div>
|
|
15
|
-
<div className="hidden lg:block
|
|
16
|
-
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2
|
|
20
|
+
<div className="hidden w-full lg:block">
|
|
21
|
+
<SearchIcon className="text-muted-foreground absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2" />
|
|
17
22
|
<Input
|
|
18
|
-
className="
|
|
23
|
+
className="dark:bg-background/95 bg-background h-9 w-full overflow-ellipsis rounded-full border pl-10 pr-0 text-sm shadow-sm sm:pr-4"
|
|
19
24
|
placeholder="Search"
|
|
20
25
|
readOnly // This input is for display only
|
|
21
26
|
/>
|
|
22
|
-
<div className="
|
|
23
|
-
<CommandIcon className="
|
|
27
|
+
<div className="dark:bg-accent bg-accent absolute right-2 top-1/2 flex -translate-y-1/2 items-center gap-0.5 rounded-full px-2 py-0.5 font-mono text-xs font-medium text-white">
|
|
28
|
+
<CommandIcon className="h-3 w-3" />
|
|
24
29
|
<span>K</span>
|
|
25
30
|
</div>
|
|
26
31
|
</div>
|
|
27
32
|
</div>
|
|
28
33
|
</div>
|
|
29
34
|
</DialogTrigger>
|
|
30
|
-
)
|
|
31
|
-
}
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -20,7 +20,7 @@ export function Leftbar() {
|
|
|
20
20
|
</div>
|
|
21
21
|
|
|
22
22
|
<div className="flex shrink-0 items-center gap-2 px-4 pb-4">
|
|
23
|
-
<Search />
|
|
23
|
+
<Search className="min-w-[250px] max-w-[250px]" />
|
|
24
24
|
</div>
|
|
25
25
|
|
|
26
26
|
{/* Scrollable Navigation */}
|
|
@@ -44,7 +44,7 @@ export function SheetLeftbar() {
|
|
|
44
44
|
<Sheet>
|
|
45
45
|
<SheetTrigger asChild>
|
|
46
46
|
<Button variant="ghost" size="icon" className="hidden max-md:flex">
|
|
47
|
-
<PanelRight className="h-6 w-6 shrink-0
|
|
47
|
+
<PanelRight className="text-muted-foreground h-6 w-6 shrink-0" />
|
|
48
48
|
</Button>
|
|
49
49
|
</SheetTrigger>
|
|
50
50
|
<SheetContent className="flex flex-col gap-4 px-0" side="right">
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createContext, useState, useId } from "react"
|
|
2
|
+
|
|
3
|
+
type AccordionGroupContextType = {
|
|
4
|
+
inGroup: boolean
|
|
5
|
+
groupId: string
|
|
6
|
+
openTitle: string | null
|
|
7
|
+
setOpenTitle: (title: string | null) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const AccordionGroupContext = createContext<AccordionGroupContextType | null>(null)
|
|
11
|
+
|
|
12
|
+
export function AccordionGroupProvider({ children }: { children: React.ReactNode }) {
|
|
13
|
+
const [openTitle, setOpenTitle] = useState<string | null>(null)
|
|
14
|
+
const groupId = useId()
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<AccordionGroupContext.Provider value={{ inGroup: true, groupId, openTitle, setOpenTitle }}>
|
|
18
|
+
{children}
|
|
19
|
+
</AccordionGroupContext.Provider>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -1,31 +1,20 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import React, { ReactNode } from "react"
|
|
4
|
-
import clsx from "clsx"
|
|
5
|
-
import {
|
|
3
|
+
import React, { ReactNode } from "react"
|
|
4
|
+
import clsx from "clsx"
|
|
5
|
+
import { AccordionGroupProvider } from "@/components/markdown/AccordionContext"
|
|
6
6
|
|
|
7
7
|
interface AccordionGroupProps {
|
|
8
|
-
children: ReactNode
|
|
9
|
-
className?: string
|
|
8
|
+
children: ReactNode
|
|
9
|
+
className?: string
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
const AccordionGroup: React.FC<AccordionGroupProps> = ({ children, className }) => {
|
|
13
|
-
|
|
14
13
|
return (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
className={clsx(
|
|
21
|
-
"border rounded-lg overflow-hidden",
|
|
22
|
-
className
|
|
23
|
-
)}
|
|
24
|
-
>
|
|
25
|
-
{children}
|
|
26
|
-
</div>
|
|
27
|
-
</AccordionGroupContext.Provider>
|
|
28
|
-
);
|
|
29
|
-
};
|
|
14
|
+
<AccordionGroupProvider>
|
|
15
|
+
<div className={clsx("overflow-hidden rounded-lg border", className)}>{children}</div>
|
|
16
|
+
</AccordionGroupProvider>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
30
19
|
|
|
31
|
-
export default AccordionGroup
|
|
20
|
+
export default AccordionGroup
|
|
@@ -1,62 +1,61 @@
|
|
|
1
|
-
"use client"
|
|
1
|
+
"use client"
|
|
2
2
|
|
|
3
|
-
import { ReactNode,
|
|
4
|
-
import { ChevronRight } from
|
|
5
|
-
import * as Icons from "lucide-react"
|
|
6
|
-
import { cn } from
|
|
7
|
-
import { AccordionGroupContext } from
|
|
3
|
+
import { ReactNode, useContext, useState } from "react"
|
|
4
|
+
import { ChevronRight } from "lucide-react"
|
|
5
|
+
import * as Icons from "lucide-react"
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
import { AccordionGroupContext } from "@/components/markdown/AccordionContext"
|
|
8
8
|
|
|
9
9
|
type AccordionProps = {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
export default Accordion;
|
|
10
|
+
title: string
|
|
11
|
+
children?: ReactNode
|
|
12
|
+
icon?: keyof typeof Icons
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const Accordion: React.FC<AccordionProps> = ({ title, children, icon }: AccordionProps) => {
|
|
16
|
+
const groupContext = useContext(AccordionGroupContext)
|
|
17
|
+
const isInGroup = groupContext?.inGroup === true
|
|
18
|
+
const groupOpen = groupContext?.openTitle === title
|
|
19
|
+
const setGroupOpen = groupContext?.setOpenTitle
|
|
20
|
+
const [localOpen, setLocalOpen] = useState(false)
|
|
21
|
+
|
|
22
|
+
const isOpen = isInGroup ? groupOpen : localOpen
|
|
23
|
+
|
|
24
|
+
const handleToggle = () => {
|
|
25
|
+
if (isInGroup && setGroupOpen) {
|
|
26
|
+
setGroupOpen(groupOpen ? null : title)
|
|
27
|
+
} else {
|
|
28
|
+
setLocalOpen(!localOpen)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const Icon = icon ? (Icons[icon] as React.FC<{ className?: string }>) : null
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
className={cn(
|
|
37
|
+
!isInGroup && "rounded-lg border shadow-sm",
|
|
38
|
+
isInGroup && "border-border border-b last:border-b-0"
|
|
39
|
+
)}
|
|
40
|
+
>
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
onClick={handleToggle}
|
|
44
|
+
className="bg-muted/40 dark:bg-muted/20 hover:bg-muted/70 dark:hover:bg-muted/70 flex w-full cursor-pointer items-center gap-2 px-4 py-3 text-start transition-colors"
|
|
45
|
+
>
|
|
46
|
+
<ChevronRight
|
|
47
|
+
className={cn(
|
|
48
|
+
"text-muted-foreground h-4 w-4 shrink-0 transition-transform duration-200",
|
|
49
|
+
isOpen && "rotate-90"
|
|
50
|
+
)}
|
|
51
|
+
/>
|
|
52
|
+
{Icon && <Icon className="text-foreground h-4 w-4 shrink-0" />}
|
|
53
|
+
<h3 className="text-foreground m-0! text-base font-medium">{title}</h3>
|
|
54
|
+
</button>
|
|
55
|
+
|
|
56
|
+
{isOpen && <div className="dark:bg-muted/10 bg-muted/15 px-4 py-3">{children}</div>}
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default Accordion
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
SiSwift,
|
|
12
12
|
SiKotlin,
|
|
13
13
|
SiHtml5,
|
|
14
|
-
|
|
14
|
+
SiCss,
|
|
15
15
|
SiSass,
|
|
16
16
|
SiPostgresql,
|
|
17
17
|
SiGraphql,
|
|
@@ -68,7 +68,7 @@ const LanguageIcon = ({ lang }: { lang: string }) => {
|
|
|
68
68
|
js: <SiJavascript {...iconProps} />,
|
|
69
69
|
javascript: <SiJavascript {...iconProps} />,
|
|
70
70
|
html: <SiHtml5 {...iconProps} />,
|
|
71
|
-
css: <
|
|
71
|
+
css: <SiCss {...iconProps} />,
|
|
72
72
|
scss: <SiSass {...iconProps} />,
|
|
73
73
|
sass: <SiSass {...iconProps} />,
|
|
74
74
|
};
|
|
@@ -33,7 +33,7 @@ export function Navbar({ id }: NavbarProps) {
|
|
|
33
33
|
<Logo />
|
|
34
34
|
</div>
|
|
35
35
|
</div>
|
|
36
|
-
<div className="flex items-center
|
|
36
|
+
<div className="flex items-center gap-0 max-md:flex-row-reverse md:gap-2">
|
|
37
37
|
<div className="text-muted-foreground hidden items-center gap-4 text-sm font-medium md:flex">
|
|
38
38
|
<NavMenu />
|
|
39
39
|
</div>
|
|
@@ -46,9 +46,9 @@ export function Navbar({ id }: NavbarProps) {
|
|
|
46
46
|
className="flex items-center gap-1 px-2 text-sm font-medium md:hidden"
|
|
47
47
|
>
|
|
48
48
|
{isMenuOpen ? (
|
|
49
|
-
<ChevronUp className="h-6 w-6
|
|
49
|
+
<ChevronUp className="text-muted-foreground h-6 w-6" />
|
|
50
50
|
) : (
|
|
51
|
-
<ChevronDown className="h-6 w-6
|
|
51
|
+
<ChevronDown className="text-muted-foreground h-6 w-6" />
|
|
52
52
|
)}
|
|
53
53
|
</Button>
|
|
54
54
|
|
|
@@ -2,7 +2,7 @@ import { PropsWithChildren } from "react";
|
|
|
2
2
|
|
|
3
3
|
export function Typography({ children }: PropsWithChildren) {
|
|
4
4
|
return (
|
|
5
|
-
<div className="prose prose-zinc dark:prose-invert prose-code:font-code dark:prose-code:bg-stone-900/25 prose-code:bg-stone-50 prose-pre:bg-background prose-headings:scroll-
|
|
5
|
+
<div className="prose prose-zinc dark:prose-invert prose-code:font-code dark:prose-code:bg-stone-900/25 prose-code:bg-stone-50 prose-pre:bg-background max-lg:prose-headings:scroll-mt-54 prose-headings:scroll-mt-4 w-[85vw] sm:w-full sm:mx-auto prose-code:text-sm prose-code:leading-6 dark:prose-code:text-white prose-code:text-stone-800 prose-code:p-1 prose-code:rounded-md prose-code:border pt-2 min-w-full! prose-img:rounded-md prose-img:border prose-code:before:content-none prose-code:after:content-none prose-code:px-1.5 prose-code:overflow-x-auto max-w-[500px]! prose-img:my-3 prose-h2:my-4 prose-h2:mt-8">
|
|
6
6
|
{children}
|
|
7
7
|
</div>
|
|
8
8
|
);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
title
|
|
2
|
+
title: Accordion Group
|
|
3
3
|
description: for writing multiple accordions and grouping them together
|
|
4
|
-
date
|
|
4
|
+
date: 10-08-2025
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
An accordion is like a button that opens and closes information. To avoid clutter, long information
|
|
7
|
+
An accordion is like a button that opens and closes information. To avoid clutter, long information
|
|
8
|
+
can be hidden first and then revealed when clicked.
|
|
8
9
|
|
|
9
10
|
<Note type="note" title="Note">
|
|
10
11
|
Group related accordions together using `<AccordionGroup>`. This creates a cohesive section of accordions that can be individually expanded or collapsed.
|
|
@@ -18,10 +19,11 @@ An accordion is like a button that opens and closes information. To avoid clutte
|
|
|
18
19
|
</Accordion>
|
|
19
20
|
|
|
20
21
|
<Accordion title="With icon props" icon="MousePointerClick">
|
|
21
|
-
|
|
22
|
+
This accordion includes a [Lucide Icon](https://lucide.dev/icons/) because the `icon` prop is
|
|
23
|
+
provided.
|
|
22
24
|
</Accordion>
|
|
23
25
|
|
|
24
|
-
<Accordion title="
|
|
26
|
+
<Accordion title="With Code Block">
|
|
25
27
|
You can put other components inside Accordions.
|
|
26
28
|
```jsx:helloword.jsx
|
|
27
29
|
class HelloWorld {
|
|
@@ -45,7 +47,7 @@ public static void main(String[] args) {
|
|
|
45
47
|
This accordion includes a Lucide icon because the `icon` prop is provided.
|
|
46
48
|
</Accordion>
|
|
47
49
|
|
|
48
|
-
<Accordion title="
|
|
50
|
+
<Accordion title="With Code Block">
|
|
49
51
|
You can put other components inside Accordions.
|
|
50
52
|
```jsx:helloword.jsx
|
|
51
53
|
class HelloWorld {
|
|
@@ -60,9 +62,8 @@ public static void main(String[] args) {
|
|
|
60
62
|
|
|
61
63
|
## Props
|
|
62
64
|
|
|
63
|
-
| Prop
|
|
64
|
-
|
|
65
|
-
| `title`
|
|
66
|
-
| `children` | ReactNode | null
|
|
67
|
-
| `
|
|
68
|
-
| `icon` | string | undefined | Optional. Adds a Lucide icon before the title in the accordion header. |
|
|
65
|
+
| Prop | Type | Default | Description |
|
|
66
|
+
| ---------- | --------- | --------- | ------------------------------------------------------------------------------------------------------------- |
|
|
67
|
+
| `title` | string | - | **Required**. The text displayed in the accordion header. |
|
|
68
|
+
| `children` | ReactNode | null | The content to be displayed when the accordion is expanded. Can be plain text, markdown, or React components. |
|
|
69
|
+
| `icon` | string | undefined | Optional. Adds a Lucide icon before the title in the accordion header. |
|
|
@@ -4,9 +4,7 @@ description: A component used to create collapsible content that can be hidden a
|
|
|
4
4
|
date: 22-12-2024
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
### Basic Usage
|
|
7
|
+
## Basic Usage
|
|
10
8
|
|
|
11
9
|
```plaintext
|
|
12
10
|
<Accordion title="Click to expand">
|
|
@@ -17,13 +15,14 @@ date: 22-12-2024
|
|
|
17
15
|
Render as :
|
|
18
16
|
|
|
19
17
|
<Accordion title="Click to expand">
|
|
20
|
-
|
|
18
|
+
This is a simple accordion component that can be toggled by clicking the header. The content can
|
|
19
|
+
include any valid React nodes, including text, components, and markdown.
|
|
21
20
|
</Accordion>
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
## With Icon
|
|
24
23
|
|
|
25
24
|
````plaintext
|
|
26
|
-
<Accordion title="Code Block"
|
|
25
|
+
<Accordion title="Code Block" icon="Code">
|
|
27
26
|
```javascript:main.js showLineNumbers {3-4}
|
|
28
27
|
function isRocketAboutToCrash() {
|
|
29
28
|
// Check if the rocket is stable
|
|
@@ -37,7 +36,7 @@ Render as :
|
|
|
37
36
|
|
|
38
37
|
Render as :
|
|
39
38
|
|
|
40
|
-
<Accordion title="Code Block"
|
|
39
|
+
<Accordion title="Code Block" icon="Code">
|
|
41
40
|
```javascript:main.js showLineNumbers {3-4}
|
|
42
41
|
function isRocketAboutToCrash() {
|
|
43
42
|
// Check if the rocket is stable
|
|
@@ -50,10 +49,8 @@ Render as :
|
|
|
50
49
|
|
|
51
50
|
## Props
|
|
52
51
|
|
|
53
|
-
| Prop
|
|
54
|
-
|
|
55
|
-
| `title`
|
|
56
|
-
| `children` | ReactNode | null
|
|
57
|
-
| `
|
|
58
|
-
| `icon` | string | undefined | Optional. Adds a Lucide icon before the title in the accordion header. |
|
|
59
|
-
|
|
52
|
+
| Prop | Type | Default | Description |
|
|
53
|
+
| ---------- | --------- | --------- | ------------------------------------------------------------------------------------------------------------- |
|
|
54
|
+
| `title` | string | - | **Required**. The text displayed in the accordion header. |
|
|
55
|
+
| `children` | ReactNode | null | The content to be displayed when the accordion is expanded. Can be plain text, markdown, or React components. |
|
|
56
|
+
| `icon` | string | undefined | Optional. Adds a Lucide icon before the title in the accordion header. |
|
package/src/dist/docu.json
CHANGED
|
@@ -8,6 +8,12 @@ export function useActiveSection(tocs: TocItem[]) {
|
|
|
8
8
|
const observerRef = useRef<IntersectionObserver | null>(null)
|
|
9
9
|
const clickedIdRef = useRef<string | null>(null)
|
|
10
10
|
|
|
11
|
+
const activeIdRef = useRef<string | null>(null)
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
activeIdRef.current = activeId
|
|
15
|
+
}, [activeId])
|
|
16
|
+
|
|
11
17
|
// Handle intersection observer for active section
|
|
12
18
|
useEffect(() => {
|
|
13
19
|
if (typeof document === "undefined" || !tocs.length) return
|
|
@@ -24,15 +30,19 @@ export function useActiveSection(tocs: TocItem[]) {
|
|
|
24
30
|
}, visibleEntries[0])
|
|
25
31
|
|
|
26
32
|
const newActiveId = mostVisibleEntry.target.id
|
|
27
|
-
if (newActiveId !==
|
|
33
|
+
if (newActiveId !== activeIdRef.current) {
|
|
28
34
|
setActiveId(newActiveId)
|
|
29
35
|
}
|
|
30
36
|
}
|
|
31
37
|
|
|
38
|
+
// Determine the scroll root: #scroll-container is only used on desktop (lg)
|
|
39
|
+
const isDesktop = window.innerWidth >= 1024
|
|
40
|
+
const container = isDesktop ? document.getElementById("scroll-container") : null
|
|
41
|
+
|
|
32
42
|
// Initialize intersection observer
|
|
33
43
|
observerRef.current = new IntersectionObserver(handleIntersect, {
|
|
34
|
-
root:
|
|
35
|
-
rootMargin: "0px 0px -60% 0px",
|
|
44
|
+
root: container,
|
|
45
|
+
rootMargin: isDesktop ? "0px 0px -60% 0px" : "-160px 0px -60% 0px",
|
|
36
46
|
threshold: 0,
|
|
37
47
|
})
|
|
38
48
|
|
|
@@ -48,7 +58,7 @@ export function useActiveSection(tocs: TocItem[]) {
|
|
|
48
58
|
return () => {
|
|
49
59
|
observerRef.current?.disconnect()
|
|
50
60
|
}
|
|
51
|
-
}, [tocs, activeId
|
|
61
|
+
}, [tocs]) // Only depend on tocs, handle activeId via ref
|
|
52
62
|
|
|
53
63
|
const handleLinkClick = useCallback((id: string) => {
|
|
54
64
|
clickedIdRef.current = id
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ROUTES, type EachRoute } from "../routes"
|
|
2
|
+
|
|
3
|
+
export type SearchResult = {
|
|
4
|
+
title: string
|
|
5
|
+
href: string
|
|
6
|
+
noLink?: boolean
|
|
7
|
+
items?: undefined
|
|
8
|
+
score?: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function helperSearch(
|
|
12
|
+
query: string,
|
|
13
|
+
node: EachRoute,
|
|
14
|
+
prefix: string,
|
|
15
|
+
currenLevel: number,
|
|
16
|
+
maxLevel?: number
|
|
17
|
+
) {
|
|
18
|
+
const res: EachRoute[] = []
|
|
19
|
+
let parentHas = false
|
|
20
|
+
|
|
21
|
+
const nextLink = `${prefix}${node.href}`
|
|
22
|
+
if (!node.noLink && node.title.toLowerCase().includes(query.toLowerCase())) {
|
|
23
|
+
res.push({ ...node, items: undefined, href: nextLink })
|
|
24
|
+
parentHas = true
|
|
25
|
+
}
|
|
26
|
+
const goNext = maxLevel ? currenLevel < maxLevel : true
|
|
27
|
+
if (goNext)
|
|
28
|
+
node.items?.forEach((item) => {
|
|
29
|
+
const innerRes = helperSearch(query, item, nextLink, currenLevel + 1, maxLevel)
|
|
30
|
+
if (!!innerRes.length && !parentHas && !node.noLink) {
|
|
31
|
+
res.push({ ...node, items: undefined, href: nextLink })
|
|
32
|
+
parentHas = true
|
|
33
|
+
}
|
|
34
|
+
res.push(...innerRes)
|
|
35
|
+
})
|
|
36
|
+
return res
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function advanceSearch(query: string) {
|
|
40
|
+
return ROUTES.map((node) =>
|
|
41
|
+
helperSearch(query, node, "", 1, query.length == 0 ? 2 : undefined)
|
|
42
|
+
).flat()
|
|
43
|
+
}
|
package/src/dist/lib/toc.ts
CHANGED
package/src/dist/lib/utils.ts
CHANGED
|
@@ -1,80 +1,39 @@
|
|
|
1
|
-
import { type ClassValue, clsx } from "clsx"
|
|
2
|
-
import { twMerge } from "tailwind-merge"
|
|
3
|
-
import { EachRoute, ROUTES } from "./routes";
|
|
1
|
+
import { type ClassValue, clsx } from "clsx"
|
|
2
|
+
import { twMerge } from "tailwind-merge"
|
|
4
3
|
|
|
5
4
|
export function cn(...inputs: ClassValue[]) {
|
|
6
|
-
return twMerge(clsx(inputs))
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function helperSearch(
|
|
10
|
-
query: string,
|
|
11
|
-
node: EachRoute,
|
|
12
|
-
prefix: string,
|
|
13
|
-
currenLevel: number,
|
|
14
|
-
maxLevel?: number
|
|
15
|
-
) {
|
|
16
|
-
const res: EachRoute[] = [];
|
|
17
|
-
let parentHas = false;
|
|
18
|
-
|
|
19
|
-
const nextLink = `${prefix}${node.href}`;
|
|
20
|
-
if (!node.noLink && node.title.toLowerCase().includes(query.toLowerCase())) {
|
|
21
|
-
res.push({ ...node, items: undefined, href: nextLink });
|
|
22
|
-
parentHas = true;
|
|
23
|
-
}
|
|
24
|
-
const goNext = maxLevel ? currenLevel < maxLevel : true;
|
|
25
|
-
if (goNext)
|
|
26
|
-
node.items?.forEach((item) => {
|
|
27
|
-
const innerRes = helperSearch(
|
|
28
|
-
query,
|
|
29
|
-
item,
|
|
30
|
-
nextLink,
|
|
31
|
-
currenLevel + 1,
|
|
32
|
-
maxLevel
|
|
33
|
-
);
|
|
34
|
-
if (!!innerRes.length && !parentHas && !node.noLink) {
|
|
35
|
-
res.push({ ...node, items: undefined, href: nextLink });
|
|
36
|
-
parentHas = true;
|
|
37
|
-
}
|
|
38
|
-
res.push(...innerRes);
|
|
39
|
-
});
|
|
40
|
-
return res;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function advanceSearch(query: string) {
|
|
44
|
-
return ROUTES.map((node) =>
|
|
45
|
-
helperSearch(query, node, "", 1, query.length == 0 ? 2 : undefined)
|
|
46
|
-
).flat();
|
|
5
|
+
return twMerge(clsx(inputs))
|
|
47
6
|
}
|
|
48
7
|
|
|
49
8
|
// Thursday, May 23, 2024
|
|
50
9
|
export function formatDate(dateStr: string): string {
|
|
51
|
-
const [day, month, year] = dateStr.split("-").map(Number)
|
|
52
|
-
const date = new Date(year, month - 1, day)
|
|
10
|
+
const [day, month, year] = dateStr.split("-").map(Number)
|
|
11
|
+
const date = new Date(year, month - 1, day)
|
|
53
12
|
|
|
54
13
|
const options: Intl.DateTimeFormatOptions = {
|
|
55
14
|
weekday: "long",
|
|
56
15
|
year: "numeric",
|
|
57
16
|
month: "long",
|
|
58
17
|
day: "numeric",
|
|
59
|
-
}
|
|
18
|
+
}
|
|
60
19
|
|
|
61
|
-
return date.toLocaleDateString("en-US", options)
|
|
20
|
+
return date.toLocaleDateString("en-US", options)
|
|
62
21
|
}
|
|
63
22
|
|
|
64
23
|
// May 23, 2024
|
|
65
24
|
export function formatDate2(dateStr: string): string {
|
|
66
|
-
const [day, month, year] = dateStr.split("-").map(Number)
|
|
67
|
-
const date = new Date(year, month - 1, day)
|
|
25
|
+
const [day, month, year] = dateStr.split("-").map(Number)
|
|
26
|
+
const date = new Date(year, month - 1, day)
|
|
68
27
|
|
|
69
28
|
const options: Intl.DateTimeFormatOptions = {
|
|
70
29
|
month: "short",
|
|
71
30
|
day: "numeric",
|
|
72
31
|
year: "numeric",
|
|
73
|
-
}
|
|
74
|
-
return date.toLocaleDateString("en-US", options)
|
|
32
|
+
}
|
|
33
|
+
return date.toLocaleDateString("en-US", options)
|
|
75
34
|
}
|
|
76
35
|
|
|
77
36
|
export function stringToDate(date: string) {
|
|
78
|
-
const [day, month, year] = date.split("-").map(Number)
|
|
79
|
-
return new Date(year, month - 1, day)
|
|
37
|
+
const [day, month, year] = date.split("-").map(Number)
|
|
38
|
+
return new Date(year, month - 1, day)
|
|
80
39
|
}
|
package/src/dist/package.json
CHANGED
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/*
|
|
2
|
-
================================================================================
|
|
3
|
-
DocSearch Component Styling (Refactored Version)
|
|
4
|
-
================================================================================
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/* -- LANGKAH 1: Definisi Variabel Global --
|
|
8
|
-
Variabel tema DocSearch sekarang didefinisikan secara global di :root.
|
|
9
|
-
Ini menyederhanakan pewarisan tema dan memastikan konsistensi.
|
|
10
|
-
Mode gelap secara otomatis menimpa variabel ini karena .dark di globals.css.
|
|
11
|
-
*/
|
|
12
1
|
:root {
|
|
13
2
|
--docsearch-primary-color: hsl(var(--primary));
|
|
14
3
|
--docsearch-text-color: hsl(var(--muted-foreground));
|
|
@@ -31,7 +20,7 @@
|
|
|
31
20
|
--docsearch-searchbox-focus-background: hsl(var(--card));
|
|
32
21
|
--docsearch-searchbox-shadow: none;
|
|
33
22
|
|
|
34
|
-
/* Hit (
|
|
23
|
+
/* Hit (Search Result) */
|
|
35
24
|
--docsearch-hit-height: 56px;
|
|
36
25
|
--docsearch-hit-color: hsl(var(--foreground));
|
|
37
26
|
--docsearch-hit-active-color: hsl(var(--primary-foreground));
|
|
@@ -49,16 +38,12 @@
|
|
|
49
38
|
--docsearch-footer-shadow: none;
|
|
50
39
|
}
|
|
51
40
|
|
|
52
|
-
/* -- LANGKAH 2: Gaya untuk Tombol Awal --
|
|
53
|
-
Gaya ini spesifik untuk tombol yang ada di Navbar,
|
|
54
|
-
yang dibungkus oleh <div class="docsearch">.
|
|
55
|
-
*/
|
|
56
41
|
.docsearch .DocSearch-Button {
|
|
57
42
|
background-color: hsl(var(--secondary));
|
|
58
43
|
border: 1px solid hsl(var(--border));
|
|
59
44
|
border-radius: 9999px;
|
|
60
|
-
width:
|
|
61
|
-
height:
|
|
45
|
+
width: 250px;
|
|
46
|
+
height: 35px;
|
|
62
47
|
color: hsl(var(--muted-foreground));
|
|
63
48
|
transition: width 0.3s ease;
|
|
64
49
|
margin: 0;
|
|
@@ -86,7 +71,6 @@
|
|
|
86
71
|
.docsearch .DocSearch-Button-Key {
|
|
87
72
|
background: var(--docsearch-primary-color);
|
|
88
73
|
color: var(--docsearch-logo-color);
|
|
89
|
-
/* Menggunakan variabel yg relevan */
|
|
90
74
|
border-radius: 6px;
|
|
91
75
|
font-size: 14px;
|
|
92
76
|
font-weight: 500;
|
|
@@ -97,10 +81,6 @@
|
|
|
97
81
|
top: 0;
|
|
98
82
|
}
|
|
99
83
|
|
|
100
|
-
/* -- LANGKAH 3: Gaya untuk Modal dan Isinya --
|
|
101
|
-
Gaya ini menargetkan elemen-elemen modal yang dirender terpisah.
|
|
102
|
-
Karena variabel sudah global, kita hanya perlu menata elemennya.
|
|
103
|
-
*/
|
|
104
84
|
.DocSearch-Container .DocSearch-Modal {
|
|
105
85
|
backdrop-filter: blur(8px);
|
|
106
86
|
}
|
|
@@ -118,7 +98,6 @@
|
|
|
118
98
|
border-top: 1px solid hsl(var(--border));
|
|
119
99
|
}
|
|
120
100
|
|
|
121
|
-
/* Gaya untuk tombol keyboard di footer */
|
|
122
101
|
.DocSearch-Footer--commands kbd {
|
|
123
102
|
background-color: hsl(var(--secondary));
|
|
124
103
|
border: 1px solid hsl(var(--border));
|
|
@@ -131,7 +110,6 @@
|
|
|
131
110
|
justify-content: center;
|
|
132
111
|
}
|
|
133
112
|
|
|
134
|
-
/* Menghilangkan gaya default dari ikon di dalam tombol footer */
|
|
135
113
|
.DocSearch-Commands-Key {
|
|
136
114
|
background: none;
|
|
137
115
|
color: hsl(var(--muted-foreground));
|
|
@@ -144,9 +122,6 @@
|
|
|
144
122
|
border-radius: 6px;
|
|
145
123
|
}
|
|
146
124
|
|
|
147
|
-
/* -- LANGKAH 4: Gaya Responsif --
|
|
148
|
-
Tidak ada perubahan, hanya mempertahankan fungsionalitas mobile.
|
|
149
|
-
*/
|
|
150
125
|
@media (max-width: 768px) {
|
|
151
126
|
.docsearch .DocSearch-Button {
|
|
152
127
|
width: 40px;
|