diff --git a/src/components/CodeBlock.tsx b/src/components/CodeBlock.tsx index 19a463fdf..ca069dc55 100644 --- a/src/components/CodeBlock.tsx +++ b/src/components/CodeBlock.tsx @@ -194,7 +194,7 @@ export function CodeBlock({ style={props.style} > {(title || showTypeCopyButton) && ( -
+
{title || (lang?.toLowerCase() === 'bash' ? 'sh' : (lang ?? ''))}
diff --git a/src/components/FrameworkCodeBlock.tsx b/src/components/FrameworkCodeBlock.tsx index 8d065baef..d00e119ef 100644 --- a/src/components/FrameworkCodeBlock.tsx +++ b/src/components/FrameworkCodeBlock.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { useLocalCurrentFramework } from './FrameworkSelect' import { useCurrentUserQuery } from '~/hooks/useCurrentUser' import { useParams } from '@tanstack/react-router' -import { FileTabs } from './FileTabs' +import { Tabs } from './Tabs' import type { Framework } from '~/libraries/types' type CodeBlockMeta = { @@ -22,8 +22,9 @@ type FrameworkCodeBlockProps = { /** * Renders code blocks for the currently selected framework. * - If no blocks for framework: shows nothing - * - If 1 block: shows just the code block (minimal style) - * - If multiple blocks: shows as FileTabs (file tabs with names) + * - 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, @@ -43,17 +44,25 @@ export function FrameworkCodeBlock({ const normalizedFramework = actualFramework.toLowerCase() // Find the framework's code blocks - const frameworkBlocks = codeBlocksByFramework[normalizedFramework] + const frameworkBlocks = codeBlocksByFramework[normalizedFramework] || [] const frameworkPanel = panelsByFramework[normalizedFramework] - if (!frameworkBlocks || frameworkBlocks.length === 0 || !frameworkPanel) { + // 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', @@ -63,9 +72,9 @@ export function FrameworkCodeBlock({ return (
- + {childrenArray} - +
) } diff --git a/src/components/PackageManagerTabs.tsx b/src/components/PackageManagerTabs.tsx index 61f2b1eb1..c6b75600a 100644 --- a/src/components/PackageManagerTabs.tsx +++ b/src/components/PackageManagerTabs.tsx @@ -7,7 +7,12 @@ import { CodeBlock } from './CodeBlock' import type { Framework } from '~/libraries/types' type PackageManager = 'bun' | 'npm' | 'pnpm' | 'yarn' -type InstallMode = 'install' | 'dev-install' +type InstallMode = + | 'install' + | 'dev-install' + | 'local-install' + | 'create' + | 'custom' // Use zustand for cross-component synchronization // This ensures all PackageManagerTabs instances on the page stay in sync @@ -27,7 +32,7 @@ const usePackageManagerStore = create<{ type PackageManagerTabsProps = { id: string - packagesByFramework: Record + packagesByFramework: Record mode: InstallMode frameworks: Framework[] } @@ -36,25 +41,88 @@ const PACKAGE_MANAGERS: PackageManager[] = ['npm', 'pnpm', 'yarn', 'bun'] function getInstallCommand( packageManager: PackageManager, - packages: string[], + packageGroups: string[][], mode: InstallMode, ): string[] { const commands: string[] = [] + if (mode === 'custom') { + for (const packages of packageGroups) { + const pkgStr = packages.join(' ') + switch (packageManager) { + case 'npm': + commands.push(`npm ${pkgStr}`) + break + case 'pnpm': + commands.push(`pnpm ${pkgStr}`) + break + case 'yarn': + commands.push(`yarn ${pkgStr}`) + break + case 'bun': + commands.push(`bun ${pkgStr}`) + break + } + } + } + + if (mode === 'create') { + for (const packages of packageGroups) { + const pkgStr = packages.join(' ') + switch (packageManager) { + case 'npm': + commands.push(`npm create ${pkgStr}`) + break + case 'pnpm': + commands.push(`pnpm create ${pkgStr}`) + break + case 'yarn': + commands.push(`yarn create ${pkgStr}`) + break + case 'bun': + commands.push(`bun create ${pkgStr}`) + break + } + } + } + + if (mode === 'local-install') { + // Each group becomes one command line + for (const packages of packageGroups) { + const pkgStr = packages.join(' ') + switch (packageManager) { + case 'npm': + commands.push(`npx ${pkgStr}`) + break + case 'pnpm': + commands.push(`pnpx ${pkgStr}`) + break + case 'yarn': + commands.push(`yarn dlx ${pkgStr}`) + break + case 'bun': + commands.push(`bunx ${pkgStr}`) + break + } + } + return commands + } + if (mode === 'dev-install') { - for (const pkg of packages) { + for (const packages of packageGroups) { + const pkgStr = packages.join(' ') switch (packageManager) { case 'npm': - commands.push(`npm i -D ${pkg}`) + commands.push(`npm i -D ${pkgStr}`) break case 'pnpm': - commands.push(`pnpm add -D ${pkg}`) + commands.push(`pnpm add -D ${pkgStr}`) break case 'yarn': - commands.push(`yarn add -D ${pkg}`) + commands.push(`yarn add -D ${pkgStr}`) break case 'bun': - commands.push(`bun add -d ${pkg}`) + commands.push(`bun add -d ${pkgStr}`) break } } @@ -62,19 +130,20 @@ function getInstallCommand( } // install mode - for (const pkg of packages) { + for (const packages of packageGroups) { + const pkgStr = packages.join(' ') switch (packageManager) { case 'npm': - commands.push(`npm i ${pkg}`) + commands.push(`npm i ${pkgStr}`) break case 'pnpm': - commands.push(`pnpm add ${pkg}`) + commands.push(`pnpm add ${pkgStr}`) break case 'yarn': - commands.push(`yarn add ${pkg}`) + commands.push(`yarn add ${pkgStr}`) break case 'bun': - commands.push(`bun add ${pkg}`) + commands.push(`bun add ${pkgStr}`) break } } @@ -100,10 +169,10 @@ export function PackageManagerTabs({ 'react') as Framework const normalizedFramework = actualFramework.toLowerCase() - const packages = packagesByFramework[normalizedFramework] + const packageGroups = packagesByFramework[normalizedFramework] // Hide component if current framework not in package list - if (!packages || packages.length === 0) { + if (!packageGroups || packageGroups.length === 0) { return null } @@ -121,7 +190,7 @@ export function PackageManagerTabs({ // Generate children (command blocks) for each package manager const children = PACKAGE_MANAGERS.map((pm) => { - const commands = getInstallCommand(pm, packages, mode) + const commands = getInstallCommand(pm, packageGroups, mode) const commandText = commands.join('\n') return ( diff --git a/src/components/Tabs.tsx b/src/components/Tabs.tsx index 586bc3e31..7a2ff7c54 100644 --- a/src/components/Tabs.tsx +++ b/src/components/Tabs.tsx @@ -50,8 +50,8 @@ export function Tabs({ if (tabsProp.length === 0) return null return ( -
-
+
+
{tabsProp.map((tab) => { return ( -
+
{childrenArray.map((child, index) => { const tab = tabsProp[index] if (!tab) return null @@ -73,7 +75,7 @@ export function Tabs({ key={`${id}-${tab.slug}`} data-tab={tab.slug} hidden={tab.slug !== activeSlug} - className="prose dark:prose-invert max-w-none flex flex-col gap-2 text-base" + className="max-w-none flex flex-col gap-2 text-base" > {child}
@@ -96,8 +98,13 @@ const Tab = React.memo( setActiveSlug: (slug: string) => void }) => { const option = React.useMemo( - () => frameworkOptions.find((o) => o.value === tab.slug), - [tab.slug], + () => + frameworkOptions.find( + (o) => + o.value === tab.slug.toLowerCase() || + o.label.toLowerCase() === tab.name.toLowerCase(), + ), + [tab.slug, tab.name], ) return ( diff --git a/src/styles/app.css b/src/styles/app.css index c2e3f92d8..cce053ce0 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -878,7 +878,7 @@ mark { /* Hide the title bar but keep copy button accessible */ .file-tabs-panel .codeblock > div:first-child { - @apply absolute right-2 top-2 bg-transparent border-0 p-0 z-10; + @apply absolute right-2 top-2 bg-transparent border-none p-0 z-10; } /* Hide the title text, keep only the button */ @@ -892,7 +892,11 @@ mark { } .package-manager-tabs [data-tab] .codeblock > div:first-child { - @apply absolute right-2 top-2 bg-transparent border-0 p-0 z-10; + @apply absolute right-2 top-2 bg-transparent border-none p-0 z-10; +} + +.package-manager-tabs .codeblock { + border: none; } .package-manager-tabs @@ -905,7 +909,7 @@ mark { /* Remove padding from tab content wrapper for package manager */ .package-manager-tabs .not-prose > div:last-child { - @apply p-0 border-0 rounded-b-none bg-transparent; + @apply p-0 border-none rounded-b-none bg-transparent; } /* Restore bottom border radius on the code block itself */ @@ -913,17 +917,7 @@ mark { @apply rounded-b-md; } -/* Framework code blocks - minimal style for single code blocks */ +/* Framework code blocks - single blocks look like regular code blocks */ .framework-code-block > .codeblock { - @apply my-4 rounded-md; -} - -/* Hide the title bar but keep copy button accessible for single blocks */ -.framework-code-block > .codeblock > div:first-child { - @apply absolute right-2 top-2 bg-transparent border-0 p-0 z-10; -} - -/* Hide the title text, keep only the button */ -.framework-code-block > .codeblock > div:first-child > div:first-child { - @apply hidden; + @apply my-4; } diff --git a/src/utils/markdown/plugins/transformTabsComponent.ts b/src/utils/markdown/plugins/transformTabsComponent.ts index ea9a9504e..b07ff1643 100644 --- a/src/utils/markdown/plugins/transformTabsComponent.ts +++ b/src/utils/markdown/plugins/transformTabsComponent.ts @@ -27,7 +27,7 @@ type TabExtraction = { } type PackageManagerExtraction = { - packagesByFramework: Record + packagesByFramework: Record mode: InstallMode } @@ -50,6 +50,7 @@ type FrameworkExtraction = { preNode: HastNode }> > + contentByFramework: Record } type FrameworkCodeBlock = { @@ -73,7 +74,9 @@ function parseAttributes(node: HastNode): Record { function resolveMode(attributes: Record): InstallMode { const mode = attributes.mode?.toLowerCase() - return mode === 'dev-install' ? 'dev-install' : 'install' + if (mode === 'dev-install') return 'dev-install' + if (mode === 'local-install') return 'local-install' + return 'install' } function normalizeFrameworkKey(key: string): string { @@ -122,7 +125,7 @@ function extractPackageManagerData( mode: InstallMode, ): PackageManagerExtraction | null { const children = node.children ?? [] - const packagesByFramework: Record = {} + const packagesByFramework: Record = {} const allText = extractText(children) const lines = allText.split('\n') @@ -133,11 +136,13 @@ function extractPackageManagerData( const parsed = parseFrameworkLine(trimmed) if (parsed) { - // Support multiple entries for same framework by concatenating packages + // Each line becomes a separate entry (array of packages) + // Multiple packages on same line = install together + // Multiple lines = install separately if (packagesByFramework[parsed.framework]) { - packagesByFramework[parsed.framework].push(...parsed.packages) + packagesByFramework[parsed.framework].push(parsed.packages) } else { - packagesByFramework[parsed.framework] = parsed.packages + packagesByFramework[parsed.framework] = [parsed.packages] } } } @@ -223,12 +228,13 @@ function extractFilesData(node: HastNode): FilesExtraction | null { } /** - * Extract framework-specific code blocks for variant="framework" tabs. - * Groups code blocks by their data-framework attribute. + * 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 @@ -237,19 +243,25 @@ function extractFrameworkData(node: HastNode): FrameworkExtraction | null { 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
-    const c = child as HastNode
-    if (c.type === 'element' && c.tagName === 'pre' && currentFramework) {
-      const codeBlockData = extractCodeBlockData(c)
+    if (child.type === 'element' && child.tagName === 'pre') {
+      const codeBlockData = extractCodeBlockData(child)
       if (!codeBlockData) continue
 
-      if (!codeBlocksByFramework[currentFramework]) {
-        codeBlocksByFramework[currentFramework] = []
-      }
-
       codeBlocksByFramework[currentFramework].push({
         title: codeBlockData.title || 'Untitled',
         code: codeBlockData.code,
@@ -259,11 +271,12 @@ function extractFrameworkData(node: HastNode): FrameworkExtraction | null {
     }
   }
 
-  if (Object.keys(codeBlocksByFramework).length === 0) {
+  // Return null only if no frameworks found at all
+  if (Object.keys(contentByFramework).length === 0) {
     return null
   }
 
-  return { codeBlocksByFramework }
+  return { codeBlocksByFramework, contentByFramework }
 }
 
 function extractTabPanels(node: HastNode): TabExtraction | null {
@@ -416,19 +429,19 @@ export function transformTabsComponent(node: HastNode) {
     })
 
     // Store available frameworks for the component
-    const availableFrameworks = Object.keys(result.codeBlocksByFramework)
+    const availableFrameworks = Object.keys(result.contentByFramework)
     node.properties['data-available-frameworks'] =
       JSON.stringify(availableFrameworks)
 
     node.children = availableFrameworks.map((fw) => {
-      const blocks = result.codeBlocksByFramework[fw]
+      const content = result.contentByFramework[fw] || []
       return {
         type: 'element',
         tagName: 'md-tab-panel',
         properties: {
           'data-framework': fw,
         },
-        children: blocks.map((block) => block.preNode),
+        children: content,
       }
     })
     return