@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docubook/create",
3
- "version": "2.5.0",
3
+ "version": "2.5.1",
4
4
  "description": "CLI to create DocuBook projects",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -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-16 bg-card dark:bg-card/20 border-muted-foreground/20 flex w-full h-full flex-col items-start rounded-xl border shadow-md backdrop-blur-sm lg:flex-row overflow-y-auto relative">
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 React from "react";
4
- import { DocSearch } from "@docsearch/react";
3
+ import { DocSearch } from "@docsearch/react"
4
+ import { algoliaConfig } from "@/lib/search/algolia"
5
+ import { cn } from "@/lib/utils"
5
6
 
6
- export default function DocSearchComponent() {
7
- const appId = process.env.NEXT_PUBLIC_ALGOLIA_DOCSEARCH_APP_ID;
8
- const apiKey = process.env.NEXT_PUBLIC_ALGOLIA_DOCSEARCH_API_KEY;
9
- const indexName = process.env.NEXT_PUBLIC_ALGOLIA_DOCSEARCH_INDEX_NAME;
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-sm text-muted-foreground" disabled>
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 path segment (last part of URL)
61
- const pageTitle = pathname?.split("/").filter(Boolean).pop() || ""
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 border-muted shadow-sm backdrop-blur-sm dark:border-foreground/10 dark:bg-background">
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 variant="ghost" size="icon" className="h-8 w-8 shrink-0" aria-label="Navigation menu">
120
- <MoreVertical className="h-5 w-5 text-muted-foreground" />
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 align="start" className="flex flex-col gap-1 p-2 min-w-[160px]">
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 text-muted-foreground" />
156
+ <PanelRight className="text-muted-foreground h-6 w-6 shrink-0" />
146
157
  </Button>
147
158
  </SheetTrigger>
148
- <SheetContent className="w-full flex flex-col gap-4 px-0 lg:w-auto" side="right">
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 DocSearchComponent from "@/components/DocSearch"
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
- * Specify which search engine to use.
13
- * @default 'default'
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 = "default" }: SearchProps) {
20
+ export default function Search({ type, className }: SearchProps) {
19
21
  const { isOpen, setIsOpen } = useSearch()
22
+ const searchType = type ?? searchConfig.type
20
23
 
21
- if (type === "algolia") {
22
- // Just render the component without passing any state props
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 { advanceSearch, cn } from "@/lib/utils";
8
- import { ScrollArea } from "@/components/ui/scroll-area";
9
- import { page_routes } from "@/lib/routes";
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="p-0 max-w-[650px] sm:top-[38%] top-[45%] rounded-md!">
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 px-6 bg-transparent border-b text-[14px] outline-none w-full"
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 sm:px-2 px-1 pb-4">
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] || 'pl-2';
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 px-3 rounded-sm text-sm flex items-center gap-2.5",
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 items-center w-full h-full py-3 gap-1.5 px-2 justify-between",
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] mr-1" />
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 md:flex items-center text-xs text-muted-foreground">
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 ml-1" />
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="md:flex md:justify-start hidden h-14 px-6 bg-transparent border-t text-[14px] outline-none">
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 border rounded p-2">
184
- <ArrowUpIcon className="w-3 h-3" />
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 border rounded p-2">
187
- <ArrowDownIcon className="w-3 h-3" />
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 border rounded p-2">
191
- <CornerDownLeftIcon className="w-3 h-3" />
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 border rounded px-2 py-1">
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
- export function SearchTrigger() {
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="lg:hidden block p-2 -ml-2">
13
- <SearchIcon className="h-6 w-6 text-muted-foreground" />
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 w-full">
16
- <SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-6 w-6 text-muted-foreground" />
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="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"
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="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">
23
- <CommandIcon className="w-3 h-3" />
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 text-muted-foreground" />
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 { AccordionGroupContext } from "@/lib/accordion-context";
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
- // Wrap all children with the AccordionGroupContext.Provider
16
- // so that any nested accordions know they are inside a group.
17
- // This enables group-specific behavior in child components.
18
- <AccordionGroupContext.Provider value={{ inGroup: true }}>
19
- <div
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, useState, useContext } 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 '@/lib/accordion-context';
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
- title: string;
11
- children?: ReactNode;
12
- defaultOpen?: boolean;
13
- icon?: keyof typeof Icons;
14
- };
15
-
16
- const Accordion: React.FC<AccordionProps> = ({
17
- title,
18
- children,
19
- defaultOpen = false,
20
- icon,
21
- }: AccordionProps) => {
22
- const groupContext = useContext(AccordionGroupContext);
23
- const isInGroup = groupContext?.inGroup === true;
24
- const [isOpen, setIsOpen] = useState(defaultOpen);
25
- const Icon = icon ? (Icons[icon] as React.FC<{ className?: string }>) : null;
26
-
27
- // The main wrapper div for the accordion.
28
- // All styling logic for the accordion container is handled here.
29
- return (
30
- <div
31
- className={cn(
32
- // Style for STANDALONE: full card with border & shadow
33
- !isInGroup && "border rounded-lg shadow-sm",
34
- // Style for IN GROUP: only a bottom border separator
35
- isInGroup && "border-b last:border-b-0 border-border"
36
- )}
37
- >
38
- <button
39
- type="button"
40
- onClick={() => setIsOpen(!isOpen)}
41
- className="flex items-center gap-2 w-full px-4 py-3 transition-colors bg-muted/40 dark:bg-muted/20 hover:bg-muted/70 dark:hover:bg-muted/70 cursor-pointer text-start"
42
- >
43
- <ChevronRight
44
- className={cn(
45
- "w-4 h-4 text-muted-foreground transition-transform duration-200 shrink-0",
46
- isOpen && "rotate-90"
47
- )}
48
- />
49
- {Icon && <Icon className="text-foreground w-4 h-4 shrink-0" />}
50
- <h3 className="font-medium text-base text-foreground m-0!">{title}</h3>
51
- </button>
52
-
53
- {isOpen && (
54
- <div className="px-4 py-3 dark:bg-muted/10 bg-muted/15">
55
- {children}
56
- </div>
57
- )}
58
- </div>
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
- SiCss3,
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: <SiCss3 {...iconProps} />,
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 md:gap-2 gap-0 max-md:flex-row-reverse">
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 text-muted-foreground" />
49
+ <ChevronUp className="text-muted-foreground h-6 w-6" />
50
50
  ) : (
51
- <ChevronDown className="h-6 w-6 text-muted-foreground" />
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-m-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">
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 : Accordion Group
2
+ title: Accordion Group
3
3
  description: for writing multiple accordions and grouping them together
4
- date : 10-08-2025
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 can be hidden first and then revealed when clicked.
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
- This accordion includes a [Lucide Icon](https://lucide.dev/icons/) because the `icon` prop is provided.
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="Expanded by Default" defaultOpen>
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="Expanded by Default" defaultOpen>
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 | Type | Default | Description |
64
- |------|------|---------|-------------|
65
- | `title` | string | - | **Required**. The text displayed in the accordion header. |
66
- | `children` | ReactNode | null | The content to be displayed when the accordion is expanded. Can be plain text, markdown, or React components. |
67
- | `defaultOpen` | boolean | false | When true, the accordion will be expanded by default. |
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
- ## Preview
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
- This is a simple accordion component that can be toggled by clicking the header. The content can include any valid React nodes, including text, components, and markdown.
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
- ### With Code Block
22
+ ## With Icon
24
23
 
25
24
  ````plaintext
26
- <Accordion title="Code Block" defaultOpen={true} icon="Code">
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" defaultOpen={true} icon="Code">
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 | Type | Default | Description |
54
- |------|------|---------|-------------|
55
- | `title` | string | - | **Required**. The text displayed in the accordion header. |
56
- | `children` | ReactNode | null | The content to be displayed when the accordion is expanded. Can be plain text, markdown, or React components. |
57
- | `defaultOpen` | boolean | false | When true, the accordion will be expanded by default. |
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. |
@@ -1,5 +1,8 @@
1
1
  {
2
2
  "$schema": "https://docubook.pro/docu.schema.json",
3
+ "search": {
4
+ "type": "default"
5
+ },
3
6
  "meta": {
4
7
  "baseURL": "https://docubook.pro",
5
8
  "title": "DocuBook",
@@ -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 !== activeId) {
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: document.getElementById("scroll-container"),
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,5 @@
1
+ export const algoliaConfig = {
2
+ appId: process.env.NEXT_PUBLIC_ALGOLIA_DOCSEARCH_APP_ID,
3
+ apiKey: process.env.NEXT_PUBLIC_ALGOLIA_DOCSEARCH_API_KEY,
4
+ indexName: process.env.NEXT_PUBLIC_ALGOLIA_DOCSEARCH_INDEX_NAME,
5
+ }
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ import docuConfig from "@/docu.json"
2
+
3
+ export type SearchType = "default" | "algolia"
4
+
5
+ export const searchConfig = {
6
+ type: (docuConfig.search?.type as SearchType) ?? "default",
7
+ }
@@ -6,4 +6,5 @@ export interface TocItem {
6
6
 
7
7
  export interface MobTocProps {
8
8
  tocs: TocItem[];
9
+ title?: string;
9
10
  }
@@ -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
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docubook",
3
- "version": "2.5.0",
3
+ "version": "2.5.1",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "dev": "next dev",
@@ -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 (Hasil Pencarian) */
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: 260px;
61
- height: 40px;
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;
@@ -1,4 +0,0 @@
1
- import { createContext } from 'react';
2
-
3
- // Create a context to check if a component is inside an accordion group
4
- export const AccordionGroupContext = createContext<{ inGroup: boolean } | null>(null);