@docubook/create 2.4.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.
Files changed (54) hide show
  1. package/package.json +1 -1
  2. package/src/dist/app/docs/[[...slug]]/page.tsx +55 -59
  3. package/src/dist/app/docs/layout.tsx +11 -4
  4. package/src/dist/app/layout.tsx +10 -7
  5. package/src/dist/app/page.tsx +1 -1
  6. package/src/dist/components/{context-popover.tsx → ContextPopover.tsx} +3 -2
  7. package/src/dist/components/DocSearch.tsx +16 -15
  8. package/src/dist/components/{docs-breadcrumb.tsx → DocsBreadcrumb.tsx} +1 -1
  9. package/src/dist/components/DocsNavbar.tsx +46 -0
  10. package/src/dist/components/DocsSidebar.tsx +207 -0
  11. package/src/dist/components/Github.tsx +26 -0
  12. package/src/dist/components/{scroll-to-top.tsx → ScrollToTop.tsx} +16 -9
  13. package/src/dist/components/SearchBox.tsx +39 -0
  14. package/src/dist/components/SearchContext.tsx +47 -0
  15. package/src/dist/components/SearchModal.tsx +77 -79
  16. package/src/dist/components/SearchTrigger.tsx +20 -15
  17. package/src/dist/components/Sponsor.tsx +2 -2
  18. package/src/dist/components/{theme-toggle.tsx → ThemeToggle.tsx} +10 -10
  19. package/src/dist/components/TocObserver.tsx +197 -0
  20. package/src/dist/components/footer.tsx +16 -12
  21. package/src/dist/components/leftbar.tsx +45 -73
  22. package/src/dist/components/markdown/AccordionContext.tsx +21 -0
  23. package/src/dist/components/markdown/AccordionGroupMdx.tsx +11 -22
  24. package/src/dist/components/markdown/AccordionMdx.tsx +58 -59
  25. package/src/dist/components/markdown/PreMdx.tsx +2 -2
  26. package/src/dist/components/navbar.tsx +130 -53
  27. package/src/dist/components/toc.tsx +16 -14
  28. package/src/dist/components/typography.tsx +1 -1
  29. package/src/dist/components/ui/icon-cloud.tsx +353 -0
  30. package/src/dist/components/ui/scroll-area.tsx +2 -2
  31. package/src/dist/components/ui/sheet.tsx +4 -4
  32. package/src/dist/components/ui/toggle.tsx +3 -3
  33. package/src/dist/docs/components/accordion-group.mdx +13 -12
  34. package/src/dist/docs/components/accordion.mdx +11 -14
  35. package/src/dist/docu.json +3 -0
  36. package/src/dist/hooks/useActiveSection.ts +45 -33
  37. package/src/dist/hooks/useScrollPosition.ts +16 -14
  38. package/src/dist/lib/search/algolia.ts +5 -0
  39. package/src/dist/lib/search/built-in.ts +43 -0
  40. package/src/dist/lib/search/config.ts +7 -0
  41. package/src/dist/lib/toc.ts +1 -0
  42. package/src/dist/lib/utils.ts +13 -54
  43. package/src/dist/package.json +1 -1
  44. package/src/dist/styles/algolia.css +12 -35
  45. package/src/dist/styles/{syntax.css → override.css} +82 -39
  46. package/src/dist/tailwind.config.ts +11 -110
  47. package/src/dist/components/AccordionContext.ts +0 -4
  48. package/src/dist/components/GithubStart.tsx +0 -44
  49. package/src/dist/components/mob-toc.tsx +0 -134
  50. package/src/dist/components/search.tsx +0 -55
  51. package/src/dist/components/toc-observer.tsx +0 -254
  52. /package/src/dist/components/{docs-menu.tsx → DocsMenu.tsx} +0 -0
  53. /package/src/dist/components/{edit-on-github.tsx → EditWithGithub.tsx} +0 -0
  54. /package/src/dist/components/{theme-provider.tsx → ThemeProvider.tsx} +0 -0
@@ -0,0 +1,197 @@
1
+ "use client"
2
+
3
+ import clsx from "clsx"
4
+ import Link from "next/link"
5
+ import { useState, useRef, useEffect, useCallback } from "react"
6
+ import { motion } from "framer-motion"
7
+ import { ScrollToTop } from "./ScrollToTop"
8
+ import { TocItem } from "@/lib/toc"
9
+
10
+ interface TocObserverProps {
11
+ data: TocItem[]
12
+ activeId?: string | null
13
+ onActiveIdChange?: (id: string | null) => void
14
+ }
15
+
16
+ export default function TocObserver({
17
+ data,
18
+ activeId: externalActiveId,
19
+ onActiveIdChange,
20
+ }: TocObserverProps) {
21
+ const itemRefs = useRef<Map<string, HTMLAnchorElement>>(new Map())
22
+
23
+ const activeId = externalActiveId ?? null
24
+
25
+ const handleLinkClick = useCallback(
26
+ (id: string) => {
27
+ onActiveIdChange?.(id)
28
+ },
29
+ [onActiveIdChange]
30
+ )
31
+
32
+ // Function to check if an item has children
33
+ const hasChildren = (currentId: string, currentLevel: number) => {
34
+ const currentIndex = data.findIndex((item) => item.href.slice(1) === currentId)
35
+ if (currentIndex === -1 || currentIndex === data.length - 1) return false
36
+
37
+ const nextItem = data[currentIndex + 1]
38
+ return nextItem.level > currentLevel
39
+ }
40
+
41
+ // Calculate scroll progress for the active section
42
+ const [scrollProgress, setScrollProgress] = useState(0)
43
+
44
+ useEffect(() => {
45
+ const handleScroll = () => {
46
+ if (!activeId) return
47
+
48
+ const activeElement = document.getElementById(activeId)
49
+ if (!activeElement) return
50
+
51
+ const rect = activeElement.getBoundingClientRect()
52
+ const windowHeight = window.innerHeight
53
+ const elementTop = rect.top
54
+ const elementHeight = rect.height
55
+
56
+ // Calculate how much of the element is visible
57
+ let progress = 0
58
+ if (elementTop < windowHeight) {
59
+ progress = Math.min(1, (windowHeight - elementTop) / (windowHeight + elementHeight))
60
+ }
61
+
62
+ setScrollProgress(progress)
63
+ }
64
+
65
+ const container = document.getElementById("scroll-container") || window
66
+
67
+ container.addEventListener("scroll", handleScroll, { passive: true })
68
+
69
+ // Initial calculation
70
+ handleScroll()
71
+
72
+ return () => container.removeEventListener("scroll", handleScroll)
73
+ }, [activeId])
74
+
75
+ return (
76
+ <div className="relative">
77
+ <div className="text-foreground/70 hover:text-foreground relative text-sm transition-colors">
78
+ <div className="flex flex-col gap-0">
79
+ {data.map(({ href, level, text }, index) => {
80
+ const id = href.slice(1)
81
+ const isActive = activeId === id
82
+ const indent = level > 1 ? (level - 1) * 20 : 0
83
+ // Prefix with underscore to indicate intentionally unused
84
+ const _isParent = hasChildren(id, level)
85
+ const _isLastInLevel = index === data.length - 1 || data[index + 1].level <= level
86
+
87
+ return (
88
+ <div key={href} className="relative">
89
+ {/* Simple L-shaped connector */}
90
+ {level > 1 && (
91
+ <div
92
+ className={clsx("absolute top-0 h-full w-6", {
93
+ "left-[6px]": indent === 20, // Level 2
94
+ "left-[22px]": indent === 40, // Level 3
95
+ "left-[38px]": indent === 60, // Level 4
96
+ })}
97
+ >
98
+ {/* Vertical line */}
99
+ <div
100
+ className={clsx(
101
+ "absolute left-0 top-0 h-full w-px",
102
+ isActive
103
+ ? "bg-primary/20 dark:bg-primary/30"
104
+ : "bg-border/50 dark:bg-border/50"
105
+ )}
106
+ >
107
+ {isActive && (
108
+ <motion.div
109
+ className="bg-primary absolute left-0 top-0 h-full w-full origin-top"
110
+ initial={{ scaleY: 0 }}
111
+ animate={{ scaleY: scrollProgress }}
112
+ transition={{ duration: 0.3 }}
113
+ />
114
+ )}
115
+ </div>
116
+
117
+ {/* Horizontal line */}
118
+ <div
119
+ className={clsx(
120
+ "absolute left-0 top-1/2 h-px w-6",
121
+ isActive
122
+ ? "bg-primary/20 dark:bg-primary/30"
123
+ : "bg-border/50 dark:bg-border/50"
124
+ )}
125
+ >
126
+ {isActive && (
127
+ <motion.div
128
+ className="bg-primary dark:bg-accent absolute left-0 top-0 h-full w-full origin-left"
129
+ initial={{ scaleX: 0 }}
130
+ animate={{ scaleX: scrollProgress }}
131
+ transition={{ duration: 0.3, delay: 0.1 }}
132
+ />
133
+ )}
134
+ </div>
135
+ </div>
136
+ )}
137
+
138
+ <Link
139
+ href={href}
140
+ onClick={() => handleLinkClick(id)}
141
+ className={clsx("relative flex items-center py-2 transition-colors", {
142
+ "text-primary dark:text-primary font-medium": isActive,
143
+ "text-muted-foreground hover:text-foreground dark:hover:text-foreground/90":
144
+ !isActive,
145
+ })}
146
+ style={{
147
+ paddingLeft: `${indent}px`,
148
+ marginLeft: level > 1 ? "12px" : "0",
149
+ }}
150
+ ref={(el) => {
151
+ const map = itemRefs.current
152
+ if (el) {
153
+ map.set(id, el)
154
+ } else {
155
+ map.delete(id)
156
+ }
157
+ }}
158
+ >
159
+ {/* Circle indicator */}
160
+ <div className="relative flex h-4 w-4 shrink-0 items-center justify-center">
161
+ <div
162
+ className={clsx(
163
+ "relative z-10 h-1.5 w-1.5 rounded-full transition-all duration-300",
164
+ {
165
+ "bg-primary dark:bg-primary/90 scale-100": isActive,
166
+ "bg-muted-foreground/30 dark:bg-muted-foreground/30 group-hover:bg-primary/50 dark:group-hover:bg-primary/50 scale-75 group-hover:scale-100":
167
+ !isActive,
168
+ }
169
+ )}
170
+ >
171
+ {isActive && (
172
+ <motion.div
173
+ className="bg-primary/20 dark:bg-primary/30 absolute inset-0 rounded-full"
174
+ initial={{ scale: 1 }}
175
+ animate={{ scale: 1.8 }}
176
+ transition={{
177
+ duration: 2,
178
+ repeat: Infinity,
179
+ repeatType: "reverse",
180
+ }}
181
+ />
182
+ )}
183
+ </div>
184
+ </div>
185
+
186
+ <span className="truncate text-sm">{text}</span>
187
+ </Link>
188
+ </div>
189
+ )
190
+ })}
191
+ </div>
192
+ </div>
193
+ {/* Add scroll to top link at the bottom of TOC */}
194
+ <ScrollToTop className="mt-6" />
195
+ </div>
196
+ )
197
+ }
@@ -1,5 +1,5 @@
1
1
  import Link from "next/link";
2
- import { ModeToggle } from "@/components/theme-toggle";
2
+ import { ModeToggle } from "@/components/ThemeToggle";
3
3
  import docuData from "@/docu.json";
4
4
  import * as LucideIcons from "lucide-react";
5
5
 
@@ -20,21 +20,25 @@ const docuConfig = docuData as {
20
20
  footer: FooterConfig;
21
21
  };
22
22
 
23
- export function Footer() {
23
+ interface FooterProps {
24
+ id?: string;
25
+ }
26
+
27
+ export function Footer({ id }: FooterProps) {
24
28
  const { footer } = docuConfig;
25
29
  return (
26
- <footer className="w-full py-8 border-t bg-background">
30
+ <footer id={id} className="w-full py-8 border-t bg-background">
27
31
  <div className="container flex flex-col lg:flex-row items-center justify-between text-sm">
28
32
  <div className="flex flex-col items-center lg:items-start justify-start gap-4 w-full lg:w-3/5 text-center lg:text-left">
29
- <p className="text-muted-foreground">
30
- Copyright © {new Date().getFullYear()} {footer.copyright} - <MadeWith />
31
- </p>
32
- <div className="flex items-center justify-center lg:justify-start gap-6 mt-2 w-full">
33
- <FooterButtons />
34
- </div>
33
+ <p className="text-muted-foreground">
34
+ Copyright © {new Date().getFullYear()} {footer.copyright} - <MadeWith />
35
+ </p>
36
+ <div className="flex items-center justify-center lg:justify-start gap-6 mt-2 w-full">
37
+ <FooterButtons />
38
+ </div>
35
39
  </div>
36
40
  <div className="hidden lg:flex items-center justify-end lg:w-2/5">
37
- <ModeToggle />
41
+ <ModeToggle />
38
42
  </div>
39
43
  </div>
40
44
  </footer>
@@ -79,9 +83,9 @@ export function MadeWith() {
79
83
  <span className="text-muted-foreground">Made with </span>
80
84
  <span className="text-primary">
81
85
  <Link href="https://www.docubook.pro" target="_blank" rel="noopener noreferrer" className="underline underline-offset-2 text-muted-foreground">
82
- DocuBook
86
+ DocuBook
83
87
  </Link>
84
- </span>
88
+ </span>
85
89
  </>
86
90
  );
87
91
  }
@@ -1,101 +1,73 @@
1
1
  "use client"
2
- import { useState } from "react";
3
- import {
4
- Sheet,
5
- SheetClose,
6
- SheetContent,
7
- SheetHeader,
8
- SheetTrigger,
9
- } from "@/components/ui/sheet";
10
- import { Logo, NavMenu } from "@/components/navbar";
11
- import { Button } from "@/components/ui/button";
12
- import { LayoutGrid, PanelLeftClose, PanelLeftOpen } from "lucide-react";
13
- import { DialogTitle, DialogDescription } from "@/components/ui/dialog";
14
- import { ScrollArea } from "@/components/ui/scroll-area";
15
- import DocsMenu from "@/components/docs-menu";
16
- import { ModeToggle } from "@/components/theme-toggle";
17
- import ContextPopover from "@/components/context-popover";
2
+ import { Sheet, SheetClose, SheetContent, SheetHeader, SheetTrigger } from "@/components/ui/sheet"
3
+ import { Logo, NavMenu } from "@/components/navbar"
4
+ import { Button } from "@/components/ui/button"
5
+ import { PanelRight } from "lucide-react"
6
+ import { DialogTitle, DialogDescription } from "@/components/ui/dialog"
7
+ import { ScrollArea } from "@/components/ui/scroll-area"
8
+ import DocsMenu from "@/components/DocsMenu"
9
+ import { ModeToggle } from "@/components/ThemeToggle"
10
+ import ContextPopover from "@/components/ContextPopover"
11
+ import Search from "@/components/SearchBox"
12
+ import GitHubButton from "@/components/Github"
18
13
 
19
- // Toggle Button Component
20
- export function ToggleButton({
21
- collapsed,
22
- onToggle
23
- }: {
24
- collapsed: boolean,
25
- onToggle: () => void
26
- }) {
14
+ export function Leftbar() {
27
15
  return (
28
- <div className="absolute top-0 right-0 py-6 z-10 -mt-4">
29
- <Button
30
- size="icon"
31
- variant="outline"
32
- className="cursor-pointer hover:bg-transparent hover:text-inherit border-none text-muted-foreground"
33
- onClick={onToggle}
34
- >
35
- {collapsed ? (
36
- <PanelLeftOpen size={18} />
37
- ) : (
38
- <PanelLeftClose size={18} />
39
- )}
40
- </Button>
41
- </div>
42
- )
43
- }
16
+ <aside className="sticky top-0 hidden h-screen w-[280px] shrink-0 flex-col lg:flex">
17
+ {/* Logo */}
18
+ <div className="flex h-14 shrink-0 items-center px-5">
19
+ <Logo />
20
+ </div>
44
21
 
45
- export function Leftbar() {
46
- const [collapsed, setCollapsed] = useState(false);
47
- const toggleCollapse = () => setCollapsed(prev => !prev);
22
+ <div className="flex shrink-0 items-center gap-2 px-4 pb-4">
23
+ <Search className="min-w-[250px] max-w-[250px]" />
24
+ </div>
48
25
 
49
- return (
50
- <aside
51
- className={`sticky lg:flex hidden top-16 h-[calc(100vh-4rem)] border-r bg-background transition-all duration-300
52
- ${collapsed ? "w-[24px]" : "w-[280px]"} flex flex-col pr-2`}
53
- >
54
- <ToggleButton collapsed={collapsed} onToggle={toggleCollapse} />
55
- {/* Scrollable Content */}
56
- <ScrollArea className="flex-1 px-0.5 pb-4">
57
- {!collapsed && (
58
- <div className="space-y-2">
59
- <ContextPopover />
60
- <DocsMenu />
61
- </div>
62
- )}
26
+ {/* Scrollable Navigation */}
27
+ <ScrollArea className="flex-1 px-4">
28
+ <div className="space-y-2">
29
+ <ContextPopover />
30
+ <DocsMenu />
31
+ </div>
63
32
  </ScrollArea>
33
+
34
+ {/* Bottom: Theme Toggle */}
35
+ <div className="flex px-4 py-3">
36
+ <ModeToggle />
37
+ </div>
64
38
  </aside>
65
- );
39
+ )
66
40
  }
67
41
 
68
42
  export function SheetLeftbar() {
69
43
  return (
70
44
  <Sheet>
71
45
  <SheetTrigger asChild>
72
- <Button variant="ghost" size="icon" className="max-lg:flex hidden">
73
- <LayoutGrid />
46
+ <Button variant="ghost" size="icon" className="hidden max-md:flex">
47
+ <PanelRight className="text-muted-foreground h-6 w-6 shrink-0" />
74
48
  </Button>
75
49
  </SheetTrigger>
76
- <SheetContent className="flex flex-col gap-4 px-0" side="left">
50
+ <SheetContent className="flex flex-col gap-4 px-0" side="right">
77
51
  <DialogTitle className="sr-only">Navigation Menu</DialogTitle>
78
52
  <DialogDescription className="sr-only">
79
53
  Main navigation menu with links to different sections
80
54
  </DialogDescription>
81
55
  <SheetHeader>
82
- <SheetClose className="px-5" asChild>
83
- <span className="px-2"><Logo /></span>
56
+ <SheetClose className="px-4" asChild>
57
+ <div className="flex items-center justify-between">
58
+ <GitHubButton />
59
+ <div className="mr-8">
60
+ <ModeToggle />
61
+ </div>
62
+ </div>
84
63
  </SheetClose>
85
64
  </SheetHeader>
86
65
  <div className="flex flex-col gap-4 overflow-y-auto">
87
- <div className="flex flex-col gap-2.5 mt-3 mx-2 px-5">
88
- <NavMenu isSheet />
89
- </div>
90
- <div className="mx-2 px-5 space-y-2">
91
- <ContextPopover />
92
- <DocsMenu isSheet />
93
- </div>
94
- <div className="flex w-2/4 px-5">
95
- <ModeToggle />
66
+ <div className="mx-2 mt-3 flex flex-col gap-2.5 px-5">
67
+ <NavMenu />
96
68
  </div>
97
69
  </div>
98
70
  </SheetContent>
99
71
  </Sheet>
100
- );
72
+ )
101
73
  }
@@ -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 "@/components/AccordionContext";
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 '@/components/AccordionContext';
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
  };