doccupine 0.0.83 → 0.0.85
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/lib/layout.js +7 -2
- package/dist/lib/structures.js +6 -0
- package/dist/templates/app/robots.d.ts +1 -0
- package/dist/templates/app/robots.js +11 -0
- package/dist/templates/app/theme.d.ts +1 -1
- package/dist/templates/app/theme.js +1 -1
- package/dist/templates/components/DocsSideBar.d.ts +1 -1
- package/dist/templates/components/DocsSideBar.js +2 -2
- package/dist/templates/components/PostHogProvider.d.ts +1 -1
- package/dist/templates/components/PostHogProvider.js +9 -62
- package/dist/templates/components/PostHogProviderLazy.d.ts +1 -0
- package/dist/templates/components/PostHogProviderLazy.js +70 -0
- package/dist/templates/components/SearchDocs.d.ts +1 -1
- package/dist/templates/components/SearchDocs.js +39 -271
- package/dist/templates/components/SearchModalContent.d.ts +1 -0
- package/dist/templates/components/SearchModalContent.js +310 -0
- package/dist/templates/components/SideBar.d.ts +1 -1
- package/dist/templates/components/SideBar.js +5 -1
- package/dist/templates/components/layout/DocsComponents.d.ts +1 -1
- package/dist/templates/components/layout/DocsComponents.js +1 -1
- package/dist/templates/components/layout/DocsNavigation.d.ts +1 -1
- package/dist/templates/components/layout/DocsNavigation.js +1 -1
- package/dist/templates/components/layout/Field.d.ts +1 -1
- package/dist/templates/components/layout/Field.js +1 -0
- package/dist/templates/components/layout/Footer.d.ts +1 -1
- package/dist/templates/components/layout/Footer.js +8 -3
- package/dist/templates/components/layout/SharedStyles.d.ts +1 -1
- package/dist/templates/components/layout/SharedStyles.js +2 -1
- package/dist/templates/components/layout/StaticLinks.d.ts +1 -1
- package/dist/templates/components/layout/StaticLinks.js +1 -1
- package/dist/templates/mdx/footer-links.mdx.d.ts +1 -1
- package/dist/templates/mdx/footer-links.mdx.js +1 -1
- package/dist/templates/mdx/platform/external-links.mdx.d.ts +1 -1
- package/dist/templates/mdx/platform/external-links.mdx.js +6 -21
- package/dist/templates/mdx/theme.mdx.d.ts +1 -1
- package/dist/templates/mdx/theme.mdx.js +1 -1
- package/dist/templates/package.js +13 -13
- package/dist/templates/services/mcp/tools.d.ts +1 -1
- package/dist/templates/services/mcp/tools.js +9 -10
- package/dist/templates/tsconfig.d.ts +1 -1
- package/dist/templates/tsconfig.js +0 -1
- package/package.json +4 -4
package/dist/lib/layout.js
CHANGED
|
@@ -48,6 +48,7 @@ ${a} >`
|
|
|
48
48
|
return `import type { Metadata } from "next";
|
|
49
49
|
${isGoogleFont(fontConfig) ? `import { ${fontConfig.googleFont.fontName} } from "next/font/google";` : isLocalFont(fontConfig) ? 'import localFont from "next/font/local";' : 'import { Inter } from "next/font/google";'}
|
|
50
50
|
import dynamic from "next/dynamic";
|
|
51
|
+
import Script from "next/script";
|
|
51
52
|
import { StyledComponentsRegistry } from "cherry-styled-components";
|
|
52
53
|
import { theme, themeDark } from "@/app/theme";
|
|
53
54
|
import { CherryThemeProvider } from "@/components/layout/CherryThemeProvider";
|
|
@@ -124,7 +125,9 @@ ${hasSections
|
|
|
124
125
|
a first visit this blocking script detects prefers-color-scheme, sets
|
|
125
126
|
the theme cookie, and hides the body until router.refresh() re-renders
|
|
126
127
|
with the correct theme (see ClientThemeProvider). */}
|
|
127
|
-
<
|
|
128
|
+
<Script
|
|
129
|
+
id="theme-init"
|
|
130
|
+
strategy="beforeInteractive"
|
|
128
131
|
dangerouslySetInnerHTML={{
|
|
129
132
|
__html: \`(function(){try{var c=document.cookie.split(";").find(function(s){return s.trim().startsWith("theme=")});if(!c){var d=window.matchMedia&&window.matchMedia("(prefers-color-scheme:dark)").matches;document.cookie="theme="+(d?"dark":"light")+";path=/;max-age=31536000;SameSite=Lax";if(d){var s=document.createElement("style");s.id="__theme-init";s.textContent="html{background:#000!important;color-scheme:dark}body{visibility:hidden}";document.head.appendChild(s)}}}catch(e){}})();\`,
|
|
130
133
|
}}
|
|
@@ -185,7 +188,9 @@ ${analyticsEnabled ? " </PostHogProvider>\n" : ""} </StyledCompo
|
|
|
185
188
|
a first visit this blocking script detects prefers-color-scheme, sets
|
|
186
189
|
the theme cookie, and hides the body until router.refresh() re-renders
|
|
187
190
|
with the correct theme (see ClientThemeProvider). */}
|
|
188
|
-
<
|
|
191
|
+
<Script
|
|
192
|
+
id="theme-init"
|
|
193
|
+
strategy="beforeInteractive"
|
|
189
194
|
dangerouslySetInnerHTML={{
|
|
190
195
|
__html: \`(function(){try{var c=document.cookie.split(";").find(function(s){return s.trim().startsWith("theme=")});if(!c){var d=window.matchMedia&&window.matchMedia("(prefers-color-scheme:dark)").matches;document.cookie="theme="+(d?"dark":"light")+";path=/;max-age=31536000;SameSite=Lax";if(d){var s=document.createElement("style");s.id="__theme-init";s.textContent="html{background:#000!important;color-scheme:dark}body{visibility:hidden}";document.head.appendChild(s)}}}catch(e){}})();\`,
|
|
191
196
|
}}
|
package/dist/lib/structures.js
CHANGED
|
@@ -10,6 +10,7 @@ import { ragRoutesTemplate } from "../templates/app/api/rag/route.js";
|
|
|
10
10
|
import { searchRoutesTemplate } from "../templates/app/api/search/route.js";
|
|
11
11
|
import { routesTemplate } from "../templates/app/api/theme/routes.js";
|
|
12
12
|
import { notFoundTemplate } from "../templates/app/not-found.js";
|
|
13
|
+
import { robotsTemplate } from "../templates/app/robots.js";
|
|
13
14
|
import { themeTemplate } from "../templates/app/theme.js";
|
|
14
15
|
import { chatTemplate } from "../templates/components/Chat.js";
|
|
15
16
|
import { clickOutsideTemplate } from "../templates/components/ClickOutside.js";
|
|
@@ -19,7 +20,9 @@ import { docsSideBarTemplate } from "../templates/components/DocsSideBar.js";
|
|
|
19
20
|
import { mdxComponentsTemplate } from "../templates/components/MDXComponents.js";
|
|
20
21
|
import { sectionNavProviderTemplate } from "../templates/components/SectionNavProvider.js";
|
|
21
22
|
import { postHogProviderTemplate } from "../templates/components/PostHogProvider.js";
|
|
23
|
+
import { postHogProviderLazyTemplate } from "../templates/components/PostHogProviderLazy.js";
|
|
22
24
|
import { searchDocsTemplate } from "../templates/components/SearchDocs.js";
|
|
25
|
+
import { searchModalContentTemplate } from "../templates/components/SearchModalContent.js";
|
|
23
26
|
import { sideBarTemplate } from "../templates/components/SideBar.js";
|
|
24
27
|
import { spinnerTemplate } from "../templates/components/Spinner.js";
|
|
25
28
|
import { sectionBarTemplate } from "../templates/components/layout/SectionBar.js";
|
|
@@ -118,6 +121,7 @@ export const appStructure = {
|
|
|
118
121
|
"package.json": packageJsonTemplate,
|
|
119
122
|
"tsconfig.json": tsconfigTemplate,
|
|
120
123
|
"app/not-found.tsx": notFoundTemplate,
|
|
124
|
+
"app/robots.ts": robotsTemplate,
|
|
121
125
|
"app/theme.ts": themeTemplate,
|
|
122
126
|
"app/api/mcp/route.ts": mcpRoutesTemplate,
|
|
123
127
|
"app/api/rag/route.ts": ragRoutesTemplate,
|
|
@@ -146,7 +150,9 @@ export const appStructure = {
|
|
|
146
150
|
"components/MDXComponents.tsx": mdxComponentsTemplate,
|
|
147
151
|
"components/SectionNavProvider.tsx": sectionNavProviderTemplate,
|
|
148
152
|
"components/PostHogProvider.tsx": postHogProviderTemplate,
|
|
153
|
+
"components/PostHogProviderLazy.tsx": postHogProviderLazyTemplate,
|
|
149
154
|
"components/SearchDocs.tsx": searchDocsTemplate,
|
|
155
|
+
"components/SearchModalContent.tsx": searchModalContentTemplate,
|
|
150
156
|
"components/SideBar.tsx": sideBarTemplate,
|
|
151
157
|
"components/Spinner.tsx": spinnerTemplate,
|
|
152
158
|
"components/layout/Accordion.tsx": accordionTemplate,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const robotsTemplate = "import type { MetadataRoute } from \"next\";\n\nexport default function robots(): MetadataRoute.Robots {\n return {\n rules: {\n userAgent: \"*\",\n allow: \"/\",\n },\n };\n}\n";
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export declare const SIDEBAR_WIDTH = 280;
|
|
2
2
|
export declare const CHAT_WIDTH = 420;
|
|
3
|
-
export declare const themeTemplate = "\"use client\";\nimport customThemeJson from \"@/theme.json\";\n\ninterface CustomTheme {\n default?: Partial<Colors>;\n dark?: Partial<Colors>;\n}\n\nconst customTheme = customThemeJson as CustomTheme;\n\nconst breakpoints: Breakpoints = {\n xs: 0,\n sm: 576,\n md: 768,\n lg: 992,\n xl: 1200,\n xxl: 1440,\n xxxl: 1920,\n};\n\nexport function mq(minWidth: keyof Breakpoints) {\n return `@media screen and (min-width: ${breakpoints[minWidth]}px)`;\n}\n\nconst spacing: Spacing = {\n maxWidth: { xs: \"1280px\", xxxl: \"1440px\" },\n padding: { xs: \"20px\", lg: \"40px\" },\n radius: { xs: \"6px\", lg: \"12px\", xl: \"30px\" },\n gridGap: { xs: \"20px\", lg: \"40px\" },\n};\n\nconst colors: Colors = {\n primaryLight: \"#d1d5db\",\n primary: \"#
|
|
3
|
+
export declare const themeTemplate = "\"use client\";\nimport customThemeJson from \"@/theme.json\";\n\ninterface CustomTheme {\n default?: Partial<Colors>;\n dark?: Partial<Colors>;\n}\n\nconst customTheme = customThemeJson as CustomTheme;\n\nconst breakpoints: Breakpoints = {\n xs: 0,\n sm: 576,\n md: 768,\n lg: 992,\n xl: 1200,\n xxl: 1440,\n xxxl: 1920,\n};\n\nexport function mq(minWidth: keyof Breakpoints) {\n return `@media screen and (min-width: ${breakpoints[minWidth]}px)`;\n}\n\nconst spacing: Spacing = {\n maxWidth: { xs: \"1280px\", xxxl: \"1440px\" },\n padding: { xs: \"20px\", lg: \"40px\" },\n radius: { xs: \"6px\", lg: \"12px\", xl: \"30px\" },\n gridGap: { xs: \"20px\", lg: \"40px\" },\n};\n\nconst colors: Colors = {\n primaryLight: \"#d1d5db\",\n primary: \"#556272\",\n primaryDark: \"#374151\",\n secondaryLight: \"#c4b5fd\",\n secondary: \"#8b5cf6\",\n secondaryDark: \"#5b21b6\",\n tertiaryLight: \"#86efac\",\n tertiary: \"#22c55e\",\n tertiaryDark: \"#15803d\",\n grayLight: \"#e5e7eb\",\n gray: \"#9ca3af\",\n grayDark: \"#4b5563\",\n success: \"#84cc16\",\n error: \"#ef4444\",\n warning: \"#eab308\",\n info: \"#06b6d4\",\n dark: \"#000000\",\n light: \"#ffffff\",\n ...(customTheme.default ? (customTheme.default as Partial<Colors>) : {}),\n};\n\nconst colorsDark: Colors = {\n primaryLight: \"#9ca3af\",\n primary: \"#6b7280\",\n primaryDark: \"#374151\",\n secondaryLight: \"#ddd6fe\",\n secondary: \"#a78bfa\",\n secondaryDark: \"#7c3aed\",\n tertiaryLight: \"#6ee7b7\",\n tertiary: \"#10b981\",\n tertiaryDark: \"#065f46\",\n grayLight: \"#1a1a1a\",\n gray: \"#454444\",\n grayDark: \"#808080\",\n success: \"#84cc16\",\n error: \"#ef4444\",\n warning: \"#eab308\",\n info: \"#06b6d4\",\n dark: \"#ffffff\",\n light: \"#000000\",\n ...(customTheme.dark ? (customTheme.dark as Partial<Colors>) : {}),\n};\n\nconst shadows: Shadows = {\n xs: \"0px 4px 4px 0px rgba(18, 18, 18, 0.04), 0px 1px 3px 0px rgba(39, 41, 45, 0.02)\",\n sm: \"0px 4px 4px 0px rgba(18, 18, 18, 0.08), 0px 1px 3px 0px rgba(39, 41, 45, 0.04)\",\n md: \"0px 8px 8px 0px rgba(18, 18, 18, 0.16), 0px 2px 3px 0px rgba(39, 41, 45, 0.06)\",\n lg: \"0px 16px 24px 0px rgba(18, 18, 18, 0.20), 0px 2px 3px 0px rgba(39, 41, 45, 0.08)\",\n xl: \"0px 24px 32px 0px rgba(18, 18, 18, 0.24), 0px 2px 3px 0px rgba(39, 41, 45, 0.12)\",\n};\n\nconst shadowsDark: Shadows = {\n xs: \"0px 4px 4px 0px rgba(255, 255, 255, 0.04), 0px 1px 3px 0px rgba(255, 255, 255, 0.02)\",\n sm: \"0px 4px 4px 0px rgba(255, 255, 255, 0.08), 0px 1px 3px 0px rgba(255, 255, 255, 0.04)\",\n md: \"0px 8px 8px 0px rgba(255, 255, 255, 0.16), 0px 2px 3px 0px rgba(255, 255, 255, 0.06)\",\n lg: \"0px 16px 24px 0px rgba(255, 255, 255, 0.20), 0px 2px 3px 0px rgba(255, 255, 255, 0.08)\",\n xl: \"0px 24px 32px 0px rgba(255, 255, 255, 0.24), 0px 2px 3px 0px rgba(255, 255, 255, 0.12)\",\n};\n\nconst fonts: Fonts = {\n text: \"Inter\",\n head: \"Inter\",\n mono: \"Roboto Mono, monospace\",\n};\n\nconst fontSizes: FontSizes = {\n hero1: { xs: \"72px\", lg: \"128px\" },\n hero2: { xs: \"60px\", lg: \"96px\" },\n hero3: { xs: \"36px\", lg: \"72px\" },\n\n h1: { xs: \"40px\", lg: \"60px\" },\n h2: { xs: \"30px\", lg: \"36px\" },\n h3: { xs: \"28px\", lg: \"30px\" },\n h4: { xs: \"24px\", lg: \"26px\" },\n h5: { xs: \"18px\", lg: \"20px\" },\n h6: { xs: \"16px\", lg: \"18px\" },\n\n text: { xs: \"14px\", lg: \"16px\" },\n strong: { xs: \"14px\", lg: \"16px\" },\n small: { xs: \"12px\", lg: \"14px\" },\n\n blockquote: { xs: \"16px\", lg: \"18px\" },\n code: { xs: \"14px\", lg: \"16px\" },\n\n button: { xs: \"16px\", lg: \"16px\" },\n buttonBig: { xs: \"18px\", lg: \"18px\" },\n buttonSmall: { xs: \"14px\", lg: \"14px\" },\n\n input: { xs: \"16px\", lg: \"16px\" },\n inputBig: { xs: \"18px\", lg: \"18px\" },\n inputSmall: { xs: \"14px\", lg: \"14px\" },\n};\n\nconst lineHeights: LineHeights = {\n hero1: { xs: \"1.1\", lg: \"1.1\" },\n hero2: { xs: \"1.1\", lg: \"1.1\" },\n hero3: { xs: \"1.17\", lg: \"1.1\" },\n\n h1: { xs: \"1\", lg: \"1.07\" },\n h2: { xs: \"1.2\", lg: \"1.2\" },\n h3: { xs: \"1.3\", lg: \"1.5\" },\n h4: { xs: \"1.3\", lg: \"1.5\" },\n h5: { xs: \"1.6\", lg: \"1.5\" },\n h6: { xs: \"1.6\", lg: \"1.6\" },\n\n text: { xs: \"1.7\", lg: \"1.7\" },\n strong: { xs: \"1.7\", lg: \"1.7\" },\n small: { xs: \"1.7\", lg: \"1.7\" },\n\n blockquote: { xs: \"1.7\", lg: \"1.7\" },\n code: { xs: \"1.7\", lg: \"1.7\" },\n\n button: { xs: \"1\", lg: \"1\" },\n buttonBig: { xs: \"1\", lg: \"1\" },\n buttonSmall: { xs: \"1\", lg: \"1\" },\n\n input: { xs: \"1\", lg: \"1\" },\n inputBig: { xs: \"1\", lg: \"1\" },\n inputSmall: { xs: \"1\", lg: \"1\" },\n};\n\nexport const theme: Theme = {\n breakpoints,\n spacing,\n colors,\n shadows,\n fonts,\n fontSizes,\n lineHeights,\n isDark: false,\n};\n\nexport const themeDark: Theme = {\n breakpoints,\n spacing,\n colors: colorsDark,\n shadows: shadowsDark,\n fonts,\n fontSizes,\n lineHeights,\n isDark: true,\n};\n\ninterface Breakpoints<TNumber = number> {\n xs: TNumber;\n sm: TNumber;\n md: TNumber;\n lg: TNumber;\n xl: TNumber;\n xxl: TNumber;\n xxxl: TNumber;\n}\n\ninterface Spacing<TString = string> {\n maxWidth: { xs: TString; xxxl: TString };\n padding: { xs: TString; lg: TString };\n radius: { xs: TString; lg: TString; xl: TString };\n gridGap: { xs: TString; lg: TString };\n}\n\ninterface Colors<TString = string> {\n primaryLight: TString;\n primary: TString;\n primaryDark: TString;\n\n secondaryLight: TString;\n secondary: TString;\n secondaryDark: TString;\n\n tertiaryLight: TString;\n tertiary: TString;\n tertiaryDark: TString;\n\n grayLight: TString;\n gray: TString;\n grayDark: TString;\n\n success: TString;\n error: TString;\n warning: TString;\n info: TString;\n\n dark: TString;\n light: TString;\n}\n\ninterface Shadows<TString = string> {\n xs: TString;\n sm: TString;\n md: TString;\n lg: TString;\n xl: TString;\n}\n\ninterface Fonts<TString = string> {\n head: TString;\n text: TString;\n mono: TString;\n}\n\ninterface FontSizes<TString = string> {\n hero1: { xs: TString; lg: TString };\n hero2: { xs: TString; lg: TString };\n hero3: { xs: TString; lg: TString };\n\n h1: { xs: TString; lg: TString };\n h2: { xs: TString; lg: TString };\n h3: { xs: TString; lg: TString };\n h4: { xs: TString; lg: TString };\n h5: { xs: TString; lg: TString };\n h6: { xs: TString; lg: TString };\n\n text: { xs: TString; lg: TString };\n strong: { xs: TString; lg: TString };\n small: { xs: TString; lg: TString };\n\n blockquote: { xs: TString; lg: TString };\n code: { xs: TString; lg: TString };\n\n button: { xs: TString; lg: TString };\n buttonBig: { xs: TString; lg: TString };\n buttonSmall: { xs: TString; lg: TString };\n\n input: { xs: TString; lg: TString };\n inputBig: { xs: TString; lg: TString };\n inputSmall: { xs: TString; lg: TString };\n}\n\ninterface LineHeights<TString = string> {\n hero1: { xs: TString; lg: TString };\n hero2: { xs: TString; lg: TString };\n hero3: { xs: TString; lg: TString };\n\n h1: { xs: TString; lg: TString };\n h2: { xs: TString; lg: TString };\n h3: { xs: TString; lg: TString };\n h4: { xs: TString; lg: TString };\n h5: { xs: TString; lg: TString };\n h6: { xs: TString; lg: TString };\n\n text: { xs: TString; lg: TString };\n strong: { xs: TString; lg: TString };\n small: { xs: TString; lg: TString };\n\n blockquote: { xs: TString; lg: TString };\n code: { xs: TString; lg: TString };\n\n button: { xs: TString; lg: TString };\n buttonBig: { xs: TString; lg: TString };\n buttonSmall: { xs: TString; lg: TString };\n\n input: { xs: TString; lg: TString };\n inputBig: { xs: TString; lg: TString };\n inputSmall: { xs: TString; lg: TString };\n}\n\nexport interface Theme {\n breakpoints: Breakpoints;\n spacing: Spacing;\n colors: Colors;\n shadows: Shadows;\n fonts: Fonts;\n fontSizes: FontSizes;\n lineHeights: LineHeights;\n isDark: boolean;\n}\n";
|
|
@@ -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
|
|
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";
|
|
@@ -140,10 +140,10 @@ export function DocsSideBar({ headings }: { headings: Heading[] }) {
|
|
|
140
140
|
return (
|
|
141
141
|
<StyledIndexSidebar data-sidebar>
|
|
142
142
|
{headings?.length > 0 && (
|
|
143
|
-
|
|
143
|
+
<li aria-hidden="true">
|
|
144
144
|
<StyledIndexSidebarLabel>On this page</StyledIndexSidebarLabel>
|
|
145
145
|
<Space $size={15} />
|
|
146
|
-
|
|
146
|
+
</li>
|
|
147
147
|
)}
|
|
148
148
|
{headings.map((heading, index) => (
|
|
149
149
|
<StyledIndexSidebarLi
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const postHogProviderTemplate = "\"use client\";\n\nimport
|
|
1
|
+
export declare const postHogProviderTemplate = "\"use client\";\n\nimport dynamic from \"next/dynamic\";\n\nconst PostHogSetup = dynamic(\n () =>\n import(\"@/components/PostHogProviderLazy\").then((mod) => mod.PostHogSetup),\n { ssr: false },\n);\n\nexport function PostHogProvider({ children }: { children: React.ReactNode }) {\n return (\n <>\n <PostHogSetup />\n {children}\n </>\n );\n}\n";
|
|
@@ -1,72 +1,19 @@
|
|
|
1
1
|
export const postHogProviderTemplate = `"use client";
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import { PostHogProvider as PHProvider } from "@posthog/react";
|
|
5
|
-
import { Suspense, useEffect, useRef, useState } from "react";
|
|
6
|
-
import { usePathname, useSearchParams } from "next/navigation";
|
|
7
|
-
import rawAnalyticsConfig from "@/analytics.json";
|
|
3
|
+
import dynamic from "next/dynamic";
|
|
8
4
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const analyticsConfig = rawAnalyticsConfig as AnalyticsConfig;
|
|
18
|
-
|
|
19
|
-
const posthogKey =
|
|
20
|
-
analyticsConfig?.provider === "posthog" ? analyticsConfig.posthog?.key : null;
|
|
21
|
-
|
|
22
|
-
function PostHogInit({ onReady }: { onReady: () => void }) {
|
|
23
|
-
const initRef = useRef(false);
|
|
24
|
-
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
if (initRef.current || !posthogKey) return;
|
|
27
|
-
initRef.current = true;
|
|
28
|
-
|
|
29
|
-
posthog.init(posthogKey, {
|
|
30
|
-
api_host: "/ingest",
|
|
31
|
-
ui_host: analyticsConfig.posthog?.host || "https://us.posthog.com",
|
|
32
|
-
capture_pageview: false,
|
|
33
|
-
capture_pageleave: true,
|
|
34
|
-
loaded: onReady,
|
|
35
|
-
});
|
|
36
|
-
}, [onReady]);
|
|
37
|
-
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function PostHogPageviewTracker() {
|
|
42
|
-
const pathname = usePathname();
|
|
43
|
-
const searchParams = useSearchParams();
|
|
44
|
-
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
if (pathname) {
|
|
47
|
-
const url = searchParams?.size
|
|
48
|
-
? \`\${pathname}?\${searchParams.toString()}\`
|
|
49
|
-
: pathname;
|
|
50
|
-
posthog.capture("$pageview", { $current_url: url });
|
|
51
|
-
}
|
|
52
|
-
}, [pathname, searchParams]);
|
|
53
|
-
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
5
|
+
const PostHogSetup = dynamic(
|
|
6
|
+
() =>
|
|
7
|
+
import("@/components/PostHogProviderLazy").then((mod) => mod.PostHogSetup),
|
|
8
|
+
{ ssr: false },
|
|
9
|
+
);
|
|
56
10
|
|
|
57
11
|
export function PostHogProvider({ children }: { children: React.ReactNode }) {
|
|
58
|
-
const [ready, setReady] = useState(false);
|
|
59
|
-
|
|
60
|
-
if (!posthogKey) {
|
|
61
|
-
return <>{children}</>;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
12
|
return (
|
|
65
|
-
|
|
66
|
-
<
|
|
67
|
-
<Suspense fallback={null}>{ready && <PostHogPageviewTracker />}</Suspense>
|
|
13
|
+
<>
|
|
14
|
+
<PostHogSetup />
|
|
68
15
|
{children}
|
|
69
|
-
|
|
16
|
+
</>
|
|
70
17
|
);
|
|
71
18
|
}
|
|
72
19
|
`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const postHogProviderLazyTemplate = "\"use client\";\n\nimport posthog from \"posthog-js\";\nimport { Suspense, useEffect, useRef, useState } from \"react\";\nimport { usePathname, useSearchParams } from \"next/navigation\";\nimport rawAnalyticsConfig from \"@/analytics.json\";\n\ninterface AnalyticsConfig {\n provider?: string;\n posthog?: {\n key?: string;\n host?: string;\n };\n}\n\nconst analyticsConfig = rawAnalyticsConfig as AnalyticsConfig;\n\nconst posthogKey =\n analyticsConfig?.provider === \"posthog\" ? analyticsConfig.posthog?.key : null;\n\nfunction PostHogInit({ onReady }: { onReady: () => void }) {\n const initRef = useRef(false);\n\n useEffect(() => {\n if (initRef.current || !posthogKey) return;\n initRef.current = true;\n\n posthog.init(posthogKey, {\n api_host: \"/ingest\",\n ui_host: analyticsConfig.posthog?.host || \"https://us.posthog.com\",\n capture_pageview: false,\n capture_pageleave: true,\n loaded: onReady,\n });\n }, [onReady]);\n\n return null;\n}\n\nfunction PostHogPageviewTracker() {\n const pathname = usePathname();\n const searchParams = useSearchParams();\n\n useEffect(() => {\n if (pathname) {\n const url = searchParams?.size\n ? `${pathname}?${searchParams.toString()}`\n : pathname;\n posthog.capture(\"$pageview\", { $current_url: url });\n }\n }, [pathname, searchParams]);\n\n return null;\n}\n\nexport function PostHogSetup() {\n const [ready, setReady] = useState(false);\n\n if (!posthogKey) {\n return null;\n }\n\n return (\n <>\n <PostHogInit onReady={() => setReady(true)} />\n <Suspense fallback={null}>{ready && <PostHogPageviewTracker />}</Suspense>\n </>\n );\n}\n";
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export const postHogProviderLazyTemplate = `"use client";
|
|
2
|
+
|
|
3
|
+
import posthog from "posthog-js";
|
|
4
|
+
import { Suspense, useEffect, useRef, useState } from "react";
|
|
5
|
+
import { usePathname, useSearchParams } from "next/navigation";
|
|
6
|
+
import rawAnalyticsConfig from "@/analytics.json";
|
|
7
|
+
|
|
8
|
+
interface AnalyticsConfig {
|
|
9
|
+
provider?: string;
|
|
10
|
+
posthog?: {
|
|
11
|
+
key?: string;
|
|
12
|
+
host?: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const analyticsConfig = rawAnalyticsConfig as AnalyticsConfig;
|
|
17
|
+
|
|
18
|
+
const posthogKey =
|
|
19
|
+
analyticsConfig?.provider === "posthog" ? analyticsConfig.posthog?.key : null;
|
|
20
|
+
|
|
21
|
+
function PostHogInit({ onReady }: { onReady: () => void }) {
|
|
22
|
+
const initRef = useRef(false);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (initRef.current || !posthogKey) return;
|
|
26
|
+
initRef.current = true;
|
|
27
|
+
|
|
28
|
+
posthog.init(posthogKey, {
|
|
29
|
+
api_host: "/ingest",
|
|
30
|
+
ui_host: analyticsConfig.posthog?.host || "https://us.posthog.com",
|
|
31
|
+
capture_pageview: false,
|
|
32
|
+
capture_pageleave: true,
|
|
33
|
+
loaded: onReady,
|
|
34
|
+
});
|
|
35
|
+
}, [onReady]);
|
|
36
|
+
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function PostHogPageviewTracker() {
|
|
41
|
+
const pathname = usePathname();
|
|
42
|
+
const searchParams = useSearchParams();
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (pathname) {
|
|
46
|
+
const url = searchParams?.size
|
|
47
|
+
? \`\${pathname}?\${searchParams.toString()}\`
|
|
48
|
+
: pathname;
|
|
49
|
+
posthog.capture("$pageview", { $current_url: url });
|
|
50
|
+
}
|
|
51
|
+
}, [pathname, searchParams]);
|
|
52
|
+
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function PostHogSetup() {
|
|
57
|
+
const [ready, setReady] = useState(false);
|
|
58
|
+
|
|
59
|
+
if (!posthogKey) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<>
|
|
65
|
+
<PostHogInit onReady={() => setReady(true)} />
|
|
66
|
+
<Suspense fallback={null}>{ready && <PostHogPageviewTracker />}</Suspense>
|
|
67
|
+
</>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const searchDocsTemplate = "\"use client\";\nimport React, {\n createContext,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport styled, { css, keyframes } from \"styled-components\";\nimport { rgba } from \"polished\";\nimport { Search } from \"lucide-react\";\nimport { mq, Theme } from \"@/app/theme\";\nimport { interactiveStyles } from \"@/components/layout/SharedStyled\";\nimport { Spinner } from \"@/components/Spinner\";\n\ninterface PageItem {\n slug: string;\n title: string;\n description?: string;\n category: string;\n section?: string;\n}\n\ninterface SectionItem {\n label: string;\n slug: string;\n}\n\ninterface ContentHit {\n slug: string;\n snippet: string;\n}\n\ninterface MergedResult {\n page: PageItem;\n snippet?: string;\n}\n\ninterface SearchContextValue {\n openSearch: () => void;\n}\n\nconst SearchContext = createContext<SearchContextValue>({\n openSearch: () => {},\n});\n\nconst ANIMATION_MS = 150;\n\nconst backdropIn = keyframes`\n from { opacity: 0; }\n to { opacity: 1; }\n`;\n\nconst backdropOut = keyframes`\n from { opacity: 1; }\n to { opacity: 0; }\n`;\n\nconst modalIn = keyframes`\n from { opacity: 0; transform: scale(0.96) translateY(-8px); }\n to { opacity: 1; transform: scale(1) translateY(0); }\n`;\n\nconst modalOut = keyframes`\n from { opacity: 1; transform: scale(1) translateY(0); }\n to { opacity: 0; transform: scale(0.96) translateY(-8px); }\n`;\n\nconst StyledBackdrop = styled.div<{ theme: Theme; $isClosing: boolean }>`\n position: fixed;\n inset: 0;\n z-index: 9999;\n background: ${({ theme }) =>\n rgba(theme.isDark ? theme.colors.light : theme.colors.dark, 0.5)};\n backdrop-filter: blur(4px);\n -webkit-backdrop-filter: blur(4px);\n display: flex;\n align-items: flex-start;\n justify-content: center;\n padding: 20px;\n animation: ${({ $isClosing }) => ($isClosing ? backdropOut : backdropIn)}\n ${ANIMATION_MS}ms ease forwards;\n\n ${mq(\"lg\")} {\n padding: 120px 20px 20px 20px;\n }\n`;\n\nconst StyledModal = styled.div<{ theme: Theme; $isClosing: boolean }>`\n background: ${({ theme }) => theme.colors.light};\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n box-shadow: ${({ theme }) => theme.shadows.xs};\n width: 100%;\n max-width: 560px;\n max-height: calc(100dvh - 40px);\n display: flex;\n flex-direction: column;\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n padding-bottom: 8px;\n animation: ${({ $isClosing }) => ($isClosing ? modalOut : modalIn)}\n ${ANIMATION_MS}ms ease forwards;\n\n ${mq(\"lg\")} {\n max-height: calc(100dvh - 240px);\n }\n`;\n\nconst StyledInputWrapper = styled.div<{ theme: Theme }>`\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 16px;\n flex-shrink: 0;\n border-bottom: solid 1px ${({ theme }) => theme.colors.grayLight};\n\n & svg.lucide {\n color: ${({ theme }) => theme.colors.gray};\n flex-shrink: 0;\n }\n`;\n\nconst StyledInput = styled.input<{ theme: Theme }>`\n flex: 1;\n border: none;\n outline: none;\n background: transparent;\n font-size: ${({ theme }) => theme.fontSizes.text.lg};\n line-height: ${({ theme }) => theme.lineHeights.text.lg};\n color: ${({ theme }) => theme.colors.dark};\n font-family: inherit;\n\n &::placeholder {\n color: ${({ theme }) => theme.colors.gray};\n }\n`;\n\nconst StyledResults = styled.ul<{ theme: Theme }>`\n list-style: none;\n margin: 8px 0 0 0;\n padding: 0 8px;\n overflow-y: auto;\n flex: 1;\n min-height: 0;\n -webkit-overflow-scrolling: touch;\n\n &::-webkit-scrollbar {\n display: none;\n }\n`;\n\nconst StyledResultItem = styled.li<{ theme: Theme; $isActive: boolean }>`\n padding: 10px 12px;\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n cursor: pointer;\n transition: background 0.15s ease;\n\n ${({ $isActive, theme }) =>\n $isActive &&\n css`\n background: ${rgba(theme.colors.primaryLight, 0.2)};\n `}\n\n &:hover {\n background: ${({ theme }) => rgba(theme.colors.primaryLight, 0.15)};\n }\n`;\n\nconst StyledResultTitle = styled.span<{ theme: Theme }>`\n font-size: ${({ theme }) => theme.fontSizes.text.lg};\n font-weight: 500;\n color: ${({ theme }) => theme.colors.dark};\n display: block;\n`;\n\nconst StyledResultMeta = styled.span<{ theme: Theme }>`\n font-size: ${({ theme }) => theme.fontSizes.small.lg};\n color: ${({ theme }) => theme.colors.gray};\n display: block;\n margin-top: 2px;\n`;\n\nconst StyledSnippet = styled.span<{ theme: Theme }>`\n font-size: ${({ theme }) => theme.fontSizes.small.lg};\n color: ${({ theme }) => theme.colors.grayDark};\n display: block;\n margin-top: 4px;\n line-height: 1.4;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n\n & mark {\n background: ${({ theme }) => rgba(theme.colors.primaryLight, 0.35)};\n color: inherit;\n border-radius: 4px;\n padding: 0 1px;\n }\n`;\n\nconst StyledEmpty = styled.div<{ theme: Theme }>`\n padding: 20px 20px 12px;\n min-height: 40px;\n display: flex;\n align-items: center;\n justify-content: center;\n text-align: center;\n font-size: ${({ theme }) => theme.fontSizes.small.lg};\n color: ${({ theme }) => theme.colors.gray};\n`;\n\nconst StyledKbd = styled.kbd<{ theme: Theme }>`\n font-size: 11px;\n font-family: inherit;\n background: ${({ theme }) => theme.colors.grayLight};\n color: ${({ theme }) => theme.colors.grayDark};\n padding: 2px 6px;\n border-radius: 4px;\n margin-left: auto;\n font-weight: 600;\n display: none;\n\n ${mq(\"lg\")} {\n display: initial;\n }\n`;\n\nconst StyledSearchButton = styled.button<{ theme: Theme }>`\n ${interactiveStyles};\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n display: flex;\n align-items: center;\n gap: 6px;\n background: ${({ theme }) => theme.colors.light};\n color: ${({ theme }) => theme.colors.primary};\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n padding: 7px 8px;\n font-family: inherit;\n cursor: pointer;\n\n ${mq(\"lg\")} {\n padding: 5px 8px;\n }\n\n & svg.lucide {\n color: inherit;\n }\n`;\n\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\");\n}\n\nfunction highlightMatch(snippet: string, query: string): string {\n const escaped = escapeHtml(snippet);\n if (!query.trim()) return escaped;\n const q = escapeHtml(query.trim());\n const regex = new RegExp(\n `(${q.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")})`,\n \"gi\",\n );\n return escaped.replace(regex, \"<mark>$1</mark>\");\n}\n\nfunction SearchProvider({\n pages,\n sections,\n children,\n}: {\n pages: PageItem[];\n sections?: SectionItem[];\n children: React.ReactNode;\n}) {\n const [isVisible, setIsVisible] = useState(false);\n const [isClosing, setIsClosing] = useState(false);\n const [query, setQuery] = useState(\"\");\n const [activeIndex, setActiveIndex] = useState(0);\n const [contentResults, setContentResults] = useState<ContentHit[]>([]);\n const [isSearching, setIsSearching] = useState(false);\n const inputRef = useRef<HTMLInputElement>(null);\n const resultsRef = useRef<HTMLUListElement>(null);\n const closingTimer = useRef<ReturnType<typeof setTimeout>>(null);\n const abortRef = useRef<AbortController | null>(null);\n const debounceRef = useRef<ReturnType<typeof setTimeout>>(null);\n const router = useRouter();\n\n const sectionLabels = useMemo(() => {\n const map: Record<string, string> = {};\n sections?.forEach((s) => {\n map[s.slug] = s.label;\n });\n return map;\n }, [sections]);\n\n const openSearch = useCallback(() => {\n if (closingTimer.current) clearTimeout(closingTimer.current);\n setIsClosing(false);\n setIsVisible(true);\n }, []);\n\n const closeSearch = useCallback(() => {\n setIsClosing(true);\n if (abortRef.current) abortRef.current.abort();\n if (debounceRef.current) clearTimeout(debounceRef.current);\n closingTimer.current = setTimeout(() => {\n setIsVisible(false);\n setIsClosing(false);\n setQuery(\"\");\n setActiveIndex(0);\n setContentResults([]);\n setIsSearching(false);\n }, ANIMATION_MS);\n }, []);\n\n // Instant title/description filtering\n const titleFiltered = useMemo(() => {\n if (!query.trim()) return pages;\n const q = query.toLowerCase();\n return pages.filter(\n (p) =>\n p.title.toLowerCase().includes(q) ||\n p.description?.toLowerCase().includes(q),\n );\n }, [pages, query]);\n\n // Merge title matches with content matches\n const merged = useMemo<MergedResult[]>(() => {\n if (!query.trim()) {\n return pages.map((p) => ({ page: p }));\n }\n\n const titleMatchSlugs = new Set(titleFiltered.map((p) => p.slug));\n const titleMatches: MergedResult[] = titleFiltered.map((p) => {\n const hit = contentResults.find((cr) => cr.slug === p.slug);\n return { page: p, snippet: hit?.snippet };\n });\n\n const pageMap = new Map(pages.map((p) => [p.slug, p]));\n const contentOnly: MergedResult[] = [];\n for (const cr of contentResults) {\n if (!titleMatchSlugs.has(cr.slug)) {\n const page = pageMap.get(cr.slug);\n if (page) {\n contentOnly.push({ page, snippet: cr.snippet });\n }\n }\n }\n\n return [...titleMatches, ...contentOnly];\n }, [pages, query, titleFiltered, contentResults]);\n\n // Debounced content search\n useEffect(() => {\n if (debounceRef.current) clearTimeout(debounceRef.current);\n if (abortRef.current) abortRef.current.abort();\n\n const q = query.trim();\n if (q.length < 2) {\n setContentResults([]);\n setIsSearching(false);\n return;\n }\n\n setIsSearching(true);\n debounceRef.current = setTimeout(async () => {\n const controller = new AbortController();\n abortRef.current = controller;\n try {\n const res = await fetch(\n `/api/search?q=${encodeURIComponent(q)}&limit=15`,\n { signal: controller.signal },\n );\n if (!res.ok) throw new Error(\"Search failed\");\n const data = await res.json();\n setContentResults(data.results ?? []);\n } catch (err: unknown) {\n if (err instanceof DOMException && err.name === \"AbortError\") return;\n setContentResults([]);\n } finally {\n if (!controller.signal.aborted) {\n setIsSearching(false);\n }\n }\n }, 300);\n\n return () => {\n if (debounceRef.current) clearTimeout(debounceRef.current);\n };\n }, [query]);\n\n const navigate = useCallback(\n (slug: string) => {\n closeSearch();\n router.push(`/${slug}`);\n },\n [closeSearch, router],\n );\n\n // Global Cmd+K / Ctrl+K listener\n const isVisibleRef = useRef(false);\n\n useEffect(() => {\n isVisibleRef.current = isVisible;\n }, [isVisible]);\n\n useEffect(() => {\n function handleKeyDown(e: KeyboardEvent) {\n if ((e.metaKey || e.ctrlKey) && e.key === \"k\") {\n e.preventDefault();\n if (isVisibleRef.current) {\n closeSearch();\n } else {\n openSearch();\n }\n }\n }\n document.addEventListener(\"keydown\", handleKeyDown);\n return () => document.removeEventListener(\"keydown\", handleKeyDown);\n }, [closeSearch, openSearch]);\n\n // Focus input on open\n useEffect(() => {\n if (isVisible && !isClosing) {\n setTimeout(() => inputRef.current?.focus(), 10);\n }\n }, [isVisible, isClosing]);\n\n // Scroll active item into view\n useEffect(() => {\n if (!resultsRef.current) return;\n const active = resultsRef.current.children[activeIndex] as HTMLElement;\n active?.scrollIntoView({ block: \"nearest\" });\n }, [activeIndex]);\n\n function handleKeyDown(e: React.KeyboardEvent) {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActiveIndex((i) => (i < merged.length - 1 ? i + 1 : 0));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActiveIndex((i) => (i > 0 ? i - 1 : merged.length - 1));\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n if (merged[activeIndex]) {\n navigate(merged[activeIndex].page.slug);\n }\n } else if (e.key === \"Escape\") {\n closeSearch();\n }\n }\n\n return (\n <SearchContext.Provider value={{ openSearch }}>\n {children}\n {isVisible && (\n <StyledBackdrop $isClosing={isClosing} onClick={closeSearch}>\n <StyledModal\n $isClosing={isClosing}\n onClick={(e) => e.stopPropagation()}\n >\n <StyledInputWrapper>\n <Search size={18} />\n <StyledInput\n ref={inputRef}\n value={query}\n onChange={(e) => {\n setQuery(e.target.value);\n setActiveIndex(0);\n }}\n onKeyDown={handleKeyDown}\n placeholder=\"Search docs...\"\n autoComplete=\"off\"\n spellCheck={false}\n />\n <StyledKbd>Esc</StyledKbd>\n </StyledInputWrapper>\n {merged.length > 0 ? (\n <StyledResults ref={resultsRef}>\n {merged.map((result, index) => (\n <StyledResultItem\n key={result.page.slug + result.page.section}\n $isActive={index === activeIndex}\n onClick={() => navigate(result.page.slug)}\n onMouseEnter={() => setActiveIndex(index)}\n >\n <StyledResultTitle>{result.page.title}</StyledResultTitle>\n <StyledResultMeta>\n {result.page.section\n ? `${sectionLabels[result.page.section] || result.page.section} / `\n : \"\"}\n {result.page.category}\n </StyledResultMeta>\n {result.snippet && (\n <StyledSnippet\n dangerouslySetInnerHTML={{\n __html: highlightMatch(result.snippet, query),\n }}\n />\n )}\n </StyledResultItem>\n ))}\n </StyledResults>\n ) : (\n <StyledEmpty>\n {isSearching ? <Spinner size={18} /> : \"No results found\"}\n </StyledEmpty>\n )}\n </StyledModal>\n </StyledBackdrop>\n )}\n </SearchContext.Provider>\n );\n}\n\nexport {\n SearchProvider,\n SearchContext,\n StyledKbd as SearchKbd,\n StyledSearchButton,\n};\n";
|
|
1
|
+
export declare const searchDocsTemplate = "\"use client\";\nimport React, {\n createContext,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport dynamic from \"next/dynamic\";\nimport styled from \"styled-components\";\nimport { mq, Theme } from \"@/app/theme\";\nimport { interactiveStyles } from \"@/components/layout/SharedStyled\";\nimport type { PageItem, MergedResult } from \"@/components/SearchModalContent\";\n\nconst SearchModalContent = dynamic(\n () =>\n import(\"@/components/SearchModalContent\").then(\n (mod) => mod.SearchModalContent,\n ),\n { ssr: false },\n);\n\ninterface SectionItem {\n label: string;\n slug: string;\n}\n\ninterface ContentHit {\n slug: string;\n snippet: string;\n}\n\ninterface SearchContextValue {\n openSearch: () => void;\n}\n\nconst SearchContext = createContext<SearchContextValue>({\n openSearch: () => {},\n});\n\nconst StyledKbd = styled.kbd<{ theme: Theme }>`\n font-size: 11px;\n font-family: inherit;\n background: ${({ theme }) => theme.colors.grayLight};\n color: ${({ theme }) => theme.colors.grayDark};\n padding: 2px 6px;\n border-radius: 4px;\n margin-left: auto;\n font-weight: 600;\n display: none;\n\n ${mq(\"lg\")} {\n display: initial;\n }\n`;\n\nconst StyledSearchButton = styled.button<{ theme: Theme }>`\n ${interactiveStyles};\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n display: flex;\n align-items: center;\n gap: 6px;\n background: ${({ theme }) => theme.colors.light};\n color: ${({ theme }) => theme.colors.primary};\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n padding: 7px 8px;\n font-family: inherit;\n cursor: pointer;\n\n ${mq(\"lg\")} {\n padding: 5px 8px;\n }\n\n & svg.lucide {\n color: inherit;\n }\n`;\n\nfunction SearchProvider({\n pages,\n sections,\n children,\n}: {\n pages: PageItem[];\n sections?: SectionItem[];\n children: React.ReactNode;\n}) {\n const [isVisible, setIsVisible] = useState(false);\n const [isClosing, setIsClosing] = useState(false);\n const [query, setQuery] = useState(\"\");\n const [activeIndex, setActiveIndex] = useState(0);\n const [contentResults, setContentResults] = useState<ContentHit[]>([]);\n const [isSearching, setIsSearching] = useState(false);\n const resultsRef = useRef<HTMLUListElement>(null);\n const closingRef = useRef(false);\n const abortRef = useRef<AbortController | null>(null);\n const debounceRef = useRef<ReturnType<typeof setTimeout>>(null);\n const router = useRouter();\n\n const sectionLabels = useMemo(() => {\n const map: Record<string, string> = {};\n sections?.forEach((s) => {\n map[s.slug] = s.label;\n });\n return map;\n }, [sections]);\n\n const openSearch = useCallback(() => {\n closingRef.current = false;\n setIsClosing(false);\n setIsVisible(true);\n }, []);\n\n const closeSearch = useCallback(() => {\n closingRef.current = true;\n setIsClosing(true);\n if (abortRef.current) abortRef.current.abort();\n if (debounceRef.current) clearTimeout(debounceRef.current);\n }, []);\n\n const handleCloseAnimationEnd = useCallback(() => {\n if (!closingRef.current) return;\n closingRef.current = false;\n setIsVisible(false);\n setIsClosing(false);\n setQuery(\"\");\n setActiveIndex(0);\n setContentResults([]);\n setIsSearching(false);\n }, []);\n\n // Instant title/description filtering\n const titleFiltered = useMemo(() => {\n if (!query.trim()) return pages;\n const q = query.toLowerCase();\n return pages.filter(\n (p) =>\n p.title.toLowerCase().includes(q) ||\n p.description?.toLowerCase().includes(q),\n );\n }, [pages, query]);\n\n // Merge title matches with content matches\n const merged = useMemo<MergedResult[]>(() => {\n if (!query.trim()) {\n return pages.map((p) => ({ page: p }));\n }\n\n const titleMatchSlugs = new Set(titleFiltered.map((p) => p.slug));\n const titleMatches: MergedResult[] = titleFiltered.map((p) => {\n const hit = contentResults.find((cr) => cr.slug === p.slug);\n return { page: p, snippet: hit?.snippet };\n });\n\n const pageMap = new Map(pages.map((p) => [p.slug, p]));\n const contentOnly: MergedResult[] = [];\n for (const cr of contentResults) {\n if (!titleMatchSlugs.has(cr.slug)) {\n const page = pageMap.get(cr.slug);\n if (page) {\n contentOnly.push({ page, snippet: cr.snippet });\n }\n }\n }\n\n return [...titleMatches, ...contentOnly];\n }, [pages, query, titleFiltered, contentResults]);\n\n // Debounced content search\n useEffect(() => {\n if (debounceRef.current) clearTimeout(debounceRef.current);\n if (abortRef.current) abortRef.current.abort();\n\n const q = query.trim();\n if (q.length < 2) {\n setContentResults([]);\n setIsSearching(false);\n return;\n }\n\n setIsSearching(true);\n debounceRef.current = setTimeout(async () => {\n const controller = new AbortController();\n abortRef.current = controller;\n try {\n const res = await fetch(\n `/api/search?q=${encodeURIComponent(q)}&limit=15`,\n { signal: controller.signal },\n );\n if (!res.ok) throw new Error(\"Search failed\");\n const data = await res.json();\n setContentResults(data.results ?? []);\n } catch (err: unknown) {\n if (err instanceof DOMException && err.name === \"AbortError\") return;\n setContentResults([]);\n } finally {\n if (!controller.signal.aborted) {\n setIsSearching(false);\n }\n }\n }, 300);\n\n return () => {\n if (debounceRef.current) clearTimeout(debounceRef.current);\n };\n }, [query]);\n\n const navigate = useCallback(\n (slug: string) => {\n closeSearch();\n router.push(`/${slug}`);\n },\n [closeSearch, router],\n );\n\n // Global Cmd+K / Ctrl+K listener\n const isVisibleRef = useRef(false);\n\n useEffect(() => {\n isVisibleRef.current = isVisible;\n }, [isVisible]);\n\n useEffect(() => {\n function handleKeyDown(e: KeyboardEvent) {\n if ((e.metaKey || e.ctrlKey) && e.key === \"k\") {\n e.preventDefault();\n if (isVisibleRef.current) {\n closeSearch();\n } else {\n openSearch();\n }\n }\n }\n document.addEventListener(\"keydown\", handleKeyDown);\n return () => document.removeEventListener(\"keydown\", handleKeyDown);\n }, [closeSearch, openSearch]);\n\n // Scroll active item into view\n useEffect(() => {\n if (!resultsRef.current) return;\n const active = resultsRef.current.children[activeIndex] as HTMLElement;\n active?.scrollIntoView({ block: \"nearest\" });\n }, [activeIndex]);\n\n function handleKeyDown(e: React.KeyboardEvent) {\n if (e.key === \"ArrowDown\") {\n e.preventDefault();\n setActiveIndex((i) => (i < merged.length - 1 ? i + 1 : 0));\n } else if (e.key === \"ArrowUp\") {\n e.preventDefault();\n setActiveIndex((i) => (i > 0 ? i - 1 : merged.length - 1));\n } else if (e.key === \"Enter\") {\n e.preventDefault();\n if (merged[activeIndex]) {\n navigate(merged[activeIndex].page.slug);\n }\n } else if (e.key === \"Escape\") {\n closeSearch();\n }\n }\n\n return (\n <SearchContext.Provider value={{ openSearch }}>\n {children}\n {isVisible && (\n <SearchModalContent\n isClosing={isClosing}\n closeSearch={closeSearch}\n onCloseAnimationEnd={handleCloseAnimationEnd}\n query={query}\n setQuery={setQuery}\n activeIndex={activeIndex}\n setActiveIndex={setActiveIndex}\n resultsRef={resultsRef}\n onKeyDown={handleKeyDown}\n merged={merged}\n sectionLabels={sectionLabels}\n isSearching={isSearching}\n navigate={navigate}\n />\n )}\n </SearchContext.Provider>\n );\n}\n\nexport {\n SearchProvider,\n SearchContext,\n StyledKbd as SearchKbd,\n StyledSearchButton,\n};\n";
|