Core & UI: Support multiple codeblocks in same tab

This commit is contained in:
Fuma Nama
2025-09-12 20:42:55 +08:00
parent a21261b635
commit 846b28a7f0
3 changed files with 46 additions and 44 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'fumadocs-core': patch
'fumadocs-ui': patch
---
Support multiple codeblocks in same tab
@@ -32,10 +32,10 @@ const Tabs = {
withMdx = false,
withParent = true,
): MdxJsxFlowElement {
const names = processTabValue(nodes);
const tabs = Array.from(processTabValue(nodes).entries());
if (!withMdx) {
const children: MdxJsxFlowElement[] = nodes.map((node, i) => {
const children: MdxJsxFlowElement[] = tabs.map(([name, codes]) => {
return {
type: 'mdxJsxFlowElement',
name: 'Tab',
@@ -43,10 +43,10 @@ const Tabs = {
{
type: 'mdxJsxAttribute',
name: 'value',
value: names[i],
value: name,
},
],
children: [node],
children: codes,
};
});
@@ -61,7 +61,7 @@ const Tabs = {
name: 'items',
value: {
type: 'mdxJsxAttributeValueExpression',
value: names.join(', '),
value: tabs.map(([name]) => name).join(', '),
data: {
estree: {
type: 'Program',
@@ -72,7 +72,7 @@ const Tabs = {
type: 'ExpressionStatement',
expression: {
type: 'ArrayExpression',
elements: names.map((name) => ({
elements: tabs.map(([name]) => ({
type: 'Literal',
value: name,
})),
@@ -93,7 +93,7 @@ const Tabs = {
type: 'mdxJsxFlowElement',
name: 'TabsList',
attributes: [],
children: names.map((name) => ({
children: tabs.map(([name]) => ({
type: 'mdxJsxFlowElement',
name: 'TabsTrigger',
attributes: [
@@ -103,14 +103,11 @@ const Tabs = {
value: name,
},
],
children: [
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- needed
mdxToAst(processor, name) as any,
],
children: [mdxToAst(processor, name) as unknown as BlockContent],
})),
},
...nodes.map(
(node, i) =>
...tabs.map(
([name, codes]) =>
({
type: 'mdxJsxFlowElement',
name: 'TabsContent',
@@ -118,10 +115,10 @@ const Tabs = {
{
type: 'mdxJsxAttribute',
name: 'value',
value: names[i],
value: name,
},
],
children: [node],
children: codes,
}) as MdxJsxFlowElement,
),
];
@@ -135,7 +132,7 @@ const Tabs = {
{
type: 'mdxJsxAttribute',
name: 'defaultValue',
value: names[0],
value: tabs[0][0],
},
],
children,
@@ -150,24 +147,24 @@ const CodeBlockTabs = {
withMdx = false,
withParent = true,
): MdxJsxFlowElement {
const names = processTabValue(nodes);
const tabs = Array.from(processTabValue(nodes).entries());
const node = generateCodeBlockTabs({
defaultValue: names[0],
triggers: names.map((name) => ({
defaultValue: tabs[0][0],
triggers: tabs.map(([name]) => ({
value: name,
children: [
withMdx
? // eslint-disable-next-line @typescript-eslint/no-explicit-any -- needed
(mdxToAst(processor, name) as any)
? (mdxToAst(processor, name) as unknown as BlockContent)
: {
type: 'text',
value: name,
},
],
})),
tabs: nodes.map((node, i) => ({
value: names[i],
children: [node],
tabs: tabs.map(([name, codes]) => ({
value: name,
children: codes,
})),
});
@@ -256,7 +253,16 @@ export function remarkCodeTab(
}
function processTabValue(nodes: Code[]) {
return nodes.map((node, i) => node.data?.tab ?? `Tab ${i + 1}`);
const out = new Map<string, Code[]>();
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
const name = node.data?.tab ?? `Tab ${i + 1}`;
const li = out.get(name) ?? [];
li.push(node);
out.set(name, li);
}
return out;
}
/**
+9 -19
View File
@@ -77,7 +77,7 @@ export function Pre(props: ComponentProps<'pre'>) {
export function CodeBlock({
ref,
title,
allowCopy,
allowCopy = true,
keepBackground = false,
icon,
viewportProps = {},
@@ -89,7 +89,6 @@ export function CodeBlock({
}: CodeBlockProps) {
const inTab = useContext(TabsContext) !== null;
const areaRef = useRef<HTMLDivElement>(null);
allowCopy ??= !inTab;
return (
<figure
@@ -97,10 +96,12 @@ export function CodeBlock({
dir="ltr"
{...props}
className={cn(
inTab ? 'bg-fd-secondary' : 'my-4 bg-fd-card',
inTab
? 'bg-fd-secondary -mx-px -mb-px last:rounded-b-xl'
: 'my-4 bg-fd-card rounded-xl',
keepBackground && 'bg-(--shiki-light-bg) dark:bg-(--shiki-dark-bg)',
'shiki relative border shadow-sm rounded-xl outline-none not-prose overflow-hidden text-sm',
'shiki relative border shadow-sm outline-none not-prose overflow-hidden text-sm',
props.className,
)}
>
@@ -221,23 +222,15 @@ export function CodeBlockTabs({ ref, ...props }: ComponentProps<typeof Tabs>) {
}
export function CodeBlockTabsList(props: ComponentProps<typeof TabsList>) {
const { containerRef, nested } = useContext(TabsContext)!;
return (
<TabsList
{...props}
className={cn(
'flex flex-row *:first:ms-2 overflow-x-auto text-fd-muted-foreground',
'flex flex-row px-2 overflow-x-auto text-fd-muted-foreground',
props.className,
)}
>
{props.children}
{!nested && (
<CopyButton
className="sticky ms-auto right-2 bg-fd-card backdrop-blur-sm"
containerRef={containerRef}
/>
)}
</TabsList>
);
}
@@ -260,10 +253,7 @@ export function CodeBlockTabsTrigger({
);
}
// TODO: currently Vite RSC plugin has problem with adding `asChild` here, maybe revisit this in future
export function CodeBlockTab({
className,
...props
}: ComponentProps<typeof TabsContent>) {
return <TabsContent className={cn('-mx-px -mb-px', className)} {...props} />;
// TODO: currently Vite RSC plugin has problem with `asChild` due to children is automatically wrapped in <Fragment />, maybe revisit this in future
export function CodeBlockTab(props: ComponentProps<typeof TabsContent>) {
return <TabsContent {...props} />;
}