This commit is contained in:
砂糖
2026-01-24 16:54:44 +08:00
commit 70f337bb92
186 changed files with 23792 additions and 0 deletions

29
components/mdx/Aside.tsx Normal file
View File

@@ -0,0 +1,29 @@
import { cn } from "@/lib/utils";
interface AsideProps {
icon?: string;
children?: React.ReactNode;
type?: "default" | "warning" | "danger";
}
export function Aside({
children,
icon,
type = "default",
...props
}: AsideProps) {
return (
<div
className={cn(
"flex border-5 py-3 px-4 ms-2 ms-md-0 my-10 rounded rounded-1 shadow",
"bg-[#6edff633] border-[#6edff633] border-l-[#f6ef6e]"
)}
{...props}
>
<div className="rounded rounded-1 text-center h-8 w-8 bg-[#6edff6] text-2xl relative top-[-30px] left-[-30px]">
{icon || "💡"}
</div>
<div>{children}</div>
</div>
);
}

View File

@@ -0,0 +1,27 @@
import { cn } from "@/lib/utils";
interface CalloutProps {
icon?: string;
children?: React.ReactNode;
type?: "default" | "warning" | "danger";
}
export function Callout({
children,
icon,
type = "default",
...props
}: CalloutProps) {
return (
<div
className={cn("my-6 flex items-start rounded-md border border-l-4 p-4", {
"border-red-900 bg-red-50": type === "danger",
"border-yellow-900 bg-yellow-50": type === "warning",
})}
{...props}
>
{icon && <span className="mr-4 text-2xl">{icon}</span>}
<div>{children}</div>
</div>
);
}

View File

@@ -0,0 +1,125 @@
import { Aside } from "@/components/mdx/Aside";
import { Callout } from "@/components/mdx/Callout";
import { MdxCard } from "@/components/mdx/MdxCard";
import React, { ReactNode } from "react";
interface HeadingProps {
level: 1 | 2 | 3 | 4 | 5 | 6;
className: string;
children: ReactNode;
}
const Heading: React.FC<HeadingProps> = ({ level, className, children }) => {
const HeadingTag = `h${level}` as keyof React.ElementType;
const headingId = children?.toString() ?? "";
return React.createElement(
HeadingTag,
{ id: headingId, className },
children
);
};
interface MDXComponentsProps {
[key: string]: React.FC<any>;
}
const MDXComponents: MDXComponentsProps = {
h1: (props) => (
<Heading level={1} className="text-4xl font-bold mt-8 mb-6" {...props} />
),
h2: (props) => (
<Heading
level={2}
className="text-3xl font-semibold mt-8 mb-6 border-b-2 border-gray-200 pb-2"
{...props}
/>
),
h3: (props) => (
<Heading
level={3}
className="text-2xl font-semibold mt-6 mb-4"
{...props}
/>
),
h4: (props) => (
<Heading level={4} className="text-xl font-semibold mt-6 mb-4" {...props} />
),
h5: (props) => (
<Heading level={5} className="text-lg font-semibold mt-6 mb-4" {...props} />
),
h6: (props) => (
<Heading
level={6}
className="text-base font-semibold mt-6 mb-4"
{...props}
/>
),
hr: (props) => <hr className="border-t border-gray-200 my-8" {...props} />,
p: (props) => (
<p
className="mt-6 mb-6 leading-relaxed text-gray-700 dark:text-gray-300"
{...props}
/>
),
a: (props) => (
<a
className="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 transition-colors underline underline-offset-4"
target="_blank"
{...props}
/>
),
ul: (props) => <ul className="list-disc pl-6 mt-0 mb-6" {...props} />,
ol: (props) => <ol className="list-decimal pl-6 mt-0 mb-6" {...props} />,
li: (props) => (
<li className="mb-3 text-gray-700 dark:text-gray-300" {...props} />
),
code: (props) => (
<code
className="bg-gray-100 dark:bg-gray-700 rounded px-2 py-1 font-mono text-sm"
{...props}
/>
),
pre: (props) => (
<pre
className="rounded-lg p-4 overflow-x-auto my-4 bg-gray-100 dark:bg-gray-800"
{...props}
/>
),
blockquote: (props) => (
<blockquote
className="pl-6 border-l-4 my-6 text-gray-600 dark:text-gray-400 italic"
{...props}
/>
),
img: (props) => (
<img className="rounded-lg border-2 border-gray-200 my-6" {...props} />
),
strong: (props) => <strong className="font-bold" {...props} />,
table: (props) => (
<div className="my-8 w-full overflow-x-auto">
<table
className="w-full shadow-sm rounded-lg overflow-hidden"
{...props}
/>
</div>
),
tr: (props) => <tr className="border-t border-gray-200" {...props} />,
th: (props) => (
<th
className="px-6 py-3 font-bold text-left bg-gray-100 dark:bg-gray-700 [&[align=center]]:text-center [&[align=right]]:text-right"
{...props}
/>
),
td: (props) => (
<td
className="px-6 py-4 text-left border-t border-gray-100 dark:border-gray-700 [&[align=center]]:text-center [&[align=right]]:text-right"
{...props}
/>
),
Aside,
Callout,
Card: MdxCard,
};
export default MDXComponents;

View File

@@ -0,0 +1,42 @@
import Link from "next/link";
import { cn } from "@/lib/utils";
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
href?: string;
disabled?: boolean;
}
export function MdxCard({
href,
className,
children,
disabled,
...props
}: CardProps) {
return (
<div
className={cn(
"group relative rounded-lg border p-6 shadow-md transition-shadow hover:shadow-lg",
disabled && "cursor-not-allowed opacity-60",
className
)}
{...props}
>
<div className="flex flex-col justify-between space-y-4">
<div className="space-y-2 [&>h3]:!mt-0 [&>h4]:!mt-0 [&>p]:text-muted-foreground">
{children}
</div>
</div>
{href && (
<Link
href={disabled ? "#" : href}
className="absolute inset-0"
prefetch={false}
>
<span className="sr-only">View</span>
</Link>
)}
</div>
);
}

View File

@@ -0,0 +1,104 @@
// components/mdx/TableOfContents.client.tsx
"use client";
import { useEffect, useState } from 'react';
interface TableOfContentsItem {
id: string;
text: string;
level: number;
}
interface TableOfContentsProps {
items: TableOfContentsItem[];
title?: string;
}
export default function TableOfContents({
items = [],
title = "目录"
}: TableOfContentsProps) {
const [activeId, setActiveId] = useState<string>("");
useEffect(() => {
if (items.length === 0) return;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setActiveId(entry.target.id);
}
});
},
{ rootMargin: "0% 0% -80% 0%" }
);
// 观察所有标题元素
items.forEach((item) => {
const element = document.getElementById(item.id);
if (element) {
observer.observe(element);
}
});
return () => {
items.forEach((item) => {
const element = document.getElementById(item.id);
if (element) {
observer.unobserve(element);
}
});
};
}, [items]);
const handleClick = (e: React.MouseEvent, id: string) => {
e.preventDefault();
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: "smooth" });
window.history.pushState(null, "", `#${id}`);
setActiveId(id);
}
};
if (!items || items.length === 0) {
return null;
}
return (
<aside className="fixed left-0 top-0 w-64 h-screen border-r border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 z-10 overflow-y-auto">
<div className="p-6">
<h3 className="text-lg font-semibold mb-4 text-gray-800 dark:text-gray-200">
{title}
</h3>
<nav>
<ul className="space-y-2 border-l-2 border-gray-200 dark:border-gray-700">
{items.map((item) => {
const isActive = activeId === item.id;
return (
<li
key={item.id}
style={{ marginLeft: `${(item.level - 2) * 0.75}rem` }}
className={`text-sm border-l-2 -ml-0.5 pl-4 transition-colors duration-200 ${isActive
? "border-blue-500 text-blue-600 dark:text-blue-400 font-medium"
: "border-transparent hover:border-blue-400"
} hover:text-blue-600 dark:hover:text-blue-400 cursor-pointer`}
>
<a
href={`#${item.id}`}
className={`block py-1 transition-all duration-200 ${isActive ? "translate-x-1" : "hover:translate-x-1"
}`}
onClick={(e) => handleClick(e, item.id)}
>
{item.text}
</a>
</li>
);
})}
</ul>
</nav>
</div>
</aside>
);
}