UI: Support CalloutContainer, CalloutTitle and CalloutDescription components

This commit is contained in:
Fuma Nama
2025-11-11 21:26:30 +08:00
parent 30b1b11aa2
commit 112e8d9878
4 changed files with 107 additions and 49 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'fumadocs-ui': patch
---
Support `CalloutContainer`, `CalloutTitle` and `CalloutDescription` components.
They're now MDX components included by default.
+90 -47
View File
@@ -1,62 +1,105 @@
import { CircleCheck, CircleX, Info, TriangleAlert } from 'lucide-react';
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
import type { ComponentProps, ReactNode } from 'react';
import { cn } from '@/utils/cn';
type CalloutProps = Omit<
HTMLAttributes<HTMLDivElement>,
'title' | 'type' | 'icon'
> & {
title?: ReactNode;
export type CalloutType = 'info' | 'warn' | 'error' | 'success' | 'warning';
const iconClass = 'size-5 -me-0.5 fill-(--callout-color) text-fd-card';
export function Callout({
children,
title,
...props
}: { title?: ReactNode } & Omit<CalloutContainerProps, 'title'>) {
return (
<CalloutContainer {...props}>
{title && <CalloutTitle>{title}</CalloutTitle>}
<CalloutDescription>{children}</CalloutDescription>
</CalloutContainer>
);
}
export interface CalloutContainerProps extends ComponentProps<'div'> {
/**
* @defaultValue info
*/
type?: 'info' | 'warn' | 'error' | 'success' | 'warning';
type?: CalloutType;
/**
* Force an icon
*/
icon?: ReactNode;
};
}
const iconClass = 'size-5 -me-0.5 fill-(--callout-color) text-fd-card';
function resolveAlias(type: CalloutType) {
if (type === 'warn') return 'warning';
if ((type as unknown) === 'tip') return 'info';
return type;
}
export const Callout = forwardRef<HTMLDivElement, CalloutProps>(
({ className, children, title, type = 'info', icon, ...props }, ref) => {
if (type === 'warn') type = 'warning';
if ((type as unknown) === 'tip') type = 'info';
export function CalloutContainer({
type: inputType = 'info',
icon,
children,
className,
style,
...props
}: CalloutContainerProps) {
const type = resolveAlias(inputType);
return (
<div
ref={ref}
className={cn(
'flex gap-2 my-4 rounded-xl border bg-fd-card p-3 ps-1 text-sm text-fd-card-foreground shadow-md',
className,
)}
{...props}
style={
{
'--callout-color': `var(--color-fd-${type}, var(--color-fd-muted))`,
...props.style,
} as object
}
>
<div role="none" className="w-0.5 bg-(--callout-color)/50 rounded-sm" />
{icon ??
{
info: <Info className={iconClass} />,
warning: <TriangleAlert className={iconClass} />,
error: <CircleX className={iconClass} />,
success: <CircleCheck className={iconClass} />,
}[type]}
<div className="flex flex-col gap-2 min-w-0 flex-1">
{title && <p className="font-medium !my-0">{title}</p>}
<div className="text-fd-muted-foreground prose-no-margin empty:hidden">
{children}
</div>
</div>
</div>
);
},
);
return (
<div
className={cn(
'flex gap-2 my-4 rounded-xl border bg-fd-card p-3 ps-1 text-sm text-fd-card-foreground shadow-md',
className,
)}
style={
{
'--callout-color': `var(--color-fd-${type}, var(--color-fd-muted))`,
...style,
} as object
}
{...props}
>
<div role="none" className="w-0.5 bg-(--callout-color)/50 rounded-sm" />
{icon ??
{
info: <Info className={iconClass} />,
warning: <TriangleAlert className={iconClass} />,
error: <CircleX className={iconClass} />,
success: <CircleCheck className={iconClass} />,
}[type]}
<div className="flex flex-col gap-2 min-w-0 flex-1">{children}</div>
</div>
);
}
Callout.displayName = 'Callout';
export function CalloutTitle({
children,
className,
...props
}: ComponentProps<'p'>) {
return (
<p className={cn('font-medium my-0!', className)} {...props}>
{children}
</p>
);
}
export function CalloutDescription({
children,
className,
...props
}: ComponentProps<'p'>) {
return (
<div
className={cn(
'text-fd-muted-foreground prose-no-margin empty:hidden',
className,
)}
{...props}
>
{children}
</div>
);
}
+1 -1
View File
@@ -42,7 +42,7 @@ export function Card({ icon, title, description, ...props }: CardProps) {
) : null}
<h3 className="not-prose mb-1 text-sm font-medium">{title}</h3>
{description ? (
<p className="!my-0 text-sm text-fd-muted-foreground">{description}</p>
<p className="my-0! text-sm text-fd-muted-foreground">{description}</p>
) : null}
<div className="text-sm text-fd-muted-foreground prose-no-margin empty:hidden">
{props.children}
+9 -1
View File
@@ -8,7 +8,12 @@ import type {
} from 'react';
import { Image as FrameworkImage } from 'fumadocs-core/framework';
import { Card, Cards } from '@/components/card';
import { Callout } from '@/components/callout';
import {
Callout,
CalloutContainer,
CalloutDescription,
CalloutTitle,
} from '@/components/callout';
import { Heading } from '@/components/heading';
import { cn } from '@/utils/cn';
import {
@@ -77,6 +82,9 @@ const defaultMdxComponents = {
),
table: Table,
Callout,
CalloutContainer,
CalloutTitle,
CalloutDescription,
};
export const createRelativeLink: typeof import('./mdx.server').createRelativeLink =