create-audora-next 0.1.6 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +15 -5
  2. package/index.ts +12 -4
  3. package/package.json +6 -1
  4. package/templates/blog/README.md +164 -0
  5. package/templates/blog/bun.lock +1341 -0
  6. package/templates/blog/env.example.template +5 -0
  7. package/templates/blog/eslint.config.mjs +18 -0
  8. package/templates/blog/gitignore.template +41 -0
  9. package/templates/blog/husky.template/pre-commit +16 -0
  10. package/templates/blog/lint-staged.config.mjs +17 -0
  11. package/templates/blog/next.config.ts +38 -0
  12. package/templates/blog/package.json +59 -0
  13. package/templates/blog/postcss.config.mjs +7 -0
  14. package/templates/blog/public/favicon/apple-touch-icon.png +0 -0
  15. package/templates/blog/public/favicon/favicon-96x96.png +0 -0
  16. package/templates/blog/public/favicon/favicon.ico +0 -0
  17. package/templates/blog/public/favicon/favicon.svg +1 -0
  18. package/templates/blog/public/favicon/site.webmanifest +21 -0
  19. package/templates/blog/public/favicon/web-app-manifest-192x192.png +0 -0
  20. package/templates/blog/public/favicon/web-app-manifest-512x512.png +0 -0
  21. package/templates/blog/public/images/screenshot-desktop-dark.webp +0 -0
  22. package/templates/blog/public/images/screenshot-desktop-light.webp +0 -0
  23. package/templates/blog/public/images/screenshot-mobile-dark.webp +0 -0
  24. package/templates/blog/public/images/screenshot-mobile-light.webp +0 -0
  25. package/templates/blog/src/app/blogs/[slug]/page.tsx +171 -0
  26. package/templates/blog/src/app/blogs/page.tsx +108 -0
  27. package/templates/blog/src/app/layout.tsx +60 -0
  28. package/templates/blog/src/app/llms-full.txt/route.ts +97 -0
  29. package/templates/blog/src/app/llms.txt/route.ts +40 -0
  30. package/templates/blog/src/app/manifest.ts +61 -0
  31. package/templates/blog/src/app/page.tsx +57 -0
  32. package/templates/blog/src/app/robots.ts +16 -0
  33. package/templates/blog/src/app/sitemap.ts +52 -0
  34. package/templates/blog/src/blogs/components/animated-blog-list.tsx +33 -0
  35. package/templates/blog/src/blogs/components/blog-post-card.tsx +46 -0
  36. package/templates/blog/src/blogs/components/blog-section.tsx +34 -0
  37. package/templates/blog/src/blogs/components/blog-table-of-contents.tsx +369 -0
  38. package/templates/blog/src/blogs/components/copy-button.tsx +46 -0
  39. package/templates/blog/src/blogs/components/mdx.tsx +225 -0
  40. package/templates/blog/src/blogs/content/cosketch/cosketch-canvas-engine.mdx +186 -0
  41. package/templates/blog/src/blogs/content/cosketch/cosketch-docker-architecture.mdx +175 -0
  42. package/templates/blog/src/blogs/content/cosketch/cosketch-eraser-and-selection.mdx +207 -0
  43. package/templates/blog/src/blogs/content/hello-world.mdx +66 -0
  44. package/templates/blog/src/blogs/data/mdx.ts +68 -0
  45. package/templates/blog/src/blogs/utils/extract-headings.ts +38 -0
  46. package/templates/blog/src/components/copyable-code.tsx +41 -0
  47. package/templates/blog/src/components/footer.tsx +25 -0
  48. package/templates/blog/src/components/header.tsx +27 -0
  49. package/templates/blog/src/components/icons.tsx +84 -0
  50. package/templates/blog/src/components/section-heading.tsx +11 -0
  51. package/templates/blog/src/components/theme-provider.tsx +11 -0
  52. package/templates/blog/src/components/theme-toggle.tsx +20 -0
  53. package/templates/blog/src/components/view-all-link.tsx +56 -0
  54. package/templates/blog/src/config/site.ts +19 -0
  55. package/templates/blog/src/data/llms.ts +112 -0
  56. package/templates/blog/src/data/site.ts +52 -0
  57. package/templates/blog/src/lib/seo.ts +190 -0
  58. package/templates/blog/src/lib/utils.ts +83 -0
  59. package/templates/blog/src/styles/globals.css +99 -0
  60. package/templates/blog/src/utils/cn.ts +7 -0
  61. package/templates/blog/tsconfig.json +34 -0
@@ -0,0 +1,369 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, useRef } from "react";
4
+ import { List, X, ChevronRight } from "lucide-react";
5
+ import { cn } from "@/lib/utils";
6
+ import type { Heading } from "@/blogs/utils/extract-headings";
7
+
8
+ type BlogTableOfContentsProps = {
9
+ headings: Heading[];
10
+ };
11
+
12
+ export function BlogTableOfContents({ headings }: BlogTableOfContentsProps) {
13
+ const [activeId, setActiveId] = useState<string>("");
14
+ const [isOpen, setIsOpen] = useState(false);
15
+ const [readingProgress, setReadingProgress] = useState(0);
16
+ const sheetRef = useRef<HTMLDivElement>(null);
17
+ const startY = useRef<number>(0);
18
+ const currentY = useRef<number>(0);
19
+ const isManualScroll = useRef(false);
20
+
21
+ // Track reading progress
22
+ useEffect(() => {
23
+ const updateProgress = () => {
24
+ const scrollTop = window.scrollY;
25
+ const docHeight =
26
+ document.documentElement.scrollHeight - window.innerHeight;
27
+ const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
28
+ setReadingProgress(Math.min(100, Math.max(0, progress)));
29
+ };
30
+
31
+ window.addEventListener("scroll", updateProgress, { passive: true });
32
+ updateProgress();
33
+ return () => window.removeEventListener("scroll", updateProgress);
34
+ }, []);
35
+
36
+ // Track active section using Intersection Observer
37
+ useEffect(() => {
38
+ const headingElements = headings
39
+ .map(({ id }) => document.getElementById(id))
40
+ .filter(Boolean);
41
+
42
+ if (headingElements.length === 0) return;
43
+
44
+ const observer = new IntersectionObserver(
45
+ (entries) => {
46
+ if (isManualScroll.current) return; // Skip updates during manual scroll
47
+
48
+ entries.forEach((entry) => {
49
+ if (entry.isIntersecting) {
50
+ setActiveId(entry.target.id);
51
+ }
52
+ });
53
+ },
54
+ {
55
+ rootMargin: "0px 0px -20% 0px", // Expanded detection area to 80% of viewport
56
+ threshold: 0.5,
57
+ },
58
+ );
59
+
60
+ headingElements.forEach((el) => el && observer.observe(el));
61
+
62
+ return () => {
63
+ headingElements.forEach((el) => el && observer.unobserve(el));
64
+ };
65
+ }, [headings]);
66
+
67
+ // Handle bottom of page for last item
68
+ useEffect(() => {
69
+ const handleScroll = () => {
70
+ if (isManualScroll.current) return; // Skip during manual scroll
71
+
72
+ if (
73
+ window.innerHeight + window.scrollY >=
74
+ document.body.offsetHeight - 100
75
+ ) {
76
+ if (headings.length > 0) {
77
+ setActiveId(headings[headings.length - 1].id);
78
+ }
79
+ }
80
+ };
81
+
82
+ window.addEventListener("scroll", handleScroll, { passive: true });
83
+ return () => window.removeEventListener("scroll", handleScroll);
84
+ }, [headings]);
85
+
86
+ // Lock body scroll when mobile sheet is open
87
+ useEffect(() => {
88
+ if (isOpen) {
89
+ document.body.style.overflow = "hidden";
90
+ } else {
91
+ document.body.style.overflow = "";
92
+ }
93
+ return () => {
94
+ document.body.style.overflow = "";
95
+ };
96
+ }, [isOpen]);
97
+
98
+ const handleClick = (e: React.MouseEvent<HTMLAnchorElement>, id: string) => {
99
+ e.preventDefault();
100
+ const element = document.getElementById(id);
101
+ if (element) {
102
+ setIsOpen(false);
103
+ isManualScroll.current = true; // Lock observer
104
+ setActiveId(id); // Set immediate active state
105
+
106
+ element.scrollIntoView({ behavior: "smooth", block: "start" });
107
+
108
+ // Unlock after animation (approx. 1000ms)
109
+ setTimeout(() => {
110
+ isManualScroll.current = false;
111
+ }, 1000);
112
+ }
113
+ };
114
+
115
+ // Touch handlers for swipe-to-dismiss
116
+ // Touch handlers for swipe-to-dismiss
117
+ const handleTouchStart = (e: React.TouchEvent) => {
118
+ startY.current = e.touches[0].clientY;
119
+ };
120
+
121
+ const handleTouchMove = (e: React.TouchEvent) => {
122
+ currentY.current = e.touches[0].clientY;
123
+ const deltaY = currentY.current - startY.current;
124
+
125
+ if (deltaY > 0 && sheetRef.current) {
126
+ sheetRef.current.style.transform = `translateY(${deltaY}px)`;
127
+ }
128
+ };
129
+
130
+ const handleTouchEnd = () => {
131
+ const deltaY = currentY.current - startY.current;
132
+
133
+ if (sheetRef.current) {
134
+ sheetRef.current.style.transform = "";
135
+ }
136
+
137
+ if (deltaY > 80) {
138
+ setIsOpen(false);
139
+ }
140
+
141
+ startY.current = 0;
142
+ currentY.current = 0;
143
+ };
144
+
145
+ if (headings.length === 0) return null;
146
+
147
+ return (
148
+ <>
149
+ {/* Mobile Floating Button - Compact with progress ring */}
150
+ <button
151
+ onClick={() => setIsOpen(!isOpen)}
152
+ className={cn(
153
+ "fixed right-4 bottom-20 z-50 flex h-11 w-11 items-center justify-center",
154
+ "rounded-full border border-border/50 bg-card/95 shadow-lg backdrop-blur-sm",
155
+ "transition-all duration-300 hover:scale-105 hover:border-border",
156
+ "xl:hidden",
157
+ )}
158
+ aria-label={
159
+ isOpen ? "Close table of contents" : "Open table of contents"
160
+ }
161
+ >
162
+ {/* Progress ring */}
163
+ <svg
164
+ className="absolute inset-0 h-full w-full -rotate-90"
165
+ viewBox="0 0 44 44"
166
+ >
167
+ <circle
168
+ cx="22"
169
+ cy="22"
170
+ r="20"
171
+ fill="none"
172
+ stroke="currentColor"
173
+ strokeWidth="2"
174
+ className="text-border/30"
175
+ />
176
+ <circle
177
+ cx="22"
178
+ cy="22"
179
+ r="20"
180
+ fill="none"
181
+ stroke="currentColor"
182
+ strokeWidth="2.5"
183
+ strokeDasharray={`${2 * Math.PI * 20}`}
184
+ strokeDashoffset={`${2 * Math.PI * 20 * (1 - readingProgress / 100)}`}
185
+ className="text-primary transition-all duration-200"
186
+ strokeLinecap="round"
187
+ />
188
+ </svg>
189
+ <List className="h-4 w-4 text-foreground" />
190
+ </button>
191
+
192
+ {/* Mobile Overlay */}
193
+ <div
194
+ className={cn(
195
+ "fixed inset-0 z-40 bg-black/50 backdrop-blur-sm transition-opacity duration-200 xl:hidden",
196
+ isOpen ? "opacity-100" : "pointer-events-none opacity-0",
197
+ )}
198
+ onClick={() => setIsOpen(false)}
199
+ />
200
+
201
+ {/* Mobile Bottom Sheet - Compact */}
202
+ <div
203
+ ref={sheetRef}
204
+ className={cn(
205
+ "fixed inset-x-0 bottom-0 z-50 max-h-[50vh] xl:hidden",
206
+ "rounded-t-2xl border-t border-border/50 bg-card shadow-xl",
207
+ "transition-transform duration-200 ease-out",
208
+ isOpen ? "translate-y-0" : "translate-y-full",
209
+ )}
210
+ onTouchStart={handleTouchStart}
211
+ onTouchMove={handleTouchMove}
212
+ onTouchEnd={handleTouchEnd}
213
+ >
214
+ {/* Drag handle */}
215
+ <div className="flex justify-center py-2">
216
+ <div className="h-1 w-8 rounded-full bg-muted-foreground/30" />
217
+ </div>
218
+
219
+ {/* Header - Compact */}
220
+ <div className="flex items-center justify-between px-4 pb-2">
221
+ <div className="flex items-center gap-2">
222
+ <List className="h-4 w-4 text-primary" />
223
+ <span className="text-sm font-medium text-foreground">
224
+ Contents
225
+ </span>
226
+ <span className="text-xs text-muted-foreground">
227
+ ({Math.round(readingProgress)}%)
228
+ </span>
229
+ </div>
230
+ <button
231
+ onClick={() => setIsOpen(false)}
232
+ className="flex h-7 w-7 items-center justify-center rounded-full text-muted-foreground hover:bg-muted hover:text-foreground"
233
+ aria-label="Close"
234
+ >
235
+ <X className="h-4 w-4" />
236
+ </button>
237
+ </div>
238
+
239
+ {/* Progress bar */}
240
+ <div className="mx-4 h-0.5 overflow-hidden rounded-full bg-border/30">
241
+ <div
242
+ className="h-full rounded-full bg-primary transition-all duration-150"
243
+ style={{ width: `${readingProgress}%` }}
244
+ />
245
+ </div>
246
+
247
+ {/* Links - Compact */}
248
+ <ul className="mt-2 flex max-h-[calc(50vh-80px)] flex-col overflow-y-auto px-3 pb-6">
249
+ {headings.map(({ id, text, level }) => (
250
+ <li key={id} className="relative">
251
+ <a
252
+ href={`#${id}`}
253
+ onClick={(e) => handleClick(e, id)}
254
+ className={cn(
255
+ "flex items-center gap-2 rounded-lg py-2.5 pr-3 transition-colors",
256
+ "active:bg-muted",
257
+ level === 3 ? "pl-8 text-xs" : "pl-3 text-sm",
258
+ activeId === id
259
+ ? "bg-primary/10 font-medium text-foreground"
260
+ : "text-muted-foreground hover:text-foreground",
261
+ )}
262
+ >
263
+ {/* Subheading connector line (Mobile) */}
264
+ {level === 3 && (
265
+ <span
266
+ className={cn(
267
+ "absolute top-1/2 left-3 h-px w-3 -translate-y-1/2 rounded-full",
268
+ activeId === id ? "bg-primary/40" : "bg-border/40",
269
+ )}
270
+ />
271
+ )}
272
+
273
+ {/* Active dot */}
274
+ <span
275
+ className={cn(
276
+ "h-1.5 w-1.5 shrink-0 rounded-full transition-colors",
277
+ activeId === id ? "bg-primary" : "bg-muted-foreground/30",
278
+ )}
279
+ />
280
+ <span className="line-clamp-1">{text}</span>
281
+ </a>
282
+ </li>
283
+ ))}
284
+ </ul>
285
+ </div>
286
+
287
+ {/* Desktop Sidebar - Premium design */}
288
+ <nav className="hidden xl:block" aria-label="Table of contents">
289
+ {/* Glassmorphism card */}
290
+ <div className="relative overflow-hidden rounded-2xl border border-white/10 bg-gradient-to-b from-white/5 to-transparent p-5 shadow-xl backdrop-blur-xl dark:from-white/5">
291
+ {/* Subtle gradient overlay */}
292
+ <div className="pointer-events-none absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-transparent" />
293
+
294
+ {/* Header - icon hidden on desktop */}
295
+ <div className="relative flex items-center gap-3">
296
+ <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/15 shadow-sm xl:hidden">
297
+ <List className="h-4 w-4 text-primary" />
298
+ </div>
299
+ <div>
300
+ <h3 className="text-sm font-semibold text-foreground">
301
+ On this page
302
+ </h3>
303
+ <p className="text-[11px] text-muted-foreground">
304
+ {headings.length} sections
305
+ </p>
306
+ </div>
307
+ </div>
308
+
309
+ {/* Progress section */}
310
+ <div className="relative mt-4 flex items-center gap-3">
311
+ <div className="h-1.5 flex-1 overflow-hidden rounded-full bg-border/30">
312
+ <div
313
+ className="h-full rounded-full bg-gradient-to-r from-primary to-primary/70 transition-all duration-300 ease-out"
314
+ style={{ width: `${readingProgress}%` }}
315
+ />
316
+ </div>
317
+ <span className="text-xs font-medium text-muted-foreground tabular-nums">
318
+ {Math.round(readingProgress)}%
319
+ </span>
320
+ </div>
321
+
322
+ {/* Links */}
323
+ <ul className="relative mt-4 flex flex-col gap-1 border-l border-border/30 pl-3">
324
+ {headings.map(({ id, text, level }) => (
325
+ <li key={id} className="relative">
326
+ <a
327
+ href={`#${id}`}
328
+ onClick={(e) => handleClick(e, id)}
329
+ className={cn(
330
+ "group relative block rounded-lg py-1.5 pr-2 transition-all duration-200",
331
+ level === 3 ? "pl-5 text-xs" : "text-[13px]",
332
+ activeId === id
333
+ ? "font-medium text-foreground"
334
+ : "text-muted-foreground hover:text-foreground",
335
+ )}
336
+ >
337
+ {/* Subheading connector line */}
338
+ {level === 3 && (
339
+ <span
340
+ className={cn(
341
+ "absolute top-1/2 left-1 h-px w-2.5 -translate-y-1/2 rounded-full transition-colors duration-200",
342
+ activeId === id
343
+ ? "bg-primary/50"
344
+ : "bg-border/40 group-hover:bg-border/80",
345
+ )}
346
+ />
347
+ )}
348
+
349
+ {/* Active indicator dot on left border */}
350
+ <span
351
+ className={cn(
352
+ "absolute top-1/2 -left-3 h-2 w-2 -translate-x-1/2 -translate-y-1/2 rounded-full transition-all duration-200",
353
+ activeId === id
354
+ ? "scale-100 bg-primary shadow-sm shadow-primary/50"
355
+ : "scale-0 bg-border",
356
+ )}
357
+ />
358
+ <span className="relative z-10 line-clamp-1">{text}</span>
359
+ </a>
360
+ </li>
361
+ ))}
362
+ </ul>
363
+ </div>
364
+ </nav>
365
+ </>
366
+ );
367
+ }
368
+
369
+ export default BlogTableOfContents;
@@ -0,0 +1,46 @@
1
+ "use client";
2
+
3
+ import { CheckIcon, CopyIcon } from "lucide-react";
4
+ import { useState } from "react";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ export function CopyButton({
8
+ text,
9
+ className,
10
+ }: {
11
+ text: string;
12
+ className?: string;
13
+ }) {
14
+ const [isCopied, setIsCopied] = useState(false);
15
+
16
+ // Extract text from children if it's a React element, or use string directly
17
+ // However, in the MDX pre component context, we usually pass the raw code string.
18
+ // The 'text' prop here is expected to be the raw code.
19
+
20
+ const copy = async () => {
21
+ try {
22
+ await navigator.clipboard.writeText(text);
23
+ setIsCopied(true);
24
+ setTimeout(() => setIsCopied(false), 2000);
25
+ } catch (err) {
26
+ console.error("Failed to copy text: ", err);
27
+ }
28
+ };
29
+
30
+ return (
31
+ <button
32
+ onClick={copy}
33
+ className={cn(
34
+ "inline-flex size-7 items-center justify-center rounded-md border border-neutral-700 bg-neutral-800 text-neutral-400 opacity-0 transition-all group-hover:opacity-100 hover:bg-neutral-700 hover:text-neutral-200 focus-visible:opacity-100",
35
+ className,
36
+ )}
37
+ aria-label="Copy code"
38
+ >
39
+ {isCopied ? (
40
+ <CheckIcon className="size-3.5" />
41
+ ) : (
42
+ <CopyIcon className="size-3.5" />
43
+ )}
44
+ </button>
45
+ );
46
+ }
@@ -0,0 +1,225 @@
1
+ import Link from "next/link";
2
+ import Image from "next/image";
3
+ import { MDXRemote, type MDXRemoteProps } from "next-mdx-remote/rsc";
4
+ import React from "react";
5
+ import rehypePrettyCode from "rehype-pretty-code";
6
+ import rehypeSlug from "rehype-slug";
7
+ import rehypeAutolinkHeadings from "rehype-autolink-headings";
8
+ import { CopyButton } from "@/blogs/components/copy-button";
9
+
10
+ function Table({ data }: { data: { headers: string[]; rows: string[][] } }) {
11
+ const headers = data.headers.map((header, index) => (
12
+ <th key={index} className="px-4 py-2 text-left font-bold">
13
+ {header}
14
+ </th>
15
+ ));
16
+ const rows = data.rows.map((row, index) => (
17
+ <tr
18
+ key={index}
19
+ className="border-t border-neutral-200 dark:border-neutral-700"
20
+ >
21
+ {row.map((cell, cellIndex) => (
22
+ <td key={cellIndex} className="px-4 py-2 text-left">
23
+ {cell}
24
+ </td>
25
+ ))}
26
+ </tr>
27
+ ));
28
+
29
+ return (
30
+ <div className="my-6 w-full overflow-y-auto">
31
+ <table className="w-full text-sm">
32
+ <thead>
33
+ <tr className="border-b border-neutral-200 dark:border-neutral-700">
34
+ {headers}
35
+ </tr>
36
+ </thead>
37
+ <tbody>{rows}</tbody>
38
+ </table>
39
+ </div>
40
+ );
41
+ }
42
+
43
+ type CustomLinkProps = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
44
+ href?: string;
45
+ };
46
+
47
+ function CustomLink(props: CustomLinkProps) {
48
+ const { href, children, ...rest } = props;
49
+
50
+ if (typeof href === "string" && href.startsWith("/")) {
51
+ return (
52
+ <Link
53
+ href={href}
54
+ className="cursor-pointer text-primary underline underline-offset-4 transition-colors hover:text-primary/80"
55
+ {...rest}
56
+ >
57
+ {children}
58
+ </Link>
59
+ );
60
+ }
61
+
62
+ if (typeof href === "string" && href.startsWith("#")) {
63
+ return (
64
+ <a
65
+ href={href}
66
+ className="cursor-pointer text-primary underline underline-offset-4 transition-colors hover:text-primary/80"
67
+ {...rest}
68
+ >
69
+ {children}
70
+ </a>
71
+ );
72
+ }
73
+
74
+ return (
75
+ <a
76
+ target="_blank"
77
+ rel="noopener noreferrer"
78
+ className="cursor-pointer text-primary underline underline-offset-4 transition-colors hover:text-primary/80"
79
+ href={href}
80
+ {...rest}
81
+ >
82
+ {children}
83
+ </a>
84
+ );
85
+ }
86
+
87
+ function RoundedImage(props: React.ComponentProps<typeof Image>) {
88
+ // eslint-disable-next-line jsx-a11y/alt-text
89
+ return <Image className="rounded-lg" {...props} />;
90
+ }
91
+
92
+ type MDXComponents = NonNullable<MDXRemoteProps["components"]>;
93
+
94
+ // Helper to recursively extract text from React children
95
+ function extractText(node: React.ReactNode): string {
96
+ if (typeof node === "string") return node;
97
+ if (typeof node === "number") return String(node);
98
+ if (!node) return "";
99
+ if (Array.isArray(node)) return node.map(extractText).join("");
100
+ if (React.isValidElement(node)) {
101
+ return extractText((node.props as { children?: React.ReactNode }).children);
102
+ }
103
+ return "";
104
+ }
105
+
106
+ const components: MDXComponents = {
107
+ h1: (props: React.ComponentProps<"h1">) => (
108
+ <h1
109
+ className="mt-2 scroll-m-20 text-4xl font-bold tracking-tight"
110
+ {...props}
111
+ />
112
+ ),
113
+ h2: (props: React.ComponentProps<"h2">) => (
114
+ <h2
115
+ className="mt-10 scroll-m-20 border-b pb-1 text-3xl font-semibold tracking-tight first:mt-0"
116
+ {...props}
117
+ />
118
+ ),
119
+ h3: (props: React.ComponentProps<"h3">) => (
120
+ <h3
121
+ className="mt-8 scroll-m-20 text-2xl font-semibold tracking-tight"
122
+ {...props}
123
+ />
124
+ ),
125
+ h4: (props: React.ComponentProps<"h4">) => (
126
+ <h4
127
+ className="mt-8 scroll-m-20 text-xl font-semibold tracking-tight"
128
+ {...props}
129
+ />
130
+ ),
131
+ p: (props: React.ComponentProps<"p">) => (
132
+ <p className="leading-7 [&:not(:first-child)]:mt-6" {...props} />
133
+ ),
134
+ ul: (props: React.ComponentProps<"ul">) => (
135
+ <ul className="my-6 ml-6 list-disc [&>li]:mt-2" {...props} />
136
+ ),
137
+ ol: (props: React.ComponentProps<"ol">) => (
138
+ <ol className="my-6 ml-6 list-decimal [&>li]:mt-2" {...props} />
139
+ ),
140
+ li: (props: React.ComponentProps<"li">) => (
141
+ <li className="leading-7" {...props} />
142
+ ),
143
+ blockquote: (props: React.ComponentProps<"blockquote">) => (
144
+ <blockquote className="mt-6 border-l-2 pl-6 italic" {...props} />
145
+ ),
146
+ hr: (props: React.ComponentProps<"hr">) => (
147
+ <hr className="my-4 md:my-8" {...props} />
148
+ ),
149
+ table: (props: React.ComponentProps<"table">) => (
150
+ <div className="my-6 w-full overflow-y-auto">
151
+ <table className="w-full text-sm" {...props} />
152
+ </div>
153
+ ),
154
+ tr: (props: React.ComponentProps<"tr">) => (
155
+ <tr className="m-0 border-t p-0 even:bg-muted" {...props} />
156
+ ),
157
+ th: (props: React.ComponentProps<"th">) => (
158
+ <th
159
+ className="border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right"
160
+ {...props}
161
+ />
162
+ ),
163
+ td: (props: React.ComponentProps<"td">) => (
164
+ <td
165
+ className="border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right"
166
+ {...props}
167
+ />
168
+ ),
169
+ pre: (props: React.ComponentProps<"pre">) => {
170
+ const { children, ...rest } = props;
171
+
172
+ let textToCopy = "";
173
+ if (React.isValidElement(children) && children.type === "code") {
174
+ textToCopy = extractText(
175
+ (children.props as { children?: React.ReactNode }).children,
176
+ );
177
+ }
178
+
179
+ return (
180
+ <div className="group relative my-6 overflow-hidden rounded-xl border border-border bg-[#141517]">
181
+ <div className="absolute top-3 right-3 z-20 opacity-0 transition-opacity group-hover:opacity-100">
182
+ {textToCopy && <CopyButton text={textToCopy} />}
183
+ </div>
184
+ <pre className="overflow-x-auto p-4 text-sm" {...rest}>
185
+ {children}
186
+ </pre>
187
+ </div>
188
+ );
189
+ },
190
+ a: CustomLink,
191
+ img: RoundedImage,
192
+ Table,
193
+ };
194
+
195
+ export function CustomMDX(props: MDXRemoteProps) {
196
+ return (
197
+ <MDXRemote
198
+ {...props}
199
+ components={{ ...components, ...(props.components || {}) }}
200
+ options={{
201
+ mdxOptions: {
202
+ rehypePlugins: [
203
+ rehypeSlug,
204
+ [
205
+ rehypePrettyCode,
206
+ {
207
+ theme: "github-dark-default",
208
+ keepBackground: true,
209
+ },
210
+ ],
211
+ [
212
+ rehypeAutolinkHeadings,
213
+ {
214
+ properties: {
215
+ className: ["subheading-anchor"],
216
+ ariaLabel: "Link to section",
217
+ },
218
+ },
219
+ ],
220
+ ],
221
+ },
222
+ }}
223
+ />
224
+ );
225
+ }