doccupine 0.0.110 → 0.0.111
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/dist/templates/components/DocsSideBar.d.ts +1 -1
- package/dist/templates/components/DocsSideBar.js +7 -2
- package/dist/templates/components/SideBar.d.ts +1 -1
- package/dist/templates/components/SideBar.js +9 -1
- package/dist/templates/eslint.config.d.ts +1 -1
- package/dist/templates/eslint.config.js +70 -25
- package/dist/templates/package.js +14 -6
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const docsSideBarTemplate = "\"use client\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { Space } from \"cherry-styled-components\";\nimport {\n StyledIndexSidebar,\n StyledIndexSidebarLink,\n StyledIndexSidebarLabel,\n StyledIndexSidebarLi,\n} from \"@/components/layout/DocsComponents\";\n\ninterface Heading {\n id: string;\n text: string;\n level: number;\n}\n\nconst FALLBACK_OFFSET = 60;\n\nfunction getOffset() {\n const header = document.getElementById(\"header\");\n return (header ? header.offsetHeight : FALLBACK_OFFSET) + 20;\n}\n\nexport function DocsSideBar({ headings }: { headings: Heading[] }) {\n const [activeId, setActiveId] = useState<string>(\"\");\n const activeRef = useRef<HTMLLIElement>(null);\n\n const handleScroll = useCallback(() => {\n if (headings.length === 0) return;\n\n const headingElements = headings\n .map((heading) => document.getElementById(heading.id))\n .filter((el): el is HTMLElement => el !== null);\n\n if (headingElements.length === 0) return;\n\n const windowHeight = window.innerHeight;\n\n const visibleHeadings = headingElements.filter((element) => {\n const rect = element.getBoundingClientRect();\n const elementTop = rect.top;\n const elementBottom = rect.bottom;\n return elementTop < windowHeight && elementBottom > -50;\n });\n\n if (visibleHeadings.length > 0) {\n let closestHeading = visibleHeadings[0];\n let closestDistance = Math.abs(\n closestHeading.getBoundingClientRect().top - getOffset(),\n );\n for (const heading of visibleHeadings) {\n const distance = Math.abs(\n heading.getBoundingClientRect().top - getOffset(),\n );\n if (\n distance < closestDistance &&\n heading.getBoundingClientRect().top <= windowHeight * 0.3\n ) {\n closestDistance = distance;\n closestHeading = heading;\n }\n }\n setActiveId(closestHeading.id);\n return;\n }\n\n let currentActiveId = headings[0].id;\n for (const element of headingElements) {\n const rect = element.getBoundingClientRect();\n if (rect.top <= getOffset()) {\n currentActiveId = element.id;\n } else {\n break;\n }\n }\n setActiveId(currentActiveId);\n }, [headings]);\n\n useEffect(() => {\n if (headings.length === 0) return;\n // Set active heading from URL hash immediately on mount (deferred to next frame)\n const hashId = window.location.hash ? window.location.hash.slice(1) : null;\n // Run initial scroll check on next frame to avoid synchronous setState in effect\n const rafId = requestAnimationFrame(() => {\n if (hashId) {\n setActiveId(hashId);\n }\n handleScroll();\n });\n // Re-check after browser finishes scrolling to hash target on new page load\n const delayedId = setTimeout(handleScroll, 300);\n let timeoutId: NodeJS.Timeout;\n const throttledHandleScroll = () => {\n clearTimeout(timeoutId);\n timeoutId = setTimeout(handleScroll, 50);\n };\n window.addEventListener(\"scroll\", throttledHandleScroll);\n window.addEventListener(\"resize\", handleScroll);\n return () => {\n window.removeEventListener(\"scroll\", throttledHandleScroll);\n window.removeEventListener(\"resize\", handleScroll);\n cancelAnimationFrame(rafId);\n clearTimeout(delayedId);\n clearTimeout(timeoutId);\n };\n }, [handleScroll, headings]);\n\n useEffect(() => {\n const el = activeRef.current;\n const container = el?.closest(\"[data-sidebar]\") as HTMLElement | null;\n if (!el || !container) return;\n const elRect = el.getBoundingClientRect();\n const cRect = container.getBoundingClientRect();\n const pad = 140;\n if (elRect.bottom + pad > cRect.bottom) {\n container.scrollBy({\n top: elRect.bottom - cRect.bottom + pad,\n behavior
|
|
1
|
+
export declare const docsSideBarTemplate = "\"use client\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { Space } from \"cherry-styled-components\";\nimport {\n StyledIndexSidebar,\n StyledIndexSidebarLink,\n StyledIndexSidebarLabel,\n StyledIndexSidebarLi,\n} from \"@/components/layout/DocsComponents\";\n\ninterface Heading {\n id: string;\n text: string;\n level: number;\n}\n\nconst FALLBACK_OFFSET = 60;\n\nfunction getOffset() {\n const header = document.getElementById(\"header\");\n return (header ? header.offsetHeight : FALLBACK_OFFSET) + 20;\n}\n\nexport function DocsSideBar({ headings }: { headings: Heading[] }) {\n const [activeId, setActiveId] = useState<string>(\"\");\n const activeRef = useRef<HTMLLIElement>(null);\n\n const handleScroll = useCallback(() => {\n if (headings.length === 0) return;\n\n const headingElements = headings\n .map((heading) => document.getElementById(heading.id))\n .filter((el): el is HTMLElement => el !== null);\n\n if (headingElements.length === 0) return;\n\n const windowHeight = window.innerHeight;\n\n const visibleHeadings = headingElements.filter((element) => {\n const rect = element.getBoundingClientRect();\n const elementTop = rect.top;\n const elementBottom = rect.bottom;\n return elementTop < windowHeight && elementBottom > -50;\n });\n\n if (visibleHeadings.length > 0) {\n let closestHeading = visibleHeadings[0];\n let closestDistance = Math.abs(\n closestHeading.getBoundingClientRect().top - getOffset(),\n );\n for (const heading of visibleHeadings) {\n const distance = Math.abs(\n heading.getBoundingClientRect().top - getOffset(),\n );\n if (\n distance < closestDistance &&\n heading.getBoundingClientRect().top <= windowHeight * 0.3\n ) {\n closestDistance = distance;\n closestHeading = heading;\n }\n }\n setActiveId(closestHeading.id);\n return;\n }\n\n let currentActiveId = headings[0].id;\n for (const element of headingElements) {\n const rect = element.getBoundingClientRect();\n if (rect.top <= getOffset()) {\n currentActiveId = element.id;\n } else {\n break;\n }\n }\n setActiveId(currentActiveId);\n }, [headings]);\n\n useEffect(() => {\n if (headings.length === 0) return;\n // Set active heading from URL hash immediately on mount (deferred to next frame)\n const hashId = window.location.hash ? window.location.hash.slice(1) : null;\n // Run initial scroll check on next frame to avoid synchronous setState in effect\n const rafId = requestAnimationFrame(() => {\n if (hashId) {\n setActiveId(hashId);\n }\n handleScroll();\n });\n // Re-check after browser finishes scrolling to hash target on new page load\n const delayedId = setTimeout(handleScroll, 300);\n let timeoutId: NodeJS.Timeout;\n const throttledHandleScroll = () => {\n clearTimeout(timeoutId);\n timeoutId = setTimeout(handleScroll, 50);\n };\n window.addEventListener(\"scroll\", throttledHandleScroll);\n window.addEventListener(\"resize\", handleScroll);\n return () => {\n window.removeEventListener(\"scroll\", throttledHandleScroll);\n window.removeEventListener(\"resize\", handleScroll);\n cancelAnimationFrame(rafId);\n clearTimeout(delayedId);\n clearTimeout(timeoutId);\n };\n }, [handleScroll, headings]);\n\n useEffect(() => {\n const el = activeRef.current;\n const container = el?.closest(\"[data-sidebar]\") as HTMLElement | null;\n if (!el || !container) return;\n const elRect = el.getBoundingClientRect();\n const cRect = container.getBoundingClientRect();\n const pad = 140;\n // Animate the scroll, but honor users who opt out of motion.\n const behavior = window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n .matches\n ? \"auto\"\n : \"smooth\";\n if (elRect.bottom + pad > cRect.bottom) {\n container.scrollBy({\n top: elRect.bottom - cRect.bottom + pad,\n behavior,\n });\n } else if (elRect.top - pad < cRect.top) {\n container.scrollBy({\n top: elRect.top - cRect.top - pad,\n behavior,\n });\n }\n }, [activeId]);\n\n const handleHeadingClick = (headingId: string) => {\n const element = document.getElementById(headingId);\n if (element) {\n const elementPosition =\n element.getBoundingClientRect().top + window.scrollY;\n window.scrollTo({\n top: elementPosition - getOffset(),\n behavior: \"smooth\",\n });\n }\n };\n\n return (\n <StyledIndexSidebar data-sidebar>\n {headings?.length > 0 && (\n <li aria-hidden=\"true\">\n <StyledIndexSidebarLabel>On this page</StyledIndexSidebarLabel>\n <Space $size={15} />\n </li>\n )}\n {headings.map((heading, index) => (\n <StyledIndexSidebarLi\n key={index}\n ref={activeId === heading.id ? activeRef : null}\n $isActive={activeId === heading.id}\n style={{ paddingLeft: `${(heading.level - 1) * 16}px` }}\n >\n <StyledIndexSidebarLink\n href={`#${heading.id}`}\n onClick={(e) => {\n e.preventDefault();\n handleHeadingClick(heading.id);\n }}\n $isActive={activeId === heading.id}\n >\n {heading.text}\n </StyledIndexSidebarLink>\n </StyledIndexSidebarLi>\n ))}\n </StyledIndexSidebar>\n );\n}\n";
|
|
@@ -112,15 +112,20 @@ export function DocsSideBar({ headings }: { headings: Heading[] }) {
|
|
|
112
112
|
const elRect = el.getBoundingClientRect();
|
|
113
113
|
const cRect = container.getBoundingClientRect();
|
|
114
114
|
const pad = 140;
|
|
115
|
+
// Animate the scroll, but honor users who opt out of motion.
|
|
116
|
+
const behavior = window.matchMedia("(prefers-reduced-motion: reduce)")
|
|
117
|
+
.matches
|
|
118
|
+
? "auto"
|
|
119
|
+
: "smooth";
|
|
115
120
|
if (elRect.bottom + pad > cRect.bottom) {
|
|
116
121
|
container.scrollBy({
|
|
117
122
|
top: elRect.bottom - cRect.bottom + pad,
|
|
118
|
-
behavior
|
|
123
|
+
behavior,
|
|
119
124
|
});
|
|
120
125
|
} else if (elRect.top - pad < cRect.top) {
|
|
121
126
|
container.scrollBy({
|
|
122
127
|
top: elRect.top - cRect.top - pad,
|
|
123
|
-
behavior
|
|
128
|
+
behavior,
|
|
124
129
|
});
|
|
125
130
|
}
|
|
126
131
|
}, [activeId]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const sideBarTemplate = "\"use client\";\nimport { useContext, useEffect, useRef, useState } from \"react\";\nimport { usePathname } from \"next/navigation\";\nimport { Flex, Space, ThemeToggle } from \"cherry-styled-components\";\nimport {\n DocsSidebar,\n SectionBarContext,\n StyledSidebar,\n StyledSidebarList,\n StyledSidebarListItem,\n StyledStrong,\n StyledSidebarListItemLink,\n StyledSidebarGroupButton,\n StyledSidebarGroupRow,\n StyledSidebarGroupLink,\n StyledSidebarGroupChevron,\n StyledSidebarGroupContent,\n StyledSidebarFooter,\n StyleMobileBar,\n StyledMobileBurger,\n} from \"@/components/layout/DocsComponents\";\nimport { Icon } from \"@/components/layout/Icon\";\nimport { useLockBodyScroll } from \"@/components/LockBodyScroll\";\n\n// A link can be a leaf (slug + title) or a group with nested children. Both the\n// category icon and the per-link icon are optional Lucide names.\ntype NavItemLink = {\n slug?: string;\n title: string;\n icon?: string;\n links?: NavItemLink[];\n};\n\ntype NavItem = {\n label: string;\n icon?: string;\n links: NavItemLink[];\n};\n\ninterface SideBarProps {\n result: NavItem[];\n}\n\nfunction linkContainsActivePath(link: NavItemLink, pathname: string): boolean {\n if (link.slug !== undefined && pathname === `/${link.slug}`) {\n return true;\n }\n return (link.links ?? []).some((child) =>\n linkContainsActivePath(child, pathname),\n );\n}\n\nfunction SidebarNavLink({\n link,\n depth,\n pathname,\n onNavigate,\n}: {\n link: NavItemLink;\n depth: number;\n pathname: string;\n onNavigate: () => void;\n}) {\n const children = link.links ?? [];\n const hasChildren = children.length > 0;\n const href = link.slug !== undefined ? `/${link.slug}` : undefined;\n const isActive = href !== undefined && pathname === href;\n const indent = { paddingLeft: `${20 + depth * 14}px` };\n\n // Open collapsible groups that contain the active page so deep links land\n // with their ancestors already expanded.\n const [isOpen, setIsOpen] = useState(\n hasChildren\n ? children.some((child) => linkContainsActivePath(child, pathname))\n : false,\n );\n\n if (!hasChildren) {\n return (\n <StyledSidebarListItem>\n <StyledSidebarListItemLink\n href={href ?? \"#\"}\n $isActive={isActive}\n aria-current={isActive ? \"page\" : undefined}\n onClick={onNavigate}\n style={indent}\n >\n {link.icon && <Icon name={link.icon} size={16} />}\n {link.title}\n </StyledSidebarListItemLink>\n </StyledSidebarListItem>\n );\n }\n\n const toggle = () => setIsOpen((prev) => !prev);\n const toggleLabel = isOpen\n ? `Collapse ${link.title}`\n : `Expand ${link.title}`;\n const groupActive = linkContainsActivePath(link, pathname);\n\n return (\n <li>\n {href !== undefined ? (\n <StyledSidebarGroupRow\n $isActive={groupActive}\n $isOpen={isOpen}\n style={indent}\n >\n <StyledSidebarGroupLink\n href={href}\n aria-current={pathname === href ? \"page\" : undefined}\n onClick={onNavigate}\n >\n {link.icon && <Icon name={link.icon} size={16} />}\n {link.title}\n </StyledSidebarGroupLink>\n <StyledSidebarGroupChevron\n type=\"button\"\n onClick={toggle}\n aria-expanded={isOpen}\n aria-label={toggleLabel}\n >\n <Icon name=\"chevron-right\" size={16} />\n </StyledSidebarGroupChevron>\n </StyledSidebarGroupRow>\n ) : (\n <StyledSidebarGroupButton\n type=\"button\"\n onClick={toggle}\n $isActive={groupActive}\n $isOpen={isOpen}\n style={indent}\n aria-expanded={isOpen}\n aria-label={toggleLabel}\n >\n {link.icon && <Icon name={link.icon} size={16} />}\n {link.title}\n <Icon name=\"chevron-right\" size={16} />\n </StyledSidebarGroupButton>\n )}\n <StyledSidebarGroupContent $isOpen={isOpen}>\n {children.map((child: NavItemLink, index: number) => (\n <SidebarNavLink\n key={index}\n link={child}\n depth={depth + 1}\n pathname={pathname}\n onNavigate={onNavigate}\n />\n ))}\n </StyledSidebarGroupContent>\n </li>\n );\n}\n\nfunction SideBar({ result }: SideBarProps) {\n const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);\n const hasSectionBar = useContext(SectionBarContext);\n const pathname = usePathname();\n const navRef = useRef<HTMLElement>(null);\n const footerRef = useRef<HTMLDivElement>(null);\n\n useLockBodyScroll(isMobileMenuOpen);\n\n // Bring the current page's link into view within the sidebar's own scroll\n // area when it starts off-screen (deep pages, in-content links, search).\n // Scoped to the nav via scrollTo so the main document never jumps.\n useEffect(() => {\n const nav = navRef.current;\n if (!nav) return;\n const active = nav.querySelector<HTMLElement>('[aria-current=\"page\"]');\n if (!active) return;\n\n const navRect = nav.getBoundingClientRect();\n const activeRect = active.getBoundingClientRect();\n\n // The theme-toggle footer is sticky over the bottom of the scroll area\n // (~60px tall), so links underneath it are covered rather than visible.\n // Clamp the visible bottom to the footer's top edge when it's pinned.\n const footerRect = footerRef.current?.getBoundingClientRect();\n const visibleBottom = footerRect\n ? Math.min(navRect.bottom, footerRect.top)\n : navRect.bottom;\n\n const isOutOfView =\n activeRect.top < navRect.top || activeRect.bottom > visibleBottom;\n if (!isOutOfView) return;\n\n // Center within the visible band (nav top .. footer top), not the full\n // nav height, so the link never settles behind the footer.\n const visibleHeight = visibleBottom - navRect.top;\n const target =\n nav.scrollTop +\n (activeRect.top - navRect.top) -\n (visibleHeight - active.clientHeight) / 2;\n nav.scrollTo({
|
|
1
|
+
export declare const sideBarTemplate = "\"use client\";\nimport { useContext, useEffect, useRef, useState } from \"react\";\nimport { usePathname } from \"next/navigation\";\nimport { Flex, Space, ThemeToggle } from \"cherry-styled-components\";\nimport {\n DocsSidebar,\n SectionBarContext,\n StyledSidebar,\n StyledSidebarList,\n StyledSidebarListItem,\n StyledStrong,\n StyledSidebarListItemLink,\n StyledSidebarGroupButton,\n StyledSidebarGroupRow,\n StyledSidebarGroupLink,\n StyledSidebarGroupChevron,\n StyledSidebarGroupContent,\n StyledSidebarFooter,\n StyleMobileBar,\n StyledMobileBurger,\n} from \"@/components/layout/DocsComponents\";\nimport { Icon } from \"@/components/layout/Icon\";\nimport { useLockBodyScroll } from \"@/components/LockBodyScroll\";\n\n// A link can be a leaf (slug + title) or a group with nested children. Both the\n// category icon and the per-link icon are optional Lucide names.\ntype NavItemLink = {\n slug?: string;\n title: string;\n icon?: string;\n links?: NavItemLink[];\n};\n\ntype NavItem = {\n label: string;\n icon?: string;\n links: NavItemLink[];\n};\n\ninterface SideBarProps {\n result: NavItem[];\n}\n\nfunction linkContainsActivePath(link: NavItemLink, pathname: string): boolean {\n if (link.slug !== undefined && pathname === `/${link.slug}`) {\n return true;\n }\n return (link.links ?? []).some((child) =>\n linkContainsActivePath(child, pathname),\n );\n}\n\nfunction SidebarNavLink({\n link,\n depth,\n pathname,\n onNavigate,\n}: {\n link: NavItemLink;\n depth: number;\n pathname: string;\n onNavigate: () => void;\n}) {\n const children = link.links ?? [];\n const hasChildren = children.length > 0;\n const href = link.slug !== undefined ? `/${link.slug}` : undefined;\n const isActive = href !== undefined && pathname === href;\n const indent = { paddingLeft: `${20 + depth * 14}px` };\n\n // Open collapsible groups that contain the active page so deep links land\n // with their ancestors already expanded.\n const [isOpen, setIsOpen] = useState(\n hasChildren\n ? children.some((child) => linkContainsActivePath(child, pathname))\n : false,\n );\n\n if (!hasChildren) {\n return (\n <StyledSidebarListItem>\n <StyledSidebarListItemLink\n href={href ?? \"#\"}\n $isActive={isActive}\n aria-current={isActive ? \"page\" : undefined}\n onClick={onNavigate}\n style={indent}\n >\n {link.icon && <Icon name={link.icon} size={16} />}\n {link.title}\n </StyledSidebarListItemLink>\n </StyledSidebarListItem>\n );\n }\n\n const toggle = () => setIsOpen((prev) => !prev);\n const toggleLabel = isOpen\n ? `Collapse ${link.title}`\n : `Expand ${link.title}`;\n const groupActive = linkContainsActivePath(link, pathname);\n\n return (\n <li>\n {href !== undefined ? (\n <StyledSidebarGroupRow\n $isActive={groupActive}\n $isOpen={isOpen}\n style={indent}\n >\n <StyledSidebarGroupLink\n href={href}\n aria-current={pathname === href ? \"page\" : undefined}\n onClick={onNavigate}\n >\n {link.icon && <Icon name={link.icon} size={16} />}\n {link.title}\n </StyledSidebarGroupLink>\n <StyledSidebarGroupChevron\n type=\"button\"\n onClick={toggle}\n aria-expanded={isOpen}\n aria-label={toggleLabel}\n >\n <Icon name=\"chevron-right\" size={16} />\n </StyledSidebarGroupChevron>\n </StyledSidebarGroupRow>\n ) : (\n <StyledSidebarGroupButton\n type=\"button\"\n onClick={toggle}\n $isActive={groupActive}\n $isOpen={isOpen}\n style={indent}\n aria-expanded={isOpen}\n aria-label={toggleLabel}\n >\n {link.icon && <Icon name={link.icon} size={16} />}\n {link.title}\n <Icon name=\"chevron-right\" size={16} />\n </StyledSidebarGroupButton>\n )}\n <StyledSidebarGroupContent $isOpen={isOpen}>\n {children.map((child: NavItemLink, index: number) => (\n <SidebarNavLink\n key={index}\n link={child}\n depth={depth + 1}\n pathname={pathname}\n onNavigate={onNavigate}\n />\n ))}\n </StyledSidebarGroupContent>\n </li>\n );\n}\n\nfunction SideBar({ result }: SideBarProps) {\n const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);\n const hasSectionBar = useContext(SectionBarContext);\n const pathname = usePathname();\n const navRef = useRef<HTMLElement>(null);\n const footerRef = useRef<HTMLDivElement>(null);\n\n useLockBodyScroll(isMobileMenuOpen);\n\n // Bring the current page's link into view within the sidebar's own scroll\n // area when it starts off-screen (deep pages, in-content links, search).\n // Scoped to the nav via scrollTo so the main document never jumps.\n useEffect(() => {\n const nav = navRef.current;\n if (!nav) return;\n const active = nav.querySelector<HTMLElement>('[aria-current=\"page\"]');\n if (!active) return;\n\n const navRect = nav.getBoundingClientRect();\n const activeRect = active.getBoundingClientRect();\n\n // The theme-toggle footer is sticky over the bottom of the scroll area\n // (~60px tall), so links underneath it are covered rather than visible.\n // Clamp the visible bottom to the footer's top edge when it's pinned.\n const footerRect = footerRef.current?.getBoundingClientRect();\n const visibleBottom = footerRect\n ? Math.min(navRect.bottom, footerRect.top)\n : navRect.bottom;\n\n const isOutOfView =\n activeRect.top < navRect.top || activeRect.bottom > visibleBottom;\n if (!isOutOfView) return;\n\n // Center within the visible band (nav top .. footer top), not the full\n // nav height, so the link never settles behind the footer.\n const visibleHeight = visibleBottom - navRect.top;\n const target =\n nav.scrollTop +\n (activeRect.top - navRect.top) -\n (visibleHeight - active.clientHeight) / 2;\n\n // Animate the scroll, but honor users who opt out of motion.\n const prefersReducedMotion = window.matchMedia(\n \"(prefers-reduced-motion: reduce)\",\n ).matches;\n nav.scrollTo({\n top: Math.max(0, target),\n behavior: prefersReducedMotion ? \"auto\" : \"smooth\",\n });\n }, [pathname]);\n\n return (\n <DocsSidebar>\n <StyleMobileBar\n onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}\n $isActive={isMobileMenuOpen}\n aria-label={\n isMobileMenuOpen ? \"Close navigation menu\" : \"Open navigation menu\"\n }\n aria-expanded={isMobileMenuOpen}\n >\n <StyledMobileBurger $isActive={isMobileMenuOpen} />\n </StyleMobileBar>\n\n <StyledSidebar\n ref={navRef}\n $isActive={isMobileMenuOpen}\n $hasSectionBar={hasSectionBar}\n >\n {result &&\n result.map((item: NavItem, index: number) => {\n return (\n <StyledSidebarList key={index}>\n <StyledSidebarListItem>\n <StyledStrong>\n {item.icon && <Icon name={item.icon} size={16} />}\n {item.label}\n </StyledStrong>{\" \"}\n </StyledSidebarListItem>\n <li>\n <Space $size={20} />\n </li>\n {item.links &&\n item.links.map((link: NavItemLink, indexChild: number) => (\n <SidebarNavLink\n key={indexChild}\n link={link}\n depth={0}\n pathname={pathname}\n onNavigate={() => setIsMobileMenuOpen(false)}\n />\n ))}\n <li aria-hidden=\"true\">\n <Space $size={20} />\n </li>\n </StyledSidebarList>\n );\n })}\n <StyledSidebarFooter ref={footerRef}>\n <Flex $xsJustifyContent=\"flex-start\" $lgJustifyContent=\"flex-end\">\n <ThemeToggle />\n </Flex>\n </StyledSidebarFooter>\n </StyledSidebar>\n </DocsSidebar>\n );\n}\n\nexport { SideBar };\n";
|
|
@@ -193,7 +193,15 @@ function SideBar({ result }: SideBarProps) {
|
|
|
193
193
|
nav.scrollTop +
|
|
194
194
|
(activeRect.top - navRect.top) -
|
|
195
195
|
(visibleHeight - active.clientHeight) / 2;
|
|
196
|
-
|
|
196
|
+
|
|
197
|
+
// Animate the scroll, but honor users who opt out of motion.
|
|
198
|
+
const prefersReducedMotion = window.matchMedia(
|
|
199
|
+
"(prefers-reduced-motion: reduce)",
|
|
200
|
+
).matches;
|
|
201
|
+
nav.scrollTo({
|
|
202
|
+
top: Math.max(0, target),
|
|
203
|
+
behavior: prefersReducedMotion ? "auto" : "smooth",
|
|
204
|
+
});
|
|
197
205
|
}, [pathname]);
|
|
198
206
|
|
|
199
207
|
return (
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const eslintConfigTemplate = "import
|
|
1
|
+
export declare const eslintConfigTemplate = "import globals from \"globals\";\nimport nextPlugin from \"@next/eslint-plugin-next\";\nimport reactPlugin from \"eslint-plugin-react\";\nimport reactHooksPlugin from \"eslint-plugin-react-hooks\";\nimport jsxA11yPlugin from \"eslint-plugin-jsx-a11y\";\nimport importPlugin from \"eslint-plugin-import\";\nimport tsParser from \"@typescript-eslint/parser\";\nimport tsPlugin from \"@typescript-eslint/eslint-plugin\";\n\n// Hand-rolled replacement for `eslint-config-next`'s default config.\n//\n// We don't use eslint-config-next under ESLint 10: its React version detection\n// (`settings.react.version: \"detect\"`) calls `context.getFilename()`, which\n// ESLint 10 removed, so it crashes on load. Composing the same plugins\n// ourselves lets us pin `react.version` (below) and stay in control.\n//\n// The recommended rule sets below mirror what eslint-config-next enables:\n// React, React Hooks, Next.js and jsx-a11y. On top of that we keep a few\n// project-specific rules (no-console and two @typescript-eslint rules).\n\nconst config = [\n {\n // node_modules/ and .git/ are ignored by default.\n ignores: [\".next/**\", \"out/**\", \"build/**\", \"next-env.d.ts\"],\n },\n {\n name: \"doccupine/base\",\n files: [\"**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}\"],\n plugins: {\n react: reactPlugin,\n \"react-hooks\": reactHooksPlugin,\n import: importPlugin,\n \"jsx-a11y\": jsxA11yPlugin,\n \"@next/next\": nextPlugin,\n \"@typescript-eslint\": tsPlugin,\n },\n languageOptions: {\n parser: tsParser,\n parserOptions: {\n sourceType: \"module\",\n ecmaFeatures: { jsx: true },\n },\n globals: { ...globals.browser, ...globals.node },\n },\n settings: {\n // Pinned (not \"detect\") on purpose: eslint-plugin-react 7.37.x's version\n // detection calls the ESLint-10-removed `context.getFilename()`. Pinning\n // skips that code path. Keep roughly in sync with the installed React.\n react: { version: \"19.2\" },\n },\n rules: {\n ...reactPlugin.configs.recommended.rules,\n ...reactHooksPlugin.configs.recommended.rules,\n ...nextPlugin.configs.recommended.rules,\n \"import/no-anonymous-default-export\": \"warn\",\n \"react/no-unknown-property\": \"off\",\n \"react/react-in-jsx-scope\": \"off\",\n \"react/prop-types\": \"off\",\n \"jsx-a11y/alt-text\": [\"warn\", { elements: [\"img\"], img: [\"Image\"] }],\n \"jsx-a11y/aria-props\": \"warn\",\n \"jsx-a11y/aria-proptypes\": \"warn\",\n \"jsx-a11y/aria-unsupported-elements\": \"warn\",\n \"jsx-a11y/role-has-required-aria-props\": \"warn\",\n \"jsx-a11y/role-supports-aria-props\": \"warn\",\n \"react/jsx-no-target-blank\": \"off\",\n \"no-console\": [\"warn\", { allow: [\"warn\", \"error\"] }],\n \"@typescript-eslint/no-explicit-any\": \"warn\",\n \"@typescript-eslint/no-unused-vars\": [\n \"warn\",\n { argsIgnorePattern: \"^_\", varsIgnorePattern: \"^_\" },\n ],\n },\n },\n];\n\nexport default config;\n";
|
|
@@ -1,32 +1,77 @@
|
|
|
1
|
-
export const eslintConfigTemplate = `import
|
|
1
|
+
export const eslintConfigTemplate = `import globals from "globals";
|
|
2
|
+
import nextPlugin from "@next/eslint-plugin-next";
|
|
3
|
+
import reactPlugin from "eslint-plugin-react";
|
|
4
|
+
import reactHooksPlugin from "eslint-plugin-react-hooks";
|
|
5
|
+
import jsxA11yPlugin from "eslint-plugin-jsx-a11y";
|
|
6
|
+
import importPlugin from "eslint-plugin-import";
|
|
7
|
+
import tsParser from "@typescript-eslint/parser";
|
|
8
|
+
import tsPlugin from "@typescript-eslint/eslint-plugin";
|
|
2
9
|
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
)
|
|
10
|
+
// Hand-rolled replacement for \`eslint-config-next\`'s default config.
|
|
11
|
+
//
|
|
12
|
+
// We don't use eslint-config-next under ESLint 10: its React version detection
|
|
13
|
+
// (\`settings.react.version: "detect"\`) calls \`context.getFilename()\`, which
|
|
14
|
+
// ESLint 10 removed, so it crashes on load. Composing the same plugins
|
|
15
|
+
// ourselves lets us pin \`react.version\` (below) and stay in control.
|
|
16
|
+
//
|
|
17
|
+
// The recommended rule sets below mirror what eslint-config-next enables:
|
|
18
|
+
// React, React Hooks, Next.js and jsx-a11y. On top of that we keep a few
|
|
19
|
+
// project-specific rules (no-console and two @typescript-eslint rules).
|
|
7
20
|
|
|
8
|
-
const config =
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
const config = [
|
|
22
|
+
{
|
|
23
|
+
// node_modules/ and .git/ are ignored by default.
|
|
24
|
+
ignores: [".next/**", "out/**", "build/**", "next-env.d.ts"],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "doccupine/base",
|
|
28
|
+
files: ["**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}"],
|
|
29
|
+
plugins: {
|
|
30
|
+
react: reactPlugin,
|
|
31
|
+
"react-hooks": reactHooksPlugin,
|
|
32
|
+
import: importPlugin,
|
|
33
|
+
"jsx-a11y": jsxA11yPlugin,
|
|
34
|
+
"@next/next": nextPlugin,
|
|
35
|
+
"@typescript-eslint": tsPlugin,
|
|
36
|
+
},
|
|
37
|
+
languageOptions: {
|
|
38
|
+
parser: tsParser,
|
|
39
|
+
parserOptions: {
|
|
40
|
+
sourceType: "module",
|
|
41
|
+
ecmaFeatures: { jsx: true },
|
|
19
42
|
},
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
43
|
+
globals: { ...globals.browser, ...globals.node },
|
|
44
|
+
},
|
|
45
|
+
settings: {
|
|
46
|
+
// Pinned (not "detect") on purpose: eslint-plugin-react 7.37.x's version
|
|
47
|
+
// detection calls the ESLint-10-removed \`context.getFilename()\`. Pinning
|
|
48
|
+
// skips that code path. Keep roughly in sync with the installed React.
|
|
49
|
+
react: { version: "19.2" },
|
|
50
|
+
},
|
|
51
|
+
rules: {
|
|
52
|
+
...reactPlugin.configs.recommended.rules,
|
|
53
|
+
...reactHooksPlugin.configs.recommended.rules,
|
|
54
|
+
...nextPlugin.configs.recommended.rules,
|
|
55
|
+
"import/no-anonymous-default-export": "warn",
|
|
56
|
+
"react/no-unknown-property": "off",
|
|
57
|
+
"react/react-in-jsx-scope": "off",
|
|
58
|
+
"react/prop-types": "off",
|
|
59
|
+
"jsx-a11y/alt-text": ["warn", { elements: ["img"], img: ["Image"] }],
|
|
60
|
+
"jsx-a11y/aria-props": "warn",
|
|
61
|
+
"jsx-a11y/aria-proptypes": "warn",
|
|
62
|
+
"jsx-a11y/aria-unsupported-elements": "warn",
|
|
63
|
+
"jsx-a11y/role-has-required-aria-props": "warn",
|
|
64
|
+
"jsx-a11y/role-supports-aria-props": "warn",
|
|
65
|
+
"react/jsx-no-target-blank": "off",
|
|
66
|
+
"no-console": ["warn", { allow: ["warn", "error"] }],
|
|
67
|
+
"@typescript-eslint/no-explicit-any": "warn",
|
|
68
|
+
"@typescript-eslint/no-unused-vars": [
|
|
69
|
+
"warn",
|
|
70
|
+
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
28
73
|
},
|
|
29
|
-
|
|
74
|
+
];
|
|
30
75
|
|
|
31
76
|
export default config;
|
|
32
77
|
`;
|
|
@@ -8,6 +8,7 @@ export const packageJsonTemplate = JSON.stringify({
|
|
|
8
8
|
start: "next start",
|
|
9
9
|
lint: "eslint .",
|
|
10
10
|
format: "prettier --write .",
|
|
11
|
+
"type-check": "tsgo --noEmit",
|
|
11
12
|
},
|
|
12
13
|
dependencies: {
|
|
13
14
|
"@langchain/anthropic": "^1.5.1",
|
|
@@ -17,13 +18,13 @@ export const packageJsonTemplate = JSON.stringify({
|
|
|
17
18
|
"@mdx-js/react": "^3.1.1",
|
|
18
19
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
19
20
|
"@posthog/react": "^1.10.3",
|
|
20
|
-
"cherry-styled-components": "^0.2.
|
|
21
|
+
"cherry-styled-components": "^0.2.10",
|
|
21
22
|
langchain: "^1.5.2",
|
|
22
23
|
"lucide-react": "^1.23.0",
|
|
23
24
|
minisearch: "^7.2.0",
|
|
24
25
|
next: "16.2.10",
|
|
25
26
|
"next-mdx-remote": "^6.0.0",
|
|
26
|
-
"posthog-js": "^1.398.
|
|
27
|
+
"posthog-js": "^1.398.6",
|
|
27
28
|
"posthog-node": "^5.40.0",
|
|
28
29
|
react: "19.2.7",
|
|
29
30
|
"react-dom": "19.2.7",
|
|
@@ -36,13 +37,20 @@ export const packageJsonTemplate = JSON.stringify({
|
|
|
36
37
|
zod: "^4.4.3",
|
|
37
38
|
},
|
|
38
39
|
devDependencies: {
|
|
40
|
+
"@next/eslint-plugin-next": "16.2.10",
|
|
39
41
|
"@types/node": "^26",
|
|
40
42
|
"@types/react": "^19",
|
|
41
43
|
"@types/react-dom": "^19",
|
|
42
|
-
"
|
|
43
|
-
eslint: "^
|
|
44
|
-
"
|
|
44
|
+
"@typescript-eslint/eslint-plugin": "^8.63.0",
|
|
45
|
+
"@typescript-eslint/parser": "^8.63.0",
|
|
46
|
+
"@typescript/native-preview": "^7.0.0-dev.20260707.2",
|
|
47
|
+
eslint: "^10",
|
|
48
|
+
"eslint-plugin-import": "^2.32.0",
|
|
49
|
+
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
50
|
+
"eslint-plugin-react": "^7.37.5",
|
|
51
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
52
|
+
globals: "^17.7.0",
|
|
45
53
|
prettier: "^3.9.4",
|
|
46
|
-
typescript: "
|
|
54
|
+
typescript: "npm:@typescript/typescript6@^6.0.2",
|
|
47
55
|
},
|
|
48
56
|
}, null, 2) + "\n";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doccupine",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.111",
|
|
4
4
|
"description": "Free and open-source documentation platform. Write MDX, get a production-ready site with AI chat, built-in components, and an MCP server - in one command.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@types/node": "^26.1.1",
|
|
50
50
|
"@types/prompts": "^2.4.9",
|
|
51
51
|
"prettier": "^3.9.4",
|
|
52
|
-
"typescript": "^
|
|
52
|
+
"typescript": "^7.0.2",
|
|
53
53
|
"vitest": "^4.1.10"
|
|
54
54
|
},
|
|
55
55
|
"files": [
|