Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 73 additions & 32 deletions src/components/PackageManagerTabs.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,33 @@
import * as React from 'react'
import { useCurrentFramework } from './FrameworkSelect'
import { useLocalStorage } from '~/utils/useLocalStorage'
import { useLocalCurrentFramework } from './FrameworkSelect'
import { useCurrentUserQuery } from '~/hooks/useCurrentUser'
import { useParams } from '@tanstack/react-router'
import { create } from 'zustand'
import { Tabs, type TabDefinition } from './Tabs'
import { CodeBlock } from './CodeBlock'
import type { Framework } from '~/libraries/types'

type PackageManager = 'bun' | 'npm' | 'pnpm' | 'yarn'
type InstallMode = 'install' | 'dev-install'

// Use zustand for cross-component synchronization
// This ensures all PackageManagerTabs instances on the page stay in sync
const usePackageManagerStore = create<{
packageManager: PackageManager
setPackageManager: (pm: PackageManager) => void
}>((set) => ({
packageManager:
typeof document !== 'undefined'
? (localStorage.getItem('packageManager') as PackageManager) || 'npm'
: 'npm',
setPackageManager: (pm: PackageManager) => {
localStorage.setItem('packageManager', pm)
set({ packageManager: pm })
},
}))

type PackageManagerTabsProps = {
id: string
packagesByFramework: Record<string, string>
packagesByFramework: Record<string, string[]>
mode: InstallMode
frameworks: Framework[]
}
Expand All @@ -19,51 +36,74 @@ const PACKAGE_MANAGERS: PackageManager[] = ['npm', 'pnpm', 'yarn', 'bun']

function getInstallCommand(
packageManager: PackageManager,
packages: string,
packages: string[],
mode: InstallMode,
): string {
): string[] {
const commands: string[] = []

if (mode === 'dev-install') {
for (const pkg of packages) {
switch (packageManager) {
case 'npm':
commands.push(`npm i -D ${pkg}`)
break
case 'pnpm':
commands.push(`pnpm add -D ${pkg}`)
break
case 'yarn':
commands.push(`yarn add -D ${pkg}`)
break
case 'bun':
commands.push(`bun add -d ${pkg}`)
break
}
}
return commands
}

// install mode
for (const pkg of packages) {
switch (packageManager) {
case 'npm':
return `npm i -D ${packages}`
commands.push(`npm i ${pkg}`)
break
case 'pnpm':
return `pnpm add -D ${packages}`
commands.push(`pnpm add ${pkg}`)
break
case 'yarn':
return `yarn add -D ${packages}`
commands.push(`yarn add ${pkg}`)
break
case 'bun':
return `bun add -d ${packages}`
commands.push(`bun add ${pkg}`)
break
}
}

// install mode
switch (packageManager) {
case 'npm':
return `npm i ${packages}`
case 'pnpm':
return `pnpm add ${packages}`
case 'yarn':
return `yarn add ${packages}`
case 'bun':
return `bun add ${packages}`
}
return commands
}

export function PackageManagerTabs({
id,
packagesByFramework,
mode,
frameworks,
}: PackageManagerTabsProps) {
const { framework: currentFramework } = useCurrentFramework(frameworks)
const [storedPackageManager, setStoredPackageManager] =
useLocalStorage<PackageManager>('packageManager', PACKAGE_MANAGERS[0])
const { packageManager: storedPackageManager, setPackageManager } =
usePackageManagerStore()

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

// Normalize framework key to lowercase for lookup
const normalizedFramework = currentFramework.toLowerCase()
const normalizedFramework = actualFramework.toLowerCase()
const packages = packagesByFramework[normalizedFramework]

// Hide component if current framework not in package list
if (!packages) {
if (!packages || packages.length === 0) {
return null
}

Expand All @@ -81,10 +121,11 @@ export function PackageManagerTabs({

// Generate children (command blocks) for each package manager
const children = PACKAGE_MANAGERS.map((pm) => {
const command = getInstallCommand(pm, packages, mode)
const commands = getInstallCommand(pm, packages, mode)
const commandText = commands.join('\n')
return (
<CodeBlock key={pm}>
<code className="language-bash">{command}</code>
<code className="language-bash">{commandText}</code>
</CodeBlock>
)
})
Expand All @@ -95,7 +136,7 @@ export function PackageManagerTabs({
tabs={tabs}
children={children}
activeSlug={selectedPackageManager}
onTabChange={(slug) => setStoredPackageManager(slug as PackageManager)}
onTabChange={(slug) => setPackageManager(slug as PackageManager)}
/>
)
}
18 changes: 12 additions & 6 deletions src/utils/markdown/plugins/transformTabsComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type TabExtraction = {
}

type PackageManagerExtraction = {
packagesByFramework: Record<string, string>
packagesByFramework: Record<string, string[]>
mode: InstallMode
}

Expand Down Expand Up @@ -53,17 +53,18 @@ function normalizeFrameworkKey(key: string): string {
*/
function parseFrameworkLine(text: string): {
framework: string
packages: string
packages: string[]
} | null {
const colonIndex = text.indexOf(':')
if (colonIndex === -1) {
return null
}

const framework = normalizeFrameworkKey(text.slice(0, colonIndex))
const packages = text.slice(colonIndex + 1).trim()
const packagesStr = text.slice(colonIndex + 1).trim()
const packages = packagesStr.split(/\s+/).filter(Boolean)

if (!framework || !packages) {
if (!framework || packages.length === 0) {
return null
}

Expand All @@ -75,7 +76,7 @@ function extractPackageManagerData(
mode: InstallMode,
): PackageManagerExtraction | null {
const children = node.children ?? []
const packagesByFramework: Record<string, string> = {}
const packagesByFramework: Record<string, string[]> = {}

// Recursively extract text from all children (including nested in <p> tags)
function extractText(nodes: any[]): string {
Expand All @@ -99,7 +100,12 @@ function extractPackageManagerData(

const parsed = parseFrameworkLine(trimmed)
if (parsed) {
packagesByFramework[parsed.framework] = parsed.packages
// Support multiple entries for same framework by concatenating packages
if (packagesByFramework[parsed.framework]) {
packagesByFramework[parsed.framework].push(...parsed.packages)
} else {
packagesByFramework[parsed.framework] = parsed.packages
}
}
}

Expand Down
Loading