Skip to content

Commit 5815ff6

Browse files
committed
2 parents e75caff + 45a5114 commit 5815ff6

File tree

4 files changed

+292
-22
lines changed

4 files changed

+292
-22
lines changed

src/components/Markdown.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { renderMarkdown } from '~/utils/markdown'
1313
import { getNetlifyImageUrl } from '~/utils/netlifyImage'
1414
import { Tabs } from '~/components/Tabs'
1515
import { CodeBlock } from './CodeBlock'
16+
import { PackageManagerTabs } from './PackageManagerTabs'
17+
import type { Framework } from '~/libraries/types'
1618

1719
type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
1820

@@ -112,6 +114,7 @@ const options: HTMLReactParserOptions = {
112114
if (domNode.name === 'md-comment-component') {
113115
const componentName = domNode.attribs['data-component']
114116
const rawAttributes = domNode.attribs['data-attributes']
117+
const pmMeta = domNode.attribs['data-package-manager-meta']
115118
const attributes: Record<string, any> = {}
116119
try {
117120
Object.assign(attributes, JSON.parse(rawAttributes))
@@ -121,9 +124,39 @@ const options: HTMLReactParserOptions = {
121124

122125
switch (componentName?.toLowerCase()) {
123126
case 'tabs': {
127+
// Check if this is a package-manager tabs (has metadata)
128+
if (pmMeta) {
129+
try {
130+
const { packagesByFramework, mode } = JSON.parse(pmMeta)
131+
const id =
132+
attributes.id ||
133+
`package-manager-tabs-${Math.random().toString(36).slice(2, 9)}`
134+
const frameworks = Object.keys(
135+
packagesByFramework,
136+
) as Framework[]
137+
138+
return (
139+
<PackageManagerTabs
140+
id={id}
141+
packagesByFramework={packagesByFramework}
142+
mode={mode}
143+
frameworks={frameworks}
144+
/>
145+
)
146+
} catch {
147+
// Fall through to default tabs if parsing fails
148+
}
149+
}
150+
151+
// Default tabs variant
124152
const tabs = attributes.tabs
125153
const id =
126154
attributes.id || `tabs-${Math.random().toString(36).slice(2, 9)}`
155+
156+
if (!tabs || !Array.isArray(tabs)) {
157+
return null
158+
}
159+
127160
const panelElements = domNode.children?.filter(
128161
(child): child is Element =>
129162
child instanceof Element && child.name === 'md-tab-panel',
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import * as React from 'react'
2+
import { useCurrentFramework } from './FrameworkSelect'
3+
import { useLocalStorage } from '~/utils/useLocalStorage'
4+
import { Tabs, type TabDefinition } from './Tabs'
5+
import { CodeBlock } from './CodeBlock'
6+
import type { Framework } from '~/libraries/types'
7+
8+
type PackageManager = 'bun' | 'npm' | 'pnpm' | 'yarn'
9+
type InstallMode = 'install' | 'dev-install'
10+
11+
type PackageManagerTabsProps = {
12+
id: string
13+
packagesByFramework: Record<string, string>
14+
mode: InstallMode
15+
frameworks: Framework[]
16+
}
17+
18+
const PACKAGE_MANAGERS: PackageManager[] = ['npm', 'pnpm', 'yarn', 'bun']
19+
20+
function getInstallCommand(
21+
packageManager: PackageManager,
22+
packages: string,
23+
mode: InstallMode,
24+
): string {
25+
if (mode === 'dev-install') {
26+
switch (packageManager) {
27+
case 'npm':
28+
return `npm i -D ${packages}`
29+
case 'pnpm':
30+
return `pnpm add -D ${packages}`
31+
case 'yarn':
32+
return `yarn add -D ${packages}`
33+
case 'bun':
34+
return `bun add -d ${packages}`
35+
}
36+
}
37+
38+
// install mode
39+
switch (packageManager) {
40+
case 'npm':
41+
return `npm i ${packages}`
42+
case 'pnpm':
43+
return `pnpm add ${packages}`
44+
case 'yarn':
45+
return `yarn add ${packages}`
46+
case 'bun':
47+
return `bun add ${packages}`
48+
}
49+
}
50+
51+
export function PackageManagerTabs({
52+
id,
53+
packagesByFramework,
54+
mode,
55+
frameworks,
56+
}: PackageManagerTabsProps) {
57+
const { framework: currentFramework } = useCurrentFramework(frameworks)
58+
const [storedPackageManager, setStoredPackageManager] =
59+
useLocalStorage<PackageManager>('packageManager', PACKAGE_MANAGERS[0])
60+
61+
// Normalize framework key to lowercase for lookup
62+
const normalizedFramework = currentFramework.toLowerCase()
63+
const packages = packagesByFramework[normalizedFramework]
64+
65+
// Hide component if current framework not in package list
66+
if (!packages) {
67+
return null
68+
}
69+
70+
// Use stored package manager if valid, otherwise default to first one
71+
const selectedPackageManager = PACKAGE_MANAGERS.includes(storedPackageManager)
72+
? storedPackageManager
73+
: PACKAGE_MANAGERS[0]
74+
75+
// Generate tabs for each package manager
76+
const tabs: TabDefinition[] = PACKAGE_MANAGERS.map((pm) => ({
77+
slug: pm,
78+
name: pm,
79+
headers: [],
80+
}))
81+
82+
// Generate children (command blocks) for each package manager
83+
const children = PACKAGE_MANAGERS.map((pm) => {
84+
const command = getInstallCommand(pm, packages, mode)
85+
return (
86+
<CodeBlock key={pm}>
87+
<code className="language-bash">{command}</code>
88+
</CodeBlock>
89+
)
90+
})
91+
92+
return (
93+
<Tabs
94+
id={id}
95+
tabs={tabs}
96+
children={children}
97+
activeSlug={selectedPackageManager}
98+
onTabChange={(slug) => setStoredPackageManager(slug as PackageManager)}
99+
/>
100+
)
101+
}

src/components/Tabs.tsx

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,37 @@ export type TabsProps = {
1212
tabs: Array<TabDefinition>
1313
children: Array<React.ReactNode>
1414
id: string
15+
activeSlug?: string
16+
onTabChange?: (slug: string) => void
1517
}
1618

17-
export function Tabs({ tabs, id, children }: TabsProps) {
19+
export function Tabs({
20+
tabs,
21+
id,
22+
children,
23+
activeSlug: controlledActiveSlug,
24+
onTabChange,
25+
}: TabsProps) {
1826
const params = useParams({ strict: false })
1927
const framework = 'framework' in params ? params.framework : undefined
2028

21-
const [activeSlug, setActiveSlug] = React.useState(
29+
const [internalActiveSlug, setInternalActiveSlug] = React.useState(
2230
() => tabs.find((tab) => tab.slug === framework)?.slug || tabs[0].slug,
2331
)
2432

33+
// Use controlled state if provided, otherwise use internal state
34+
const activeSlug = controlledActiveSlug ?? internalActiveSlug
35+
const setActiveSlug = React.useCallback(
36+
(slug: string) => {
37+
if (onTabChange) {
38+
onTabChange(slug)
39+
} else {
40+
setInternalActiveSlug(slug)
41+
}
42+
},
43+
[onTabChange],
44+
)
45+
2546
return (
2647
<div className="not-prose my-4">
2748
<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">
@@ -65,7 +86,7 @@ const Tab = ({
6586
id: string
6687
tab: TabDefinition
6788
activeSlug: string
68-
setActiveSlug: React.Dispatch<React.SetStateAction<string>>
89+
setActiveSlug: (slug: string) => void
6990
}) => {
7091
const option = React.useMemo(
7192
() => frameworkOptions.find((o) => o.value === tab.slug),

0 commit comments

Comments
 (0)