refactor: streamline MDX handling and enhance content collections
- Replaced the custom Mdx component with MDXContent from @content-collections/mdx/react for improved MDX processing. - Updated content handling in various legal and blog pages to directly use the transformed body from content collections. - Introduced estimated reading time calculation in the posts collection for better user experience. - Added a new mdx.css file for consistent styling across MDX components. - Removed the obsolete shared mdx-component file to reduce redundancy.
This commit is contained in:
parent
c3a774e1cb
commit
08fee6d1ec
@ -1,19 +1,11 @@
|
||||
import { DEFAULT_LOCALE, LOCALES } from "@/i18n/routing";
|
||||
import { defineCollection, defineConfig } from "@content-collections/core";
|
||||
import { compileMDX } from "@content-collections/mdx";
|
||||
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
||||
import rehypePrettyCode, { Options } from 'rehype-pretty-code';
|
||||
import rehypeSlug from 'rehype-slug';
|
||||
import { codeImport } from 'remark-code-import';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { createHighlighter } from 'shiki';
|
||||
import path from "path";
|
||||
import { LOCALES, DEFAULT_LOCALE } from "@/i18n/routing";
|
||||
import { visit } from 'unist-util-visit';
|
||||
import {
|
||||
createMetaSchema,
|
||||
createDocSchema,
|
||||
createMetaSchema,
|
||||
transformMDX,
|
||||
} from '@fumadocs/content-collections/configuration';
|
||||
import path from "path";
|
||||
|
||||
/**
|
||||
* Content Collections documentation
|
||||
@ -123,10 +115,12 @@ export const posts = defineCollection({
|
||||
published: z.boolean().default(true),
|
||||
categories: z.array(z.string()),
|
||||
author: z.string(),
|
||||
locale: z.enum(LOCALES as [string, ...string[]]).optional()
|
||||
locale: z.enum(LOCALES as [string, ...string[]]).optional(),
|
||||
estimatedTime: z.number().optional() // Reading time in minutes
|
||||
}),
|
||||
transform: async (data, context) => {
|
||||
const body = await compileWithCodeCopy(context, data);
|
||||
// Use Fumadocs transformMDX for consistent MDX processing
|
||||
const transformedData = await transformMDX(data, context);
|
||||
|
||||
// Determine the locale from the file path or use the provided locale
|
||||
const pathParts = data._meta.path.split(path.sep);
|
||||
@ -166,6 +160,11 @@ export const posts = defineCollection({
|
||||
const slugParamsParts = slugPath.split(path.sep).slice(1);
|
||||
const slugAsParams = slugParamsParts.join('/');
|
||||
|
||||
// Calculate estimated reading time
|
||||
const wordCount = data.content.split(/\s+/).length;
|
||||
const wordsPerMinute = 200; // Average reading speed: 200 words per minute
|
||||
const estimatedTime = Math.max(Math.ceil(wordCount / wordsPerMinute), 1);
|
||||
|
||||
return {
|
||||
...data,
|
||||
locale,
|
||||
@ -173,10 +172,9 @@ export const posts = defineCollection({
|
||||
categories: blogCategories,
|
||||
slug: `/${slugPath}`,
|
||||
slugAsParams,
|
||||
body: {
|
||||
raw: data.content,
|
||||
code: body
|
||||
}
|
||||
estimatedTime,
|
||||
body: transformedData.body, // Use processed MDX content directly
|
||||
toc: transformedData.toc
|
||||
};
|
||||
}
|
||||
});
|
||||
@ -206,7 +204,8 @@ export const pages = defineCollection({
|
||||
locale: z.enum(LOCALES as [string, ...string[]]).optional()
|
||||
}),
|
||||
transform: async (data, context) => {
|
||||
const body = await compileWithCodeCopy(context, data);
|
||||
// Use Fumadocs transformMDX for consistent MDX processing
|
||||
const transformedData = await transformMDX(data, context);
|
||||
|
||||
// Determine the locale from the file path or use the provided locale
|
||||
const pathParts = data._meta.path.split(path.sep);
|
||||
@ -230,10 +229,8 @@ export const pages = defineCollection({
|
||||
locale,
|
||||
slug: `/${slugPath}`,
|
||||
slugAsParams,
|
||||
body: {
|
||||
raw: data.content,
|
||||
code: body
|
||||
}
|
||||
body: transformedData.body,
|
||||
toc: transformedData.toc
|
||||
};
|
||||
}
|
||||
});
|
||||
@ -264,7 +261,8 @@ export const releases = defineCollection({
|
||||
locale: z.enum(LOCALES as [string, ...string[]]).optional()
|
||||
}),
|
||||
transform: async (data, context) => {
|
||||
const body = await compileWithCodeCopy(context, data);
|
||||
// Use Fumadocs transformMDX for consistent MDX processing
|
||||
const transformedData = await transformMDX(data, context);
|
||||
|
||||
// Determine the locale from the file path or use the provided locale
|
||||
const pathParts = data._meta.path.split(path.sep);
|
||||
@ -288,80 +286,12 @@ export const releases = defineCollection({
|
||||
locale,
|
||||
slug: `/${slugPath}`,
|
||||
slugAsParams,
|
||||
body: {
|
||||
raw: data.content,
|
||||
code: body
|
||||
}
|
||||
body: transformedData.body,
|
||||
toc: transformedData.toc
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const prettyCodeOptions: Options = {
|
||||
theme: 'github-dark',
|
||||
getHighlighter: (options) =>
|
||||
createHighlighter({
|
||||
...options
|
||||
}),
|
||||
onVisitLine(node) {
|
||||
// Prevent lines from collapsing in `display: grid` mode, and allow empty
|
||||
// lines to be copy/pasted
|
||||
if (node.children.length === 0) {
|
||||
node.children = [{ type: 'text', value: ' ' }];
|
||||
}
|
||||
},
|
||||
onVisitHighlightedLine(node) {
|
||||
if (!node.properties.className) {
|
||||
node.properties.className = [];
|
||||
}
|
||||
node.properties.className.push('line--highlighted');
|
||||
},
|
||||
onVisitHighlightedChars(node) {
|
||||
if (!node.properties.className) {
|
||||
node.properties.className = [];
|
||||
}
|
||||
node.properties.className = ['word--highlighted'];
|
||||
}
|
||||
};
|
||||
|
||||
const compileWithCodeCopy = async (
|
||||
context: any,
|
||||
data: any,
|
||||
options: {
|
||||
remarkPlugins?: any[];
|
||||
rehypePlugins?: any[];
|
||||
} = {}
|
||||
) => {
|
||||
return await compileMDX(context, data, {
|
||||
...options,
|
||||
remarkPlugins: [
|
||||
remarkGfm,
|
||||
codeImport,
|
||||
...(options.remarkPlugins || [])
|
||||
],
|
||||
rehypePlugins: [
|
||||
rehypeSlug,
|
||||
[rehypePrettyCode, prettyCodeOptions],
|
||||
// add __rawString__ to pre element
|
||||
() => (tree) => {
|
||||
visit(tree, (node) => {
|
||||
if (node?.type === "element" && node?.tagName === "pre") {
|
||||
const [codeEl] = node.children;
|
||||
if (codeEl.tagName !== "code") return;
|
||||
|
||||
// add __rawString__ as a property that will be passed to the React component
|
||||
if (!node.properties) {
|
||||
node.properties = {};
|
||||
}
|
||||
node.properties.__rawString__ = codeEl.children?.[0]?.value;
|
||||
}
|
||||
});
|
||||
},
|
||||
rehypeAutolinkHeadings,
|
||||
...(options.rehypePlugins || [])
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
collections: [docs, metas, authors, categories, posts, pages, releases]
|
||||
});
|
@ -50,7 +50,7 @@ export default async function CookiePolicyPage(props: NextPageProps) {
|
||||
title={page.title}
|
||||
description={page.description}
|
||||
date={page.date}
|
||||
content={page.body.code}
|
||||
content={page.body}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import Container from '@/components/container';
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import '@/styles/mdx.css';
|
||||
|
||||
export default function LegalLayout({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<Container className="py-16 px-4">
|
||||
|
@ -50,7 +50,7 @@ export default async function PrivacyPolicyPage(props: NextPageProps) {
|
||||
title={page.title}
|
||||
description={page.description}
|
||||
date={page.date}
|
||||
content={page.body.code}
|
||||
content={page.body}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { CustomPage } from '@/components/page/custom-page';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { getCustomPage } from '@/lib/page/get-custom-page';
|
||||
import { getBaseUrlWithLocale } from '@/lib/urls/get-base-url';
|
||||
import type { NextPageProps } from '@/types/next-page-props';
|
||||
import type { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
@ -50,7 +50,7 @@ export default async function TermsOfServicePage(props: NextPageProps) {
|
||||
title={page.title}
|
||||
description={page.description}
|
||||
date={page.date}
|
||||
content={page.body.code}
|
||||
content={page.body}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ import { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import '@/styles/mdx.css';
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
@ -60,7 +62,7 @@ export default async function ChangelogPage(props: NextPageProps) {
|
||||
description={release.description}
|
||||
date={release.date}
|
||||
version={release.version}
|
||||
content={release.body.code}
|
||||
content={release.body}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -1,10 +1,9 @@
|
||||
import AllPostsButton from '@/components/blog/all-posts-button';
|
||||
import { BlogToc } from '@/components/blog/blog-toc';
|
||||
import { Mdx } from '@/components/shared/mdx-component';
|
||||
import { LocaleLink } from '@/i18n/navigation';
|
||||
import { getTableOfContents } from '@/lib/blog/toc';
|
||||
import { getBaseUrlWithLocale } from '@/lib/urls/get-base-url';
|
||||
import { estimateReadingTime, getLocaleDate } from '@/lib/utils';
|
||||
import { getLocaleDate } from '@/lib/utils';
|
||||
import type { NextPageProps } from '@/types/next-page-props';
|
||||
import { allPosts } from 'content-collections';
|
||||
import { CalendarIcon, ClockIcon } from 'lucide-react';
|
||||
@ -15,6 +14,10 @@ import { notFound } from 'next/navigation';
|
||||
import { constructMetadata } from '@/lib/metadata';
|
||||
import { Locale } from 'next-intl';
|
||||
import { NewsletterCard } from '@/components/newsletter/newsletter-card';
|
||||
import { MDXContent } from '@content-collections/mdx/react';
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
|
||||
import '@/styles/mdx.css';
|
||||
|
||||
/**
|
||||
* Gets the blog post from the params
|
||||
@ -134,7 +137,7 @@ export default async function BlogPostPage(props: NextPageProps) {
|
||||
<div className="flex items-center gap-2">
|
||||
<ClockIcon className="size-4 text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground leading-none my-auto">
|
||||
{estimateReadingTime(post.body.raw)}
|
||||
{post.estimatedTime ? `${post.estimatedTime} min read` : 'Quick read'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -147,7 +150,12 @@ export default async function BlogPostPage(props: NextPageProps) {
|
||||
</div>
|
||||
|
||||
{/* blog post content */}
|
||||
<Mdx code={post.body.code} />
|
||||
<div className="max-w-none prose prose-slate dark:prose-invert prose-img:rounded-lg">
|
||||
<MDXContent
|
||||
code={post.body}
|
||||
components={defaultMdxComponents}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-start my-16">
|
||||
<AllPostsButton />
|
||||
|
@ -12,7 +12,7 @@ import { Locale } from 'next-intl';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import '@/styles/docs.css';
|
||||
import '@/styles/mdx.css';
|
||||
|
||||
// available languages that will be displayed on UI
|
||||
// make sure `locale` is consistent with your i18n config
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { Mdx } from '@/components/shared/mdx-component';
|
||||
import { getLocaleDate } from '@/lib/utils';
|
||||
import { Separator } from '@radix-ui/react-separator';
|
||||
import { Badge, CalendarIcon, TagIcon } from 'lucide-react';
|
||||
import { version } from 'os';
|
||||
import { Card, CardHeader, CardContent } from '../ui/card';
|
||||
import { MDXContent } from '@content-collections/mdx/react';
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
|
||||
interface CustomPageProps {
|
||||
title: string;
|
||||
@ -39,7 +40,9 @@ export function CustomPage({
|
||||
{/* Content */}
|
||||
<Card className="mb-8">
|
||||
<CardContent>
|
||||
<Mdx code={content} />
|
||||
<div className="max-w-none prose prose-slate dark:prose-invert prose-img:rounded-lg">
|
||||
<MDXContent code={content} components={defaultMdxComponents} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { Mdx } from '@/components/shared/mdx-component';
|
||||
import { getLocaleDate } from '@/lib/utils';
|
||||
import { CalendarIcon, TagIcon } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { MDXContent } from '@content-collections/mdx/react';
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
|
||||
interface ReleaseCardProps {
|
||||
title: string;
|
||||
@ -40,7 +41,9 @@ export function ReleaseCard({
|
||||
<Separator />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Mdx code={content} />
|
||||
<div className="max-w-none prose prose-slate dark:prose-invert prose-img:rounded-lg">
|
||||
<MDXContent code={content} components={defaultMdxComponents} />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
@ -1,262 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useMDXComponent } from '@content-collections/mdx/react';
|
||||
import { Callout } from '@/components/shared/callout';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '@/components/ui/accordion';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CopyButton } from './copy-button';
|
||||
|
||||
const components = {
|
||||
h1: ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<h1
|
||||
className={cn(
|
||||
'font-heading mt-2 scroll-m-24 text-4xl font-bold',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h2: ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<h2
|
||||
className={cn(
|
||||
'font-heading mb-4 mt-8 scroll-m-24 text-2xl font-semibold leading-8 tracking-tight',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h3: ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<h3
|
||||
className={cn(
|
||||
'font-heading mt-8 scroll-m-24 text-xl font-medium tracking-tight',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h4: ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<h4
|
||||
className={cn(
|
||||
'font-heading mt-8 scroll-m-24 text-lg font-medium tracking-tight',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h5: ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<h5
|
||||
className={cn(
|
||||
'mt-8 scroll-m-24 text-base font-medium tracking-tight',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
h6: ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
|
||||
<h6
|
||||
className={cn(
|
||||
'mt-8 scroll-m-24 text-sm font-medium tracking-tight',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
a: ({ className, ...props }: React.HTMLAttributes<HTMLAnchorElement>) => (
|
||||
<a
|
||||
className={cn(
|
||||
'font-medium text-primary underline underline-offset-4',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
p: ({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) => (
|
||||
<p className={cn('text-muted-foreground mt-6 leading-7', className)} {...props} />
|
||||
),
|
||||
ul: ({ className, ...props }: React.HTMLAttributes<HTMLUListElement>) => (
|
||||
<ul className={cn('text-muted-foreground my-6 ml-6 list-disc', className)} {...props} />
|
||||
),
|
||||
ol: ({ className, ...props }: React.HTMLAttributes<HTMLOListElement>) => (
|
||||
<ol className={cn('text-muted-foreground my-6 ml-6 list-decimal', className)} {...props} />
|
||||
),
|
||||
li: ({ className, ...props }: React.HTMLAttributes<HTMLElement>) => (
|
||||
<li className={cn('text-muted-foreground mt-2', className)} {...props} />
|
||||
),
|
||||
blockquote: ({ className, ...props }: React.HTMLAttributes<HTMLElement>) => (
|
||||
<blockquote
|
||||
className={cn('mt-6 border-l-2 pl-6 italic', className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
strong: ({ className, ...props }: React.HTMLAttributes<HTMLElement>) => (
|
||||
<strong className={cn('text-foreground', className)} {...props} />
|
||||
),
|
||||
img: ({
|
||||
className,
|
||||
alt,
|
||||
...props
|
||||
}: React.ImgHTMLAttributes<HTMLImageElement>) => (
|
||||
<img className={cn('rounded-md', className)} alt={alt} {...props} />
|
||||
),
|
||||
hr: ({ ...props }: React.HTMLAttributes<HTMLHRElement>) => (
|
||||
<hr className="my-4 md:my-8" {...props} />
|
||||
),
|
||||
table: ({ className, ...props }: React.HTMLAttributes<HTMLTableElement>) => (
|
||||
<div className="my-6 w-full overflow-y-auto rounded-none">
|
||||
<table
|
||||
className={cn('w-full overflow-hidden rounded-none', className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
tr: ({ className, ...props }: React.HTMLAttributes<HTMLTableRowElement>) => (
|
||||
<tr className={cn('m-0 border-t p-0', className)} {...props} />
|
||||
),
|
||||
th: ({ className, ...props }: React.HTMLAttributes<HTMLTableCellElement>) => (
|
||||
<th
|
||||
className={cn(
|
||||
'border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
td: ({ className, ...props }: React.HTMLAttributes<HTMLTableCellElement>) => (
|
||||
<td
|
||||
className={cn(
|
||||
'text-muted-foreground border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
pre: ({
|
||||
className,
|
||||
__rawString__,
|
||||
children,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLPreElement> & { __rawString__?: string }) => {
|
||||
const preRef = React.useRef<HTMLPreElement>(null);
|
||||
const [codeContent, setCodeContent] = React.useState<string>(__rawString__ || '');
|
||||
|
||||
// Extract the text content from the pre element after rendering
|
||||
React.useEffect(() => {
|
||||
if (preRef.current && !codeContent) {
|
||||
// Find the code element inside the pre element
|
||||
const codeElement = preRef.current.querySelector('code');
|
||||
if (codeElement) {
|
||||
// Get the text content of the code element
|
||||
const text = codeElement.textContent || '';
|
||||
setCodeContent(text);
|
||||
}
|
||||
}
|
||||
}, [codeContent]);
|
||||
|
||||
return (
|
||||
<div className="my-4 group relative w-full overflow-hidden">
|
||||
<pre
|
||||
ref={preRef}
|
||||
className={cn(
|
||||
"max-h-[650px] overflow-x-auto rounded-lg border bg-zinc-900 py-4 dark:bg-zinc-900",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</pre>
|
||||
{(codeContent || __rawString__) && (
|
||||
<CopyButton
|
||||
value={codeContent || __rawString__ || ''}
|
||||
className="cursor-pointer absolute right-4 top-4 z-20 opacity-70 hover:opacity-100 transition-all"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
code: ({ className, ...props }: React.HTMLAttributes<HTMLElement>) => (
|
||||
<code
|
||||
className={cn(
|
||||
'relative font-mono text-sm',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
Accordion,
|
||||
AccordionContent: ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionContent>) => (
|
||||
<AccordionContent className={cn('[&>p]:m-0', className)} {...props} />
|
||||
),
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Callout,
|
||||
Image: ({ className, ...props }: React.ComponentProps<typeof Image>) => (
|
||||
<Image
|
||||
className={cn('my-4 w-full rounded-lg', className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
Tabs: ({ className, ...props }: React.ComponentProps<typeof Tabs>) => (
|
||||
<Tabs className={cn('relative mt-6 w-full', className)} {...props} />
|
||||
),
|
||||
TabsList: ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsList>) => (
|
||||
<TabsList className={cn('w-full border-b', className)} {...props} />
|
||||
),
|
||||
TabsTrigger: ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsTrigger>) => (
|
||||
<TabsTrigger className={cn('', className)} {...props} />
|
||||
),
|
||||
TabsContent: ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsContent>) => (
|
||||
<TabsContent className={cn('p-4', className)} {...props} />
|
||||
),
|
||||
Link: ({ className, ...props }: React.ComponentProps<typeof Link>) => (
|
||||
<Link
|
||||
className={cn('font-medium underline underline-offset-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
LinkedCard: ({ className, ...props }: React.ComponentProps<typeof Link>) => (
|
||||
<Link
|
||||
className={cn(
|
||||
'flex w-full flex-col items-center rounded-xl border bg-card p-6 text-card-foreground shadow-sm transition-colors hover:bg-muted/50 sm:p-10',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
interface MdxProps {
|
||||
code: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* render mdx content with custom components
|
||||
*/
|
||||
export function Mdx({ code }: MdxProps) {
|
||||
const Component = useMDXComponent(code);
|
||||
return (
|
||||
<div className="mdx">
|
||||
<Component components={components} />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -3,6 +3,10 @@
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
/*
|
||||
* How to Add a Theme Selector to Your Next.js App
|
||||
* https://ouassim.tech/notes/how-to-add-a-theme-selector-to-your-nextjs-app/
|
||||
*/
|
||||
@theme inline {
|
||||
--font-sans: var(--font-dm-sans);
|
||||
--font-mono: var(--font-dm-mono);
|
||||
@ -302,44 +306,3 @@ body {
|
||||
@apply !rounded-none !shadow-none;
|
||||
}
|
||||
}
|
||||
|
||||
/* MDX styles for code highlighting */
|
||||
@layer components {
|
||||
[data-rehype-pretty-code-figure] code {
|
||||
@apply grid min-w-full break-words rounded-none border-0 bg-transparent p-0;
|
||||
counter-reset: line;
|
||||
box-decoration-break: clone;
|
||||
}
|
||||
|
||||
[data-rehype-pretty-code-figure] [data-line] {
|
||||
@apply inline-block min-h-[1rem] w-full px-4 py-0.5;
|
||||
}
|
||||
|
||||
[data-rehype-pretty-code-figure] [data-line-numbers] [data-line] {
|
||||
@apply px-2;
|
||||
}
|
||||
|
||||
[data-rehype-pretty-code-title] {
|
||||
@apply mt-2 px-4 pt-6 text-sm font-medium text-foreground;
|
||||
}
|
||||
|
||||
[data-rehype-pretty-code-title] + pre {
|
||||
@apply mt-2;
|
||||
}
|
||||
|
||||
/* Standalone code elements */
|
||||
code {
|
||||
@apply relative rounded px-[0.3rem] py-[0.2rem] text-foreground bg-accent;
|
||||
}
|
||||
|
||||
/* Code elements inside pre */
|
||||
pre code {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@utility line-highlighted {
|
||||
[data-rehype-pretty-code-figure] & span {
|
||||
@apply relative;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user