doccupine 0.0.101 → 0.0.103

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/index.js +7 -0
  2. package/dist/lib/layout.js +1 -0
  3. package/dist/lib/structures.js +2 -0
  4. package/dist/lib/types.d.ts +2 -0
  5. package/dist/templates/components/Docs.d.ts +1 -1
  6. package/dist/templates/components/Docs.js +18 -13
  7. package/dist/templates/components/MDXComponents.d.ts +1 -1
  8. package/dist/templates/components/MDXComponents.js +16 -15
  9. package/dist/templates/components/SectionNavProvider.d.ts +1 -1
  10. package/dist/templates/components/SectionNavProvider.js +8 -1
  11. package/dist/templates/components/SideBar.d.ts +1 -1
  12. package/dist/templates/components/SideBar.js +136 -19
  13. package/dist/templates/components/layout/DocsComponents.d.ts +1 -1
  14. package/dist/templates/components/layout/DocsComponents.js +102 -6
  15. package/dist/templates/components/layout/DocsNavigation.d.ts +1 -1
  16. package/dist/templates/components/layout/DocsNavigation.js +27 -12
  17. package/dist/templates/components/layout/Slug.d.ts +1 -0
  18. package/dist/templates/components/layout/Slug.js +32 -0
  19. package/dist/templates/components/layout/Tabs.d.ts +1 -1
  20. package/dist/templates/components/layout/Tabs.js +11 -0
  21. package/dist/templates/components/layout/Update.d.ts +1 -1
  22. package/dist/templates/components/layout/Update.js +19 -4
  23. package/dist/templates/mdx/components.mdx.d.ts +1 -1
  24. package/dist/templates/mdx/components.mdx.js +1 -0
  25. package/dist/templates/mdx/globals.mdx.d.ts +1 -1
  26. package/dist/templates/mdx/globals.mdx.js +1 -0
  27. package/dist/templates/mdx/index.mdx.d.ts +1 -1
  28. package/dist/templates/mdx/index.mdx.js +1 -0
  29. package/dist/templates/mdx/navigation.mdx.d.ts +1 -1
  30. package/dist/templates/mdx/navigation.mdx.js +69 -1
  31. package/dist/templates/mdx/platform/build-and-deploy.mdx.d.ts +1 -1
  32. package/dist/templates/mdx/platform/build-and-deploy.mdx.js +1 -0
  33. package/dist/templates/mdx/platform/file-editor.mdx.d.ts +1 -1
  34. package/dist/templates/mdx/platform/file-editor.mdx.js +1 -0
  35. package/dist/templates/mdx/platform/index.mdx.d.ts +1 -1
  36. package/dist/templates/mdx/platform/index.mdx.js +1 -0
  37. package/dist/templates/mdx/platform/project-settings.mdx.d.ts +1 -1
  38. package/dist/templates/mdx/platform/project-settings.mdx.js +1 -0
  39. package/dist/templates/mdx/platform/site-settings.mdx.d.ts +1 -1
  40. package/dist/templates/mdx/platform/site-settings.mdx.js +1 -0
  41. package/dist/templates/mdx/tabs.mdx.d.ts +1 -1
  42. package/dist/templates/mdx/tabs.mdx.js +13 -6
  43. package/dist/templates/mdx/update.mdx.d.ts +1 -1
  44. package/dist/templates/mdx/update.mdx.js +6 -3
  45. package/dist/templates/utils/orderNavItems.d.ts +1 -1
  46. package/dist/templates/utils/orderNavItems.js +11 -0
  47. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -667,6 +667,13 @@ class MDXToNextJSGenerator {
667
667
  categoryOrder: frontmatter.categoryOrder || 0,
668
668
  order: frontmatter.order || 0,
669
669
  section: sectionSlug,
670
+ // Sidebar icons (Lucide names). Kept separate from `icon`, which is
671
+ // reserved for the favicon/OG metadata. Only emitted when set so the
672
+ // generated page literal stays lean.
673
+ ...(frontmatter.navIcon ? { navIcon: String(frontmatter.navIcon) } : {}),
674
+ ...(frontmatter.categoryIcon
675
+ ? { categoryIcon: String(frontmatter.categoryIcon) }
676
+ : {}),
670
677
  lastModified,
671
678
  };
672
679
  }
@@ -248,6 +248,7 @@ ${hasSections
248
248
  date: "2025-01-15",
249
249
  category: "Introduction",
250
250
  categoryOrder: 0,
251
+ categoryIcon: "rocket",
251
252
  order: 0,
252
253
  },
253
254
  ];
@@ -45,6 +45,7 @@ import { iconTemplate } from "../templates/components/layout/Icon.js";
45
45
  import { pictogramsTemplate } from "../templates/components/layout/Pictograms.js";
46
46
  import { sharedStyledTemplate } from "../templates/components/layout/SharedStyles.js";
47
47
  import { siteGateComponentTemplate } from "../templates/components/layout/SiteGate.js";
48
+ import { slugTemplate } from "../templates/components/layout/Slug.js";
48
49
  import { staticLinksTemplate } from "../templates/components/layout/StaticLinks.js";
49
50
  import { stepsTemplate } from "../templates/components/layout/Steps.js";
50
51
  import { tabsTemplate } from "../templates/components/layout/Tabs.js";
@@ -176,6 +177,7 @@ export const appStructure = {
176
177
  "components/layout/Pictograms.tsx": pictogramsTemplate,
177
178
  "components/layout/SharedStyled.ts": sharedStyledTemplate,
178
179
  "components/layout/SiteGate.tsx": siteGateComponentTemplate,
180
+ "components/layout/Slug.ts": slugTemplate,
179
181
  "components/layout/StaticLinks.tsx": staticLinksTemplate,
180
182
  "components/layout/Steps.tsx": stepsTemplate,
181
183
  "components/layout/Tabs.tsx": tabsTemplate,
@@ -14,6 +14,8 @@ export interface PageMeta {
14
14
  categoryOrder: number;
15
15
  order: number;
16
16
  section: string;
17
+ navIcon?: string;
18
+ categoryIcon?: string;
17
19
  lastModified?: string;
18
20
  }
19
21
  export interface SectionConfig {
@@ -1 +1 @@
1
- export declare const docsTemplate = "import React from \"react\";\nimport { Flex } from \"cherry-styled-components\";\nimport {\n DocsContainer,\n StyledMarkdownContainer,\n StyledMissingComponent,\n} from \"@/components/layout/DocsComponents\";\nimport { MDXRemote } from \"next-mdx-remote/rsc\";\nimport remarkGfm from \"remark-gfm\";\nimport { useMDXComponents } from \"@/components/MDXComponents\";\nimport { DocsSideBar } from \"@/components/DocsSideBar\";\nimport { ActionBar } from \"@/components/layout/ActionBar\";\n\ninterface DocsProps {\n content: string;\n}\n\ninterface Heading {\n id: string;\n text: string;\n level: number;\n}\n\nfunction generateId(text: string): string {\n return text\n .toLowerCase()\n .replace(/[^\\w\\s-]/g, \"\")\n .replace(/\\s+/g, \"-\")\n .trim();\n}\n\nfunction extractHeadings(content: string): Heading[] {\n const contentWithoutCodeBlocks = content.replace(/```[\\s\\S]*?```/g, \"\");\n const headingRegex = /^(#{1,6})\\s+(.+)$/gm;\n const headings: Heading[] = [];\n let match;\n\n while ((match = headingRegex.exec(contentWithoutCodeBlocks)) !== null) {\n const level = match[1].length;\n const text = match[2].trim();\n const id = generateId(text);\n headings.push({ id, text, level });\n }\n\n return headings;\n}\n\nfunction extractComponentNames(source: string): string[] {\n const stripped = source\n .replace(/```[\\s\\S]*?```/g, \"\")\n .replace(/`[^`]*`/g, \"\");\n const tagRegex = /<([A-Z][a-zA-Z0-9]*)/g;\n const names = new Set<string>();\n let match;\n while ((match = tagRegex.exec(stripped)) !== null) {\n names.add(match[1]);\n }\n return Array.from(names);\n}\n\nfunction MissingComponent({\n componentName,\n children: _children,\n}: {\n componentName: string;\n children?: React.ReactNode;\n}) {\n return (\n <StyledMissingComponent>\n Missing component: &lt;{componentName} /&gt;\n </StyledMissingComponent>\n );\n}\n\nfunction Docs({ content }: DocsProps) {\n const headings = extractHeadings(content);\n const components = useMDXComponents({});\n\n const knownNames = Object.keys(components);\n const usedNames = extractComponentNames(content);\n const missingNames = usedNames.filter((name) => !knownNames.includes(name));\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const stubs: Record<string, React.ComponentType<any>> = {};\n for (const name of missingNames) {\n stubs[name] = ({ children }: { children?: React.ReactNode }) => (\n <MissingComponent componentName={name}>{children}</MissingComponent>\n );\n }\n\n const allComponents = { ...components, ...stubs };\n\n return (\n <>\n <DocsContainer>\n <ActionBar content={content}>\n <Flex $gap={20}>\n <StyledMarkdownContainer>\n {content && (\n <MDXRemote\n source={content}\n options={{\n blockJS: false,\n mdxOptions: {\n remarkPlugins: [remarkGfm],\n },\n }}\n components={allComponents}\n />\n )}\n </StyledMarkdownContainer>\n </Flex>\n </ActionBar>\n </DocsContainer>\n <DocsSideBar headings={headings} />\n </>\n );\n}\n\nexport { Docs };\n";
1
+ export declare const docsTemplate = "import React from \"react\";\nimport { Flex } from \"cherry-styled-components\";\nimport {\n DocsContainer,\n StyledMarkdownContainer,\n StyledMissingComponent,\n} from \"@/components/layout/DocsComponents\";\nimport { MDXRemote } from \"next-mdx-remote/rsc\";\nimport remarkGfm from \"remark-gfm\";\nimport { useMDXComponents } from \"@/components/MDXComponents\";\nimport { DocsSideBar } from \"@/components/DocsSideBar\";\nimport { ActionBar } from \"@/components/layout/ActionBar\";\nimport { createSlugger } from \"@/components/layout/Slug\";\n\ninterface DocsProps {\n content: string;\n}\n\ninterface Heading {\n id: string;\n text: string;\n level: number;\n}\n\nfunction extractHeadings(content: string): Heading[] {\n const contentWithoutCodeBlocks = content.replace(/```[\\s\\S]*?```/g, \"\");\n const entries: { text: string; level: number; position: number }[] = [];\n let match;\n\n // Markdown headings (# .. ######)\n const headingRegex = /^(#{1,6})\\s+(.+)$/gm;\n while ((match = headingRegex.exec(contentWithoutCodeBlocks)) !== null) {\n const level = match[1].length;\n const text = match[2].trim();\n entries.push({ text, level, position: match.index });\n }\n\n // <Update label=\"...\"> blocks surface their label as a top-level entry\n const updateRegex = /<Update\\b[^>]*?\\blabel=[\"']([^\"']+)[\"'][^>]*>/g;\n while ((match = updateRegex.exec(contentWithoutCodeBlocks)) !== null) {\n entries.push({ text: match[1].trim(), level: 1, position: match.index });\n }\n\n // Assign ids in document order with a shared slugger so repeated heading\n // text produces unique anchors (\"setup\", \"setup-1\", ...) that stay in sync\n // with the ids rendered by MDXComponents/Update.\n const slug = createSlugger();\n return entries\n .sort((a, b) => a.position - b.position)\n .map(({ text, level }) => ({ id: slug(text), text, level }));\n}\n\nfunction extractComponentNames(source: string): string[] {\n const stripped = source\n .replace(/```[\\s\\S]*?```/g, \"\")\n .replace(/`[^`]*`/g, \"\");\n const tagRegex = /<([A-Z][a-zA-Z0-9]*)/g;\n const names = new Set<string>();\n let match;\n while ((match = tagRegex.exec(stripped)) !== null) {\n names.add(match[1]);\n }\n return Array.from(names);\n}\n\nfunction MissingComponent({\n componentName,\n children: _children,\n}: {\n componentName: string;\n children?: React.ReactNode;\n}) {\n return (\n <StyledMissingComponent>\n Missing component: &lt;{componentName} /&gt;\n </StyledMissingComponent>\n );\n}\n\nfunction Docs({ content }: DocsProps) {\n const headings = extractHeadings(content);\n const components = useMDXComponents({});\n\n const knownNames = Object.keys(components);\n const usedNames = extractComponentNames(content);\n const missingNames = usedNames.filter((name) => !knownNames.includes(name));\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const stubs: Record<string, React.ComponentType<any>> = {};\n for (const name of missingNames) {\n stubs[name] = ({ children }: { children?: React.ReactNode }) => (\n <MissingComponent componentName={name}>{children}</MissingComponent>\n );\n }\n\n const allComponents = { ...components, ...stubs };\n\n return (\n <>\n <DocsContainer>\n <ActionBar content={content}>\n <Flex $gap={20}>\n <StyledMarkdownContainer>\n {content && (\n <MDXRemote\n source={content}\n options={{\n blockJS: false,\n mdxOptions: {\n remarkPlugins: [remarkGfm],\n },\n }}\n components={allComponents}\n />\n )}\n </StyledMarkdownContainer>\n </Flex>\n </ActionBar>\n </DocsContainer>\n <DocsSideBar headings={headings} />\n </>\n );\n}\n\nexport { Docs };\n";
@@ -10,6 +10,7 @@ import remarkGfm from "remark-gfm";
10
10
  import { useMDXComponents } from "@/components/MDXComponents";
11
11
  import { DocsSideBar } from "@/components/DocsSideBar";
12
12
  import { ActionBar } from "@/components/layout/ActionBar";
13
+ import { createSlugger } from "@/components/layout/Slug";
13
14
 
14
15
  interface DocsProps {
15
16
  content: string;
@@ -21,28 +22,32 @@ interface Heading {
21
22
  level: number;
22
23
  }
23
24
 
24
- function generateId(text: string): string {
25
- return text
26
- .toLowerCase()
27
- .replace(/[^\\w\\s-]/g, "")
28
- .replace(/\\s+/g, "-")
29
- .trim();
30
- }
31
-
32
25
  function extractHeadings(content: string): Heading[] {
33
26
  const contentWithoutCodeBlocks = content.replace(/\`\`\`[\\s\\S]*?\`\`\`/g, "");
34
- const headingRegex = /^(#{1,6})\\s+(.+)$/gm;
35
- const headings: Heading[] = [];
27
+ const entries: { text: string; level: number; position: number }[] = [];
36
28
  let match;
37
29
 
30
+ // Markdown headings (# .. ######)
31
+ const headingRegex = /^(#{1,6})\\s+(.+)$/gm;
38
32
  while ((match = headingRegex.exec(contentWithoutCodeBlocks)) !== null) {
39
33
  const level = match[1].length;
40
34
  const text = match[2].trim();
41
- const id = generateId(text);
42
- headings.push({ id, text, level });
35
+ entries.push({ text, level, position: match.index });
36
+ }
37
+
38
+ // <Update label="..."> blocks surface their label as a top-level entry
39
+ const updateRegex = /<Update\\b[^>]*?\\blabel=["']([^"']+)["'][^>]*>/g;
40
+ while ((match = updateRegex.exec(contentWithoutCodeBlocks)) !== null) {
41
+ entries.push({ text: match[1].trim(), level: 1, position: match.index });
43
42
  }
44
43
 
45
- return headings;
44
+ // Assign ids in document order with a shared slugger so repeated heading
45
+ // text produces unique anchors ("setup", "setup-1", ...) that stay in sync
46
+ // with the ids rendered by MDXComponents/Update.
47
+ const slug = createSlugger();
48
+ return entries
49
+ .sort((a, b) => a.position - b.position)
50
+ .map(({ text, level }) => ({ id: slug(text), text, level }));
46
51
  }
47
52
 
48
53
  function extractComponentNames(source: string): string[] {
@@ -1 +1 @@
1
- export declare const mdxComponentsTemplate = "import React from \"react\";\nimport Link from \"next/link\";\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype MDXComponents = Record<string, React.ComponentType<any>>;\nimport { Space } from \"cherry-styled-components\";\nimport { Code as CodeBlock, CodeTabs } from \"@/components/layout/Code\";\nimport { Card } from \"@/components/layout/Card\";\nimport { Accordion } from \"@/components/layout/Accordion\";\nimport { Tabs, TabContent } from \"@/components/layout/Tabs\";\nimport { Callout } from \"@/components/layout/Callout\";\nimport { Icon } from \"@/components/layout/Icon\";\nimport { Columns } from \"@/components/layout/Columns\";\nimport { Field } from \"@/components/layout/Field\";\nimport { Update } from \"@/components/layout/Update\";\nimport { Steps, Step } from \"@/components/layout/Steps\";\nimport { Button } from \"@/components/layout/Button\";\nimport { ColorSwatch, ColorSwatchGroup } from \"@/components/layout/ColorSwatch\";\nimport { DemoTheme } from \"@/components/layout/DemoTheme\";\n\ninterface HeadingProps extends React.HTMLAttributes<HTMLHeadingElement> {\n children?: React.ReactNode;\n}\n\ninterface PreProps extends React.HTMLAttributes<HTMLPreElement> {\n children?: React.ReactNode;\n}\n\nfunction extractAllTextFromChildren(children: React.ReactNode): string {\n if (children == null) return \"\";\n if (typeof children === \"string\") return children;\n if (typeof children === \"number\") return String(children);\n if (typeof children === \"boolean\") return \"\";\n if (Array.isArray(children))\n return children.map(extractAllTextFromChildren).join(\"\");\n if (React.isValidElement(children)) {\n const element = children as React.ReactElement<{\n children?: React.ReactNode;\n }>;\n return extractAllTextFromChildren(element.props.children);\n }\n return \"\";\n}\n\nfunction generateId(text: string): string {\n return text\n .toLowerCase()\n .replace(/[^\\w\\s-]/g, \"\")\n .replace(/\\s+/g, \"-\")\n .trim();\n}\n\n// Map <pre><code class=\"language-xyz\"> to our <Code /> component\nfunction Pre(props: PreProps) {\n const child = React.Children.only(props.children) as React.ReactElement<{\n className?: string;\n children?: React.ReactNode;\n }> | null;\n if (child && child.type === \"code\") {\n const className = child.props.className || \"\";\n const match = /language-(\\w+)/.exec(className);\n const language = match ? match[1] : undefined;\n const code = extractAllTextFromChildren(child.props.children).replace(\n /\\n$/,\n \"\",\n );\n if (language) {\n return (\n <CodeBlock className={className} code={code} language={language} />\n );\n }\n }\n return <pre {...props} />;\n}\n\nexport function useMDXComponents(components: MDXComponents): MDXComponents {\n return {\n // Headings with auto-generated ids for TOC and deep links\n h1: ({ children, ...props }: HeadingProps) => {\n const id = generateId(extractAllTextFromChildren(children));\n return (\n <h1 id={id} {...props}>\n {children}\n </h1>\n );\n },\n h2: ({ children, ...props }: HeadingProps) => {\n const id = generateId(extractAllTextFromChildren(children));\n return (\n <h2 id={id} {...props}>\n {children}\n </h2>\n );\n },\n h3: ({ children, ...props }: HeadingProps) => {\n const id = generateId(extractAllTextFromChildren(children));\n return (\n <h3 id={id} {...props}>\n {children}\n </h3>\n );\n },\n h4: ({ children, ...props }: HeadingProps) => {\n const id = generateId(extractAllTextFromChildren(children));\n return (\n <h4 id={id} {...props}>\n {children}\n </h4>\n );\n },\n h5: ({ children, ...props }: HeadingProps) => {\n const id = generateId(extractAllTextFromChildren(children));\n return (\n <h5 id={id} {...props}>\n {children}\n </h5>\n );\n },\n h6: ({ children, ...props }: HeadingProps) => {\n const id = generateId(extractAllTextFromChildren(children));\n return (\n <h6 id={id} {...props}>\n {children}\n </h6>\n );\n },\n\n // Links - use Next.js Link for internal paths\n a: ({\n href,\n children,\n ...props\n }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {\n if (href && href.startsWith(\"/\")) {\n return (\n <Link href={href} {...props}>\n {children}\n </Link>\n );\n }\n return (\n <a href={href} target=\"_blank\" rel=\"noopener noreferrer\" {...props}>\n {children}\n </a>\n );\n },\n\n // Tables - wrap in a div for responsive overflow\n table: (props: React.TableHTMLAttributes<HTMLTableElement>) => (\n <div className=\"table-wrapper\">\n <table {...props} />\n </div>\n ),\n\n // Code blocks\n pre: Pre,\n\n // Expose your custom components for MDX usage\n Code: CodeBlock,\n CodeTabs,\n Card,\n Accordion,\n Tabs,\n TabContent,\n Callout,\n Icon,\n Columns,\n Field,\n Update,\n Steps,\n Step,\n Button,\n ColorSwatch,\n ColorSwatchGroup,\n DemoTheme,\n Space,\n ...components,\n };\n}\n";
1
+ export declare const mdxComponentsTemplate = "import React from \"react\";\nimport Link from \"next/link\";\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype MDXComponents = Record<string, React.ComponentType<any>>;\nimport { Space } from \"cherry-styled-components\";\nimport { Code as CodeBlock, CodeTabs } from \"@/components/layout/Code\";\nimport { Card } from \"@/components/layout/Card\";\nimport { Accordion } from \"@/components/layout/Accordion\";\nimport { Tabs, TabContent } from \"@/components/layout/Tabs\";\nimport { Callout } from \"@/components/layout/Callout\";\nimport { Icon } from \"@/components/layout/Icon\";\nimport { Columns } from \"@/components/layout/Columns\";\nimport { Field } from \"@/components/layout/Field\";\nimport { Update } from \"@/components/layout/Update\";\nimport { Steps, Step } from \"@/components/layout/Steps\";\nimport { Button } from \"@/components/layout/Button\";\nimport { ColorSwatch, ColorSwatchGroup } from \"@/components/layout/ColorSwatch\";\nimport { DemoTheme } from \"@/components/layout/DemoTheme\";\nimport { createSlugger } from \"@/components/layout/Slug\";\n\ninterface HeadingProps extends React.HTMLAttributes<HTMLHeadingElement> {\n children?: React.ReactNode;\n}\n\ninterface PreProps extends React.HTMLAttributes<HTMLPreElement> {\n children?: React.ReactNode;\n}\n\nfunction extractAllTextFromChildren(children: React.ReactNode): string {\n if (children == null) return \"\";\n if (typeof children === \"string\") return children;\n if (typeof children === \"number\") return String(children);\n if (typeof children === \"boolean\") return \"\";\n if (Array.isArray(children))\n return children.map(extractAllTextFromChildren).join(\"\");\n if (React.isValidElement(children)) {\n const element = children as React.ReactElement<{\n children?: React.ReactNode;\n }>;\n return extractAllTextFromChildren(element.props.children);\n }\n return \"\";\n}\n\n// Map <pre><code class=\"language-xyz\"> to our <Code /> component\nfunction Pre(props: PreProps) {\n const child = React.Children.only(props.children) as React.ReactElement<{\n className?: string;\n children?: React.ReactNode;\n }> | null;\n if (child && child.type === \"code\") {\n const className = child.props.className || \"\";\n const match = /language-(\\w+)/.exec(className);\n const language = match ? match[1] : undefined;\n const code = extractAllTextFromChildren(child.props.children).replace(\n /\\n$/,\n \"\",\n );\n if (language) {\n return (\n <CodeBlock className={className} code={code} language={language} />\n );\n }\n }\n return <pre {...props} />;\n}\n\nexport function useMDXComponents(components: MDXComponents): MDXComponents {\n // One slugger per render so repeated heading text (and <Update> labels)\n // resolve to unique, document-order ids that match the index sidebar built\n // in components/Docs.tsx.\n const slug = createSlugger();\n return {\n // Headings with auto-generated ids for TOC and deep links\n h1: ({ children, ...props }: HeadingProps) => {\n const id = slug(extractAllTextFromChildren(children));\n return (\n <h1 id={id} {...props}>\n {children}\n </h1>\n );\n },\n h2: ({ children, ...props }: HeadingProps) => {\n const id = slug(extractAllTextFromChildren(children));\n return (\n <h2 id={id} {...props}>\n {children}\n </h2>\n );\n },\n h3: ({ children, ...props }: HeadingProps) => {\n const id = slug(extractAllTextFromChildren(children));\n return (\n <h3 id={id} {...props}>\n {children}\n </h3>\n );\n },\n h4: ({ children, ...props }: HeadingProps) => {\n const id = slug(extractAllTextFromChildren(children));\n return (\n <h4 id={id} {...props}>\n {children}\n </h4>\n );\n },\n h5: ({ children, ...props }: HeadingProps) => {\n const id = slug(extractAllTextFromChildren(children));\n return (\n <h5 id={id} {...props}>\n {children}\n </h5>\n );\n },\n h6: ({ children, ...props }: HeadingProps) => {\n const id = slug(extractAllTextFromChildren(children));\n return (\n <h6 id={id} {...props}>\n {children}\n </h6>\n );\n },\n\n // Links - use Next.js Link for internal paths\n a: ({\n href,\n children,\n ...props\n }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {\n if (href && href.startsWith(\"/\")) {\n return (\n <Link href={href} {...props}>\n {children}\n </Link>\n );\n }\n return (\n <a href={href} target=\"_blank\" rel=\"noopener noreferrer\" {...props}>\n {children}\n </a>\n );\n },\n\n // Tables - wrap in a div for responsive overflow\n table: (props: React.TableHTMLAttributes<HTMLTableElement>) => (\n <div className=\"table-wrapper\">\n <table {...props} />\n </div>\n ),\n\n // Code blocks\n pre: Pre,\n\n // Expose your custom components for MDX usage\n Code: CodeBlock,\n CodeTabs,\n Card,\n Accordion,\n Tabs,\n TabContent,\n Callout,\n Icon,\n Columns,\n Field,\n // Share the heading slugger so an <Update> label anchor stays unique and\n // in document order alongside the surrounding headings.\n Update: (props: React.ComponentProps<typeof Update>) => (\n <Update {...props} id={slug(props.label)} />\n ),\n Steps,\n Step,\n Button,\n ColorSwatch,\n ColorSwatchGroup,\n DemoTheme,\n Space,\n ...components,\n };\n}\n";
@@ -17,6 +17,7 @@ import { Steps, Step } from "@/components/layout/Steps";
17
17
  import { Button } from "@/components/layout/Button";
18
18
  import { ColorSwatch, ColorSwatchGroup } from "@/components/layout/ColorSwatch";
19
19
  import { DemoTheme } from "@/components/layout/DemoTheme";
20
+ import { createSlugger } from "@/components/layout/Slug";
20
21
 
21
22
  interface HeadingProps extends React.HTMLAttributes<HTMLHeadingElement> {
22
23
  children?: React.ReactNode;
@@ -42,14 +43,6 @@ function extractAllTextFromChildren(children: React.ReactNode): string {
42
43
  return "";
43
44
  }
44
45
 
45
- function generateId(text: string): string {
46
- return text
47
- .toLowerCase()
48
- .replace(/[^\\w\\s-]/g, "")
49
- .replace(/\\s+/g, "-")
50
- .trim();
51
- }
52
-
53
46
  // Map <pre><code class="language-xyz"> to our <Code /> component
54
47
  function Pre(props: PreProps) {
55
48
  const child = React.Children.only(props.children) as React.ReactElement<{
@@ -74,10 +67,14 @@ function Pre(props: PreProps) {
74
67
  }
75
68
 
76
69
  export function useMDXComponents(components: MDXComponents): MDXComponents {
70
+ // One slugger per render so repeated heading text (and <Update> labels)
71
+ // resolve to unique, document-order ids that match the index sidebar built
72
+ // in components/Docs.tsx.
73
+ const slug = createSlugger();
77
74
  return {
78
75
  // Headings with auto-generated ids for TOC and deep links
79
76
  h1: ({ children, ...props }: HeadingProps) => {
80
- const id = generateId(extractAllTextFromChildren(children));
77
+ const id = slug(extractAllTextFromChildren(children));
81
78
  return (
82
79
  <h1 id={id} {...props}>
83
80
  {children}
@@ -85,7 +82,7 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
85
82
  );
86
83
  },
87
84
  h2: ({ children, ...props }: HeadingProps) => {
88
- const id = generateId(extractAllTextFromChildren(children));
85
+ const id = slug(extractAllTextFromChildren(children));
89
86
  return (
90
87
  <h2 id={id} {...props}>
91
88
  {children}
@@ -93,7 +90,7 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
93
90
  );
94
91
  },
95
92
  h3: ({ children, ...props }: HeadingProps) => {
96
- const id = generateId(extractAllTextFromChildren(children));
93
+ const id = slug(extractAllTextFromChildren(children));
97
94
  return (
98
95
  <h3 id={id} {...props}>
99
96
  {children}
@@ -101,7 +98,7 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
101
98
  );
102
99
  },
103
100
  h4: ({ children, ...props }: HeadingProps) => {
104
- const id = generateId(extractAllTextFromChildren(children));
101
+ const id = slug(extractAllTextFromChildren(children));
105
102
  return (
106
103
  <h4 id={id} {...props}>
107
104
  {children}
@@ -109,7 +106,7 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
109
106
  );
110
107
  },
111
108
  h5: ({ children, ...props }: HeadingProps) => {
112
- const id = generateId(extractAllTextFromChildren(children));
109
+ const id = slug(extractAllTextFromChildren(children));
113
110
  return (
114
111
  <h5 id={id} {...props}>
115
112
  {children}
@@ -117,7 +114,7 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
117
114
  );
118
115
  },
119
116
  h6: ({ children, ...props }: HeadingProps) => {
120
- const id = generateId(extractAllTextFromChildren(children));
117
+ const id = slug(extractAllTextFromChildren(children));
121
118
  return (
122
119
  <h6 id={id} {...props}>
123
120
  {children}
@@ -166,7 +163,11 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
166
163
  Icon,
167
164
  Columns,
168
165
  Field,
169
- Update,
166
+ // Share the heading slugger so an <Update> label anchor stays unique and
167
+ // in document order alongside the surrounding headings.
168
+ Update: (props: React.ComponentProps<typeof Update>) => (
169
+ <Update {...props} id={slug(props.label)} />
170
+ ),
170
171
  Steps,
171
172
  Step,
172
173
  Button,
@@ -1 +1 @@
1
- export declare const sectionNavProviderTemplate = "\"use client\";\nimport { useMemo } from \"react\";\nimport { usePathname } from \"next/navigation\";\nimport { SideBar } from \"@/components/SideBar\";\nimport { DocsNavigation } from \"@/components/layout/DocsNavigation\";\nimport { SectionBarProvider } from \"@/components/layout/DocsComponents\";\nimport { Footer } from \"@/components/layout/Footer\";\nimport { StaticLinks } from \"@/components/layout/StaticLinks\";\nimport {\n transformPagesToGroupedStructure,\n type PagesProps,\n} from \"@/utils/orderNavItems\";\nimport rawNavigation from \"@/navigation.json\";\n\n// navigation.json can be an array (root section only) or an object keyed by section slug\ntype NavItem = { label: string; links: { slug: string; title: string }[] };\ntype NavigationConfig = NavItem[] | Record<string, NavItem[]>;\n\nconst navigation = rawNavigation as NavigationConfig;\n\nfunction getNavigationForSection(\n nav: NavigationConfig,\n sectionSlug: string,\n): NavItem[] | null {\n if (Array.isArray(nav)) {\n return sectionSlug === \"\" && nav.length ? nav : null;\n }\n const sectionNav = nav[sectionSlug];\n return sectionNav && sectionNav.length ? sectionNav : null;\n}\n\ninterface SectionConfig {\n label: string;\n slug: string;\n directory?: string;\n}\n\ninterface SectionNavProviderProps {\n sections: SectionConfig[];\n allPages: PagesProps[];\n hideBranding: boolean;\n children: React.ReactNode;\n}\n\nfunction SectionNavProvider({\n sections,\n allPages,\n hideBranding,\n children,\n}: SectionNavProviderProps) {\n const pathname = usePathname();\n const currentPath = pathname.replace(/^\\//, \"\").replace(/\\/$/, \"\");\n\n const activeSectionSlug = useMemo(() => {\n const match = sections.find((section) => {\n if (section.slug === \"\") return false;\n return (\n currentPath === section.slug ||\n currentPath.startsWith(section.slug + \"/\")\n );\n });\n return match ? match.slug : \"\";\n }, [sections, currentPath]);\n\n const result = useMemo(() => {\n const sectionNav = getNavigationForSection(navigation, activeSectionSlug);\n if (sectionNav) return sectionNav;\n const filtered = allPages.filter(\n (page) => (page.section || \"\") === activeSectionSlug,\n );\n return transformPagesToGroupedStructure(filtered);\n }, [allPages, activeSectionSlug]);\n\n // Fallback when no pages exist yet (also defined in layout.tsx for the non-sections path)\n const defaultPages = [\n {\n slug: \"\",\n title: \"Getting Started\",\n description:\n \"Doccupine is a 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.\",\n date: \"2025-01-15\",\n category: \"Introduction\",\n categoryOrder: 0,\n order: 0,\n },\n ];\n\n const defaultResults = transformPagesToGroupedStructure(defaultPages);\n\n return (\n <SectionBarProvider hasSectionBar={true}>\n <SideBar result={result.length ? result : defaultResults} />\n {children}\n <DocsNavigation result={result.length ? result : defaultResults} />\n <StaticLinks />\n <Footer hideBranding={hideBranding} />\n </SectionBarProvider>\n );\n}\n\nexport { SectionNavProvider };\n";
1
+ export declare const sectionNavProviderTemplate = "\"use client\";\nimport { useMemo } from \"react\";\nimport { usePathname } from \"next/navigation\";\nimport { SideBar } from \"@/components/SideBar\";\nimport { DocsNavigation } from \"@/components/layout/DocsNavigation\";\nimport { SectionBarProvider } from \"@/components/layout/DocsComponents\";\nimport { Footer } from \"@/components/layout/Footer\";\nimport { StaticLinks } from \"@/components/layout/StaticLinks\";\nimport {\n transformPagesToGroupedStructure,\n type PagesProps,\n} from \"@/utils/orderNavItems\";\nimport rawNavigation from \"@/navigation.json\";\n\n// navigation.json can be an array (root section only) or an object keyed by section slug\ntype NavLink = {\n slug?: string;\n title: string;\n icon?: string;\n links?: NavLink[];\n};\ntype NavItem = { label: string; icon?: string; links: NavLink[] };\ntype NavigationConfig = NavItem[] | Record<string, NavItem[]>;\n\nconst navigation = rawNavigation as NavigationConfig;\n\nfunction getNavigationForSection(\n nav: NavigationConfig,\n sectionSlug: string,\n): NavItem[] | null {\n if (Array.isArray(nav)) {\n return sectionSlug === \"\" && nav.length ? nav : null;\n }\n const sectionNav = nav[sectionSlug];\n return sectionNav && sectionNav.length ? sectionNav : null;\n}\n\ninterface SectionConfig {\n label: string;\n slug: string;\n directory?: string;\n}\n\ninterface SectionNavProviderProps {\n sections: SectionConfig[];\n allPages: PagesProps[];\n hideBranding: boolean;\n children: React.ReactNode;\n}\n\nfunction SectionNavProvider({\n sections,\n allPages,\n hideBranding,\n children,\n}: SectionNavProviderProps) {\n const pathname = usePathname();\n const currentPath = pathname.replace(/^\\//, \"\").replace(/\\/$/, \"\");\n\n const activeSectionSlug = useMemo(() => {\n const match = sections.find((section) => {\n if (section.slug === \"\") return false;\n return (\n currentPath === section.slug ||\n currentPath.startsWith(section.slug + \"/\")\n );\n });\n return match ? match.slug : \"\";\n }, [sections, currentPath]);\n\n const result = useMemo(() => {\n const sectionNav = getNavigationForSection(navigation, activeSectionSlug);\n if (sectionNav) return sectionNav;\n const filtered = allPages.filter(\n (page) => (page.section || \"\") === activeSectionSlug,\n );\n return transformPagesToGroupedStructure(filtered);\n }, [allPages, activeSectionSlug]);\n\n // Fallback when no pages exist yet (also defined in layout.tsx for the non-sections path)\n const defaultPages = [\n {\n slug: \"\",\n title: \"Getting Started\",\n description:\n \"Doccupine is a 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.\",\n date: \"2025-01-15\",\n category: \"Introduction\",\n categoryOrder: 0,\n categoryIcon: \"rocket\",\n order: 0,\n },\n ];\n\n const defaultResults = transformPagesToGroupedStructure(defaultPages);\n\n return (\n <SectionBarProvider hasSectionBar={true}>\n <SideBar result={result.length ? result : defaultResults} />\n {children}\n <DocsNavigation result={result.length ? result : defaultResults} />\n <StaticLinks />\n <Footer hideBranding={hideBranding} />\n </SectionBarProvider>\n );\n}\n\nexport { SectionNavProvider };\n";
@@ -14,7 +14,13 @@ import {
14
14
  import rawNavigation from "@/navigation.json";
15
15
 
16
16
  // navigation.json can be an array (root section only) or an object keyed by section slug
17
- type NavItem = { label: string; links: { slug: string; title: string }[] };
17
+ type NavLink = {
18
+ slug?: string;
19
+ title: string;
20
+ icon?: string;
21
+ links?: NavLink[];
22
+ };
23
+ type NavItem = { label: string; icon?: string; links: NavLink[] };
18
24
  type NavigationConfig = NavItem[] | Record<string, NavItem[]>;
19
25
 
20
26
  const navigation = rawNavigation as NavigationConfig;
@@ -82,6 +88,7 @@ function SectionNavProvider({
82
88
  date: "2025-01-15",
83
89
  category: "Introduction",
84
90
  categoryOrder: 0,
91
+ categoryIcon: "rocket",
85
92
  order: 0,
86
93
  },
87
94
  ];
@@ -1 +1 @@
1
- export declare const sideBarTemplate = "\"use client\";\nimport { useContext, 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 StyledSidebarFooter,\n StyleMobileBar,\n StyledMobileBurger,\n} from \"@/components/layout/DocsComponents\";\nimport { useLockBodyScroll } from \"@/components/LockBodyScroll\";\n\ntype NavItem = {\n label: string;\n links: NavItemLink[];\n};\n\ntype NavItemLink = {\n slug: string;\n title: string;\n};\n\ninterface SideBarProps {\n result: NavItem[];\n}\n\nfunction SideBar({ result }: SideBarProps) {\n const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);\n const hasSectionBar = useContext(SectionBarContext);\n const pathname = usePathname();\n\n useLockBodyScroll(isMobileMenuOpen);\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 $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>{item.label}</StyledStrong>{\" \"}\n </StyledSidebarListItem>\n <li>\n <Space $size={20} />\n </li>\n {item.links &&\n item.links.map((link: NavItemLink, indexChild: number) => {\n return (\n <StyledSidebarListItem key={indexChild}>\n <StyledSidebarListItemLink\n href={`/${link.slug}`}\n $isActive={pathname === `/${link.slug}`}\n onClick={() => setIsMobileMenuOpen(false)}\n >\n {link.title}\n </StyledSidebarListItemLink>\n </StyledSidebarListItem>\n );\n })}\n <li aria-hidden=\"true\">\n <Space $size={20} />\n </li>\n </StyledSidebarList>\n );\n })}\n <StyledSidebarFooter>\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, 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 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 href={href} onClick={onNavigate}>\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\n useLockBodyScroll(isMobileMenuOpen);\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 $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>\n <Flex $xsJustifyContent=\"flex-start\" $lgJustifyContent=\"flex-end\">\n <ThemeToggle />\n </Flex>\n </StyledSidebarFooter>\n </StyledSidebar>\n </DocsSidebar>\n );\n}\n\nexport { SideBar };\n";
@@ -10,26 +10,144 @@ import {
10
10
  StyledSidebarListItem,
11
11
  StyledStrong,
12
12
  StyledSidebarListItemLink,
13
+ StyledSidebarGroupButton,
14
+ StyledSidebarGroupRow,
15
+ StyledSidebarGroupLink,
16
+ StyledSidebarGroupChevron,
17
+ StyledSidebarGroupContent,
13
18
  StyledSidebarFooter,
14
19
  StyleMobileBar,
15
20
  StyledMobileBurger,
16
21
  } from "@/components/layout/DocsComponents";
22
+ import { Icon } from "@/components/layout/Icon";
17
23
  import { useLockBodyScroll } from "@/components/LockBodyScroll";
18
24
 
25
+ // A link can be a leaf (slug + title) or a group with nested children. Both the
26
+ // category icon and the per-link icon are optional Lucide names.
27
+ type NavItemLink = {
28
+ slug?: string;
29
+ title: string;
30
+ icon?: string;
31
+ links?: NavItemLink[];
32
+ };
33
+
19
34
  type NavItem = {
20
35
  label: string;
36
+ icon?: string;
21
37
  links: NavItemLink[];
22
38
  };
23
39
 
24
- type NavItemLink = {
25
- slug: string;
26
- title: string;
27
- };
28
-
29
40
  interface SideBarProps {
30
41
  result: NavItem[];
31
42
  }
32
43
 
44
+ function linkContainsActivePath(link: NavItemLink, pathname: string): boolean {
45
+ if (link.slug !== undefined && pathname === \`/\${link.slug}\`) {
46
+ return true;
47
+ }
48
+ return (link.links ?? []).some((child) =>
49
+ linkContainsActivePath(child, pathname),
50
+ );
51
+ }
52
+
53
+ function SidebarNavLink({
54
+ link,
55
+ depth,
56
+ pathname,
57
+ onNavigate,
58
+ }: {
59
+ link: NavItemLink;
60
+ depth: number;
61
+ pathname: string;
62
+ onNavigate: () => void;
63
+ }) {
64
+ const children = link.links ?? [];
65
+ const hasChildren = children.length > 0;
66
+ const href = link.slug !== undefined ? \`/\${link.slug}\` : undefined;
67
+ const isActive = href !== undefined && pathname === href;
68
+ const indent = { paddingLeft: \`\${20 + depth * 14}px\` };
69
+
70
+ // Open collapsible groups that contain the active page so deep links land
71
+ // with their ancestors already expanded.
72
+ const [isOpen, setIsOpen] = useState(
73
+ hasChildren
74
+ ? children.some((child) => linkContainsActivePath(child, pathname))
75
+ : false,
76
+ );
77
+
78
+ if (!hasChildren) {
79
+ return (
80
+ <StyledSidebarListItem>
81
+ <StyledSidebarListItemLink
82
+ href={href ?? "#"}
83
+ $isActive={isActive}
84
+ onClick={onNavigate}
85
+ style={indent}
86
+ >
87
+ {link.icon && <Icon name={link.icon} size={16} />}
88
+ {link.title}
89
+ </StyledSidebarListItemLink>
90
+ </StyledSidebarListItem>
91
+ );
92
+ }
93
+
94
+ const toggle = () => setIsOpen((prev) => !prev);
95
+ const toggleLabel = isOpen
96
+ ? \`Collapse \${link.title}\`
97
+ : \`Expand \${link.title}\`;
98
+ const groupActive = linkContainsActivePath(link, pathname);
99
+
100
+ return (
101
+ <li>
102
+ {href !== undefined ? (
103
+ <StyledSidebarGroupRow
104
+ $isActive={groupActive}
105
+ $isOpen={isOpen}
106
+ style={indent}
107
+ >
108
+ <StyledSidebarGroupLink href={href} onClick={onNavigate}>
109
+ {link.icon && <Icon name={link.icon} size={16} />}
110
+ {link.title}
111
+ </StyledSidebarGroupLink>
112
+ <StyledSidebarGroupChevron
113
+ type="button"
114
+ onClick={toggle}
115
+ aria-expanded={isOpen}
116
+ aria-label={toggleLabel}
117
+ >
118
+ <Icon name="chevron-right" size={16} />
119
+ </StyledSidebarGroupChevron>
120
+ </StyledSidebarGroupRow>
121
+ ) : (
122
+ <StyledSidebarGroupButton
123
+ type="button"
124
+ onClick={toggle}
125
+ $isActive={groupActive}
126
+ $isOpen={isOpen}
127
+ style={indent}
128
+ aria-expanded={isOpen}
129
+ aria-label={toggleLabel}
130
+ >
131
+ {link.icon && <Icon name={link.icon} size={16} />}
132
+ {link.title}
133
+ <Icon name="chevron-right" size={16} />
134
+ </StyledSidebarGroupButton>
135
+ )}
136
+ <StyledSidebarGroupContent $isOpen={isOpen}>
137
+ {children.map((child: NavItemLink, index: number) => (
138
+ <SidebarNavLink
139
+ key={index}
140
+ link={child}
141
+ depth={depth + 1}
142
+ pathname={pathname}
143
+ onNavigate={onNavigate}
144
+ />
145
+ ))}
146
+ </StyledSidebarGroupContent>
147
+ </li>
148
+ );
149
+ }
150
+
33
151
  function SideBar({ result }: SideBarProps) {
34
152
  const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
35
153
  const hasSectionBar = useContext(SectionBarContext);
@@ -59,25 +177,24 @@ function SideBar({ result }: SideBarProps) {
59
177
  return (
60
178
  <StyledSidebarList key={index}>
61
179
  <StyledSidebarListItem>
62
- <StyledStrong>{item.label}</StyledStrong>{" "}
180
+ <StyledStrong>
181
+ {item.icon && <Icon name={item.icon} size={16} />}
182
+ {item.label}
183
+ </StyledStrong>{" "}
63
184
  </StyledSidebarListItem>
64
185
  <li>
65
186
  <Space $size={20} />
66
187
  </li>
67
188
  {item.links &&
68
- item.links.map((link: NavItemLink, indexChild: number) => {
69
- return (
70
- <StyledSidebarListItem key={indexChild}>
71
- <StyledSidebarListItemLink
72
- href={\`/\${link.slug}\`}
73
- $isActive={pathname === \`/\${link.slug}\`}
74
- onClick={() => setIsMobileMenuOpen(false)}
75
- >
76
- {link.title}
77
- </StyledSidebarListItemLink>
78
- </StyledSidebarListItem>
79
- );
80
- })}
189
+ item.links.map((link: NavItemLink, indexChild: number) => (
190
+ <SidebarNavLink
191
+ key={indexChild}
192
+ link={link}
193
+ depth={0}
194
+ pathname={pathname}
195
+ onNavigate={() => setIsMobileMenuOpen(false)}
196
+ />
197
+ ))}
81
198
  <li aria-hidden="true">
82
199
  <Space $size={20} />
83
200
  </li>
@@ -1 +1 @@
1
- export declare const docsComponentsTemplate = "\"use client\";\nimport React, { createContext, useContext } from \"react\";\nimport styled, { css } from \"styled-components\";\nimport {\n resetButton,\n styledSmall,\n styledStrong,\n styledText,\n} from \"cherry-styled-components\";\nimport Link from \"next/link\";\nimport { mq, Theme } from \"@/app/theme\";\nimport {\n styledAnchor,\n styledTable,\n stylesLists,\n} from \"@/components/layout/SharedStyled\";\nimport { ChatContext } from \"@/components/Chat\";\n\nconst SectionBarContext = createContext(false);\n\nfunction SectionBarProvider({\n hasSectionBar,\n children,\n}: {\n hasSectionBar: boolean;\n children: React.ReactNode;\n}) {\n return (\n <SectionBarContext.Provider value={hasSectionBar}>\n {children}\n </SectionBarContext.Provider>\n );\n}\n\ninterface DocsProps {\n children: React.ReactNode;\n}\n\nconst StyledDocsWrapper = styled.main<{ theme: Theme }>`\n position: relative;\n`;\n\nconst StyledDocsSidebar = styled.div<{ theme: Theme }>`\n clear: both;\n`;\n\nconst StyledDocsContainer = styled.div<{ theme: Theme; $isChatOpen?: boolean }>`\n position: relative;\n padding: 0 20px 100px 20px;\n width: 100%;\n ${({ theme }) => styledText(theme)};\n transition: all 0.3s ease;\n\n ${mq(\"lg\")} {\n padding: 0 300px 80px 300px;\n\n ${({ $isChatOpen }) =>\n $isChatOpen &&\n css`\n padding: 0 440px 80px 300px;\n `}\n }\n\n & p {\n color: ${({ theme }) => theme.colors.grayDark};\n hyphens: auto;\n }\n\n & pre {\n max-width: 100%;\n }\n\n ${styledAnchor};\n ${stylesLists};\n ${styledTable};\n\n & img,\n & video,\n & iframe {\n max-width: 100%;\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n }\n\n & code:not([class]) {\n background: ${({ theme }) =>\n `color-mix(in srgb, ${theme.colors.primaryLight} 20%, transparent)`};\n color: ${({ theme }) => theme.colors.dark};\n padding: 2px 4px;\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n white-space: pre;\n }\n\n & .lucide {\n color: ${({ theme }) => theme.colors.primary};\n }\n\n & .aspect-video {\n aspect-ratio: 16 / 9;\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n }\n`;\n\nexport const StyledMarkdownContainer = styled.div`\n display: flex;\n flex-direction: column;\n gap: 20px;\n flex-wrap: wrap;\n flex: 1;\n max-width: 640px;\n margin: auto;\n`;\n\ninterface Props {\n theme?: Theme;\n $isActive?: boolean;\n $hasSectionBar?: boolean;\n}\n\nexport const StyledSidebar = styled.nav<Props>`\n position: fixed;\n overflow-y: auto;\n max-height: calc(\n 100dvh - ${({ $hasSectionBar }) => ($hasSectionBar ? 104 : 62)}px\n );\n width: 100%;\n z-index: 99;\n top: ${({ $hasSectionBar }) => ($hasSectionBar ? 104 : 62)}px;\n height: 100%;\n padding: 20px;\n opacity: 0;\n pointer-events: none;\n transition: all 0.3s ease;\n transform: translateY(30px);\n left: 0;\n background: ${({ theme }) => theme.colors.light};\n -webkit-overflow-scrolling: touch;\n display: flex;\n flex-direction: column;\n\n &::-webkit-scrollbar {\n display: none;\n }\n\n ${mq(\"lg\")} {\n border-right: solid 1px ${({ theme }) => theme.colors.grayLight};\n transition: none;\n max-height: 100dvh;\n width: 220px;\n background: transparent;\n padding: 82px 20px 20px 20px;\n opacity: 1;\n pointer-events: all;\n transform: translateY(0);\n background: ${({ theme }) =>\n `color-mix(in srgb, ${theme.colors.primaryLight} 5%, transparent)`};\n top: 0;\n width: 280px;\n }\n\n ${({ $isActive }) =>\n $isActive &&\n css`\n transform: translateY(0);\n opacity: 1;\n pointer-events: all;\n `}\n`;\n\nexport const StyledSidebarFooter = styled.div`\n padding: 22px 20px;\n position: sticky;\n border-top: 1px solid ${({ theme }) => theme.colors.grayLight};\n background: ${({ theme }) =>\n `color-mix(in srgb, ${theme.colors.primaryLight} 5%, transparent)`};\n margin: 0 -20px -20px;\n bottom: -20px;\n backdrop-filter: blur(10px);\n\n ${mq(\"lg\")} {\n padding: 16px 20px;\n }\n`;\n\nexport const StyledIndexSidebar = styled.ul<{ theme: Theme }>`\n display: none;\n list-style: none;\n margin: 0;\n padding: 0;\n position: fixed;\n top: 0;\n right: 0;\n width: 280px;\n height: 100dvh;\n overflow-y: auto;\n z-index: 1;\n padding: 82px 20px 20px 20px;\n background: ${({ theme }) => theme.colors.light};\n border-left: solid 1px ${({ theme }) => theme.colors.grayLight};\n -webkit-overflow-scrolling: touch;\n\n &::-webkit-scrollbar {\n display: none;\n }\n\n ${mq(\"lg\")} {\n display: block;\n }\n\n & li {\n padding: 5px 0;\n }\n`;\n\nexport const StyledIndexSidebarLabel = styled.span<{ theme: Theme }>`\n ${({ theme }) => styledSmall(theme)};\n color: ${({ theme }) => theme.colors.grayDark};\n`;\n\nexport const StyledIndexSidebarLi = styled.li<{\n theme: Theme;\n $isActive: boolean;\n}>`\n &::before {\n content: \"\";\n display: block;\n position: absolute;\n left: 0;\n height: 20px;\n width: 1px;\n background: transparent;\n transition: all 0.3s ease;\n }\n\n ${({ $isActive, theme }) =>\n $isActive &&\n css`\n &::before {\n background: ${theme.colors.primary};\n }\n `}\n`;\n\nexport const StyledIndexSidebarLink = styled.a<{\n theme: Theme;\n $isActive: boolean;\n}>`\n ${({ theme }) => styledSmall(theme)};\n color: ${({ theme, $isActive }) =>\n $isActive ? theme.colors.primary : theme.colors.dark};\n font-weight: ${({ $isActive }) => ($isActive ? \"600\" : \"400\")};\n text-decoration: none;\n transition: all 0.3s ease;\n\n &:hover {\n color: ${({ theme }) => theme.colors.primary};\n }\n`;\n\nexport const StyledSidebarList = styled.ul`\n list-style: none;\n margin: 0;\n padding: 0;\n\n &:last-of-type {\n margin-bottom: auto;\n }\n`;\n\nexport const StyledStrong = styled.strong<{ theme: Theme }>`\n font-weight: 600;\n ${({ theme }) => styledStrong(theme)};\n color: ${({ theme }) => theme.colors.accentStrong};\n`;\n\nexport const StyledSidebarListItem = styled.li`\n display: flex;\n gap: 10px;\n clear: both;\n`;\n\nexport const StyledSidebarListItemLink = styled(Link)<Props>`\n text-decoration: none;\n font-size: ${({ theme }) => theme.fontSizes.small.lg};\n line-height: 1.6;\n color: ${({ theme }) => theme.colors.accentMuted};\n padding: 5px 0 5px 20px;\n display: flex;\n transition: all 0.3s ease;\n border-left: solid 1px ${({ theme }) => theme.colors.grayLight};\n\n &:hover {\n color: ${({ theme }) => theme.colors.accent};\n border-color: ${({ theme }) => theme.colors.primary};\n }\n\n ${({ $isActive, theme }) =>\n $isActive &&\n `\n\t\t\tcolor: ${theme.colors.accentStrong};\n\t\t\tborder-color: ${theme.colors.primary};\n\t\t\tfont-weight: 600;\n\t`};\n`;\n\nexport const StyleMobileBar = styled.button<Props>`\n ${resetButton};\n position: fixed;\n z-index: 999;\n bottom: 0;\n right: 20px;\n font-size: ${({ theme }) => theme.fontSizes.strong.lg};\n line-height: ${({ theme }) => theme.fontSizes.strong.lg};\n box-shadow: ${({ theme }) => theme.shadows.sm};\n background: ${({ theme }) => theme.colors.primary};\n color: ${({ theme }) => theme.colors.surface};\n backdrop-filter: blur(10px);\n -webkit-backdrop-filter: blur(10px);\n padding: 10px;\n border-radius: 100px;\n margin: 0 0 20px 0;\n font-weight: 600;\n display: flex;\n justify-content: flex-start;\n width: auto;\n\n ${mq(\"lg\")} {\n display: none;\n }\n\n ${({ $isActive }) => $isActive && `position: fixed;`};\n`;\n\nexport const StyledMobileBurger = styled.span<Props>`\n display: block;\n margin: auto 0;\n width: 18px;\n height: 18px;\n position: relative;\n overflow: hidden;\n background: transparent;\n position: relative;\n transform: scale(0.8);\n\n &::before,\n &::after {\n content: \"\";\n display: block;\n position: absolute;\n width: 18px;\n height: 3px;\n border-radius: 3px;\n background: ${({ theme }) => theme.colors.surface};\n transition: all 0.3s ease;\n }\n\n &::before {\n top: 3px;\n }\n\n &::after {\n bottom: 3px;\n }\n\n ${({ $isActive }) =>\n $isActive &&\n css`\n &::before {\n transform: translateY(5px) rotate(45deg);\n }\n\n &::after {\n transform: translateY(-4px) rotate(-45deg);\n }\n `};\n`;\n\nexport const StyledMissingComponent = styled.div`\n background: ${({ theme }) => theme.colors.error};\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n padding: 20px;\n font-size: ${({ theme }) => theme.fontSizes.small.lg};\n color: ${({ theme }) => theme.colors.surface};\n font-weight: 600;\n display: flex;\n gap: 10px;\n align-items: center;\n`;\n\ninterface DocsWrapperProps {\n children: React.ReactNode;\n}\n\nfunction DocsWrapper({ children }: DocsWrapperProps) {\n return <StyledDocsWrapper>{children}</StyledDocsWrapper>;\n}\n\nfunction DocsSidebar({ children }: DocsProps) {\n return <StyledDocsSidebar>{children}</StyledDocsSidebar>;\n}\n\nfunction DocsContainer({ children }: DocsProps) {\n const { isOpen } = useContext(ChatContext);\n\n return (\n <StyledDocsContainer $isChatOpen={isOpen}>{children}</StyledDocsContainer>\n );\n}\n\nexport {\n DocsWrapper,\n DocsSidebar,\n DocsContainer,\n SectionBarContext,\n SectionBarProvider,\n};\n";
1
+ export declare const docsComponentsTemplate = "\"use client\";\nimport React, { createContext, useContext } from \"react\";\nimport styled, { css } from \"styled-components\";\nimport {\n resetButton,\n styledSmall,\n styledStrong,\n styledText,\n} from \"cherry-styled-components\";\nimport Link from \"next/link\";\nimport { mq, Theme } from \"@/app/theme\";\nimport {\n styledAnchor,\n styledTable,\n stylesLists,\n} from \"@/components/layout/SharedStyled\";\nimport { ChatContext } from \"@/components/Chat\";\n\nconst SectionBarContext = createContext(false);\n\nfunction SectionBarProvider({\n hasSectionBar,\n children,\n}: {\n hasSectionBar: boolean;\n children: React.ReactNode;\n}) {\n return (\n <SectionBarContext.Provider value={hasSectionBar}>\n {children}\n </SectionBarContext.Provider>\n );\n}\n\ninterface DocsProps {\n children: React.ReactNode;\n}\n\nconst StyledDocsWrapper = styled.main<{ theme: Theme }>`\n position: relative;\n`;\n\nconst StyledDocsSidebar = styled.div<{ theme: Theme }>`\n clear: both;\n`;\n\nconst StyledDocsContainer = styled.div<{ theme: Theme; $isChatOpen?: boolean }>`\n position: relative;\n padding: 0 20px 100px 20px;\n width: 100%;\n ${({ theme }) => styledText(theme)};\n transition: all 0.3s ease;\n\n ${mq(\"lg\")} {\n padding: 0 300px 80px 300px;\n\n ${({ $isChatOpen }) =>\n $isChatOpen &&\n css`\n padding: 0 440px 80px 300px;\n `}\n }\n\n & p {\n color: ${({ theme }) => theme.colors.grayDark};\n hyphens: auto;\n }\n\n & pre {\n max-width: 100%;\n }\n\n ${styledAnchor};\n ${stylesLists};\n ${styledTable};\n\n & img,\n & video,\n & iframe {\n max-width: 100%;\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n }\n\n & code:not([class]) {\n background: ${({ theme }) =>\n `color-mix(in srgb, ${theme.colors.primaryLight} 20%, transparent)`};\n color: ${({ theme }) => theme.colors.dark};\n padding: 2px 4px;\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n white-space: pre;\n }\n\n & .lucide {\n color: ${({ theme }) => theme.colors.primary};\n }\n\n & .aspect-video {\n aspect-ratio: 16 / 9;\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n }\n`;\n\nexport const StyledMarkdownContainer = styled.div`\n display: flex;\n flex-direction: column;\n gap: 20px;\n flex-wrap: wrap;\n flex: 1;\n max-width: 640px;\n margin: auto;\n`;\n\ninterface Props {\n theme?: Theme;\n $isActive?: boolean;\n $isOpen?: boolean;\n $hasSectionBar?: boolean;\n}\n\nexport const StyledSidebar = styled.nav<Props>`\n position: fixed;\n overflow-y: auto;\n max-height: calc(\n 100dvh - ${({ $hasSectionBar }) => ($hasSectionBar ? 104 : 62)}px\n );\n width: 100%;\n z-index: 99;\n top: ${({ $hasSectionBar }) => ($hasSectionBar ? 104 : 62)}px;\n height: 100%;\n padding: 20px;\n opacity: 0;\n pointer-events: none;\n transition: all 0.3s ease;\n transform: translateY(30px);\n left: 0;\n background: ${({ theme }) => theme.colors.light};\n -webkit-overflow-scrolling: touch;\n display: flex;\n flex-direction: column;\n\n &::-webkit-scrollbar {\n display: none;\n }\n\n ${mq(\"lg\")} {\n border-right: solid 1px ${({ theme }) => theme.colors.grayLight};\n transition: none;\n max-height: 100dvh;\n width: 220px;\n background: transparent;\n padding: 82px 20px 20px 20px;\n opacity: 1;\n pointer-events: all;\n transform: translateY(0);\n background: ${({ theme }) =>\n `color-mix(in srgb, ${theme.colors.primaryLight} 5%, transparent)`};\n top: 0;\n width: 280px;\n }\n\n ${({ $isActive }) =>\n $isActive &&\n css`\n transform: translateY(0);\n opacity: 1;\n pointer-events: all;\n `}\n`;\n\nexport const StyledSidebarFooter = styled.div`\n padding: 22px 20px;\n position: sticky;\n border-top: 1px solid ${({ theme }) => theme.colors.grayLight};\n background: ${({ theme }) =>\n `color-mix(in srgb, ${theme.colors.primaryLight} 5%, transparent)`};\n margin: 0 -20px -20px;\n bottom: -20px;\n backdrop-filter: blur(10px);\n\n ${mq(\"lg\")} {\n padding: 16px 20px;\n }\n`;\n\nexport const StyledIndexSidebar = styled.ul<{ theme: Theme }>`\n display: none;\n list-style: none;\n margin: 0;\n padding: 0;\n position: fixed;\n top: 0;\n right: 0;\n width: 280px;\n height: 100dvh;\n overflow-y: auto;\n z-index: 1;\n padding: 82px 20px 20px 20px;\n background: ${({ theme }) => theme.colors.light};\n border-left: solid 1px ${({ theme }) => theme.colors.grayLight};\n -webkit-overflow-scrolling: touch;\n\n &::-webkit-scrollbar {\n display: none;\n }\n\n ${mq(\"lg\")} {\n display: block;\n }\n\n & li {\n padding: 5px 0;\n }\n`;\n\nexport const StyledIndexSidebarLabel = styled.span<{ theme: Theme }>`\n ${({ theme }) => styledSmall(theme)};\n color: ${({ theme }) => theme.colors.grayDark};\n`;\n\nexport const StyledIndexSidebarLi = styled.li<{\n theme: Theme;\n $isActive: boolean;\n}>`\n &::before {\n content: \"\";\n display: block;\n position: absolute;\n left: 0;\n height: 20px;\n width: 1px;\n background: transparent;\n transition: all 0.3s ease;\n }\n\n ${({ $isActive, theme }) =>\n $isActive &&\n css`\n &::before {\n background: ${theme.colors.primary};\n }\n `}\n`;\n\nexport const StyledIndexSidebarLink = styled.a<{\n theme: Theme;\n $isActive: boolean;\n}>`\n ${({ theme }) => styledSmall(theme)};\n color: ${({ theme, $isActive }) =>\n $isActive ? theme.colors.primary : theme.colors.dark};\n font-weight: ${({ $isActive }) => ($isActive ? \"600\" : \"400\")};\n text-decoration: none;\n transition: all 0.3s ease;\n\n &:hover {\n color: ${({ theme }) => theme.colors.primary};\n }\n`;\n\nexport const StyledSidebarList = styled.ul`\n list-style: none;\n margin: 0;\n padding: 0;\n\n &:last-of-type {\n margin-bottom: auto;\n }\n`;\n\nexport const StyledStrong = styled.strong<{ theme: Theme }>`\n font-weight: 600;\n ${({ theme }) => styledStrong(theme)};\n color: ${({ theme }) => theme.colors.accentStrong};\n display: inline-flex;\n align-items: center;\n gap: 8px;\n\n & svg {\n flex-shrink: 0;\n }\n`;\n\nexport const StyledSidebarListItem = styled.li`\n display: flex;\n gap: 10px;\n clear: both;\n`;\n\n// Shared appearance for every sidebar row - leaf links AND nested group\n// headers - so hover, active state, and the left rail read identically.\nconst sidebarRowStyles = css<Props>`\n text-decoration: none;\n font-size: ${({ theme }) => theme.fontSizes.small.lg};\n line-height: 1.6;\n color: ${({ theme }) => theme.colors.accentMuted};\n padding: 5px 0 5px 20px;\n display: flex;\n align-items: center;\n gap: 8px;\n flex: 1;\n min-width: 0;\n transition: all 0.3s ease;\n border-left: solid 1px ${({ theme }) => theme.colors.grayLight};\n\n & svg {\n flex-shrink: 0;\n }\n\n &:hover {\n color: ${({ theme }) => theme.colors.accent};\n border-color: ${({ theme }) => theme.colors.primary};\n }\n\n ${({ $isActive, theme }) =>\n $isActive &&\n css`\n color: ${theme.colors.accentStrong};\n border-color: ${theme.colors.primary};\n font-weight: 600;\n `};\n`;\n\n// The collapse chevron points right when closed and rotates to point down\n// when the group is open.\nconst sidebarChevron = css<Props>`\n & .lucide-chevron-right {\n margin-left: auto;\n transition: transform 0.3s ease;\n\n ${({ $isOpen }) =>\n $isOpen &&\n css`\n transform: rotate(90deg);\n `}\n }\n`;\n\nexport const StyledSidebarListItemLink = styled(Link)<Props>`\n ${sidebarRowStyles};\n`;\n\n// Nested navigation group. The header shares the link appearance/hover; a\n// non-navigable group is a single toggle button, while a group that is also a\n// page pairs a link with a chevron toggle. Children live in\n// StyledSidebarGroupContent, which animates open/closed via height 0 <-> auto\n// (enabled by interpolate-size in GlobalStyles, same as the Accordion).\nexport const StyledSidebarGroupButton = styled.button<Props>`\n ${resetButton};\n ${sidebarRowStyles};\n ${sidebarChevron};\n width: 100%;\n text-align: left;\n`;\n\nexport const StyledSidebarGroupRow = styled.div<Props>`\n ${sidebarRowStyles};\n ${sidebarChevron};\n`;\n\nexport const StyledSidebarGroupLink = styled(Link)`\n display: flex;\n align-items: center;\n gap: 8px;\n flex: 1;\n min-width: 0;\n color: inherit;\n text-decoration: none;\n\n & svg {\n flex-shrink: 0;\n }\n`;\n\nexport const StyledSidebarGroupChevron = styled.button`\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n padding: 0;\n border: 0;\n background: none;\n color: inherit;\n cursor: pointer;\n`;\n\nexport const StyledSidebarGroupContent = styled.ul<Props>`\n list-style: none;\n margin: 0;\n padding: 0;\n height: 0;\n overflow: clip;\n transition: all 0.3s ease;\n\n ${({ $isOpen }) =>\n $isOpen &&\n css`\n height: auto;\n `}\n`;\n\nexport const StyleMobileBar = styled.button<Props>`\n ${resetButton};\n position: fixed;\n z-index: 999;\n bottom: 0;\n right: 20px;\n font-size: ${({ theme }) => theme.fontSizes.strong.lg};\n line-height: ${({ theme }) => theme.fontSizes.strong.lg};\n box-shadow: ${({ theme }) => theme.shadows.sm};\n background: ${({ theme }) => theme.colors.primary};\n color: ${({ theme }) => theme.colors.surface};\n backdrop-filter: blur(10px);\n -webkit-backdrop-filter: blur(10px);\n padding: 10px;\n border-radius: 100px;\n margin: 0 0 20px 0;\n font-weight: 600;\n display: flex;\n justify-content: flex-start;\n width: auto;\n\n ${mq(\"lg\")} {\n display: none;\n }\n\n ${({ $isActive }) => $isActive && `position: fixed;`};\n`;\n\nexport const StyledMobileBurger = styled.span<Props>`\n display: block;\n margin: auto 0;\n width: 18px;\n height: 18px;\n position: relative;\n overflow: hidden;\n background: transparent;\n position: relative;\n transform: scale(0.8);\n\n &::before,\n &::after {\n content: \"\";\n display: block;\n position: absolute;\n width: 18px;\n height: 3px;\n border-radius: 3px;\n background: ${({ theme }) => theme.colors.surface};\n transition: all 0.3s ease;\n }\n\n &::before {\n top: 3px;\n }\n\n &::after {\n bottom: 3px;\n }\n\n ${({ $isActive }) =>\n $isActive &&\n css`\n &::before {\n transform: translateY(5px) rotate(45deg);\n }\n\n &::after {\n transform: translateY(-4px) rotate(-45deg);\n }\n `};\n`;\n\nexport const StyledMissingComponent = styled.div`\n background: ${({ theme }) => theme.colors.error};\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n padding: 20px;\n font-size: ${({ theme }) => theme.fontSizes.small.lg};\n color: ${({ theme }) => theme.colors.surface};\n font-weight: 600;\n display: flex;\n gap: 10px;\n align-items: center;\n`;\n\ninterface DocsWrapperProps {\n children: React.ReactNode;\n}\n\nfunction DocsWrapper({ children }: DocsWrapperProps) {\n return <StyledDocsWrapper>{children}</StyledDocsWrapper>;\n}\n\nfunction DocsSidebar({ children }: DocsProps) {\n return <StyledDocsSidebar>{children}</StyledDocsSidebar>;\n}\n\nfunction DocsContainer({ children }: DocsProps) {\n const { isOpen } = useContext(ChatContext);\n\n return (\n <StyledDocsContainer $isChatOpen={isOpen}>{children}</StyledDocsContainer>\n );\n}\n\nexport {\n DocsWrapper,\n DocsSidebar,\n DocsContainer,\n SectionBarContext,\n SectionBarProvider,\n};\n";