diff --git a/cloud/app/components/docs-sidebar.tsx b/cloud/app/components/docs-sidebar.tsx index 447a2d015b..ce85bf9ff1 100644 --- a/cloud/app/components/docs-sidebar.tsx +++ b/cloud/app/components/docs-sidebar.tsx @@ -30,7 +30,11 @@ function createSidebarConfig(): SidebarConfig { allDocInfo.forEach((doc) => { // Extract the slug pattern from the path - const keyPath = doc.path; + // Strip "docs/" prefix to match the lookup format + const prefix = "docs/"; + const keyPath = doc.path.startsWith(prefix) + ? doc.path.slice(prefix.length) + : doc.path; slugToRoutePathMap.set(keyPath, doc.routePath); }); diff --git a/cloud/app/components/mdx/component-registry.tsx b/cloud/app/components/mdx/component-registry.tsx index 7e2e91506c..1dfbac0c2f 100644 --- a/cloud/app/components/mdx/component-registry.tsx +++ b/cloud/app/components/mdx/component-registry.tsx @@ -34,7 +34,7 @@ import { Button } from "@/app/components/ui/button"; import { ButtonLink } from "@/app/components/ui/button-link"; import MirascopeLogo from "@/app/components/blocks/branding/mirascope-logo"; import ProductLogo from "@/app/components/blocks/branding/mirascope-logo"; -// import { ProviderCodeWrapper } from "./ProviderCodeWrapper"; +import { ModelProviderCodeWrapper } from "@/app/components/mdx/elements/model-provider-code-wrapper"; // import { ResponsiveImage } from "@/src/components/mdx/providers/ResponsiveImage"; // import { devComponents } from "@/app/components/mdx/elements/DevComponents"; import { idSlugFromChildren } from "@/app/lib/mdx/heading-utils"; @@ -338,95 +338,95 @@ const mediaElements = { }, }; -// todo(sebastian): bring back code elements -// const codeElements = { -// // Inline code - this is only for inline elements, not code blocks -// code: (props: React.ComponentPropsWithoutRef<"code">) => { -// // Don't apply inline code styling to code blocks (which are children of pre tags) -// const isInPre = React.useRef(false); -// React.useLayoutEffect(() => { -// // Type assertion for DOM properties access -// const element = props as unknown as { -// parentElement?: { tagName: string }; -// }; -// const parentIsPre = -// props.className?.includes("language-") || -// props.className?.includes("shiki") || -// element.parentElement?.tagName === "PRE"; -// isInPre.current = !!parentIsPre; -// }, [props]); - -// // Only apply inline code styling to actual inline code, not code blocks -// if (isInPre.current) { -// return ; -// } - -// return ( -// -// ); -// }, - -// // Code blocks - use our custom CodeBlock component with provider substitution -// pre: (props: React.ComponentPropsWithoutRef<"pre">) => { -// // Get meta information from our data attribute or initialize to empty -// let meta = (props as any)["data-meta"] || ""; - -// // Initialize variables for code content and language -// let codeContent = ""; -// let language = "txt"; - -// // Process children to find code content and language -// if (props.children) { -// const children = React.Children.toArray(props.children); - -// // Loop through children to find code content (typically there's only one child) -// for (const child of children) { -// if (!React.isValidElement(child)) continue; - -// // Check if this is a code element or has code-like properties -// const childProps = child.props as { -// className?: string; -// children?: React.ReactNode | string; -// }; - -// // Extract language from className -// if (childProps.className?.includes("language-")) { -// language = -// (childProps.className.match(/language-(\w+)/) || [])[1] || "txt"; - -// // Also check for meta in className (legacy approach) -// // This looks for patterns like {1-3} or {1,3} after the language -// if (!meta) { -// const metaMatch = childProps.className.match(/\{([^}]+)\}/); -// meta = metaMatch ? `{${metaMatch[1]}}` : ""; -// } -// } - -// // Get code content -// if (typeof childProps.children === "string") { -// codeContent = childProps.children; -// break; -// } -// } -// } - -// // Handle mermaid diagrams -// if (language === "mermaid" && codeContent) { -// return ; -// } - -// return ( -// -// ); -// }, -// }; +const codeElements = { + // Inline code - this is only for inline elements, not code blocks + code: (props: React.ComponentPropsWithoutRef<"code">) => { + // Don't apply inline code styling to code blocks (which are children of pre tags) + const isInPre = React.useRef(false); + React.useLayoutEffect(() => { + // Type assertion for DOM properties access + const element = props as unknown as { + parentElement?: { tagName: string }; + }; + const parentIsPre = + props.className?.includes("language-") || + props.className?.includes("shiki") || + element.parentElement?.tagName === "PRE"; + isInPre.current = !!parentIsPre; + }, [props]); + + // Only apply inline code styling to actual inline code, not code blocks + if (isInPre.current) { + return ; + } + + return ( + + ); + }, + + // Code blocks - use our custom CodeBlock component with provider substitution + pre: (props: React.ComponentPropsWithoutRef<"pre">) => { + // Get meta information from our data attribute or initialize to empty + let meta = + (props as unknown as { "data-meta"?: string })["data-meta"] || ""; + + // Initialize variables for code content and language + let codeContent = ""; + let language = "txt"; + + // Process children to find code content and language + if (props.children) { + const children = React.Children.toArray(props.children); + + // Loop through children to find code content (typically there's only one child) + for (const child of children) { + if (!React.isValidElement(child)) continue; + + // Check if this is a code element or has code-like properties + const childProps = child.props as { + className?: string; + children?: React.ReactNode | string; + }; + + // Extract language from className + if (childProps.className?.includes("language-")) { + language = + (childProps.className.match(/language-(\w+)/) || [])[1] || "txt"; + + // Also check for meta in className (legacy approach) + // This looks for patterns like {1-3} or {1,3} after the language + if (!meta) { + const metaMatch = childProps.className.match(/\{([^}]+)\}/); + meta = metaMatch ? `{${metaMatch[1]}}` : ""; + } + } + + // Get code content + if (typeof childProps.children === "string") { + codeContent = childProps.children; + break; + } + } + } + + // Handle mermaid diagrams + if (language === "mermaid" && codeContent) { + return ; + } + + return ( + + ); + }, +}; // ----------------------------------------------------------------------------- // Complete Component Registry @@ -439,5 +439,5 @@ export default { ...listElements, ...tableElements, ...mediaElements, - // ...codeElements, + ...codeElements, }; diff --git a/cloud/app/components/mdx/elements/analytics-code-block.tsx b/cloud/app/components/mdx/elements/analytics-code-block.tsx new file mode 100644 index 0000000000..225dcbcf35 --- /dev/null +++ b/cloud/app/components/mdx/elements/analytics-code-block.tsx @@ -0,0 +1,63 @@ +import { useRef, useMemo } from "react"; +import { CodeBlock } from "@/app/components/blocks/code-block/code-block"; +// import analyticsManager from "@/src/lib/services/analytics"; + +interface AnalyticsCodeBlockProps { + code: string; + language?: string; + meta?: string; + className?: string; + showLineNumbers?: boolean; +} + +export function AnalyticsCodeBlock({ + code, + language, + meta, + className, + showLineNumbers, +}: AnalyticsCodeBlockProps) { + const codeRef = useRef(null); + + // Create a stable identifier for this code block based on its content + // This ensures the ID remains consistent across rerenders + const codeHash = useMemo(() => { + // Simple hash function for the code content + let hash = 0; + for (let i = 0; i < code.length; i++) { + const char = code.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; // Convert to 32bit integer + } + return Math.abs(hash).toString(16).substring(0, 8); + }, [code]); + + const onCopy = () => { + // const pagePath = window.location.pathname; + // Use path, language and hash of code to create a stable identifier + // const itemId = `${pagePath}#${language || "code"}-${codeHash}`; + // analyticsManager.trackCopyEvent({ + // contentType: "code_snippet", + // itemId, + // product, + // language: language || "text", + // }); + }; + + return ( +
+ +
+ ); +} diff --git a/cloud/app/components/mdx/elements/code-block-provider.tsx b/cloud/app/components/mdx/elements/code-block-provider.tsx index 4a5a6d9096..591c19ae86 100644 --- a/cloud/app/components/mdx/elements/code-block-provider.tsx +++ b/cloud/app/components/mdx/elements/code-block-provider.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import LoadingContent from "@/app/components/blocks/loading-content"; import { useProvider } from "@/app/components/mdx/elements/model-provider-provider"; -// import { AnalyticsCodeBlock } from "./AnalyticsCodeBlock"; +import { AnalyticsCodeBlock } from "./analytics-code-block"; interface ProviderCodeBlockProps { examplePath: string; // Path relative to public/examples language?: string; @@ -14,7 +14,7 @@ interface ProviderCodeBlockProps { */ export default function ProviderCodeBlock({ examplePath, - // language = "python", + language = "python", className = "", }: ProviderCodeBlockProps) { // Get the currently selected provider @@ -81,8 +81,9 @@ export default function ProviderCodeBlock({ Example for {provider} not available yet. )} - {/* {currentProviderCode && } */} - {currentProviderCode && "AnalyticsCodeBlock"} + {currentProviderCode && ( + + )} ); } diff --git a/cloud/app/components/mdx/elements/install-snippet.tsx b/cloud/app/components/mdx/elements/install-snippet.tsx index 1bdacfda19..7a5fb0e22b 100644 --- a/cloud/app/components/mdx/elements/install-snippet.tsx +++ b/cloud/app/components/mdx/elements/install-snippet.tsx @@ -1,6 +1,6 @@ import { useProvider } from "@/app/components/mdx/elements/model-provider-provider"; import type { Provider } from "@/app/components/mdx/elements/model-provider-provider"; -// import { AnalyticsCodeBlock } from "@/app/components/blocks/mdx/AnalyticsCodeBlock"; +import { AnalyticsCodeBlock } from "@/app/components/mdx/elements/analytics-code-block"; import { cn } from "@/app/lib/utils"; import { TabbedSection, Tab } from "./tabbed-section"; @@ -44,8 +44,6 @@ export function InstallSnippet({ className = "" }: InstallSnippetProps) { const { provider } = useProvider(); // Generate install commands for each OS - // @ts-expect-error until todo(sebastian): add back code elements - // eslint-disable-next-line @typescript-eslint/no-unused-vars const generateCommand = (os: OS) => { const setEnvCmd = os === "MacOS / Linux" ? "export" : "set"; const apiKeyVar = providerApiKeys[provider]; @@ -63,8 +61,7 @@ export function InstallSnippet({ className = "" }: InstallSnippetProps) { {operatingSystems.map((os) => ( - {/* */} - AnalyticsCodeBlock + ))} diff --git a/cloud/app/components/mdx/elements/model-provider-code-wrapper.tsx b/cloud/app/components/mdx/elements/model-provider-code-wrapper.tsx new file mode 100644 index 0000000000..8c296d0bef --- /dev/null +++ b/cloud/app/components/mdx/elements/model-provider-code-wrapper.tsx @@ -0,0 +1,38 @@ +// No need to import React with JSX transform +import { + useProvider, + replaceProviderVariables, +} from "@/app/components/mdx/elements/model-provider-provider"; +import { AnalyticsCodeBlock } from "@/app/components/mdx/elements/analytics-code-block"; + +/** + * A wrapper component for code blocks that handles provider-specific substitutions. + * This allows us to use standard markdown code blocks with provider-specific variables. + */ +export function ModelProviderCodeWrapper({ + code, + language, + meta, + className, +}: { + code: string; + language: string; + meta?: string; + className?: string; +}) { + const { provider } = useProvider(); + + // Only process python or bash code + if (code && (language === "python" || language === "bash")) { + code = replaceProviderVariables(code, provider); + } + + return ( + + ); +} diff --git a/cloud/app/components/ui/button.tsx b/cloud/app/components/ui/button.tsx index bab4e18b13..230d6edb03 100644 --- a/cloud/app/components/ui/button.tsx +++ b/cloud/app/components/ui/button.tsx @@ -12,6 +12,7 @@ const buttonVariants = cva( "[&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0", "outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "aria-invalid:ring-destructive/20 aria-invalid:border-destructive", + "font-handwriting", ].join(" "), { variants: { diff --git a/cloud/app/lib/content/mdx-compile.test.ts b/cloud/app/lib/content/mdx-compile.test.ts index a3f8b8e446..a75111f65f 100644 --- a/cloud/app/lib/content/mdx-compile.test.ts +++ b/cloud/app/lib/content/mdx-compile.test.ts @@ -173,15 +173,15 @@ More text.`; expect(result.jsxCode).toContain("Links"); }); - it("should skip syntax highlighting when skipHighlighting is true", async () => { + it("should compile code blocks with language identifiers", async () => { const content = `\`\`\`typescript const x = 1; \`\`\``; - const result = await compileMDXContent(content, { skipHighlighting: true }); + const result = await compileMDXContent(content); expect(result.jsxCode).toBeDefined(); - // The code should still compile, just without syntax highlighting + // The code should compile with the language identifier preserved expect(result.jsxCode).toContain("typescript"); }); diff --git a/cloud/app/lib/content/mdx-compile.ts b/cloud/app/lib/content/mdx-compile.ts index cd02e2f613..de44616989 100644 --- a/cloud/app/lib/content/mdx-compile.ts +++ b/cloud/app/lib/content/mdx-compile.ts @@ -4,17 +4,17 @@ * Shared MDX compilation logic used by both the vite plugin and tests. */ +// NOTE: All imports in this file must use relative paths. +// Vite plugins cannot resolve path aliases such as "@/app/...", +// so using aliases here will cause module resolution failures. +// This ensures compatibility in both the vite plugin and other consumers. import { compile, type CompileOptions } from "@mdx-js/mdx"; import remarkGfm from "remark-gfm"; -import rehypePrettyCode from "rehype-pretty-code"; +import { rehypeCodeMeta } from "./rehype-code-meta"; import type { ProcessedMDX, Frontmatter } from "@/app/lib/mdx/types"; import type { TOCItem } from "@/app/lib/content/types"; import { parseFrontmatter } from "./frontmatter"; import { extractHeadings } from "../mdx/heading-utils"; -export interface CompileMDXOptions { - /** Skip syntax highlighting (faster for tests) */ - skipHighlighting?: boolean; -} export interface CompiledMDXResult { /** The compiled JSX code string */ @@ -34,7 +34,6 @@ export interface CompiledMDXResult { */ export async function compileMDXContent( rawContent: string, - options: CompileMDXOptions = {}, ): Promise { // Parse frontmatter const { frontmatter, content } = parseFrontmatter(rawContent); @@ -47,17 +46,7 @@ export async function compileMDXContent( development: process.env.NODE_ENV === "development", outputFormat: "program", remarkPlugins: [remarkGfm], - rehypePlugins: options.skipHighlighting - ? [] - : [ - [ - rehypePrettyCode, - { - theme: "github-dark", - keepBackground: false, - }, - ], - ], + rehypePlugins: [rehypeCodeMeta], }; // Compile MDX to JSX diff --git a/cloud/app/lib/content/rehype-code-meta.ts b/cloud/app/lib/content/rehype-code-meta.ts new file mode 100644 index 0000000000..5e0fc0d757 --- /dev/null +++ b/cloud/app/lib/content/rehype-code-meta.ts @@ -0,0 +1,38 @@ +import { visit } from "unist-util-visit"; +import type { Element, Root, RootContent } from "hast"; + +/** + * Type definition for a hast element with data property + */ +interface ElementWithData extends Element { + data?: { + meta?: string; + [key: string]: unknown; + }; +} + +/** + * Rehype plugin to preserve code block meta information + * by adding it as a data attribute to the pre element + */ +export function rehypeCodeMeta() { + return (tree: Root) => { + visit(tree, "element", (node: Element) => { + // Check for pre > code structure + if (node.tagName === "pre") { + const codeNode = node.children.find( + (child: RootContent): child is ElementWithData => + child.type === "element" && child.tagName === "code", + ); + + if (codeNode?.data?.meta) { + // Ensure properties object exists + node.properties = node.properties || {}; + + // Use standard HTML data-* attribute format which is more likely to be preserved + node.properties["data-meta"] = codeNode.data.meta; + } + } + }); + }; +} diff --git a/cloud/app/routes/docs.tsx b/cloud/app/routes/docs.tsx index bb534691c6..6534a84a96 100644 --- a/cloud/app/routes/docs.tsx +++ b/cloud/app/routes/docs.tsx @@ -1,4 +1,5 @@ import { createFileRoute, Outlet } from "@tanstack/react-router"; +import { TabbedSectionMemoryProvider } from "../components/blocks/tabbed-section-provider"; export const Route = createFileRoute("/docs")({ component: DocsLayout, @@ -9,7 +10,9 @@ function DocsLayout() {
{/* This Outlet will render child routes like /docs/v1/placeholder */} {/* Eventually this will include sidebar, header, etc. */} - + + +
); } diff --git a/cloud/app/components/blocks/code-block/code-block.css b/cloud/app/styles/code-highlighting.css similarity index 100% rename from cloud/app/components/blocks/code-block/code-block.css rename to cloud/app/styles/code-highlighting.css diff --git a/cloud/app/styles/globals.css b/cloud/app/styles/globals.css index 128d754322..77b991d169 100644 --- a/cloud/app/styles/globals.css +++ b/cloud/app/styles/globals.css @@ -2,6 +2,7 @@ @import "tw-animate-css"; @import "./content.css"; +@import "./code-highlighting.css"; @custom-variant dark (&:is(.dark *)); @@ -29,21 +30,36 @@ --mirple-accent: color-mix(in srgb, var(--mirple) 60%, transparent); --mirple-dark: rgba(84, 82, 220, 1); /* Darker purple */ + --text-2xs: clamp(0.5rem, 0.45rem + 0.4vw, 0.625rem); /* ~8-10px */ + --text-xs: clamp(0.6rem, 0.55rem + 0.5vw, 0.8rem); /* ~10-13px */ + --text-sm: clamp(0.7rem, 0.6rem + 0.75vw, 0.875rem); /* ~11-14px */ + --text-base: clamp(0.825rem, 0.7rem + 0.875vw, 1rem); /* ~13-16px */ + --text-lg: clamp(0.9rem, 0.75rem + 1vw, 1.125rem); /* ~14-18px */ + --text-xl: clamp(1rem, 0.825rem + 1.125vw, 1.25rem); /* ~16-20px */ + --text-2xl: clamp(1.125rem, 0.9rem + 1.5vw, 1.5rem); /* ~18-24px */ + --text-3xl: clamp(1.25rem, 0.95rem + 1.75vw, 1.75rem); /* ~20-28px */ + --text-4xl: clamp(1.5rem, 1.125rem + 2vw, 2.25rem); /* ~24-36px */ + --text-5xl: clamp(1.75rem, 1.25rem + 2.5vw, 2.75rem); /* ~28-44px */ + --text-6xl: clamp(2.25rem, 1.5rem + 3.5vw, 3.5rem); /* ~36-56px */ + --text-7xl: clamp(2.75rem, 1.75rem + 4.5vw, 4.5rem); /* ~44-72px */ + --text-8xl: clamp(3.5rem, 2.5rem + 5vw, 5.5rem); /* ~56-88px */ + --text-9xl: clamp(4.5rem, 3rem + 6vw, 7rem); /* ~72-112px */ + --background: oklch(1 0 0); --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); + --card: rgba(99, 102, 241, 0.2); + --card-foreground: oklch(0.1 0.2041 282.8); + --popover: oklch(0.995 0 0); + --popover-foreground: oklch(0.1 0.2041 282.8); --primary: var(--mirple); --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.68 0.15 180); + --secondary-foreground: oklch(1 0 0); --muted: oklch(0.97 0 0); --muted-foreground: oklch(0.556 0 0); --accent: var(--mirple-accent); --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); + --destructive: oklch(0.65 0.15 30); --destructive-foreground: oklch(0.985 0 0); --border: oklch(0.922 0 0); --input: oklch(0.922 0 0); @@ -93,10 +109,10 @@ .dark { --background: oklch(0.11 0.005 285.823); --foreground: oklch(0.95 0 0); - --card: oklch(0.145 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.145 0 0); - --popover-foreground: oklch(0.985 0 0); + --card: rgba(99, 102, 241, 0.2); + --card-foreground: oklch(0.95 0 0); + --popover: oklch(0.11 0.005 285.823); + --popover-foreground: oklch(0.95 0 0); --primary-foreground: oklch(0.205 0 0); --secondary: oklch(0.5 0.12 195); --secondary-foreground: oklch(0.9 0 0); @@ -104,8 +120,8 @@ --muted-foreground: oklch(0.75 0.015 286.067); --accent: var(--mirple-accent); --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.396 0.141 25.723); - --destructive-foreground: oklch(0.985 0 0); + --destructive: oklch(0.5 0.17 30); + --destructive-foreground: oklch(0.95 0 0); --border: oklch(0.269 0 0); --input: oklch(0.269 0 0); --ring: oklch(0.439 0 0); @@ -180,6 +196,7 @@ --color-primary-foreground: var(--primary-foreground); --color-secondary: var(--secondary); --color-secondary-foreground: var(--secondary-foreground); + --color-code-background: var(--color-background); --color-muted: var(--muted); --color-muted-foreground: var(--muted-foreground); --color-accent: var(--accent); @@ -207,6 +224,23 @@ --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); + --font-size-code: clamp(0.6rem, 0.55rem + 0.5vw, 0.75rem); + + --font-size-2xs: var(--text-2xs); + --font-size-xs: var(--text-xs); + --font-size-sm: var(--text-sm); + --font-size-base: var(--text-base); + --font-size-lg: var(--text-lg); + --font-size-xl: var(--text-xl); + --font-size-2xl: var(--text-2xl); + --font-size-3xl: var(--text-3xl); + --font-size-4xl: var(--text-4xl); + --font-size-5xl: var(--text-5xl); + --font-size-6xl: var(--text-6xl); + --font-size-7xl: var(--text-7xl); + --font-size-8xl: var(--text-8xl); + --font-size-9xl: var(--text-9xl); + /* Font stacks - Tailwind v4 maps these directly to font-* utilities */ --font-sans: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, @@ -227,6 +261,14 @@ @apply m-0 font-normal font-handwriting tracking-[0.01em] antialiased bg-background text-foreground; } + code, + kbd, + samp, + pre { + @apply font-mono; + font-size: var(--font-size-code); + } + /* Paper texture overlay for all non-home pages */ body:not(:has(.watercolor-bg))::before { content: "";