[feat]: bundler tabs (#943) · TanStack/tanstack.com@320867c · GitHub
Skip to content

Commit 320867c

Browse files
[feat]: bundler tabs (#943)
1 parent 8735dde commit 320867c

14 files changed

Lines changed: 503 additions & 159 deletions

‎docs-info.md‎

Lines changed: 30 additions & 0 deletions
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
'use client'
2+
3+
import * as React from 'react'
4+
import { Tabs, type TabDefinition } from './Tabs'
5+
import { createPersistedEnumStore } from './usePersistedEnumStore'
6+
import {
7+
BUNDLERS,
8+
BUNDLER_LABELS,
9+
DEFAULT_BUNDLER,
10+
isBundler,
11+
type Bundler,
12+
} from '~/utils/markdown/bundler'
13+
14+
const bundlerStore = createPersistedEnumStore<Bundler>({
15+
storageKey: 'bundler',
16+
values: BUNDLERS,
17+
defaultValue: DEFAULT_BUNDLER,
18+
})
19+
20+
export type BundlerTabsProps = {
21+
tabs: Array<{ slug: string; name: string }>
22+
panelContent?: Record<string, 'code-only' | 'mixed'>
23+
children: Array<React.ReactNode> | React.ReactNode
24+
}
25+
26+
export function BundlerTabs({
27+
tabs,
28+
panelContent,
29+
children,
30+
}: BundlerTabsProps) {
31+
bundlerStore.useHydrate()
32+
33+
const activeBundler = bundlerStore((s) => s.value)
34+
const setBundler = bundlerStore((s) => s.setValue)
35+
36+
const childrenArray = React.Children.toArray(children)
37+
38+
const panelsBySlug = React.useMemo(() => {
39+
const map = new Map<string, React.ReactNode>()
40+
tabs.forEach((tab, index) => {
41+
map.set(tab.slug, childrenArray[index])
42+
})
43+
return map
44+
}, [tabs, childrenArray])
45+
46+
const tabDefinitions = React.useMemo<Array<TabDefinition>>(
47+
() =>
48+
tabs
49+
.filter((tab) => isBundler(tab.slug))
50+
.map((tab) => ({
51+
slug: tab.slug,
52+
name: BUNDLER_LABELS[tab.slug as Bundler] ?? tab.name,
53+
headers: [],
54+
})),
55+
[tabs],
56+
)
57+
58+
const orderedChildren = React.useMemo(
59+
() => tabDefinitions.map((tab) => panelsBySlug.get(tab.slug)),
60+
[tabDefinitions, panelsBySlug],
61+
)
62+
63+
const resolvedActiveSlug = tabDefinitions.some(
64+
(tab) => tab.slug === activeBundler,
65+
)
66+
? activeBundler
67+
: (tabDefinitions[0]?.slug ?? activeBundler)
68+
69+
const handleTabChange = React.useCallback(
70+
(slug: string) => {
71+
if (isBundler(slug)) {
72+
setBundler(slug)
73+
}
74+
},
75+
[setBundler],
76+
)
77+
78+
if (tabDefinitions.length === 0) return null
79+
80+
return (
81+
<Tabs
82+
tabs={tabDefinitions}
83+
activeSlug={resolvedActiveSlug}
84+
onTabChange={handleTabChange}
85+
panelContent={panelContent}
86+
>
87+
{orderedChildren.map((child, index) => (
88+
<React.Fragment key={tabDefinitions[index]?.slug ?? index}>
89+
{child}
90+
</React.Fragment>
91+
))}
92+
</Tabs>
93+
)
94+
}

‎src/components/markdown/CodeBlockView.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export function CodeBlockView({
3131
return (
3232
<div
3333
className={twMerge(
34-
'codeblock w-full max-w-full relative not-prose border border-gray-500/20 rounded-md [&_pre]:rounded-md',
34+
'codeblock w-full max-w-full relative not-prose border border-gray-500/20 rounded-md overflow-hidden [&_pre]:rounded-none',
3535
className,
3636
)}
3737
style={style}

‎src/components/markdown/FileTabs.tsx‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,15 @@ export function FileTabs({ tabs, children }: FileTabsProps) {
4343
{childrenArray.map((child, index) => {
4444
const tab = tabs[index]
4545
if (!tab) return null
46+
const isActive = tab.slug === activeSlug
4647
return (
4748
<div
4849
key={`${id}-${tab.slug}-panel`}
4950
data-tab={tab.slug}
50-
hidden={tab.slug !== activeSlug}
51-
className="file-tabs-panel"
51+
data-content="code-only"
52+
className={`border border-t-0 border-gray-500/20 rounded-b-md overflow-hidden ${
53+
isActive ? '' : 'hidden'
54+
}`}
5255
>
5356
{child}
5457
</div>

‎src/components/markdown/MdComponents.tsx‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as React from 'react'
22
import { FileTabs } from './FileTabs'
33
import { FrameworkContent } from './FrameworkContent'
44
import { PackageManagerTabs } from './PackageManagerTabs'
5+
import { BundlerTabs } from './BundlerTabs'
56
import { CodeBlock } from './CodeBlock.server'
67
import { Tabs } from './Tabs'
78
import {
@@ -27,6 +28,7 @@ type MdCommentComponentProps = {
2728
'data-component'?: string
2829
'data-files-meta'?: string
2930
'data-package-manager-meta'?: string
31+
'data-bundler-meta'?: string
3032
preserveTabPanels?: boolean
3133
children?: React.ReactNode
3234
}
@@ -67,6 +69,7 @@ export function MdCommentComponent({
6769
'data-component': componentName,
6870
'data-files-meta': filesMeta,
6971
'data-package-manager-meta': packageManagerMeta,
72+
'data-bundler-meta': bundlerMeta,
7073
preserveTabPanels = false,
7174
children,
7275
}: MdCommentComponentProps) {
@@ -123,6 +126,43 @@ export function MdCommentComponent({
123126
const childArray = React.Children.toArray(children)
124127
const panels = childArray.filter(isMdTabPanelElement)
125128

129+
const parsedBundlerMeta = parseJson(bundlerMeta)
130+
131+
if (
132+
parsedBundlerMeta &&
133+
typeof parsedBundlerMeta === 'object' &&
134+
panels.length
135+
) {
136+
const tabs = Array.isArray((attributes as { tabs?: unknown }).tabs)
137+
? ((attributes as { tabs: Array<{ name: string; slug: string }> })
138+
.tabs ?? [])
139+
: []
140+
141+
const panelContent: Record<string, 'code-only' | 'mixed'> = {}
142+
const childrenBySlug = new Map<string, React.ReactNode>()
143+
panels.forEach((panel, index) => {
144+
const slug = panel.props['data-tab-slug']
145+
if (!slug) return
146+
const content = panel.props['data-content']
147+
if (content === 'code-only' || content === 'mixed') {
148+
panelContent[slug] = content
149+
}
150+
childrenBySlug.set(slug, panel.props.children)
151+
// Preserve insertion order for tabs that came in without metadata
152+
void index
153+
})
154+
155+
return (
156+
<BundlerTabs tabs={tabs} panelContent={panelContent}>
157+
{tabs.map((tab) => (
158+
<React.Fragment key={tab.slug}>
159+
{childrenBySlug.get(tab.slug)}
160+
</React.Fragment>
161+
))}
162+
</BundlerTabs>
163+
)
164+
}
165+
126166
const parsedFilesMeta = parseJson(filesMeta)
127167

128168
if (
@@ -164,6 +204,9 @@ export function MdCommentComponent({
164204
}
165205

166206
type MdTabPanelProps = {
207+
'data-tab-slug'?: string
208+
'data-tab-index'?: string
209+
'data-content'?: 'code-only' | 'mixed'
167210
children?: React.ReactNode
168211
}
169212

‎src/components/markdown/PackageManagerTabs.tsx‎

Lines changed: 25 additions & 21 deletions

0 commit comments

Comments
 (0)