Skip to content
Merged
2 changes: 1 addition & 1 deletion src/components/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export function CodeBlock({
style={props.style}
>
{(title || showTypeCopyButton) && (
<div className="flex items-center justify-between px-4 py-2 border-b border-gray-500/20 bg-gray-50 dark:bg-gray-900">
<div className="flex items-center justify-between px-4 py-2 bg-gray-50 dark:bg-gray-900">
<div className="text-xs text-gray-700 dark:text-gray-300">
{title || (lang?.toLowerCase() === 'bash' ? 'sh' : (lang ?? ''))}
</div>
Expand Down
23 changes: 16 additions & 7 deletions src/components/FrameworkCodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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,
Expand All @@ -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 <div className="framework-code-block">{frameworkPanel}</div>
}

// If 1 code block, render minimal style
if (frameworkBlocks.length === 1) {
return <div className="framework-code-block">{frameworkPanel}</div>
}

// Multiple code blocks - show as file tabs
const tabs = frameworkBlocks.map((block, index) => ({
slug: `file-${index}`,
name: block.title || 'Untitled',
Expand All @@ -63,9 +72,9 @@ export function FrameworkCodeBlock({

return (
<div className="framework-code-block">
<FileTabs id={`${id}-${normalizedFramework}`} tabs={tabs}>
<Tabs id={`${id}-${normalizedFramework}`} tabs={tabs} variant="files">
{childrenArray}
</FileTabs>
</Tabs>
</div>
)
}
101 changes: 85 additions & 16 deletions src/components/PackageManagerTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,7 +32,7 @@ const usePackageManagerStore = create<{

type PackageManagerTabsProps = {
id: string
packagesByFramework: Record<string, string[]>
packagesByFramework: Record<string, string[][]>
mode: InstallMode
frameworks: Framework[]
}
Expand All @@ -36,45 +41,109 @@ 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
}
}
return commands
}

// 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
}
}
Expand All @@ -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
}

Expand All @@ -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 (
<CodeBlock key={pm}>
Expand Down
19 changes: 13 additions & 6 deletions src/components/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ export function Tabs({
if (tabsProp.length === 0) return null

return (
<div className="not-prose my-4">
<div className="flex items-center justify-start gap-2 rounded-t-md border-1 border-b-none border-gray-200 dark:border-gray-800 overflow-x-auto overflow-y-hidden bg-white dark:bg-gray-950">
<div className="my-4">
<div className="not-prose flex items-center justify-start gap-2 rounded-t-md border-1 border-b-none border-gray-200 dark:border-gray-800 overflow-x-auto overflow-y-hidden bg-white dark:bg-gray-950">
{tabsProp.map((tab) => {
return (
<Tab
Expand All @@ -64,7 +64,9 @@ export function Tabs({
)
})}
</div>
<div className="border border-gray-500/20 rounded-b-md p-4 bg-gray-100 dark:bg-gray-900">
<div
className={`border border-gray-500/20 rounded-b-md bg-gray-100 dark:bg-gray-900`}
>
{childrenArray.map((child, index) => {
const tab = tabsProp[index]
if (!tab) return null
Expand All @@ -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}
</div>
Expand All @@ -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 (
Expand Down
24 changes: 9 additions & 15 deletions src/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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
Expand All @@ -905,25 +909,15 @@ 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 */
.package-manager-tabs [data-tab] .codeblock {
@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;
}
Loading
Loading