From d15ea3bea28c21ed6142f8e79e29aa9ac43fdd29 Mon Sep 17 00:00:00 2001 From: ladybluenotes Date: Sun, 11 Jan 2026 13:28:55 -0800 Subject: [PATCH 01/16] feat: add framework content handling and refactor tab components --- src/components/FrameworkCodeBlock.tsx | 80 ------------- src/components/Markdown.tsx | 87 +++++++------- src/components/Tabs.tsx | 2 +- src/utils/markdown/plugins/index.ts | 2 + .../plugins/transformTabsComponent.ts | 111 ------------------ src/utils/markdown/processor.ts | 2 + 6 files changed, 50 insertions(+), 234 deletions(-) delete mode 100644 src/components/FrameworkCodeBlock.tsx diff --git a/src/components/FrameworkCodeBlock.tsx b/src/components/FrameworkCodeBlock.tsx deleted file mode 100644 index d00e119ef..000000000 --- a/src/components/FrameworkCodeBlock.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import * as React from 'react' -import { useLocalCurrentFramework } from './FrameworkSelect' -import { useCurrentUserQuery } from '~/hooks/useCurrentUser' -import { useParams } from '@tanstack/react-router' -import { Tabs } from './Tabs' -import type { Framework } from '~/libraries/types' - -type CodeBlockMeta = { - title: string - code: string - language: string -} - -type FrameworkCodeBlockProps = { - id: string - codeBlocksByFramework: Record - availableFrameworks: string[] - /** Pre-rendered React children for each framework (from domToReact) */ - panelsByFramework: Record -} - -/** - * Renders code blocks for the currently selected framework. - * - If no blocks for framework: shows nothing - * - If 1 code block: shows just the code block (minimal style) - * - If multiple code blocks: shows as file tabs - * - If no code blocks but has content: shows the content directly - */ -export function FrameworkCodeBlock({ - id, - codeBlocksByFramework, - panelsByFramework, -}: FrameworkCodeBlockProps) { - const { framework: paramsFramework } = useParams({ strict: false }) - const localCurrentFramework = useLocalCurrentFramework() - const userQuery = useCurrentUserQuery() - const userFramework = userQuery.data?.lastUsedFramework - - const actualFramework = (paramsFramework || - userFramework || - localCurrentFramework.currentFramework || - 'react') as Framework - - const normalizedFramework = actualFramework.toLowerCase() - - // Find the framework's code blocks - const frameworkBlocks = codeBlocksByFramework[normalizedFramework] || [] - const frameworkPanel = panelsByFramework[normalizedFramework] - - // If no panel content at all for this framework, show nothing - if (!frameworkPanel) { - return null - } - - // If no code blocks, just render the content directly - if (frameworkBlocks.length === 0) { - return
{frameworkPanel}
- } - - // If 1 code block, render minimal style - if (frameworkBlocks.length === 1) { - return
{frameworkPanel}
- } - - // Multiple code blocks - show as file tabs - const tabs = frameworkBlocks.map((block, index) => ({ - slug: `file-${index}`, - name: block.title || 'Untitled', - })) - - const childrenArray = React.Children.toArray(frameworkPanel) - - return ( -
- - {childrenArray} - -
- ) -} diff --git a/src/components/Markdown.tsx b/src/components/Markdown.tsx index cb1afd20e..a89458591 100644 --- a/src/components/Markdown.tsx +++ b/src/components/Markdown.tsx @@ -16,7 +16,7 @@ import { CodeBlock } from './CodeBlock' import { PackageManagerTabs } from './PackageManagerTabs' import type { Framework } from '~/libraries/types' import { FileTabs } from './FileTabs' -import { FrameworkCodeBlock } from './FrameworkCodeBlock' +import { FrameworkContent } from './FrameworkContent' type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' @@ -175,47 +175,6 @@ const options: HTMLReactParserOptions = { } } - const frameworkMeta = domNode.attribs['data-framework-meta'] - if (frameworkMeta) { - try { - const { codeBlocksByFramework } = JSON.parse(frameworkMeta) - const availableFrameworks = JSON.parse( - domNode.attribs['data-available-frameworks'] || '[]', - ) - const id = - attributes.id || - `framework-${Math.random().toString(36).slice(2, 9)}` - - const panelElements = domNode.children?.filter( - (child): child is Element => - child instanceof Element && child.name === 'md-tab-panel', - ) - - // Build panelsByFramework map - const panelsByFramework: Record = {} - panelElements?.forEach((panel) => { - const fw = panel.attribs['data-framework'] - if (fw) { - panelsByFramework[fw] = domToReact( - panel.children as any, - options, - ) - } - }) - - return ( - - ) - } catch { - // Fall through to default tabs if parsing fails - } - } - const tabs = attributes.tabs const id = attributes.id || `tabs-${Math.random().toString(36).slice(2, 9)}` @@ -237,6 +196,50 @@ const options: HTMLReactParserOptions = { return } + case 'framework': { + const frameworkMeta = domNode.attribs['data-framework-meta'] + if (!frameworkMeta) { + return null + } + + try { + const { codeBlocksByFramework } = JSON.parse(frameworkMeta) + const availableFrameworks = JSON.parse( + domNode.attribs['data-available-frameworks'] || '[]', + ) + const id = + attributes.id || + `framework-${Math.random().toString(36).slice(2, 9)}` + + const panelElements = domNode.children?.filter( + (child): child is Element => + child instanceof Element && child.name === 'md-framework-panel', + ) + + // Build panelsByFramework map + const panelsByFramework: Record = {} + panelElements?.forEach((panel) => { + const fw = panel.attribs['data-framework'] + if (fw) { + panelsByFramework[fw] = domToReact( + panel.children as any, + options, + ) + } + }) + + return ( + + ) + } catch { + return null + } + } default: return
{domToReact(domNode.children as any, options)}
} diff --git a/src/components/Tabs.tsx b/src/components/Tabs.tsx index 7a2ff7c54..85270e0d0 100644 --- a/src/components/Tabs.tsx +++ b/src/components/Tabs.tsx @@ -75,7 +75,7 @@ export function Tabs({ key={`${id}-${tab.slug}`} data-tab={tab.slug} hidden={tab.slug !== activeSlug} - className="max-w-none flex flex-col gap-2 text-base" + className="max-w-none flex flex-col gap-2 text-base px-4" > {child} diff --git a/src/utils/markdown/plugins/index.ts b/src/utils/markdown/plugins/index.ts index ee8ef639f..07beb8f80 100644 --- a/src/utils/markdown/plugins/index.ts +++ b/src/utils/markdown/plugins/index.ts @@ -1,4 +1,6 @@ export { rehypeParseCommentComponents } from './parseCommentComponents' export { rehypeTransformCommentComponents } from './transformCommentComponents' +export { rehypeTransformFrameworkComponents } from './transformFrameworkComponents' export { transformTabsComponent } from './transformTabsComponent' +export { transformFrameworkComponent } from './transformFrameworkComponent' export { type MarkdownHeading, rehypeCollectHeadings } from './collectHeadings' diff --git a/src/utils/markdown/plugins/transformTabsComponent.ts b/src/utils/markdown/plugins/transformTabsComponent.ts index 6e996685e..f78716e3f 100644 --- a/src/utils/markdown/plugins/transformTabsComponent.ts +++ b/src/utils/markdown/plugins/transformTabsComponent.ts @@ -40,25 +40,6 @@ type FilesExtraction = { }> } -type FrameworkExtraction = { - codeBlocksByFramework: Record< - string, - Array<{ - title: string - code: string - language: string - preNode: HastNode - }> - > - contentByFramework: Record -} - -type FrameworkCodeBlock = { - title: string - code: string - language: string - preNode: HastNode -} function parseAttributes(node: HastNode): Record { const rawAttributes = node.properties?.['data-attributes'] @@ -227,58 +208,6 @@ function extractFilesData(node: HastNode): FilesExtraction | null { return { files } } -/** - * Extract framework-specific content for variant="framework" tabs. - * Groups all content (code blocks and general content) by framework headings. - */ -function extractFrameworkData(node: HastNode): FrameworkExtraction | null { - const children = node.children ?? [] - const codeBlocksByFramework: Record = {} - const contentByFramework: Record = {} - - let currentFramework: string | null = null - - for (const child of children) { - if (isHeading(child)) { - currentFramework = toString(child as any) - .trim() - .toLowerCase() - // Initialize arrays for this framework - if (currentFramework && !contentByFramework[currentFramework]) { - contentByFramework[currentFramework] = [] - codeBlocksByFramework[currentFramework] = [] - } - continue - } - - // Skip if no framework heading found yet - if (!currentFramework) continue - - // Add all content to contentByFramework - contentByFramework[currentFramework].push(child) - - // Look for
 elements (code blocks) under current framework
-    if (child.type === 'element' && child.tagName === 'pre') {
-      const codeBlockData = extractCodeBlockData(child)
-      if (!codeBlockData) continue
-
-      codeBlocksByFramework[currentFramework].push({
-        title: codeBlockData.title || 'Untitled',
-        code: codeBlockData.code,
-        language: codeBlockData.language,
-        preNode: child,
-      })
-    }
-  }
-
-  // Return null only if no frameworks found at all
-  if (Object.keys(contentByFramework).length === 0) {
-    return null
-  }
-
-  return { codeBlocksByFramework, contentByFramework }
-}
-
 function extractTabPanels(node: HastNode): TabExtraction | null {
   const children = node.children ?? []
   const headings = children.filter(isHeading)
@@ -407,46 +336,6 @@ export function transformTabsComponent(node: HastNode) {
     return
   }
 
-  if (variant === 'framework') {
-    const result = extractFrameworkData(node)
-
-    if (!result) {
-      return
-    }
-
-    node.properties = node.properties || {}
-    node.properties['data-framework-meta'] = JSON.stringify({
-      codeBlocksByFramework: Object.fromEntries(
-        Object.entries(result.codeBlocksByFramework).map(([fw, blocks]) => [
-          fw,
-          blocks.map((b) => ({
-            title: b.title,
-            code: b.code,
-            language: b.language,
-          })),
-        ]),
-      ),
-    })
-
-    // Store available frameworks for the component
-    const availableFrameworks = Object.keys(result.contentByFramework)
-    node.properties['data-available-frameworks'] =
-      JSON.stringify(availableFrameworks)
-
-    node.children = availableFrameworks.map((fw) => {
-      const content = result.contentByFramework[fw] || []
-      return {
-        type: 'element',
-        tagName: 'md-tab-panel',
-        properties: {
-          'data-framework': fw,
-        },
-        children: content,
-      }
-    })
-    return
-  }
-
   // Handle default tabs variant
   const result = extractTabPanels(node)
   if (!result) {
diff --git a/src/utils/markdown/processor.ts b/src/utils/markdown/processor.ts
index 295ca3080..1430f2d58 100644
--- a/src/utils/markdown/processor.ts
+++ b/src/utils/markdown/processor.ts
@@ -12,6 +12,7 @@ import {
   rehypeCollectHeadings,
   rehypeParseCommentComponents,
   rehypeTransformCommentComponents,
+  rehypeTransformFrameworkComponents,
 } from '~/utils/markdown/plugins'
 import { extractCodeMeta } from '~/utils/markdown/plugins/extractCodeMeta'
 
@@ -68,6 +69,7 @@ export function renderMarkdown(content: string): MarkdownRenderResult {
       },
     } as any)
     .use(rehypeSlug)
+    .use(rehypeTransformFrameworkComponents)
     .use(rehypeTransformCommentComponents)
     .use(rehypeAutolinkHeadings, {
       behavior: 'wrap',

From bf0ec023f67a71d8ca64da1a9f2e98a16251a54d Mon Sep 17 00:00:00 2001
From: ladybluenotes 
Date: Sun, 11 Jan 2026 14:07:30 -0800
Subject: [PATCH 02/16] refactor: update import paths for Markdown and
 CodeBlock components

---
 src/components/CodeBlock.tsx                  | 239 ---------------
 src/components/CodeExampleCard.tsx            |   2 +-
 src/components/CodeExplorer.tsx               |   2 +-
 src/components/FeedEntry.tsx                  |   2 +-
 src/components/FeedEntryTimeline.tsx          |   2 +-
 src/components/FileTabs.tsx                   |  58 ----
 src/components/Markdown.tsx                   | 287 ------------------
 src/components/MarkdownContent.tsx            | 105 +------
 src/components/MarkdownHeadingContext.tsx     |  30 --
 src/components/MarkdownLink.tsx               |  46 ---
 src/components/PackageManagerTabs.tsx         | 213 -------------
 src/components/SimpleMarkdown.tsx             |   2 +-
 src/components/Tabs.tsx                       | 127 --------
 src/components/admin/FeedEntryEditor.tsx      |   2 +-
 src/routes/_libraries/account/api-keys.tsx    |   2 +-
 src/routes/_libraries/blog.$.tsx              |   8 +-
 src/routes/_libraries/feed.$id.tsx            |   2 +-
 .../_libraries/table.$version.index.tsx       |   2 +-
 .../plugins/transformTabsComponent.ts         |   2 +-
 19 files changed, 15 insertions(+), 1118 deletions(-)
 delete mode 100644 src/components/CodeBlock.tsx
 delete mode 100644 src/components/FileTabs.tsx
 delete mode 100644 src/components/Markdown.tsx
 delete mode 100644 src/components/MarkdownHeadingContext.tsx
 delete mode 100644 src/components/MarkdownLink.tsx
 delete mode 100644 src/components/PackageManagerTabs.tsx
 delete mode 100644 src/components/Tabs.tsx

diff --git a/src/components/CodeBlock.tsx b/src/components/CodeBlock.tsx
deleted file mode 100644
index ca069dc55..000000000
--- a/src/components/CodeBlock.tsx
+++ /dev/null
@@ -1,239 +0,0 @@
-import * as React from 'react'
-import { twMerge } from 'tailwind-merge'
-import { useToast } from '~/components/ToastProvider'
-import { Copy } from 'lucide-react'
-import type { Mermaid } from 'mermaid'
-import { transformerNotationDiff } from '@shikijs/transformers'
-import { createHighlighter, type HighlighterGeneric } from 'shiki'
-import { Button } from './Button'
-
-// Language aliases mapping
-const LANG_ALIASES: Record = {
-  ts: 'typescript',
-  js: 'javascript',
-  sh: 'bash',
-  shell: 'bash',
-  console: 'bash',
-  zsh: 'bash',
-  md: 'markdown',
-  txt: 'plaintext',
-  text: 'plaintext',
-}
-
-// Lazy highlighter singleton
-let highlighterPromise: Promise> | null = null
-let mermaidInstance: Mermaid | null = null
-const genSvgMap = new Map()
-
-async function getHighlighter(language: string) {
-  if (!highlighterPromise) {
-    highlighterPromise = createHighlighter({
-      themes: ['github-light', 'vitesse-dark'],
-      langs: [
-        'typescript',
-        'javascript',
-        'tsx',
-        'jsx',
-        'bash',
-        'json',
-        'html',
-        'css',
-        'markdown',
-        'plaintext',
-      ],
-    })
-  }
-
-  const highlighter = await highlighterPromise
-  const normalizedLang = LANG_ALIASES[language] || language
-  const langToLoad = normalizedLang === 'mermaid' ? 'plaintext' : normalizedLang
-
-  // Load language if not already loaded
-  if (!highlighter.getLoadedLanguages().includes(langToLoad as any)) {
-    try {
-      await highlighter.loadLanguage(langToLoad as any)
-    } catch {
-      console.warn(`Shiki: Language "${langToLoad}" not found, using plaintext`)
-    }
-  }
-
-  return highlighter
-}
-
-// Lazy load mermaid only when needed
-async function getMermaid(): Promise {
-  if (!mermaidInstance) {
-    const { default: mermaid } = await import('mermaid')
-    mermaid.initialize({ startOnLoad: false, securityLevel: 'loose' })
-    mermaidInstance = mermaid
-  }
-  return mermaidInstance
-}
-
-function extractPreAttributes(html: string): {
-  class: string | null
-  style: string | null
-} {
-  const match = html.match(/]*)>/i)
-  if (!match) {
-    return { class: null, style: null }
-  }
-
-  const attributes = match[1]
-
-  const classMatch = attributes.match(/\bclass\s*=\s*["']([^"']*)["']/i)
-  const styleMatch = attributes.match(/\bstyle\s*=\s*["']([^"']*)["']/i)
-
-  return {
-    class: classMatch ? classMatch[1] : null,
-    style: styleMatch ? styleMatch[1] : null,
-  }
-}
-
-export function CodeBlock({
-  isEmbedded,
-  showTypeCopyButton = true,
-  ...props
-}: React.HTMLProps & {
-  isEmbedded?: boolean
-  showTypeCopyButton?: boolean
-}) {
-  // Extract title from data-code-title attribute, handling both camelCase and kebab-case
-  const rawTitle = ((props as any)?.dataCodeTitle ||
-    (props as any)?.['data-code-title']) as string | undefined
-
-  // Filter out "undefined" strings, null, and empty strings
-  const title =
-    rawTitle && rawTitle !== 'undefined' && rawTitle.trim().length > 0
-      ? rawTitle.trim()
-      : undefined
-
-  const childElement = props.children as
-    | undefined
-    | { props?: { className?: string; children?: string } }
-  let lang = childElement?.props?.className?.replace('language-', '')
-
-  if (lang === 'diff') {
-    lang = 'plaintext'
-  }
-
-  const children = props.children as
-    | undefined
-    | {
-        props: {
-          children: string
-        }
-      }
-
-  const [copied, setCopied] = React.useState(false)
-  const ref = React.useRef(null)
-  const { notify } = useToast()
-
-  const code = children?.props.children
-
-  const [codeElement, setCodeElement] = React.useState(
-    
-      {lang === 'mermaid' ?  : code}
-    
, - ) - - React[ - typeof document !== 'undefined' ? 'useLayoutEffect' : 'useEffect' - ](() => { - ;(async () => { - const themes = ['github-light', 'vitesse-dark'] - const langStr = lang || 'plaintext' - const normalizedLang = LANG_ALIASES[langStr] || langStr - const effectiveLang = - normalizedLang === 'mermaid' ? 'plaintext' : normalizedLang - - const highlighter = await getHighlighter(langStr) - - const htmls = await Promise.all( - themes.map(async (theme) => { - const output = highlighter.codeToHtml(code || '', { - lang: effectiveLang, - theme, - transformers: [transformerNotationDiff()], - }) - - if (lang === 'mermaid') { - const preAttributes = extractPreAttributes(output) - let svgHtml = genSvgMap.get(code || '') - if (!svgHtml) { - const mermaid = await getMermaid() - const { svg } = await mermaid.render('foo', code || '') - genSvgMap.set(code || '', svg) - svgHtml = svg - } - return `
${svgHtml}
` - } - - return output - }), - ) - - setCodeElement( -
pre]:h-full [&>pre]:rounded-none' : '', - )} - dangerouslySetInnerHTML={{ __html: htmls.join('') }} - ref={ref} - />, - ) - })() - }, [code, lang]) - - return ( -
- {(title || showTypeCopyButton) && ( -
-
- {title || (lang?.toLowerCase() === 'bash' ? 'sh' : (lang ?? ''))} -
- - -
- )} - {codeElement} -
- ) -} diff --git a/src/components/CodeExampleCard.tsx b/src/components/CodeExampleCard.tsx index ae5e251f5..4d7320283 100644 --- a/src/components/CodeExampleCard.tsx +++ b/src/components/CodeExampleCard.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' import { Card } from '~/components/Card' -import { CodeBlock } from '~/components/CodeBlock' +import { CodeBlock } from '~/components/markdown/CodeBlock' import { FrameworkIconTabs } from '~/components/FrameworkIconTabs' import type { Framework } from '~/libraries' diff --git a/src/components/CodeExplorer.tsx b/src/components/CodeExplorer.tsx index 8c25781b3..775707fd5 100644 --- a/src/components/CodeExplorer.tsx +++ b/src/components/CodeExplorer.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { CodeBlock } from '~/components/CodeBlock' +import { CodeBlock } from '~/components/markdown/CodeBlock' import { FileExplorer } from './FileExplorer' import { InteractiveSandbox } from './InteractiveSandbox' import { CodeExplorerTopBar } from './CodeExplorerTopBar' diff --git a/src/components/FeedEntry.tsx b/src/components/FeedEntry.tsx index 382bf8aea..3e2b999a5 100644 --- a/src/components/FeedEntry.tsx +++ b/src/components/FeedEntry.tsx @@ -1,5 +1,5 @@ import { format, formatDistanceToNow } from '~/utils/dates' -import { Markdown } from '~/components/Markdown' +import { Markdown } from '~/components/markdown/Markdown' import { libraries } from '~/libraries' import { partners } from '~/utils/partners' import { twMerge } from 'tailwind-merge' diff --git a/src/components/FeedEntryTimeline.tsx b/src/components/FeedEntryTimeline.tsx index cff0f1652..e142e40b5 100644 --- a/src/components/FeedEntryTimeline.tsx +++ b/src/components/FeedEntryTimeline.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { format, formatDistanceToNow } from '~/utils/dates' -import { Markdown } from '~/components/Markdown' +import { Markdown } from '~/components/markdown/Markdown' import { Card } from '~/components/Card' import { libraries } from '~/libraries' import { partners } from '~/utils/partners' diff --git a/src/components/FileTabs.tsx b/src/components/FileTabs.tsx deleted file mode 100644 index a325e8a7f..000000000 --- a/src/components/FileTabs.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import * as React from 'react' - -export type FileTabDefinition = { - slug: string - name: string -} - -export type FileTabsProps = { - tabs: Array - children: Array | React.ReactNode - id: string -} - -export function FileTabs({ tabs, id, children }: FileTabsProps) { - const childrenArray = React.Children.toArray(children) - const [activeSlug, setActiveSlug] = React.useState(tabs[0]?.slug ?? '') - - if (tabs.length === 0) return null - - return ( -
-
- {tabs.map((tab) => ( - - ))} -
-
- {childrenArray.map((child, index) => { - const tab = tabs[index] - if (!tab) return null - return ( - - ) - })} -
-
- ) -} diff --git a/src/components/Markdown.tsx b/src/components/Markdown.tsx deleted file mode 100644 index a89458591..000000000 --- a/src/components/Markdown.tsx +++ /dev/null @@ -1,287 +0,0 @@ -import * as React from 'react' -import { MarkdownLink } from '~/components/MarkdownLink' -import type { HTMLProps } from 'react' - -import parse, { - attributesToProps, - domToReact, - Element, - HTMLReactParserOptions, -} from 'html-react-parser' - -import { renderMarkdown } from '~/utils/markdown' -import { getNetlifyImageUrl } from '~/utils/netlifyImage' -import { Tabs } from '~/components/Tabs' -import { CodeBlock } from './CodeBlock' -import { PackageManagerTabs } from './PackageManagerTabs' -import type { Framework } from '~/libraries/types' -import { FileTabs } from './FileTabs' -import { FrameworkContent } from './FrameworkContent' - -type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' - -const CustomHeading = ({ - Comp, - id, - children, - ...props -}: HTMLProps & { - Comp: HeadingLevel -}) => { - // Convert children to array and strip any inner anchor (native 'a' or MarkdownLink) - const childrenArray = React.Children.toArray(children) - const sanitizedChildren = childrenArray.map((child) => { - if ( - React.isValidElement(child) && - (child.type === 'a' || child.type === MarkdownLink) - ) { - // replace anchor child with its own children so outer anchor remains the only link - return (child.props as { children?: React.ReactNode }).children ?? null - } - return child - }) - - const heading = ( - - {sanitizedChildren} - - ) - - if (id) { - return ( - - {heading} - - ) - } - - return heading -} - -const makeHeading = - (type: HeadingLevel) => (props: HTMLProps) => ( - - ) - -const markdownComponents: Record = { - a: MarkdownLink, - pre: CodeBlock, - h1: makeHeading('h1'), - h2: makeHeading('h2'), - h3: makeHeading('h3'), - h4: makeHeading('h4'), - h5: makeHeading('h5'), - h6: makeHeading('h6'), - code: function Code({ className, ...rest }: HTMLProps) { - return ( - - ) - }, - iframe: (props) => ( -