doccupine 0.0.97 → 0.0.99

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 (123) hide show
  1. package/dist/index.js +0 -0
  2. package/dist/templates/components/MDXComponents.d.ts +1 -1
  3. package/dist/templates/components/MDXComponents.js +3 -1
  4. package/dist/templates/components/layout/Card.d.ts +1 -1
  5. package/dist/templates/components/layout/Card.js +10 -1
  6. package/dist/templates/components/layout/Code.d.ts +1 -1
  7. package/dist/templates/components/layout/Code.js +228 -3
  8. package/dist/templates/components/layout/Footer.d.ts +1 -1
  9. package/dist/templates/components/layout/Footer.js +9 -1
  10. package/dist/templates/components/layout/SiteGate.d.ts +1 -1
  11. package/dist/templates/components/layout/SiteGate.js +8 -1
  12. package/dist/templates/mdx/code.mdx.d.ts +1 -1
  13. package/dist/templates/mdx/code.mdx.js +43 -1
  14. package/dist/templates/package.js +2 -2
  15. package/package.json +1 -1
  16. package/dist/index.test.d.ts +0 -1
  17. package/dist/index.test.js +0 -48
  18. package/dist/metadata.d.ts +0 -11
  19. package/dist/metadata.js +0 -52
  20. package/dist/templates/accordion.mdx.d.ts +0 -1
  21. package/dist/templates/accordion.mdx.js +0 -41
  22. package/dist/templates/api/theme/routes.d.ts +0 -1
  23. package/dist/templates/api/theme/routes.js +0 -28
  24. package/dist/templates/app/api/theme/routes.d.ts +0 -1
  25. package/dist/templates/app/api/theme/routes.js +0 -29
  26. package/dist/templates/app/error.d.ts +0 -1
  27. package/dist/templates/app/error.js +0 -45
  28. package/dist/templates/app/layout.d.ts +0 -34
  29. package/dist/templates/app/layout.js +0 -184
  30. package/dist/templates/callouts.mdx.d.ts +0 -1
  31. package/dist/templates/callouts.mdx.js +0 -57
  32. package/dist/templates/cards.mdx.d.ts +0 -1
  33. package/dist/templates/cards.mdx.js +0 -25
  34. package/dist/templates/click-outside.d.ts +0 -1
  35. package/dist/templates/click-outside.js +0 -28
  36. package/dist/templates/code.d.ts +0 -1
  37. package/dist/templates/code.js +0 -202
  38. package/dist/templates/code.mdx.d.ts +0 -1
  39. package/dist/templates/code.mdx.js +0 -46
  40. package/dist/templates/commands.mdx.d.ts +0 -1
  41. package/dist/templates/commands.mdx.js +0 -48
  42. package/dist/templates/components/ClickOutside.d.ts +0 -1
  43. package/dist/templates/components/ClickOutside.js +0 -29
  44. package/dist/templates/components/layout/ClientThemeProvider.d.ts +0 -1
  45. package/dist/templates/components/layout/ClientThemeProvider.js +0 -85
  46. package/dist/templates/components/layout/CopyButton.d.ts +0 -1
  47. package/dist/templates/components/layout/CopyButton.js +0 -85
  48. package/dist/templates/components/layout/RenderMarkdown.d.ts +0 -1
  49. package/dist/templates/components/layout/RenderMarkdown.js +0 -61
  50. package/dist/templates/components/layout/Stepts.d.ts +0 -1
  51. package/dist/templates/components/layout/Stepts.js +0 -114
  52. package/dist/templates/components/layout/ThemeToggle.d.ts +0 -1
  53. package/dist/templates/components/layout/ThemeToggle.js +0 -107
  54. package/dist/templates/constants.d.ts +0 -5
  55. package/dist/templates/constants.js +0 -5
  56. package/dist/templates/docs-components.d.ts +0 -1
  57. package/dist/templates/docs-components.js +0 -441
  58. package/dist/templates/docs.d.ts +0 -1
  59. package/dist/templates/docs.js +0 -48
  60. package/dist/templates/fields.mdx.d.ts +0 -1
  61. package/dist/templates/fields.mdx.js +0 -42
  62. package/dist/templates/footer.d.ts +0 -1
  63. package/dist/templates/footer.js +0 -9
  64. package/dist/templates/header.d.ts +0 -1
  65. package/dist/templates/header.js +0 -275
  66. package/dist/templates/home.d.ts +0 -1
  67. package/dist/templates/home.js +0 -74
  68. package/dist/templates/home.mdx.d.ts +0 -1
  69. package/dist/templates/home.mdx.js +0 -44
  70. package/dist/templates/homemdx.d.ts +0 -1
  71. package/dist/templates/homemdx.js +0 -59
  72. package/dist/templates/icon.d.ts +0 -1
  73. package/dist/templates/icon.js +0 -20
  74. package/dist/templates/icons.mdx.d.ts +0 -1
  75. package/dist/templates/icons.mdx.js +0 -44
  76. package/dist/templates/image-embeds.mdx.d.ts +0 -1
  77. package/dist/templates/image-embeds.mdx.js +0 -103
  78. package/dist/templates/layout.d.ts +0 -1
  79. package/dist/templates/layout.js +0 -69
  80. package/dist/templates/list-table.mdx.d.ts +0 -1
  81. package/dist/templates/list-table.mdx.js +0 -85
  82. package/dist/templates/mdx/config.mdx.d.ts +0 -1
  83. package/dist/templates/mdx/config.mdx.js +0 -47
  84. package/dist/templates/mdx/deployment.mdx.d.ts +0 -1
  85. package/dist/templates/mdx/deployment.mdx.js +0 -61
  86. package/dist/templates/mdx/image-embeds.mdx.d.ts +0 -1
  87. package/dist/templates/mdx/image-embeds.mdx.js +0 -95
  88. package/dist/templates/mdx/links.mdx.d.ts +0 -1
  89. package/dist/templates/mdx/links.mdx.js +0 -45
  90. package/dist/templates/mdx/list-and-tables.mdx.d.ts +0 -1
  91. package/dist/templates/mdx/list-and-tables.mdx.js +0 -78
  92. package/dist/templates/mdx/list-table.mdx.d.ts +0 -1
  93. package/dist/templates/mdx/list-table.mdx.js +0 -78
  94. package/dist/templates/mdx/platform/deployments.mdx.d.ts +0 -1
  95. package/dist/templates/mdx/platform/deployments.mdx.js +0 -36
  96. package/dist/templates/mdx/platform/sections-settings.mdx.d.ts +0 -1
  97. package/dist/templates/mdx/platform/sections-settings.mdx.js +0 -46
  98. package/dist/templates/mdx/stepts.mdx.d.ts +0 -1
  99. package/dist/templates/mdx/stepts.mdx.js +0 -60
  100. package/dist/templates/mdx/text.mdx.d.ts +0 -1
  101. package/dist/templates/mdx/text.mdx.js +0 -139
  102. package/dist/templates/middleware.d.ts +0 -1
  103. package/dist/templates/middleware.js +0 -28
  104. package/dist/templates/not-found.d.ts +0 -1
  105. package/dist/templates/not-found.js +0 -22
  106. package/dist/templates/pictograms.d.ts +0 -1
  107. package/dist/templates/pictograms.js +0 -67
  108. package/dist/templates/shared-styles.d.ts +0 -1
  109. package/dist/templates/shared-styles.js +0 -792
  110. package/dist/templates/structures.d.ts +0 -2
  111. package/dist/templates/structures.js +0 -208
  112. package/dist/templates/tabs.mdx.d.ts +0 -1
  113. package/dist/templates/tabs.mdx.js +0 -59
  114. package/dist/templates/text.mdx.d.ts +0 -1
  115. package/dist/templates/text.mdx.js +0 -142
  116. package/dist/templates/theme-toggle.d.ts +0 -1
  117. package/dist/templates/theme-toggle.js +0 -111
  118. package/dist/templates/theme.d.ts +0 -1
  119. package/dist/templates/theme.js +0 -307
  120. package/dist/templates/typography.d.ts +0 -1
  121. package/dist/templates/typography.js +0 -52
  122. package/dist/templates/update.mdx.d.ts +0 -1
  123. package/dist/templates/update.mdx.js +0 -58
package/dist/index.js CHANGED
File without changes
@@ -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 } 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 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\";\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";
@@ -4,7 +4,7 @@ import Link from "next/link";
4
4
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
5
  type MDXComponents = Record<string, React.ComponentType<any>>;
6
6
  import { Space } from "cherry-styled-components";
7
- import { Code as CodeBlock } from "@/components/layout/Code";
7
+ import { Code as CodeBlock, CodeTabs } from "@/components/layout/Code";
8
8
  import { Card } from "@/components/layout/Card";
9
9
  import { Accordion } from "@/components/layout/Accordion";
10
10
  import { Tabs, TabContent } from "@/components/layout/Tabs";
@@ -156,6 +156,8 @@ export function useMDXComponents(components: MDXComponents): MDXComponents {
156
156
  pre: Pre,
157
157
 
158
158
  // Expose your custom components for MDX usage
159
+ Code: CodeBlock,
160
+ CodeTabs,
159
161
  Card,
160
162
  Accordion,
161
163
  Tabs,
@@ -1 +1 @@
1
- export declare const cardTemplate = "\"use client\";\nimport Link from \"next/link\";\nimport styled, { css, useTheme } from \"styled-components\";\nimport { styledText } from \"cherry-styled-components\";\nimport { Theme } from \"@/app/theme\";\nimport { Icon, IconProps } from \"@/components/layout/Icon\";\nimport { interactiveStyles } from \"@/components/layout/SharedStyled\";\n\nconst cardStyles = css<{ theme: Theme }>`\n background: ${({ theme }) => theme.colors.light};\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n padding: 20px;\n margin: 0;\n ${({ theme }) => styledText(theme)}\n color: ${({ theme }) => theme.colors.grayDark};\n`;\n\nconst StyledCard = styled.div<{ theme: Theme }>`\n ${cardStyles}\n`;\n\nconst StyledCardLink = styled(Link)<{ theme: Theme }>`\n ${interactiveStyles};\n ${cardStyles}\n text-decoration: none;\n`;\n\nconst StyledCardTitle = styled.p<{ theme: Theme }>`\n font-weight: bold;\n margin: 5px 0;\n color: ${({ theme }) => theme.colors.dark};\n ${({ theme }) => styledText(theme)};\n`;\n\ninterface CardProps extends React.HTMLAttributes<HTMLDivElement> {\n children: React.ReactNode;\n title: string;\n icon?: IconProps;\n href?: string;\n}\n\nfunction Card({ children, title, icon, href }: CardProps) {\n const theme = useTheme() as Theme;\n\n const content = (\n <>\n {icon && <Icon name={icon} color={theme.colors.primary} />}\n <StyledCardTitle>{title}</StyledCardTitle>\n {children}\n </>\n );\n\n if (href) {\n return <StyledCardLink href={href}>{content}</StyledCardLink>;\n }\n\n return <StyledCard>{content}</StyledCard>;\n}\n\nexport { Card };\n";
1
+ export declare const cardTemplate = "\"use client\";\nimport Link from \"next/link\";\nimport styled, { css, useTheme } from \"styled-components\";\nimport { styledText } from \"cherry-styled-components\";\nimport { Theme } from \"@/app/theme\";\nimport { Icon, IconProps } from \"@/components/layout/Icon\";\nimport { interactiveStyles } from \"@/components/layout/SharedStyled\";\n\nconst cardStyles = css<{ theme: Theme }>`\n background: ${({ theme }) => theme.colors.light};\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n padding: 20px;\n margin: 0;\n ${({ theme }) => styledText(theme)}\n color: ${({ theme }) => theme.colors.grayDark};\n`;\n\nconst StyledCard = styled.div<{ theme: Theme }>`\n ${cardStyles}\n`;\n\nconst StyledCardLink = styled(Link)<{ theme: Theme }>`\n ${interactiveStyles};\n ${cardStyles}\n text-decoration: none;\n`;\n\nconst StyledCardTitle = styled.p<{ theme: Theme }>`\n font-weight: bold;\n margin: 5px 0;\n color: ${({ theme }) => theme.colors.dark};\n ${({ theme }) => styledText(theme)};\n`;\n\ninterface CardProps extends React.HTMLAttributes<HTMLDivElement> {\n children: React.ReactNode;\n title: string;\n icon?: IconProps;\n href?: string;\n}\n\nfunction Card({ children, title, icon, href }: CardProps) {\n const theme = useTheme() as Theme;\n\n const content = (\n <>\n {icon && <Icon name={icon} color={theme.colors.primary} />}\n <StyledCardTitle>{title}</StyledCardTitle>\n {children}\n </>\n );\n\n if (href) {\n const isExternal = /^(https?:)?\\/\\//i.test(href);\n return (\n <StyledCardLink\n href={href}\n target={isExternal ? \"_blank\" : undefined}\n rel={isExternal ? \"noopener noreferrer\" : undefined}\n >\n {content}\n </StyledCardLink>\n );\n }\n\n return <StyledCard>{content}</StyledCard>;\n}\n\nexport { Card };\n";
@@ -52,7 +52,16 @@ function Card({ children, title, icon, href }: CardProps) {
52
52
  );
53
53
 
54
54
  if (href) {
55
- return <StyledCardLink href={href}>{content}</StyledCardLink>;
55
+ const isExternal = /^(https?:)?\\/\\//i.test(href);
56
+ return (
57
+ <StyledCardLink
58
+ href={href}
59
+ target={isExternal ? "_blank" : undefined}
60
+ rel={isExternal ? "noopener noreferrer" : undefined}
61
+ >
62
+ {content}
63
+ </StyledCardLink>
64
+ );
56
65
  }
57
66
 
58
67
  return <StyledCard>{content}</StyledCard>;
@@ -1 +1 @@
1
- export declare const codeTemplate = "\"use client\";\nimport { useState, useCallback, useMemo } from \"react\";\nimport styled, { css } from \"styled-components\";\nimport {\n interactiveStyles,\n resetButton,\n styledCode,\n} from \"cherry-styled-components\";\nimport { Theme } from \"@/app/theme\";\nimport { unified } from \"unified\";\nimport rehypeParse from \"rehype-parse\";\nimport rehypeHighlight from \"rehype-highlight\";\nimport rehypeStringify from \"rehype-stringify\";\nimport { Icon } from \"@/components/layout/Icon\";\nimport { thinScrollbar } from \"@/components/layout/SharedStyled\";\n\ninterface CodeProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n \"theme\"\n> {\n code: string;\n language?: string;\n theme?: Theme;\n}\n\nconst CodeWrapper = styled.span<{ theme: Theme }>`\n position: relative;\n z-index: 2;\n display: block;\n width: 100%;\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n border: solid 1px rgba(0, 0, 0, 0.1);\n\n :root.dark & {\n border-color: rgba(255, 255, 255, 0.2);\n }\n`;\n\n/* Code block uses a fixed GitHub-style palette in both modes. Independent of\n theme.json so syntax highlighting stays legible regardless of brand colors.\n Dark variants live in :root.dark & blocks so the swap happens via the\n active <html> class with no re-render. */\nconst TopBar = styled.div<{ theme: Theme }>`\n background: #f6f8fa;\n border-top-left-radius: ${({ theme }) => theme.spacing.radius.lg};\n border-top-right-radius: ${({ theme }) => theme.spacing.radius.lg};\n border-bottom: solid 1px rgba(0, 0, 0, 0.1);\n height: 33px;\n width: 100%;\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 5px;\n padding: 0 10px;\n\n :root.dark & {\n background: #0d1117;\n border-bottom-color: rgba(255, 255, 255, 0.1);\n }\n`;\n\nconst DotsContainer = styled.div`\n display: flex;\n gap: 5px;\n`;\n\nconst Dot = styled.span<{ theme: Theme }>`\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: rgba(0, 0, 0, 0.1);\n\n :root.dark & {\n background: rgba(255, 255, 255, 0.1);\n }\n`;\n\n/* Icon-only copy button. interactiveStyles supplies the border highlight on\n hover plus the focus/active rings (no scale effect); the GitHub-style\n copied/base colors stay fixed like the rest of the code block. The dark\n block re-declares the hover border because its higher-specificity\n :root.dark & border-color would otherwise override the mixin's hover. */\nconst CopyButton = styled.button<{ theme: Theme; $copied: boolean }>`\n ${resetButton}\n ${interactiveStyles}\n background: ${({ $copied }) =>\n $copied ? \"rgba(45, 164, 78, 0.1)\" : \"transparent\"};\n border-color: ${({ $copied }) => ($copied ? \"#2da44e\" : \"rgba(0, 0, 0, 0.1)\")};\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n padding: 4px;\n display: flex;\n align-items: center;\n justify-content: center;\n margin-right: -6px;\n\n & svg.lucide {\n color: ${({ $copied }) => ($copied ? \"#2da44e\" : \"#57606a\")};\n }\n\n :root.dark & {\n background: ${({ $copied }) =>\n $copied ? \"rgba(126, 231, 135, 0.2)\" : \"transparent\"};\n border-color: ${({ $copied }) =>\n $copied ? \"#7ee787\" : \"rgba(255, 255, 255, 0.1)\"};\n\n & svg.lucide {\n color: ${({ $copied }) => ($copied ? \"#7ee787\" : \"#c9d1d9\")};\n }\n\n &:hover {\n border-color: ${({ theme }) => theme.colors.primary};\n }\n }\n`;\n\n/* GitHub Light syntax highlighting by default; GitHub Dark in :root.dark.\n Browser resolves which rule wins based on the active <html> class with no\n JS or React re-render involved. */\nconst lightSyntaxHighlight = css`\n & .hljs {\n color: #24292f;\n background: #ffffff;\n }\n & .hljs-doctag,\n & .hljs-keyword,\n & .hljs-meta .hljs-keyword,\n & .hljs-template-tag,\n & .hljs-template-variable,\n & .hljs-type,\n & .hljs-variable.language_ {\n color: #cf222e;\n }\n & .hljs-title,\n & .hljs-title.class_,\n & .hljs-title.class_.inherited__,\n & .hljs-title.function_ {\n color: #8250df;\n }\n & .hljs-attr,\n & .hljs-attribute,\n & .hljs-literal,\n & .hljs-meta,\n & .hljs-number,\n & .hljs-operator,\n & .hljs-selector-attr,\n & .hljs-selector-class,\n & .hljs-selector-id,\n & .hljs-variable {\n color: #0550ae;\n }\n & .hljs-meta .hljs-string,\n & .hljs-regexp,\n & .hljs-string {\n color: #0a3069;\n }\n & .hljs-built_in,\n & .hljs-symbol {\n color: #953800;\n }\n & .hljs-code,\n & .hljs-comment,\n & .hljs-formula {\n color: #6e7781;\n }\n & .hljs-name,\n & .hljs-quote,\n & .hljs-selector-pseudo,\n & .hljs-selector-tag {\n color: #116329;\n }\n & .hljs-subst {\n color: #24292f;\n }\n & .hljs-section {\n color: #0550ae;\n font-weight: 700;\n }\n & .hljs-bullet {\n color: #953800;\n }\n & .hljs-emphasis {\n color: #24292f;\n font-style: italic;\n }\n & .hljs-strong {\n color: #24292f;\n font-weight: 700;\n }\n & .hljs-addition {\n color: #116329;\n background-color: #dafbe1;\n }\n & .hljs-deletion {\n color: #82071e;\n background-color: #ffebe9;\n }\n`;\n\nconst darkSyntaxHighlight = css`\n & .hljs {\n color: #c9d1d9;\n background: #0d1117;\n }\n & .hljs-doctag,\n & .hljs-keyword,\n & .hljs-meta .hljs-keyword,\n & .hljs-template-tag,\n & .hljs-template-variable,\n & .hljs-type,\n & .hljs-variable.language_ {\n color: #ff7b72;\n }\n & .hljs-title,\n & .hljs-title.class_,\n & .hljs-title.class_.inherited__,\n & .hljs-title.function_ {\n color: #d2a8ff;\n }\n & .hljs-attr,\n & .hljs-attribute,\n & .hljs-literal,\n & .hljs-meta,\n & .hljs-number,\n & .hljs-operator,\n & .hljs-selector-attr,\n & .hljs-selector-class,\n & .hljs-selector-id,\n & .hljs-variable {\n color: #79c0ff;\n }\n & .hljs-meta .hljs-string,\n & .hljs-regexp,\n & .hljs-string {\n color: #a5d6ff;\n }\n & .hljs-built_in,\n & .hljs-symbol {\n color: #ffa657;\n }\n & .hljs-code,\n & .hljs-comment,\n & .hljs-formula {\n color: #8b949e;\n }\n & .hljs-name,\n & .hljs-quote,\n & .hljs-selector-pseudo,\n & .hljs-selector-tag {\n color: #7ee787;\n }\n & .hljs-subst {\n color: #c9d1d9;\n }\n & .hljs-section {\n color: #1f6feb;\n font-weight: 700;\n }\n & .hljs-bullet {\n color: #f2cc60;\n }\n & .hljs-emphasis {\n color: #c9d1d9;\n font-style: italic;\n }\n & .hljs-strong {\n color: #c9d1d9;\n font-weight: 700;\n }\n & .hljs-addition {\n color: #aff5b4;\n background-color: #033a16;\n }\n & .hljs-deletion {\n color: #ffdcd7;\n background-color: #67060c;\n }\n`;\n\nconst Body = styled.div<{ theme: Theme }>`\n background: #ffffff;\n border-bottom-left-radius: ${({ theme }) => theme.spacing.radius.lg};\n border-bottom-right-radius: ${({ theme }) => theme.spacing.radius.lg};\n color: #24292f;\n padding: 20px;\n font-family: ${({ theme }) => theme.fonts.mono};\n text-align: left;\n overflow-x: auto;\n overflow-y: auto;\n max-height: calc(100dvh - 400px);\n ${thinScrollbar};\n ${({ theme }) => styledCode(theme)};\n ${lightSyntaxHighlight};\n\n :root.dark & {\n background: #0d1117;\n color: #ffffff;\n ${darkSyntaxHighlight};\n }\n`;\n\nconst escapeHtml = (unsafe: string): string => {\n return unsafe\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#039;\");\n};\n\nconst sanitizeLanguage = (lang: string): string =>\n lang.replace(/[^a-zA-Z0-9_-]/g, \"\");\n\nconst highlightCode = (code: string, language: string): string => {\n const escapedCode = escapeHtml(code);\n const safeLang = sanitizeLanguage(language);\n const result = unified()\n .use(rehypeParse, { fragment: true })\n .use(rehypeHighlight, {\n detect: true,\n ignoreMissing: true,\n })\n .use(rehypeStringify)\n .processSync(\n `<pre><code class=\"language-${safeLang}\">${escapedCode}</code></pre>`,\n );\n\n return String(result);\n};\n\nfunction Code({ code, language = \"javascript\", theme, className }: CodeProps) {\n const [copied, setCopied] = useState(false);\n const highlightedCode = useMemo(\n () => highlightCode(code, language),\n [code, language],\n );\n\n const handleCopy = useCallback(async () => {\n try {\n await navigator.clipboard.writeText(code);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n } catch (err) {\n console.error(\"Failed to copy code:\", err);\n }\n }, [code]);\n\n return (\n <CodeWrapper\n className={`${className ?? \"\"} code-wrapper`.trim()}\n theme={theme}\n >\n <TopBar theme={theme}>\n <DotsContainer>\n <Dot theme={theme} />\n <Dot theme={theme} />\n <Dot theme={theme} />\n </DotsContainer>\n <CopyButton\n onClick={handleCopy}\n $copied={copied}\n theme={theme}\n aria-label={copied ? \"Copied\" : \"Copy code\"}\n >\n <Icon name={copied ? \"check\" : \"copy\"} size={12} />\n </CopyButton>\n </TopBar>\n <Body\n dangerouslySetInnerHTML={{ __html: highlightedCode }}\n theme={theme}\n className=\"code-wrapper-body\"\n />\n </CodeWrapper>\n );\n}\n\nexport { Code };\n";
1
+ export declare const codeTemplate = "\"use client\";\nimport { useState, useCallback, useMemo, useId } from \"react\";\nimport styled, { css } from \"styled-components\";\nimport {\n interactiveStyles,\n resetButton,\n styledCode,\n} from \"cherry-styled-components\";\nimport { Theme } from \"@/app/theme\";\nimport { unified } from \"unified\";\nimport rehypeParse from \"rehype-parse\";\nimport rehypeHighlight from \"rehype-highlight\";\nimport rehypeStringify from \"rehype-stringify\";\nimport { Icon } from \"@/components/layout/Icon\";\nimport { thinScrollbar } from \"@/components/layout/SharedStyled\";\n\ninterface CodeProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n \"theme\"\n> {\n code: string;\n language?: string;\n title?: string;\n theme?: Theme;\n}\n\ninterface CodeTab {\n label: string;\n code: string;\n language?: string;\n}\n\ninterface CodeTabsProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n \"theme\"\n> {\n tabs: CodeTab[];\n theme?: Theme;\n}\n\nconst CodeWrapper = styled.span<{ theme: Theme }>`\n position: relative;\n z-index: 2;\n display: block;\n width: 100%;\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n border: solid 1px rgba(0, 0, 0, 0.1);\n\n :root.dark & {\n border-color: rgba(255, 255, 255, 0.2);\n }\n`;\n\n/* Code block uses a fixed GitHub-style palette in both modes. Independent of\n theme.json so syntax highlighting stays legible regardless of brand colors.\n Dark variants live in :root.dark & blocks so the swap happens via the\n active <html> class with no re-render. */\nconst TopBar = styled.div<{ theme: Theme }>`\n position: relative;\n background: #f6f8fa;\n border-top-left-radius: ${({ theme }) => theme.spacing.radius.lg};\n border-top-right-radius: ${({ theme }) => theme.spacing.radius.lg};\n border-bottom: solid 1px rgba(0, 0, 0, 0.1);\n height: 33px;\n width: 100%;\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 5px;\n padding: 0 10px;\n\n :root.dark & {\n background: #0d1117;\n border-bottom-color: rgba(255, 255, 255, 0.1);\n }\n`;\n\nconst DotsContainer = styled.div`\n display: flex;\n gap: 5px;\n`;\n\nconst Dot = styled.span<{ theme: Theme }>`\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: rgba(0, 0, 0, 0.1);\n\n :root.dark & {\n background: rgba(255, 255, 255, 0.1);\n }\n`;\n\n/* Icon-only copy button. interactiveStyles supplies the border highlight on\n hover plus the focus/active rings (no scale effect); the GitHub-style\n copied/base colors stay fixed like the rest of the code block. The dark\n block re-declares the hover border because its higher-specificity\n :root.dark & border-color would otherwise override the mixin's hover. */\nconst CopyButton = styled.button<{ theme: Theme; $copied: boolean }>`\n ${resetButton}\n ${interactiveStyles}\n background: ${({ $copied }) =>\n $copied ? \"rgba(45, 164, 78, 0.1)\" : \"transparent\"};\n border-color: ${({ $copied }) => ($copied ? \"#2da44e\" : \"rgba(0, 0, 0, 0.1)\")};\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n padding: 4px;\n display: flex;\n align-items: center;\n justify-content: center;\n margin-right: -6px;\n\n & svg.lucide {\n color: ${({ $copied }) => ($copied ? \"#2da44e\" : \"#57606a\")};\n }\n\n :root.dark & {\n background: ${({ $copied }) =>\n $copied ? \"rgba(126, 231, 135, 0.2)\" : \"transparent\"};\n border-color: ${({ $copied }) =>\n $copied ? \"#7ee787\" : \"rgba(255, 255, 255, 0.1)\"};\n\n & svg.lucide {\n color: ${({ $copied }) => ($copied ? \"#7ee787\" : \"#c9d1d9\")};\n }\n\n &:hover {\n border-color: ${({ theme }) => theme.colors.primary};\n }\n }\n`;\n\n/* Centered file name in the TopBar. Sits absolutely over the flex row so the\n dots and copy button keep their edge alignment. Monospace + muted GitHub\n grays, swapped via :root.dark like the rest of the block. pointer-events off\n so clicks fall through to nothing and never steal focus from the buttons. */\nconst TopBarTitle = styled.span<{ theme: Theme }>`\n position: absolute;\n left: 50%;\n transform: translateX(-50%);\n max-width: 60%;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n pointer-events: none;\n font-family: ${({ theme }) => theme.fonts.mono};\n font-size: 12px;\n line-height: 2;\n color: #57606a;\n\n :root.dark & {\n color: #8b949e;\n }\n`;\n\n/* Segmented control living in the TopBar for CodeTabs. Scrolls horizontally\n with the thin scrollbar when the labels overflow the 33px bar. */\nconst TabList = styled.div`\n display: flex;\n align-items: center;\n gap: 2px;\n min-width: 0;\n overflow-x: auto;\n ${thinScrollbar};\n margin-left: -6px;\n`;\n\n/* Individual tab button. Active tab reads as part of the window: a light body\n fill (#ffffff) that echoes the code area, with a soft border. Inactive tabs\n are muted and transparent. resetButton strips native styling; focus-visible\n uses the brand primary ring for keyboard users. All colors stay fixed and\n swap purely via :root.dark, matching the no-re-render palette approach. */\nconst CodeTab = styled.button<{ theme: Theme; $active: boolean }>`\n ${resetButton}\n flex: 0 0 auto;\n font-family: ${({ theme }) => theme.fonts.mono};\n font-size: 12px;\n line-height: 1;\n padding: 5px;\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n cursor: pointer;\n white-space: nowrap;\n transition:\n color 0.15s ease,\n background 0.15s ease,\n border-color 0.15s ease;\n border: solid 1px\n ${({ $active }) => ($active ? \"rgba(0, 0, 0, 0.1)\" : \"transparent\")};\n background: ${({ $active }) => ($active ? \"#ffffff\" : \"transparent\")};\n color: ${({ $active }) => ($active ? \"#24292f\" : \"#57606a\")};\n\n &:hover {\n color: #24292f;\n background: ${({ $active }) =>\n $active ? \"#ffffff\" : \"rgba(0, 0, 0, 0.04)\"};\n }\n\n &:focus-visible {\n outline: solid 2px ${({ theme }) => theme.colors.primary};\n outline-offset: 1px;\n }\n\n :root.dark & {\n border-color: ${({ $active }) =>\n $active ? \"rgba(255, 255, 255, 0.15)\" : \"transparent\"};\n background: ${({ $active }) => ($active ? \"#161b22\" : \"transparent\")};\n color: ${({ $active }) => ($active ? \"#e6edf3\" : \"#8b949e\")};\n\n &:hover {\n color: #e6edf3;\n background: ${({ $active }) =>\n $active ? \"#161b22\" : \"rgba(255, 255, 255, 0.06)\"};\n }\n }\n`;\n\n/* GitHub Light syntax highlighting by default; GitHub Dark in :root.dark.\n Browser resolves which rule wins based on the active <html> class with no\n JS or React re-render involved. */\nconst lightSyntaxHighlight = css`\n & .hljs {\n color: #24292f;\n background: #ffffff;\n min-width: min-content;\n width: 100%;\n display: block;\n padding-right: 20px;\n }\n & .hljs-doctag,\n & .hljs-keyword,\n & .hljs-meta .hljs-keyword,\n & .hljs-template-tag,\n & .hljs-template-variable,\n & .hljs-type,\n & .hljs-variable.language_ {\n color: #cf222e;\n }\n & .hljs-title,\n & .hljs-title.class_,\n & .hljs-title.class_.inherited__,\n & .hljs-title.function_ {\n color: #8250df;\n }\n & .hljs-attr,\n & .hljs-attribute,\n & .hljs-literal,\n & .hljs-meta,\n & .hljs-number,\n & .hljs-operator,\n & .hljs-selector-attr,\n & .hljs-selector-class,\n & .hljs-selector-id,\n & .hljs-variable {\n color: #0550ae;\n }\n & .hljs-meta .hljs-string,\n & .hljs-regexp,\n & .hljs-string {\n color: #0a3069;\n }\n & .hljs-built_in,\n & .hljs-symbol {\n color: #953800;\n }\n & .hljs-code,\n & .hljs-comment,\n & .hljs-formula {\n color: #6e7781;\n }\n & .hljs-name,\n & .hljs-quote,\n & .hljs-selector-pseudo,\n & .hljs-selector-tag {\n color: #116329;\n }\n & .hljs-subst {\n color: #24292f;\n }\n & .hljs-section {\n color: #0550ae;\n font-weight: 700;\n }\n & .hljs-bullet {\n color: #953800;\n }\n & .hljs-emphasis {\n color: #24292f;\n font-style: italic;\n }\n & .hljs-strong {\n color: #24292f;\n font-weight: 700;\n }\n & .hljs-addition {\n color: #116329;\n background-color: #dafbe1;\n }\n & .hljs-deletion {\n color: #82071e;\n background-color: #ffebe9;\n }\n`;\n\nconst darkSyntaxHighlight = css`\n & .hljs {\n color: #c9d1d9;\n background: #0d1117;\n }\n & .hljs-doctag,\n & .hljs-keyword,\n & .hljs-meta .hljs-keyword,\n & .hljs-template-tag,\n & .hljs-template-variable,\n & .hljs-type,\n & .hljs-variable.language_ {\n color: #ff7b72;\n }\n & .hljs-title,\n & .hljs-title.class_,\n & .hljs-title.class_.inherited__,\n & .hljs-title.function_ {\n color: #d2a8ff;\n }\n & .hljs-attr,\n & .hljs-attribute,\n & .hljs-literal,\n & .hljs-meta,\n & .hljs-number,\n & .hljs-operator,\n & .hljs-selector-attr,\n & .hljs-selector-class,\n & .hljs-selector-id,\n & .hljs-variable {\n color: #79c0ff;\n }\n & .hljs-meta .hljs-string,\n & .hljs-regexp,\n & .hljs-string {\n color: #a5d6ff;\n }\n & .hljs-built_in,\n & .hljs-symbol {\n color: #ffa657;\n }\n & .hljs-code,\n & .hljs-comment,\n & .hljs-formula {\n color: #8b949e;\n }\n & .hljs-name,\n & .hljs-quote,\n & .hljs-selector-pseudo,\n & .hljs-selector-tag {\n color: #7ee787;\n }\n & .hljs-subst {\n color: #c9d1d9;\n }\n & .hljs-section {\n color: #1f6feb;\n font-weight: 700;\n }\n & .hljs-bullet {\n color: #f2cc60;\n }\n & .hljs-emphasis {\n color: #c9d1d9;\n font-style: italic;\n }\n & .hljs-strong {\n color: #c9d1d9;\n font-weight: 700;\n }\n & .hljs-addition {\n color: #aff5b4;\n background-color: #033a16;\n }\n & .hljs-deletion {\n color: #ffdcd7;\n background-color: #67060c;\n }\n`;\n\nconst Body = styled.div<{ theme: Theme }>`\n background: #ffffff;\n border-bottom-left-radius: ${({ theme }) => theme.spacing.radius.lg};\n border-bottom-right-radius: ${({ theme }) => theme.spacing.radius.lg};\n color: #24292f;\n padding: 20px;\n font-family: ${({ theme }) => theme.fonts.mono};\n text-align: left;\n overflow-x: auto;\n overflow-y: auto;\n max-height: calc(100dvh - 400px);\n ${thinScrollbar};\n ${({ theme }) => styledCode(theme)};\n ${lightSyntaxHighlight};\n\n /* Diff lines: highlight.js wraps each +/- line in a single span, so\n stretching it to the full row reads like a GitHub diff. inline-block keeps\n the trailing newline as the line break instead of adding an empty row. */\n & .hljs-addition,\n & .hljs-deletion {\n display: inline-table;\n width: 100%;\n }\n\n :root.dark & {\n background: #0d1117;\n color: #ffffff;\n ${darkSyntaxHighlight};\n }\n`;\n\nconst escapeHtml = (unsafe: string): string => {\n return unsafe\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#039;\");\n};\n\nconst sanitizeLanguage = (lang: string): string =>\n lang.replace(/[^a-zA-Z0-9_-]/g, \"\");\n\nconst highlightCode = (code: string, language: string): string => {\n const escapedCode = escapeHtml(code);\n const safeLang = sanitizeLanguage(language);\n const result = unified()\n .use(rehypeParse, { fragment: true })\n .use(rehypeHighlight, {\n detect: true,\n ignoreMissing: true,\n })\n .use(rehypeStringify)\n .processSync(\n `<pre><code class=\"language-${safeLang}\">${escapedCode}</code></pre>`,\n );\n\n return String(result);\n};\n\nfunction Code({\n code,\n language = \"javascript\",\n title,\n theme,\n className,\n}: CodeProps) {\n const [copied, setCopied] = useState(false);\n const highlightedCode = useMemo(\n () => highlightCode(code, language),\n [code, language],\n );\n\n const handleCopy = useCallback(async () => {\n try {\n await navigator.clipboard.writeText(code);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n } catch (err) {\n console.error(\"Failed to copy code:\", err);\n }\n }, [code]);\n\n return (\n <CodeWrapper\n className={`${className ?? \"\"} code-wrapper`.trim()}\n theme={theme}\n >\n <TopBar theme={theme}>\n <DotsContainer>\n <Dot theme={theme} />\n <Dot theme={theme} />\n <Dot theme={theme} />\n </DotsContainer>\n {title ? <TopBarTitle theme={theme}>{title}</TopBarTitle> : null}\n <CopyButton\n onClick={handleCopy}\n $copied={copied}\n theme={theme}\n aria-label={copied ? \"Copied\" : \"Copy code\"}\n >\n <Icon name={copied ? \"check\" : \"copy\"} size={12} />\n </CopyButton>\n </TopBar>\n <Body\n dangerouslySetInnerHTML={{ __html: highlightedCode }}\n theme={theme}\n className=\"code-wrapper-body\"\n />\n </CodeWrapper>\n );\n}\n\n/* Multi-variant code block (npm / pnpm / yarn, etc). Tabs replace the macOS\n dots in the TopBar as a keyboard-accessible tablist; the copy button copies\n whichever tab is active and resets its copied state when the tab changes. */\nfunction CodeTabs({ tabs, theme, className }: CodeTabsProps) {\n const baseId = useId();\n const [active, setActive] = useState(0);\n const [copied, setCopied] = useState(false);\n\n const safeActive = active < tabs.length ? active : 0;\n const activeTab = tabs[safeActive];\n\n const highlightedCode = useMemo(\n () =>\n activeTab\n ? highlightCode(activeTab.code, activeTab.language ?? \"bash\")\n : \"\",\n [activeTab],\n );\n\n const handleSelect = useCallback((index: number) => {\n setActive(index);\n setCopied(false);\n }, []);\n\n const handleCopy = useCallback(async () => {\n if (!activeTab) return;\n try {\n await navigator.clipboard.writeText(activeTab.code);\n setCopied(true);\n setTimeout(() => setCopied(false), 2000);\n } catch (err) {\n console.error(\"Failed to copy code:\", err);\n }\n }, [activeTab]);\n\n const handleKeyDown = useCallback(\n (event: React.KeyboardEvent<HTMLButtonElement>) => {\n let next = safeActive;\n if (event.key === \"ArrowRight\" || event.key === \"ArrowDown\") {\n next = (safeActive + 1) % tabs.length;\n } else if (event.key === \"ArrowLeft\" || event.key === \"ArrowUp\") {\n next = (safeActive - 1 + tabs.length) % tabs.length;\n } else if (event.key === \"Home\") {\n next = 0;\n } else if (event.key === \"End\") {\n next = tabs.length - 1;\n } else {\n return;\n }\n event.preventDefault();\n handleSelect(next);\n const target = document.getElementById(`${baseId}-tab-${next}`);\n if (target) target.focus();\n },\n [baseId, handleSelect, safeActive, tabs.length],\n );\n\n if (!tabs || tabs.length === 0) return null;\n\n return (\n <CodeWrapper\n className={`${className ?? \"\"} code-wrapper`.trim()}\n theme={theme}\n >\n <TopBar theme={theme}>\n <TabList role=\"tablist\" aria-label=\"Code variants\">\n {tabs.map((tab, index) => (\n <CodeTab\n key={`${tab.label}-${index}`}\n type=\"button\"\n role=\"tab\"\n id={`${baseId}-tab-${index}`}\n aria-selected={index === safeActive}\n aria-controls={`${baseId}-panel`}\n tabIndex={index === safeActive ? 0 : -1}\n onClick={() => handleSelect(index)}\n onKeyDown={handleKeyDown}\n $active={index === safeActive}\n theme={theme}\n >\n {tab.label}\n </CodeTab>\n ))}\n </TabList>\n <CopyButton\n onClick={handleCopy}\n $copied={copied}\n theme={theme}\n aria-label={copied ? \"Copied\" : \"Copy code\"}\n >\n <Icon name={copied ? \"check\" : \"copy\"} size={12} />\n </CopyButton>\n </TopBar>\n <Body\n id={`${baseId}-panel`}\n role=\"tabpanel\"\n aria-labelledby={`${baseId}-tab-${safeActive}`}\n dangerouslySetInnerHTML={{ __html: highlightedCode }}\n theme={theme}\n className=\"code-wrapper-body\"\n />\n </CodeWrapper>\n );\n}\n\nexport { Code, CodeTabs };\n";
@@ -1,5 +1,5 @@
1
1
  export const codeTemplate = `"use client";
2
- import { useState, useCallback, useMemo } from "react";
2
+ import { useState, useCallback, useMemo, useId } from "react";
3
3
  import styled, { css } from "styled-components";
4
4
  import {
5
5
  interactiveStyles,
@@ -20,6 +20,21 @@ interface CodeProps extends Omit<
20
20
  > {
21
21
  code: string;
22
22
  language?: string;
23
+ title?: string;
24
+ theme?: Theme;
25
+ }
26
+
27
+ interface CodeTab {
28
+ label: string;
29
+ code: string;
30
+ language?: string;
31
+ }
32
+
33
+ interface CodeTabsProps extends Omit<
34
+ React.HTMLAttributes<HTMLDivElement>,
35
+ "theme"
36
+ > {
37
+ tabs: CodeTab[];
23
38
  theme?: Theme;
24
39
  }
25
40
 
@@ -41,6 +56,7 @@ const CodeWrapper = styled.span<{ theme: Theme }>\`
41
56
  Dark variants live in :root.dark & blocks so the swap happens via the
42
57
  active <html> class with no re-render. */
43
58
  const TopBar = styled.div<{ theme: Theme }>\`
59
+ position: relative;
44
60
  background: #f6f8fa;
45
61
  border-top-left-radius: \${({ theme }) => theme.spacing.radius.lg};
46
62
  border-top-right-radius: \${({ theme }) => theme.spacing.radius.lg};
@@ -113,6 +129,90 @@ const CopyButton = styled.button<{ theme: Theme; $copied: boolean }>\`
113
129
  }
114
130
  \`;
115
131
 
132
+ /* Centered file name in the TopBar. Sits absolutely over the flex row so the
133
+ dots and copy button keep their edge alignment. Monospace + muted GitHub
134
+ grays, swapped via :root.dark like the rest of the block. pointer-events off
135
+ so clicks fall through to nothing and never steal focus from the buttons. */
136
+ const TopBarTitle = styled.span<{ theme: Theme }>\`
137
+ position: absolute;
138
+ left: 50%;
139
+ transform: translateX(-50%);
140
+ max-width: 60%;
141
+ overflow: hidden;
142
+ text-overflow: ellipsis;
143
+ white-space: nowrap;
144
+ pointer-events: none;
145
+ font-family: \${({ theme }) => theme.fonts.mono};
146
+ font-size: 12px;
147
+ line-height: 2;
148
+ color: #57606a;
149
+
150
+ :root.dark & {
151
+ color: #8b949e;
152
+ }
153
+ \`;
154
+
155
+ /* Segmented control living in the TopBar for CodeTabs. Scrolls horizontally
156
+ with the thin scrollbar when the labels overflow the 33px bar. */
157
+ const TabList = styled.div\`
158
+ display: flex;
159
+ align-items: center;
160
+ gap: 2px;
161
+ min-width: 0;
162
+ overflow-x: auto;
163
+ \${thinScrollbar};
164
+ margin-left: -6px;
165
+ \`;
166
+
167
+ /* Individual tab button. Active tab reads as part of the window: a light body
168
+ fill (#ffffff) that echoes the code area, with a soft border. Inactive tabs
169
+ are muted and transparent. resetButton strips native styling; focus-visible
170
+ uses the brand primary ring for keyboard users. All colors stay fixed and
171
+ swap purely via :root.dark, matching the no-re-render palette approach. */
172
+ const CodeTab = styled.button<{ theme: Theme; $active: boolean }>\`
173
+ \${resetButton}
174
+ flex: 0 0 auto;
175
+ font-family: \${({ theme }) => theme.fonts.mono};
176
+ font-size: 12px;
177
+ line-height: 1;
178
+ padding: 5px;
179
+ border-radius: \${({ theme }) => theme.spacing.radius.xs};
180
+ cursor: pointer;
181
+ white-space: nowrap;
182
+ transition:
183
+ color 0.15s ease,
184
+ background 0.15s ease,
185
+ border-color 0.15s ease;
186
+ border: solid 1px
187
+ \${({ $active }) => ($active ? "rgba(0, 0, 0, 0.1)" : "transparent")};
188
+ background: \${({ $active }) => ($active ? "#ffffff" : "transparent")};
189
+ color: \${({ $active }) => ($active ? "#24292f" : "#57606a")};
190
+
191
+ &:hover {
192
+ color: #24292f;
193
+ background: \${({ $active }) =>
194
+ $active ? "#ffffff" : "rgba(0, 0, 0, 0.04)"};
195
+ }
196
+
197
+ &:focus-visible {
198
+ outline: solid 2px \${({ theme }) => theme.colors.primary};
199
+ outline-offset: 1px;
200
+ }
201
+
202
+ :root.dark & {
203
+ border-color: \${({ $active }) =>
204
+ $active ? "rgba(255, 255, 255, 0.15)" : "transparent"};
205
+ background: \${({ $active }) => ($active ? "#161b22" : "transparent")};
206
+ color: \${({ $active }) => ($active ? "#e6edf3" : "#8b949e")};
207
+
208
+ &:hover {
209
+ color: #e6edf3;
210
+ background: \${({ $active }) =>
211
+ $active ? "#161b22" : "rgba(255, 255, 255, 0.06)"};
212
+ }
213
+ }
214
+ \`;
215
+
116
216
  /* GitHub Light syntax highlighting by default; GitHub Dark in :root.dark.
117
217
  Browser resolves which rule wins based on the active <html> class with no
118
218
  JS or React re-render involved. */
@@ -120,6 +220,10 @@ const lightSyntaxHighlight = css\`
120
220
  & .hljs {
121
221
  color: #24292f;
122
222
  background: #ffffff;
223
+ min-width: min-content;
224
+ width: 100%;
225
+ display: block;
226
+ padding-right: 20px;
123
227
  }
124
228
  & .hljs-doctag,
125
229
  & .hljs-keyword,
@@ -291,6 +395,15 @@ const Body = styled.div<{ theme: Theme }>\`
291
395
  \${({ theme }) => styledCode(theme)};
292
396
  \${lightSyntaxHighlight};
293
397
 
398
+ /* Diff lines: highlight.js wraps each +/- line in a single span, so
399
+ stretching it to the full row reads like a GitHub diff. inline-block keeps
400
+ the trailing newline as the line break instead of adding an empty row. */
401
+ & .hljs-addition,
402
+ & .hljs-deletion {
403
+ display: inline-table;
404
+ width: 100%;
405
+ }
406
+
294
407
  :root.dark & {
295
408
  background: #0d1117;
296
409
  color: #ffffff;
@@ -327,7 +440,13 @@ const highlightCode = (code: string, language: string): string => {
327
440
  return String(result);
328
441
  };
329
442
 
330
- function Code({ code, language = "javascript", theme, className }: CodeProps) {
443
+ function Code({
444
+ code,
445
+ language = "javascript",
446
+ title,
447
+ theme,
448
+ className,
449
+ }: CodeProps) {
331
450
  const [copied, setCopied] = useState(false);
332
451
  const highlightedCode = useMemo(
333
452
  () => highlightCode(code, language),
@@ -355,6 +474,109 @@ function Code({ code, language = "javascript", theme, className }: CodeProps) {
355
474
  <Dot theme={theme} />
356
475
  <Dot theme={theme} />
357
476
  </DotsContainer>
477
+ {title ? <TopBarTitle theme={theme}>{title}</TopBarTitle> : null}
478
+ <CopyButton
479
+ onClick={handleCopy}
480
+ $copied={copied}
481
+ theme={theme}
482
+ aria-label={copied ? "Copied" : "Copy code"}
483
+ >
484
+ <Icon name={copied ? "check" : "copy"} size={12} />
485
+ </CopyButton>
486
+ </TopBar>
487
+ <Body
488
+ dangerouslySetInnerHTML={{ __html: highlightedCode }}
489
+ theme={theme}
490
+ className="code-wrapper-body"
491
+ />
492
+ </CodeWrapper>
493
+ );
494
+ }
495
+
496
+ /* Multi-variant code block (npm / pnpm / yarn, etc). Tabs replace the macOS
497
+ dots in the TopBar as a keyboard-accessible tablist; the copy button copies
498
+ whichever tab is active and resets its copied state when the tab changes. */
499
+ function CodeTabs({ tabs, theme, className }: CodeTabsProps) {
500
+ const baseId = useId();
501
+ const [active, setActive] = useState(0);
502
+ const [copied, setCopied] = useState(false);
503
+
504
+ const safeActive = active < tabs.length ? active : 0;
505
+ const activeTab = tabs[safeActive];
506
+
507
+ const highlightedCode = useMemo(
508
+ () =>
509
+ activeTab
510
+ ? highlightCode(activeTab.code, activeTab.language ?? "bash")
511
+ : "",
512
+ [activeTab],
513
+ );
514
+
515
+ const handleSelect = useCallback((index: number) => {
516
+ setActive(index);
517
+ setCopied(false);
518
+ }, []);
519
+
520
+ const handleCopy = useCallback(async () => {
521
+ if (!activeTab) return;
522
+ try {
523
+ await navigator.clipboard.writeText(activeTab.code);
524
+ setCopied(true);
525
+ setTimeout(() => setCopied(false), 2000);
526
+ } catch (err) {
527
+ console.error("Failed to copy code:", err);
528
+ }
529
+ }, [activeTab]);
530
+
531
+ const handleKeyDown = useCallback(
532
+ (event: React.KeyboardEvent<HTMLButtonElement>) => {
533
+ let next = safeActive;
534
+ if (event.key === "ArrowRight" || event.key === "ArrowDown") {
535
+ next = (safeActive + 1) % tabs.length;
536
+ } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
537
+ next = (safeActive - 1 + tabs.length) % tabs.length;
538
+ } else if (event.key === "Home") {
539
+ next = 0;
540
+ } else if (event.key === "End") {
541
+ next = tabs.length - 1;
542
+ } else {
543
+ return;
544
+ }
545
+ event.preventDefault();
546
+ handleSelect(next);
547
+ const target = document.getElementById(\`\${baseId}-tab-\${next}\`);
548
+ if (target) target.focus();
549
+ },
550
+ [baseId, handleSelect, safeActive, tabs.length],
551
+ );
552
+
553
+ if (!tabs || tabs.length === 0) return null;
554
+
555
+ return (
556
+ <CodeWrapper
557
+ className={\`\${className ?? ""} code-wrapper\`.trim()}
558
+ theme={theme}
559
+ >
560
+ <TopBar theme={theme}>
561
+ <TabList role="tablist" aria-label="Code variants">
562
+ {tabs.map((tab, index) => (
563
+ <CodeTab
564
+ key={\`\${tab.label}-\${index}\`}
565
+ type="button"
566
+ role="tab"
567
+ id={\`\${baseId}-tab-\${index}\`}
568
+ aria-selected={index === safeActive}
569
+ aria-controls={\`\${baseId}-panel\`}
570
+ tabIndex={index === safeActive ? 0 : -1}
571
+ onClick={() => handleSelect(index)}
572
+ onKeyDown={handleKeyDown}
573
+ $active={index === safeActive}
574
+ theme={theme}
575
+ >
576
+ {tab.label}
577
+ </CodeTab>
578
+ ))}
579
+ </TabList>
358
580
  <CopyButton
359
581
  onClick={handleCopy}
360
582
  $copied={copied}
@@ -365,6 +587,9 @@ function Code({ code, language = "javascript", theme, className }: CodeProps) {
365
587
  </CopyButton>
366
588
  </TopBar>
367
589
  <Body
590
+ id={\`\${baseId}-panel\`}
591
+ role="tabpanel"
592
+ aria-labelledby={\`\${baseId}-tab-\${safeActive}\`}
368
593
  dangerouslySetInnerHTML={{ __html: highlightedCode }}
369
594
  theme={theme}
370
595
  className="code-wrapper-body"
@@ -373,5 +598,5 @@ function Code({ code, language = "javascript", theme, className }: CodeProps) {
373
598
  );
374
599
  }
375
600
 
376
- export { Code };
601
+ export { Code, CodeTabs };
377
602
  `;
@@ -1 +1 @@
1
- export declare const footerTemplate = "\"use client\";\nimport { useContext } from \"react\";\nimport styled, { css } from \"styled-components\";\nimport { Space, styledSmall } from \"cherry-styled-components\";\nimport { ChatContext } from \"@/components/Chat\";\nimport { mq, Theme } from \"@/app/theme\";\nimport { GitHubLogo } from \"@/components/layout/Pictograms\";\nimport linksData from \"@/links.json\";\n\ninterface LinkProps {\n title: string;\n url: string;\n icon?: string;\n}\n\nconst links = linksData as LinkProps[];\n\nconst StyledFooter = styled.footer<{\n theme: Theme;\n $isChatOpen?: boolean;\n $hasLinks?: boolean;\n}>`\n padding: 0 20px;\n transition: all 0.3s ease;\n\n ${({ $hasLinks }) =>\n $hasLinks &&\n css`\n margin-top: 20px;\n `}\n\n ${mq(\"lg\")} {\n margin: 0;\n padding: 0 300px 0 300px;\n\n ${({ $isChatOpen }) =>\n $isChatOpen &&\n css`\n padding: 0 440px 0 300px;\n `}\n }\n`;\n\nconst StyledFooterInner = styled.div<{ theme: Theme }>`\n border-top: solid 1px ${({ theme }) => theme.colors.grayLight};\n max-width: 640px;\n margin: 0 auto;\n padding: 28px 0;\n color: ${({ theme }) => theme.colors.grayDark};\n ${({ theme }) => styledSmall(theme)};\n\n ${mq(\"lg\")} {\n padding: 20px 0;\n }\n\n & a {\n font-weight: 700;\n color: ${({ theme }) => theme.colors.accent};\n text-decoration: none;\n transition: all 0.3s ease;\n display: inline-flex;\n\n &:hover {\n color: ${({ theme }) => theme.colors.primary};\n }\n\n & svg {\n width: 18px;\n height: 18px;\n }\n }\n`;\n\nconst StyledFooterFlex = styled.div`\n display: flex;\n justify-content: flex-start;\n align-items: center;\n gap: 20px;\n\n ${mq(\"lg\")} {\n justify-content: space-between;\n }\n`;\n\nfunction Footer({ hideBranding }: { hideBranding?: boolean }) {\n const { isOpen } = useContext(ChatContext);\n\n if (hideBranding) return <Space $xs={80} $lg=\"none\" />;\n\n return (\n <StyledFooter $isChatOpen={isOpen} $hasLinks={links.length > 0}>\n <StyledFooterInner>\n <StyledFooterFlex>\n <span>\n Powered by <a href=\"https://doccupine.com\">Doccupine</a>\n </span>\n <a\n href=\"https://github.com/doccupine/cli\"\n target=\"_blank\"\n aria-label=\"Doccupine on GitHub\"\n >\n <GitHubLogo />\n </a>\n </StyledFooterFlex>\n </StyledFooterInner>\n </StyledFooter>\n );\n}\n\nexport { Footer };\n";
1
+ export declare const footerTemplate = "\"use client\";\nimport { useContext } from \"react\";\nimport styled, { css } from \"styled-components\";\nimport { Space, styledSmall } from \"cherry-styled-components\";\nimport { ChatContext } from \"@/components/Chat\";\nimport { mq, Theme } from \"@/app/theme\";\nimport { GitHubLogo } from \"@/components/layout/Pictograms\";\nimport linksData from \"@/links.json\";\n\ninterface LinkProps {\n title: string;\n url: string;\n icon?: string;\n}\n\nconst links = linksData as LinkProps[];\n\nconst StyledFooter = styled.footer<{\n theme: Theme;\n $isChatOpen?: boolean;\n $hasLinks?: boolean;\n}>`\n padding: 0 20px;\n transition: all 0.3s ease;\n\n ${({ $hasLinks }) =>\n $hasLinks &&\n css`\n margin-top: 20px;\n `}\n\n ${mq(\"lg\")} {\n margin: 0;\n padding: 0 300px 0 300px;\n\n ${({ $isChatOpen }) =>\n $isChatOpen &&\n css`\n padding: 0 440px 0 300px;\n `}\n }\n`;\n\nconst StyledFooterInner = styled.div<{ theme: Theme }>`\n border-top: solid 1px ${({ theme }) => theme.colors.grayLight};\n max-width: 640px;\n margin: 0 auto;\n padding: 28px 0;\n color: ${({ theme }) => theme.colors.grayDark};\n ${({ theme }) => styledSmall(theme)};\n\n ${mq(\"lg\")} {\n padding: 20px 0;\n }\n\n & a {\n font-weight: 700;\n color: ${({ theme }) => theme.colors.accent};\n text-decoration: none;\n transition: all 0.3s ease;\n display: inline-flex;\n\n &:hover {\n color: ${({ theme }) => theme.colors.primary};\n }\n\n & svg {\n width: 18px;\n height: 18px;\n }\n }\n`;\n\nconst StyledFooterFlex = styled.div`\n display: flex;\n justify-content: flex-start;\n align-items: center;\n gap: 20px;\n\n ${mq(\"lg\")} {\n justify-content: space-between;\n }\n`;\n\nfunction Footer({ hideBranding }: { hideBranding?: boolean }) {\n const { isOpen } = useContext(ChatContext);\n\n if (hideBranding) return <Space $xs={80} $lg=\"none\" />;\n\n return (\n <StyledFooter $isChatOpen={isOpen} $hasLinks={links.length > 0}>\n <StyledFooterInner>\n <StyledFooterFlex>\n <span>\n Powered by{\" \"}\n <a\n href=\"https://doccupine.com\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Doccupine\n </a>\n </span>\n <a\n href=\"https://github.com/doccupine/cli\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n aria-label=\"Doccupine on GitHub\"\n >\n <GitHubLogo />\n </a>\n </StyledFooterFlex>\n </StyledFooterInner>\n </StyledFooter>\n );\n}\n\nexport { Footer };\n";
@@ -93,11 +93,19 @@ function Footer({ hideBranding }: { hideBranding?: boolean }) {
93
93
  <StyledFooterInner>
94
94
  <StyledFooterFlex>
95
95
  <span>
96
- Powered by <a href="https://doccupine.com">Doccupine</a>
96
+ Powered by{" "}
97
+ <a
98
+ href="https://doccupine.com"
99
+ target="_blank"
100
+ rel="noopener noreferrer"
101
+ >
102
+ Doccupine
103
+ </a>
97
104
  </span>
98
105
  <a
99
106
  href="https://github.com/doccupine/cli"
100
107
  target="_blank"
108
+ rel="noopener noreferrer"
101
109
  aria-label="Doccupine on GitHub"
102
110
  >
103
111
  <GitHubLogo />
@@ -1 +1 @@
1
- export declare const siteGateComponentTemplate = "\"use client\";\nimport { useState, type FormEvent } from \"react\";\nimport styled from \"styled-components\";\nimport { Input, ThemeToggle } from \"cherry-styled-components\";\nimport { Lock } from \"lucide-react\";\nimport { Theme } from \"@/app/theme\";\nimport { Button } from \"@/components/layout/Button\";\nimport { Callout } from \"@/components/layout/Callout\";\nimport { config } from \"@/utils/config\";\n\nconst StyledWrapper = styled.div<{ theme: Theme }>`\n min-height: 100dvh;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px;\n background: ${({ theme }) => theme.colors.light};\n`;\n\nconst StyledInner = styled.div`\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 20px;\n width: 100%;\n max-width: 380px;\n`;\n\nconst StyledFooter = styled.div`\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 12px;\n`;\n\nconst StyledBranding = styled.p<{ theme: Theme }>`\n margin: 0;\n font-size: 14px;\n color: ${({ theme }) => theme.colors.grayDark};\n\n & a {\n color: ${({ theme }) => theme.colors.accent};\n font-weight: 700;\n text-decoration: none;\n transition: all 0.3s ease;\n }\n\n & a:hover {\n color: ${({ theme }) => theme.colors.primary};\n }\n`;\n\nconst StyledCard = styled.div<{ theme: Theme }>`\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 20px;\n width: 100%;\n padding: 40px 32px;\n background: ${({ theme }) => theme.colors.light};\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n box-shadow: ${({ theme }) => theme.shadows.sm};\n text-align: center;\n`;\n\nconst StyledIcon = styled.div<{ theme: Theme }>`\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 48px;\n height: 48px;\n border-radius: 50%;\n color: ${({ theme }) => theme.colors.accent};\n background: ${({ theme }) => theme.colors.grayLight};\n\n & svg {\n width: 22px;\n height: 22px;\n }\n`;\n\nconst StyledTitle = styled.h1<{ theme: Theme }>`\n margin: 0;\n font-size: 22px;\n color: ${({ theme }) => theme.colors.accent};\n`;\n\nconst StyledLede = styled.p<{ theme: Theme }>`\n margin: 0;\n font-size: 15px;\n color: ${({ theme }) => theme.colors.grayDark};\n`;\n\nconst StyledForm = styled.form`\n display: flex;\n flex-direction: column;\n gap: 20px;\n width: 100%;\n`;\n\nconst StyledAlert = styled.div`\n width: 100%;\n text-align: left;\n`;\n\nexport function SiteGate({ hideBranding }: { hideBranding?: boolean }) {\n const [password, setPassword] = useState(\"\");\n const [status, setStatus] = useState<\"idle\" | \"loading\" | \"error\">(\"idle\");\n\n async function handleSubmit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault();\n if (!password || status === \"loading\") return;\n\n setStatus(\"loading\");\n try {\n const res = await fetch(\"/api/gate\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ password }),\n });\n if (res.ok) {\n window.location.reload();\n return;\n }\n setStatus(\"error\");\n } catch {\n setStatus(\"error\");\n }\n }\n\n return (\n <StyledWrapper>\n <StyledInner>\n <StyledCard>\n <StyledIcon>\n <Lock />\n </StyledIcon>\n <StyledTitle>{config.name || \"Documentation\"}</StyledTitle>\n <StyledLede>This site is password protected.</StyledLede>\n <StyledForm onSubmit={handleSubmit} noValidate>\n <Input\n type=\"password\"\n name=\"password\"\n autoComplete=\"current-password\"\n placeholder=\"Enter password\"\n aria-label=\"Password\"\n autoFocus\n value={password}\n $error={status === \"error\"}\n $fullWidth\n onChange={(e) => {\n setPassword(e.target.value);\n if (status === \"error\") setStatus(\"idle\");\n }}\n />\n <Button\n type=\"submit\"\n fullWidth\n disabled={status === \"loading\" || !password}\n >\n {status === \"loading\" ? \"Unlocking...\" : \"Enter\"}\n </Button>\n </StyledForm>\n {status === \"error\" && (\n <StyledAlert>\n <Callout type=\"danger\">\n <p>Incorrect password. Try again.</p>\n </Callout>\n </StyledAlert>\n )}\n </StyledCard>\n <StyledFooter>\n <ThemeToggle />\n {!hideBranding && (\n <StyledBranding>\n Powered by <a href=\"https://doccupine.com\">Doccupine</a>\n </StyledBranding>\n )}\n </StyledFooter>\n </StyledInner>\n </StyledWrapper>\n );\n}\n";
1
+ export declare const siteGateComponentTemplate = "\"use client\";\nimport { useState, type FormEvent } from \"react\";\nimport styled from \"styled-components\";\nimport { Input, ThemeToggle } from \"cherry-styled-components\";\nimport { Lock } from \"lucide-react\";\nimport { Theme } from \"@/app/theme\";\nimport { Button } from \"@/components/layout/Button\";\nimport { Callout } from \"@/components/layout/Callout\";\nimport { config } from \"@/utils/config\";\n\nconst StyledWrapper = styled.div<{ theme: Theme }>`\n min-height: 100dvh;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px;\n background: ${({ theme }) => theme.colors.light};\n`;\n\nconst StyledInner = styled.div`\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 20px;\n width: 100%;\n max-width: 380px;\n`;\n\nconst StyledFooter = styled.div`\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 12px;\n`;\n\nconst StyledBranding = styled.p<{ theme: Theme }>`\n margin: 0;\n font-size: 14px;\n color: ${({ theme }) => theme.colors.grayDark};\n\n & a {\n color: ${({ theme }) => theme.colors.accent};\n font-weight: 700;\n text-decoration: none;\n transition: all 0.3s ease;\n }\n\n & a:hover {\n color: ${({ theme }) => theme.colors.primary};\n }\n`;\n\nconst StyledCard = styled.div<{ theme: Theme }>`\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 20px;\n width: 100%;\n padding: 40px 32px;\n background: ${({ theme }) => theme.colors.light};\n border: solid 1px ${({ theme }) => theme.colors.grayLight};\n border-radius: ${({ theme }) => theme.spacing.radius.lg};\n box-shadow: ${({ theme }) => theme.shadows.sm};\n text-align: center;\n`;\n\nconst StyledIcon = styled.div<{ theme: Theme }>`\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 48px;\n height: 48px;\n border-radius: 50%;\n color: ${({ theme }) => theme.colors.accent};\n background: ${({ theme }) => theme.colors.grayLight};\n\n & svg {\n width: 22px;\n height: 22px;\n }\n`;\n\nconst StyledTitle = styled.h1<{ theme: Theme }>`\n margin: 0;\n font-size: 22px;\n color: ${({ theme }) => theme.colors.accent};\n`;\n\nconst StyledLede = styled.p<{ theme: Theme }>`\n margin: 0;\n font-size: 15px;\n color: ${({ theme }) => theme.colors.grayDark};\n`;\n\nconst StyledForm = styled.form`\n display: flex;\n flex-direction: column;\n gap: 20px;\n width: 100%;\n`;\n\nconst StyledAlert = styled.div`\n width: 100%;\n text-align: left;\n`;\n\nexport function SiteGate({ hideBranding }: { hideBranding?: boolean }) {\n const [password, setPassword] = useState(\"\");\n const [status, setStatus] = useState<\"idle\" | \"loading\" | \"error\">(\"idle\");\n\n async function handleSubmit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault();\n if (!password || status === \"loading\") return;\n\n setStatus(\"loading\");\n try {\n const res = await fetch(\"/api/gate\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ password }),\n });\n if (res.ok) {\n window.location.reload();\n return;\n }\n setStatus(\"error\");\n } catch {\n setStatus(\"error\");\n }\n }\n\n return (\n <StyledWrapper>\n <StyledInner>\n <StyledCard>\n <StyledIcon>\n <Lock />\n </StyledIcon>\n <StyledTitle>{config.name || \"Documentation\"}</StyledTitle>\n <StyledLede>This site is password protected.</StyledLede>\n <StyledForm onSubmit={handleSubmit} noValidate>\n <Input\n type=\"password\"\n name=\"password\"\n autoComplete=\"current-password\"\n placeholder=\"Enter password\"\n aria-label=\"Password\"\n autoFocus\n value={password}\n $error={status === \"error\"}\n $fullWidth\n onChange={(e) => {\n setPassword(e.target.value);\n if (status === \"error\") setStatus(\"idle\");\n }}\n />\n <Button\n type=\"submit\"\n fullWidth\n disabled={status === \"loading\" || !password}\n >\n {status === \"loading\" ? \"Unlocking...\" : \"Enter\"}\n </Button>\n </StyledForm>\n {status === \"error\" && (\n <StyledAlert>\n <Callout type=\"danger\">\n <p>Incorrect password. Try again.</p>\n </Callout>\n </StyledAlert>\n )}\n </StyledCard>\n <StyledFooter>\n <ThemeToggle />\n {!hideBranding && (\n <StyledBranding>\n Powered by{\" \"}\n <a\n href=\"https://doccupine.com\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Doccupine\n </a>\n </StyledBranding>\n )}\n </StyledFooter>\n </StyledInner>\n </StyledWrapper>\n );\n}\n";
@@ -174,7 +174,14 @@ export function SiteGate({ hideBranding }: { hideBranding?: boolean }) {
174
174
  <ThemeToggle />
175
175
  {!hideBranding && (
176
176
  <StyledBranding>
177
- Powered by <a href="https://doccupine.com">Doccupine</a>
177
+ Powered by{" "}
178
+ <a
179
+ href="https://doccupine.com"
180
+ target="_blank"
181
+ rel="noopener noreferrer"
182
+ >
183
+ Doccupine
184
+ </a>
178
185
  </StyledBranding>
179
186
  )}
180
187
  </StyledFooter>
@@ -1 +1 @@
1
- export declare const codeMdxTemplate = "---\ntitle: \"Code\"\ndescription: \"Learn how to display inline code and code blocks in documentation.\"\ndate: \"2026-02-19\"\ncategory: \"Components\"\ncategoryOrder: 1\norder: 3\n---\n\n# Code\n\nLearn how to display inline code and code blocks in documentation.\n\n## Adding Code Samples\n\nBoth inline code snippets and full code blocks are supported. Code blocks offer customization for syntax highlighting and more to improve readability and user experience.\n\n### Inline Code\n\nHighlight code within text by wrapping it with backticks:\n\n```text\nEnclose any `word` or `phrase` in backticks to format it as code.\n```\n\nEnclose any `word` or `phrase` in backticks to format it as code.\n\n## Code Blocks\n\nTo present larger code samples, use triple backticks for fenced code blocks. Each block can be copied, and\u2014if assistant features are enabled\u2014users can request explanations.\n\nYou may specify the language for highlighting:\n\n````text\n```java\nclass HelloWorld {\n public static void main(String[] args) {\n System.out.println(\"Hello, World!\");\n }\n}\n```\n````\n\n```java\nclass HelloWorld {\n public static void main(String[] args) {\n System.out.println(\"Hello, World!\");\n }\n}\n```";
1
+ export declare const codeMdxTemplate = "---\ntitle: \"Code\"\ndescription: \"Learn how to display inline code and code blocks in documentation.\"\ndate: \"2026-02-19\"\ncategory: \"Components\"\ncategoryOrder: 1\norder: 3\n---\n\n# Code\n\nLearn how to display inline code and code blocks in documentation.\n\n## Adding Code Samples\n\nBoth inline code snippets and full code blocks are supported. Code blocks offer customization for syntax highlighting and more to improve readability and user experience.\n\n### Inline Code\n\nHighlight code within text by wrapping it with backticks:\n\n```text\nEnclose any `word` or `phrase` in backticks to format it as code.\n```\n\nEnclose any `word` or `phrase` in backticks to format it as code.\n\n## Code Blocks\n\nTo present larger code samples, use triple backticks for fenced code blocks. Each block can be copied, and\u2014if assistant features are enabled\u2014users can request explanations.\n\nYou may specify the language for highlighting:\n\n````text\n```java\nclass HelloWorld {\n public static void main(String[] args) {\n System.out.println(\"Hello, World!\");\n }\n}\n```\n````\n\n```java\nclass HelloWorld {\n public static void main(String[] args) {\n System.out.println(\"Hello, World!\");\n }\n}\n```\n\n## Highlighting Diffs\n\nShow a visual diff of added or removed lines in your code blocks. Added lines are highlighted in green and removed lines are highlighted in red. Set the language to `diff` and prefix each changed line with `+` or `-`:\n\n````text\n```diff\nfunction calculateTotal(items) {\n- return items.reduce((sum, item) => sum + item.price, 0);\n+ const total = items.reduce((sum, item) => sum + item.price, 0);\n+ return Math.round(total * 100) / 100;\n}\n```\n````\n\n```diff\nfunction calculateTotal(items) {\n- return items.reduce((sum, item) => sum + item.price, 0);\n+ const total = items.reduce((sum, item) => sum + item.price, 0);\n+ return Math.round(total * 100) / 100;\n}\n```\n\n## File Names\n\nAdd a `title` to a code block to display a file name in the window bar, styled to match the GitHub-style header. Pass it through the `<Code />` component:\n\n```text\n<Code title=\"package.json\" language=\"json\" code={`{\\n \"name\": \"my-app\",\\n \"version\": \"1.0.0\"\\n}`} />\n```\n\n<Code title=\"package.json\" language=\"json\" code={`{\\n \"name\": \"my-app\",\\n \"version\": \"1.0.0\"\\n}`} />\n\n## Tabbed Code Blocks\n\nUse `<CodeTabs />` to show several variants of the same snippet - for example the same install command across package managers. Each tab is a keyboard-accessible button, and the copy button copies whichever tab is active. Each tab may set a `language` for highlighting (defaults to `bash`).\n\n```text\n<CodeTabs tabs={[{ label: \"npm\", code: \"npm install doccupine\" }, { label: \"pnpm\", code: \"pnpm add doccupine\" }, { label: \"yarn\", code: \"yarn add doccupine\" }]} />\n```\n\n<CodeTabs tabs={[{ label: \"npm\", code: \"npm install doccupine\" }, { label: \"pnpm\", code: \"pnpm add doccupine\" }, { label: \"yarn\", code: \"yarn add doccupine\" }]} />";
@@ -47,4 +47,46 @@ class HelloWorld {
47
47
  System.out.println("Hello, World!");
48
48
  }
49
49
  }
50
- \`\`\``;
50
+ \`\`\`
51
+
52
+ ## Highlighting Diffs
53
+
54
+ Show a visual diff of added or removed lines in your code blocks. Added lines are highlighted in green and removed lines are highlighted in red. Set the language to \`diff\` and prefix each changed line with \`+\` or \`-\`:
55
+
56
+ \`\`\`\`text
57
+ \`\`\`diff
58
+ function calculateTotal(items) {
59
+ - return items.reduce((sum, item) => sum + item.price, 0);
60
+ + const total = items.reduce((sum, item) => sum + item.price, 0);
61
+ + return Math.round(total * 100) / 100;
62
+ }
63
+ \`\`\`
64
+ \`\`\`\`
65
+
66
+ \`\`\`diff
67
+ function calculateTotal(items) {
68
+ - return items.reduce((sum, item) => sum + item.price, 0);
69
+ + const total = items.reduce((sum, item) => sum + item.price, 0);
70
+ + return Math.round(total * 100) / 100;
71
+ }
72
+ \`\`\`
73
+
74
+ ## File Names
75
+
76
+ Add a \`title\` to a code block to display a file name in the window bar, styled to match the GitHub-style header. Pass it through the \`<Code />\` component:
77
+
78
+ \`\`\`text
79
+ <Code title="package.json" language="json" code={\`{\\n "name": "my-app",\\n "version": "1.0.0"\\n}\`} />
80
+ \`\`\`
81
+
82
+ <Code title="package.json" language="json" code={\`{\\n "name": "my-app",\\n "version": "1.0.0"\\n}\`} />
83
+
84
+ ## Tabbed Code Blocks
85
+
86
+ Use \`<CodeTabs />\` to show several variants of the same snippet - for example the same install command across package managers. Each tab is a keyboard-accessible button, and the copy button copies whichever tab is active. Each tab may set a \`language\` for highlighting (defaults to \`bash\`).
87
+
88
+ \`\`\`text
89
+ <CodeTabs tabs={[{ label: "npm", code: "npm install doccupine" }, { label: "pnpm", code: "pnpm add doccupine" }, { label: "yarn", code: "yarn add doccupine" }]} />
90
+ \`\`\`
91
+
92
+ <CodeTabs tabs={[{ label: "npm", code: "npm install doccupine" }, { label: "pnpm", code: "pnpm add doccupine" }, { label: "yarn", code: "yarn add doccupine" }]} />`;
@@ -17,7 +17,7 @@ export const packageJsonTemplate = JSON.stringify({
17
17
  "@mdx-js/react": "^3.1.1",
18
18
  "@modelcontextprotocol/sdk": "^1.29.0",
19
19
  "@posthog/react": "^1.10.3",
20
- "cherry-styled-components": "^0.2.5",
20
+ "cherry-styled-components": "^0.2.7",
21
21
  langchain: "^1.5.2",
22
22
  "lucide-react": "^1.23.0",
23
23
  minisearch: "^7.2.0",
@@ -39,7 +39,7 @@ export const packageJsonTemplate = JSON.stringify({
39
39
  "@types/node": "^26",
40
40
  "@types/react": "^19",
41
41
  "@types/react-dom": "^19",
42
- "baseline-browser-mapping": "^2.10.41",
42
+ "baseline-browser-mapping": "^2.10.42",
43
43
  eslint: "^9",
44
44
  "eslint-config-next": "16.2.10",
45
45
  prettier: "^3.9.4",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doccupine",
3
- "version": "0.0.97",
3
+ "version": "0.0.99",
4
4
  "description": "Free and open-source documentation platform. Write MDX, get a production-ready site with AI chat, built-in components, and an MCP server - in one command.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -1 +0,0 @@
1
- export {};