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.
@@ -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: \"smooth\",\n });\n } else if (elRect.top - pad < cRect.top) {\n container.scrollBy({\n top: elRect.top - cRect.top - pad,\n behavior: \"smooth\",\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";
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: "smooth",
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: "smooth",
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({ top: Math.max(0, target) });\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";
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
- nav.scrollTo({ top: Math.max(0, target) });
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 nextConfig from \"eslint-config-next\";\n\n// Find the config object that already has @typescript-eslint plugin registered\nconst tsConfigIndex = nextConfig.findIndex(\n (c) => c.plugins && \"@typescript-eslint\" in c.plugins,\n);\n\nconst config = nextConfig.map((c, i) => {\n if (i === tsConfigIndex) {\n return {\n ...c,\n rules: {\n ...c.rules,\n \"@typescript-eslint/no-explicit-any\": \"warn\",\n \"@typescript-eslint/no-unused-vars\": [\n \"warn\",\n { argsIgnorePattern: \"^_\", varsIgnorePattern: \"^_\" },\n ],\n },\n };\n }\n return c;\n});\n\nconfig.push({\n rules: {\n \"no-console\": [\"warn\", { allow: [\"warn\", \"error\"] }],\n },\n});\n\nexport default config;\n";
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 nextConfig from "eslint-config-next";
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
- // Find the config object that already has @typescript-eslint plugin registered
4
- const tsConfigIndex = nextConfig.findIndex(
5
- (c) => c.plugins && "@typescript-eslint" in c.plugins,
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 = nextConfig.map((c, i) => {
9
- if (i === tsConfigIndex) {
10
- return {
11
- ...c,
12
- rules: {
13
- ...c.rules,
14
- "@typescript-eslint/no-explicit-any": "warn",
15
- "@typescript-eslint/no-unused-vars": [
16
- "warn",
17
- { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
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
- return c;
23
- });
24
-
25
- config.push({
26
- rules: {
27
- "no-console": ["warn", { allow: ["warn", "error"] }],
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.9",
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.4",
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
- "baseline-browser-mapping": "^2.10.42",
43
- eslint: "^9",
44
- "eslint-config-next": "16.2.10",
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: "^6",
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.110",
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": "^6.0.3",
52
+ "typescript": "^7.0.2",
53
53
  "vitest": "^4.1.10"
54
54
  },
55
55
  "files": [