@docubook/create 2.4.0 → 2.5.0

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 (43) 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/{docs-breadcrumb.tsx → DocsBreadcrumb.tsx} +1 -1
  8. package/src/dist/components/DocsNavbar.tsx +46 -0
  9. package/src/dist/components/DocsSidebar.tsx +196 -0
  10. package/src/dist/components/Github.tsx +26 -0
  11. package/src/dist/components/{scroll-to-top.tsx → ScrollToTop.tsx} +16 -9
  12. package/src/dist/components/SearchBox.tsx +37 -0
  13. package/src/dist/components/SearchContext.tsx +47 -0
  14. package/src/dist/components/SearchModal.tsx +1 -1
  15. package/src/dist/components/SearchTrigger.tsx +5 -5
  16. package/src/dist/components/Sponsor.tsx +2 -2
  17. package/src/dist/components/{theme-toggle.tsx → ThemeToggle.tsx} +10 -10
  18. package/src/dist/components/TocObserver.tsx +197 -0
  19. package/src/dist/components/footer.tsx +16 -12
  20. package/src/dist/components/leftbar.tsx +45 -73
  21. package/src/dist/components/markdown/AccordionGroupMdx.tsx +1 -1
  22. package/src/dist/components/markdown/AccordionMdx.tsx +1 -1
  23. package/src/dist/components/navbar.tsx +130 -53
  24. package/src/dist/components/toc.tsx +16 -14
  25. package/src/dist/components/typography.tsx +1 -1
  26. package/src/dist/components/ui/icon-cloud.tsx +353 -0
  27. package/src/dist/components/ui/scroll-area.tsx +2 -2
  28. package/src/dist/components/ui/sheet.tsx +4 -4
  29. package/src/dist/components/ui/toggle.tsx +3 -3
  30. package/src/dist/hooks/useActiveSection.ts +34 -32
  31. package/src/dist/hooks/useScrollPosition.ts +16 -14
  32. package/src/dist/package.json +1 -1
  33. package/src/dist/styles/algolia.css +11 -9
  34. package/src/dist/styles/{syntax.css → override.css} +82 -39
  35. package/src/dist/tailwind.config.ts +11 -110
  36. package/src/dist/components/GithubStart.tsx +0 -44
  37. package/src/dist/components/mob-toc.tsx +0 -134
  38. package/src/dist/components/search.tsx +0 -55
  39. package/src/dist/components/toc-observer.tsx +0 -254
  40. /package/src/dist/components/{docs-menu.tsx → DocsMenu.tsx} +0 -0
  41. /package/src/dist/components/{edit-on-github.tsx → EditWithGithub.tsx} +0 -0
  42. /package/src/dist/components/{theme-provider.tsx → ThemeProvider.tsx} +0 -0
  43. /package/src/dist/{components/AccordionContext.ts → lib/accordion-context.ts} +0 -0
@@ -1,134 +0,0 @@
1
- "use client";
2
-
3
- import { List, ChevronDown, ChevronUp } from "lucide-react";
4
- import TocObserver from "./toc-observer";
5
- import * as React from "react";
6
- import { useRef, useMemo } from "react";
7
- import { usePathname } from "next/navigation";
8
- import { Button } from "./ui/button";
9
- import { motion, AnimatePresence } from "framer-motion";
10
- import { useActiveSection } from "@/hooks";
11
- import { TocItem } from "@/lib/toc";
12
-
13
- interface MobTocProps {
14
- tocs: TocItem[];
15
- }
16
-
17
- const useClickOutside = (ref: React.RefObject<HTMLElement | null>, callback: () => void) => {
18
- const handleClick = React.useCallback((event: MouseEvent) => {
19
- if (ref.current && !ref.current.contains(event.target as Node)) {
20
- callback();
21
- }
22
- }, [ref, callback]);
23
-
24
- React.useEffect(() => {
25
- document.addEventListener('mousedown', handleClick);
26
- return () => {
27
- document.removeEventListener('mousedown', handleClick);
28
- };
29
- }, [handleClick]);
30
- };
31
-
32
- export default function MobToc({ tocs }: MobTocProps) {
33
- const pathname = usePathname();
34
- const [isExpanded, setIsExpanded] = React.useState(false);
35
- const tocRef = useRef<HTMLDivElement>(null);
36
- const contentRef = useRef<HTMLDivElement>(null);
37
-
38
- // Use custom hooks
39
- const { activeId, setActiveId } = useActiveSection(tocs);
40
-
41
- // Only show on /docs pages
42
- const isDocsPage = useMemo(() => pathname?.startsWith('/docs'), [pathname]);
43
-
44
- const [mounted, setMounted] = React.useState(false);
45
-
46
- React.useEffect(() => {
47
- setMounted(true);
48
- }, []);
49
-
50
- // Toggle expanded state
51
- const toggleExpanded = React.useCallback((e: React.MouseEvent) => {
52
- e.stopPropagation();
53
- setIsExpanded(prev => !prev);
54
- }, []);
55
-
56
- // Close TOC when clicking outside
57
- useClickOutside(tocRef, () => {
58
- if (isExpanded) {
59
- setIsExpanded(false);
60
- }
61
- });
62
-
63
- // Handle body overflow when TOC is expanded
64
- React.useEffect(() => {
65
- if (isExpanded) {
66
- document.body.style.overflow = 'hidden';
67
- } else {
68
- document.body.style.overflow = '';
69
- }
70
-
71
- return () => {
72
- document.body.style.overflow = '';
73
- };
74
- }, [isExpanded]);
75
-
76
- // Don't render anything if not on docs page or no TOC items
77
- if (!isDocsPage || !tocs?.length || !mounted) return null;
78
-
79
- const chevronIcon = isExpanded ? (
80
- <ChevronUp className="w-4 h-4 text-muted-foreground flex-shrink-0" />
81
- ) : (
82
- <ChevronDown className="w-4 h-4 text-muted-foreground flex-shrink-0" />
83
- );
84
-
85
- return (
86
- <AnimatePresence>
87
- <motion.div
88
- ref={tocRef}
89
- className="lg:hidden fixed top-16 left-0 right-0 z-50"
90
- initial={{ y: -100, opacity: 0 }}
91
- animate={{ y: 0, opacity: 1 }}
92
- exit={{ y: -100, opacity: 0 }}
93
- transition={{ duration: 0.2, ease: 'easeInOut' }}
94
- >
95
- <div className="w-full bg-background/95 backdrop-blur-sm border-b border-stone-200 dark:border-stone-800 shadow-sm">
96
- <div className="sm:px-8 px-4 py-2">
97
- <Button
98
- variant="ghost"
99
- size="sm"
100
- className="w-full justify-between h-auto py-2 px-2 -mx-1 rounded-md hover:bg-transparent hover:text-inherit"
101
- onClick={toggleExpanded}
102
- aria-label={isExpanded ? 'Collapse table of contents' : 'Expand table of contents'}
103
- >
104
- <div className="flex items-center gap-2">
105
- <List className="w-4 h-4 text-muted-foreground flex-shrink-0" />
106
- <span className="font-medium text-sm">On this page</span>
107
- </div>
108
- {chevronIcon}
109
- </Button>
110
-
111
- <AnimatePresence>
112
- {isExpanded && (
113
- <motion.div
114
- ref={contentRef}
115
- className="mt-2 pb-2 max-h-[60vh] overflow-y-auto px-1 -mx-1"
116
- initial={{ opacity: 0, height: 0 }}
117
- animate={{ opacity: 1, height: 'auto' }}
118
- exit={{ opacity: 0, height: 0 }}
119
- transition={{ duration: 0.2, ease: 'easeInOut' }}
120
- >
121
- <TocObserver
122
- data={tocs}
123
- activeId={activeId}
124
- onActiveIdChange={setActiveId}
125
- />
126
- </motion.div>
127
- )}
128
- </AnimatePresence>
129
- </div>
130
- </div>
131
- </motion.div>
132
- </AnimatePresence>
133
- );
134
- }
@@ -1,55 +0,0 @@
1
- "use client";
2
-
3
- import { useState, useEffect } from "react";
4
- import { Dialog } from "@/components/ui/dialog";
5
- import { SearchTrigger } from "@/components/SearchTrigger";
6
- import { SearchModal } from "@/components/SearchModal";
7
- import DocSearchComponent from "@/components/DocSearch";
8
- import { DialogTrigger } from "@radix-ui/react-dialog";
9
-
10
- interface SearchProps {
11
- /**
12
- * Specify which search engine to use.
13
- * @default 'default'
14
- */
15
- type?: "default" | "algolia";
16
- }
17
-
18
- export default function Search({ type = "default" }: SearchProps) {
19
- const [isOpen, setIsOpen] = useState(false);
20
-
21
- // The useEffect below is ONLY for the 'default' type, which is correct.
22
- // DocSearch handles its own keyboard shortcut.
23
- useEffect(() => {
24
- if (type === 'default') {
25
- const handleKeyDown = (event: KeyboardEvent) => {
26
- if ((event.ctrlKey || event.metaKey) && event.key === "k") {
27
- event.preventDefault();
28
- setIsOpen((open) => !open);
29
- }
30
- };
31
-
32
- window.addEventListener("keydown", handleKeyDown);
33
- return () => {
34
- window.removeEventListener("keydown", handleKeyDown);
35
- };
36
- }
37
- }, [type]);
38
-
39
- if (type === "algolia") {
40
- // Just render the component without passing any state props
41
- return <DocSearchComponent />;
42
- }
43
-
44
- // Logic for 'default' search
45
- return (
46
- <div>
47
- <Dialog open={isOpen} onOpenChange={setIsOpen}>
48
- <DialogTrigger asChild>
49
- <SearchTrigger />
50
- </DialogTrigger>
51
- <SearchModal isOpen={isOpen} setIsOpen={setIsOpen} />
52
- </Dialog>
53
- </div>
54
- );
55
- }
@@ -1,254 +0,0 @@
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 "./scroll-to-top";
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 [internalActiveId, setInternalActiveId] = useState<string | null>(null);
22
- const observer = useRef<IntersectionObserver | null>(null);
23
- const [clickedId, setClickedId] = useState<string | null>(null);
24
- const itemRefs = useRef<Map<string, HTMLAnchorElement>>(new Map());
25
-
26
- // Use external activeId if provided, otherwise use internal state
27
- const activeId = externalActiveId !== undefined ? externalActiveId : internalActiveId;
28
- const setActiveId = onActiveIdChange || setInternalActiveId;
29
-
30
- // Handle intersection observer for auto-highlighting
31
- useEffect(() => {
32
- const handleIntersect = (entries: IntersectionObserverEntry[]) => {
33
- const visibleEntries = entries.filter(entry => entry.isIntersecting);
34
-
35
- // Find the most recently scrolled-into-view element
36
- const mostVisibleEntry = visibleEntries.reduce((prev, current) => {
37
- // Prefer the entry that's more visible or higher on the page
38
- const prevRatio = prev?.intersectionRatio || 0;
39
- const currentRatio = current.intersectionRatio;
40
-
41
- if (currentRatio > prevRatio) return current;
42
- if (currentRatio === prevRatio &&
43
- current.boundingClientRect.top < prev.boundingClientRect.top) {
44
- return current;
45
- }
46
- return prev;
47
- }, visibleEntries[0]);
48
-
49
- if (mostVisibleEntry && !clickedId) {
50
- const newActiveId = mostVisibleEntry.target.id;
51
- if (newActiveId !== activeId) {
52
- setActiveId(newActiveId);
53
- }
54
- }
55
- };
56
-
57
- observer.current = new IntersectionObserver(handleIntersect, {
58
- root: null,
59
- rootMargin: "-20% 0px -70% 0px", // Adjusted margins for better section detection
60
- threshold: [0, 0.1, 0.5, 0.9, 1], // Multiple thresholds for better accuracy
61
- });
62
-
63
- const elements = data.map((item) =>
64
- document.getElementById(item.href.slice(1))
65
- );
66
-
67
- elements.forEach((el) => {
68
- if (el && observer.current) {
69
- observer.current.observe(el);
70
- }
71
- });
72
-
73
- // Set initial active ID if none is set
74
- if (!activeId && elements[0]) {
75
- setActiveId(elements[0].id);
76
- }
77
-
78
- return () => {
79
- if (observer.current) {
80
- elements.forEach((el) => {
81
- if (el) {
82
- observer.current!.unobserve(el);
83
- }
84
- });
85
- }
86
- };
87
- }, [data, clickedId, activeId, setActiveId]);
88
-
89
- const handleLinkClick = useCallback((id: string) => {
90
- setClickedId(id);
91
- setActiveId(id);
92
-
93
- // Reset the clicked state after a delay to allow for smooth scrolling
94
- const timer = setTimeout(() => {
95
- setClickedId(null);
96
- }, 1000);
97
-
98
- return () => clearTimeout(timer);
99
- }, [setActiveId]);
100
-
101
- // Function to check if an item has children
102
- const hasChildren = (currentId: string, currentLevel: number) => {
103
- const currentIndex = data.findIndex(item => item.href.slice(1) === currentId);
104
- if (currentIndex === -1 || currentIndex === data.length - 1) return false;
105
-
106
- const nextItem = data[currentIndex + 1];
107
- return nextItem.level > currentLevel;
108
- };
109
-
110
- // Calculate scroll progress for the active section
111
- const [scrollProgress, setScrollProgress] = useState(0);
112
-
113
- useEffect(() => {
114
- const handleScroll = () => {
115
- if (!activeId) return;
116
-
117
- const activeElement = document.getElementById(activeId);
118
- if (!activeElement) return;
119
-
120
- const rect = activeElement.getBoundingClientRect();
121
- const windowHeight = window.innerHeight;
122
- const elementTop = rect.top;
123
- const elementHeight = rect.height;
124
-
125
- // Calculate how much of the element is visible
126
- let progress = 0;
127
- if (elementTop < windowHeight) {
128
- progress = Math.min(1, (windowHeight - elementTop) / (windowHeight + elementHeight));
129
- }
130
-
131
- setScrollProgress(progress);
132
- };
133
-
134
- window.addEventListener('scroll', handleScroll, { passive: true });
135
- return () => window.removeEventListener('scroll', handleScroll);
136
- }, [activeId]);
137
-
138
-
139
- return (
140
- <div className="relative">
141
- <div className="relative text-sm text-foreground/70 hover:text-foreground transition-colors">
142
- <div className="flex flex-col gap-0">
143
- {data.map(({ href, level, text }, index) => {
144
- const id = href.slice(1);
145
- const isActive = activeId === id;
146
- const indent = level > 1 ? (level - 1) * 20 : 0;
147
- // Prefix with underscore to indicate intentionally unused
148
- const _isParent = hasChildren(id, level);
149
- const _isLastInLevel = index === data.length - 1 || data[index + 1].level <= level;
150
-
151
- return (
152
- <div key={href} className="relative">
153
- {/* Simple L-shaped connector */}
154
- {level > 1 && (
155
- <div
156
- className={clsx("absolute top-0 h-full w-6", {
157
- "left-[6px]": indent === 20, // Level 2
158
- "left-[22px]": indent === 40, // Level 3
159
- "left-[38px]": indent === 60, // Level 4
160
- })}
161
- >
162
- {/* Vertical line */}
163
- <div className={clsx(
164
- "absolute left-0 top-0 h-full w-px",
165
- isActive ? "bg-primary/20 dark:bg-primary/30" : "bg-border/50 dark:bg-border/50"
166
- )}>
167
- {isActive && (
168
- <motion.div
169
- className="absolute left-0 top-0 w-full h-full bg-primary origin-top"
170
- initial={{ scaleY: 0 }}
171
- animate={{ scaleY: scrollProgress }}
172
- transition={{ duration: 0.3 }}
173
- />
174
- )}
175
- </div>
176
-
177
- {/* Horizontal line */}
178
- <div className={clsx(
179
- "absolute left-0 top-1/2 h-px w-6",
180
- isActive ? "bg-primary/20 dark:bg-primary/30" : "bg-border/50 dark:bg-border/50"
181
- )}>
182
- {isActive && (
183
- <motion.div
184
- className="absolute left-0 top-0 h-full w-full bg-primary dark:bg-accent origin-left"
185
- initial={{ scaleX: 0 }}
186
- animate={{ scaleX: scrollProgress }}
187
- transition={{ duration: 0.3, delay: 0.1 }}
188
- />
189
- )}
190
- </div>
191
- </div>
192
- )}
193
-
194
- <Link
195
- href={href}
196
- onClick={() => handleLinkClick(id)}
197
- className={clsx(
198
- "relative flex items-center py-2 transition-colors",
199
- {
200
- "text-primary dark:text-primary font-medium": isActive,
201
- "text-muted-foreground hover:text-foreground dark:hover:text-foreground/90": !isActive,
202
- }
203
- )}
204
- style={{
205
- paddingLeft: `${indent}px`,
206
- marginLeft: level > 1 ? '12px' : '0',
207
- }}
208
- ref={(el) => {
209
- const map = itemRefs.current;
210
- if (el) {
211
- map.set(id, el);
212
- } else {
213
- map.delete(id);
214
- }
215
- }}
216
- >
217
- {/* Circle indicator */}
218
- <div className="relative w-4 h-4 flex items-center justify-center flex-shrink-0">
219
- <div className={clsx(
220
- "w-1.5 h-1.5 rounded-full transition-all duration-300 relative z-10",
221
- {
222
- "bg-primary scale-100 dark:bg-primary/90": isActive,
223
- "bg-muted-foreground/30 dark:bg-muted-foreground/30 scale-75 group-hover:scale-100 group-hover:bg-primary/50 dark:group-hover:bg-primary/50": !isActive,
224
- }
225
- )}>
226
- {isActive && (
227
- <motion.div
228
- className="absolute inset-0 rounded-full bg-primary/20 dark:bg-primary/30"
229
- initial={{ scale: 1 }}
230
- animate={{ scale: 1.8 }}
231
- transition={{
232
- duration: 2,
233
- repeat: Infinity,
234
- repeatType: "reverse"
235
- }}
236
- />
237
- )}
238
- </div>
239
- </div>
240
-
241
- <span className="truncate text-sm">
242
- {text}
243
- </span>
244
- </Link>
245
- </div>
246
- );
247
- })}
248
- </div>
249
- </div>
250
- {/* Add scroll to top link at the bottom of TOC */}
251
- <ScrollToTop className="mt-6" />
252
- </div>
253
- );
254
- }