create-unmint 1.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 (55) hide show
  1. package/README.md +57 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +499 -0
  4. package/package.json +48 -0
  5. package/template/LICENSE +21 -0
  6. package/template/README.md +278 -0
  7. package/template/__tests__/components/callout.test.tsx +46 -0
  8. package/template/__tests__/components/card.test.tsx +59 -0
  9. package/template/__tests__/components/tabs.test.tsx +61 -0
  10. package/template/__tests__/theme-config.test.ts +49 -0
  11. package/template/__tests__/utils.test.ts +25 -0
  12. package/template/app/api/og/route.tsx +90 -0
  13. package/template/app/api/search/route.ts +6 -0
  14. package/template/app/components/docs/docs-pager.tsx +41 -0
  15. package/template/app/components/docs/docs-sidebar.tsx +143 -0
  16. package/template/app/components/docs/docs-toc.tsx +61 -0
  17. package/template/app/components/docs/mdx/accordion.tsx +54 -0
  18. package/template/app/components/docs/mdx/callout.tsx +102 -0
  19. package/template/app/components/docs/mdx/card.tsx +110 -0
  20. package/template/app/components/docs/mdx/code-block.tsx +42 -0
  21. package/template/app/components/docs/mdx/frame.tsx +14 -0
  22. package/template/app/components/docs/mdx/index.tsx +167 -0
  23. package/template/app/components/docs/mdx/pre.tsx +82 -0
  24. package/template/app/components/docs/mdx/steps.tsx +59 -0
  25. package/template/app/components/docs/mdx/tabs.tsx +60 -0
  26. package/template/app/components/docs/mdx/youtube.tsx +18 -0
  27. package/template/app/components/docs/search-dialog.tsx +281 -0
  28. package/template/app/components/docs/theme-toggle.tsx +35 -0
  29. package/template/app/docs/[[...slug]]/page.tsx +139 -0
  30. package/template/app/docs/layout.tsx +98 -0
  31. package/template/app/globals.css +151 -0
  32. package/template/app/layout.tsx +33 -0
  33. package/template/app/page.tsx +5 -0
  34. package/template/app/providers/theme-provider.tsx +8 -0
  35. package/template/content/docs/components.mdx +82 -0
  36. package/template/content/docs/customization.mdx +34 -0
  37. package/template/content/docs/deployment.mdx +28 -0
  38. package/template/content/docs/index.mdx +91 -0
  39. package/template/content/docs/meta.json +13 -0
  40. package/template/content/docs/quickstart.mdx +110 -0
  41. package/template/content/docs/theming.mdx +41 -0
  42. package/template/lib/docs-source.ts +7 -0
  43. package/template/lib/theme-config.ts +89 -0
  44. package/template/lib/utils.ts +6 -0
  45. package/template/next.config.mjs +10 -0
  46. package/template/package-lock.json +10695 -0
  47. package/template/package.json +45 -0
  48. package/template/postcss.config.mjs +7 -0
  49. package/template/public/logo.png +0 -0
  50. package/template/public/logo.svg +9 -0
  51. package/template/public/logo.txt +1 -0
  52. package/template/source.config.ts +22 -0
  53. package/template/tailwind.config.ts +34 -0
  54. package/template/tsconfig.json +33 -0
  55. package/template/vitest.config.ts +16 -0
@@ -0,0 +1,143 @@
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+ import { usePathname } from 'next/navigation'
5
+ import { cn } from '@/lib/utils'
6
+ import { siteConfig } from '@/lib/theme-config'
7
+ import type { Root, Node } from 'fumadocs-core/page-tree'
8
+
9
+ interface DocsSidebarProps {
10
+ tree: Root
11
+ }
12
+
13
+ export function DocsSidebar({ tree }: DocsSidebarProps) {
14
+ const pathname = usePathname()
15
+
16
+ return (
17
+ <aside className="hidden lg:block w-64 shrink-0">
18
+ <nav className="sticky top-36 max-h-[calc(100vh-10rem)] overflow-y-auto pb-10 pr-4">
19
+ {/* Quick links */}
20
+ <div className="mb-6 pb-5 border-b border-border">
21
+ <ul className="space-y-2">
22
+ <li>
23
+ <Link
24
+ href="/docs"
25
+ className="flex items-center gap-3 py-1 text-sm text-[var(--accent)] font-medium hover:opacity-80 transition-opacity"
26
+ >
27
+ <span className="flex items-center justify-center w-7 h-7 rounded-md bg-[var(--accent-muted)]">
28
+ <svg className="w-4 h-4 text-[var(--accent)]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
29
+ <path strokeLinecap="round" strokeLinejoin="round" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
30
+ </svg>
31
+ </span>
32
+ Documentation
33
+ </Link>
34
+ </li>
35
+ {siteConfig.links.github && (
36
+ <li>
37
+ <a
38
+ href={siteConfig.links.github}
39
+ target="_blank"
40
+ rel="noopener noreferrer"
41
+ className="flex items-center gap-3 py-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
42
+ >
43
+ <span className="flex items-center justify-center w-7 h-7 rounded-md bg-gray-100 dark:bg-gray-800">
44
+ <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
45
+ <path fillRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clipRule="evenodd" />
46
+ </svg>
47
+ </span>
48
+ GitHub
49
+ </a>
50
+ </li>
51
+ )}
52
+ {siteConfig.links.support && (
53
+ <li>
54
+ <a
55
+ href={siteConfig.links.support}
56
+ className="flex items-center gap-3 py-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
57
+ >
58
+ <span className="flex items-center justify-center w-7 h-7 rounded-md bg-gray-100 dark:bg-gray-800">
59
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
60
+ <path strokeLinecap="round" strokeLinejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
61
+ </svg>
62
+ </span>
63
+ Support
64
+ </a>
65
+ </li>
66
+ )}
67
+ </ul>
68
+ </div>
69
+
70
+ <SidebarNodes nodes={tree.children} pathname={pathname} level={0} />
71
+ </nav>
72
+ </aside>
73
+ )
74
+ }
75
+
76
+ interface SidebarNodesProps {
77
+ nodes: Node[]
78
+ pathname: string
79
+ level: number
80
+ }
81
+
82
+ function SidebarNodes({ nodes, pathname, level }: SidebarNodesProps) {
83
+ return (
84
+ <div className="space-y-1">
85
+ {nodes.map((node, index) => (
86
+ <SidebarNode key={index} node={node} pathname={pathname} level={level} />
87
+ ))}
88
+ </div>
89
+ )
90
+ }
91
+
92
+ interface SidebarNodeProps {
93
+ node: Node
94
+ pathname: string
95
+ level: number
96
+ }
97
+
98
+ function SidebarNode({ node, pathname, level }: SidebarNodeProps) {
99
+ if (node.type === 'separator') {
100
+ return (
101
+ <div className="pt-4 first:pt-0">
102
+ <h5 className="text-sm font-semibold text-foreground mb-1.5">
103
+ {node.name}
104
+ </h5>
105
+ </div>
106
+ )
107
+ }
108
+
109
+ if (node.type === 'folder') {
110
+ return (
111
+ <div>
112
+ <span className="block py-1 text-sm font-medium text-muted-foreground">
113
+ {node.name}
114
+ </span>
115
+ {node.children && (
116
+ <ul className="ml-3 mt-1 space-y-0.5 border-l border-border pl-3">
117
+ {node.children.map((child, index) => (
118
+ <SidebarNode key={index} node={child} pathname={pathname} level={level + 1} />
119
+ ))}
120
+ </ul>
121
+ )}
122
+ </div>
123
+ )
124
+ }
125
+
126
+ const isActive = pathname === node.url
127
+
128
+ return (
129
+ <li className="list-none">
130
+ <Link
131
+ href={node.url}
132
+ className={cn(
133
+ 'flex items-center gap-2 py-1 px-2 text-sm transition-colors rounded-md',
134
+ isActive
135
+ ? 'text-[var(--accent)] font-medium bg-[var(--accent-muted)]'
136
+ : 'text-muted-foreground hover:text-foreground'
137
+ )}
138
+ >
139
+ <span>{node.name}</span>
140
+ </Link>
141
+ </li>
142
+ )
143
+ }
@@ -0,0 +1,61 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { cn } from '@/lib/utils'
5
+ import type { TOCItemType } from 'fumadocs-core/toc'
6
+
7
+ interface DocsTOCProps {
8
+ toc: TOCItemType[]
9
+ }
10
+
11
+ export function DocsTOC({ toc }: DocsTOCProps) {
12
+ const [activeId, setActiveId] = useState<string>('')
13
+
14
+ useEffect(() => {
15
+ const observer = new IntersectionObserver(
16
+ (entries) => {
17
+ entries.forEach((entry) => {
18
+ if (entry.isIntersecting) {
19
+ setActiveId(entry.target.id)
20
+ }
21
+ })
22
+ },
23
+ { rootMargin: '-100px 0px -66%' }
24
+ )
25
+
26
+ const headings = document.querySelectorAll('h2, h3')
27
+ headings.forEach((heading) => observer.observe(heading))
28
+
29
+ return () => {
30
+ headings.forEach((heading) => observer.unobserve(heading))
31
+ }
32
+ }, [])
33
+
34
+ if (!toc || toc.length === 0) return null
35
+
36
+ return (
37
+ <aside className="hidden xl:block w-56 shrink-0">
38
+ <nav className="sticky top-36 max-h-[calc(100vh-10rem)] overflow-y-auto">
39
+ <p className="text-sm font-semibold text-foreground mb-4">On this page</p>
40
+ <ul className="space-y-2 text-sm">
41
+ {toc.map((item) => (
42
+ <li key={item.url}>
43
+ <a
44
+ href={item.url}
45
+ className={cn(
46
+ 'block py-1 transition-colors',
47
+ item.depth === 3 && 'pl-4',
48
+ activeId === item.url.slice(1)
49
+ ? 'text-[var(--accent)] font-medium'
50
+ : 'text-muted-foreground hover:text-foreground'
51
+ )}
52
+ >
53
+ {item.title}
54
+ </a>
55
+ </li>
56
+ ))}
57
+ </ul>
58
+ </nav>
59
+ </aside>
60
+ )
61
+ }
@@ -0,0 +1,54 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { cn } from '@/lib/utils'
5
+
6
+ interface AccordionGroupProps {
7
+ children: React.ReactNode
8
+ }
9
+
10
+ export function AccordionGroup({ children }: AccordionGroupProps) {
11
+ return (
12
+ <div className="my-6 divide-y divide-border rounded-lg border border-border">
13
+ {children}
14
+ </div>
15
+ )
16
+ }
17
+
18
+ interface AccordionProps {
19
+ title: string
20
+ children: React.ReactNode
21
+ defaultOpen?: boolean
22
+ }
23
+
24
+ export function Accordion({ title, children, defaultOpen = false }: AccordionProps) {
25
+ const [isOpen, setIsOpen] = useState(defaultOpen)
26
+
27
+ return (
28
+ <div>
29
+ <button
30
+ onClick={() => setIsOpen(!isOpen)}
31
+ className="flex w-full items-center justify-between px-4 py-4 text-left font-medium text-foreground hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors"
32
+ >
33
+ <span>{title}</span>
34
+ <svg
35
+ className={cn(
36
+ 'w-5 h-5 text-muted-foreground transition-transform duration-200',
37
+ isOpen && 'rotate-180'
38
+ )}
39
+ fill="none"
40
+ viewBox="0 0 24 24"
41
+ stroke="currentColor"
42
+ strokeWidth={2}
43
+ >
44
+ <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
45
+ </svg>
46
+ </button>
47
+ {isOpen && (
48
+ <div className="px-4 pb-4 text-muted-foreground [&>p]:m-0">
49
+ {children}
50
+ </div>
51
+ )}
52
+ </div>
53
+ )
54
+ }
@@ -0,0 +1,102 @@
1
+ import { cn } from '@/lib/utils'
2
+
3
+ interface CalloutProps {
4
+ children: React.ReactNode
5
+ title?: string
6
+ }
7
+
8
+ const calloutStyles = {
9
+ info: {
10
+ container: 'bg-blue-50 dark:bg-blue-950/30 border-blue-200 dark:border-blue-800',
11
+ icon: 'text-blue-600',
12
+ title: 'text-blue-800 dark:text-blue-400',
13
+ content: 'text-blue-700 dark:text-blue-300',
14
+ },
15
+ tip: {
16
+ container: 'bg-emerald-50 dark:bg-emerald-950/30 border-emerald-200 dark:border-emerald-800',
17
+ icon: 'text-emerald-600',
18
+ title: 'text-emerald-800 dark:text-emerald-400',
19
+ content: 'text-emerald-700 dark:text-emerald-300',
20
+ },
21
+ warning: {
22
+ container: 'bg-amber-50 dark:bg-amber-950/30 border-amber-200 dark:border-amber-800',
23
+ icon: 'text-amber-600',
24
+ title: 'text-amber-800 dark:text-amber-400',
25
+ content: 'text-amber-700 dark:text-amber-300',
26
+ },
27
+ note: {
28
+ container: 'bg-gray-50 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700',
29
+ icon: 'text-gray-600',
30
+ title: 'text-gray-800 dark:text-gray-200',
31
+ content: 'text-gray-700 dark:text-gray-300',
32
+ },
33
+ check: {
34
+ container: 'bg-green-50 dark:bg-green-950/30 border-green-200 dark:border-green-800',
35
+ icon: 'text-green-600',
36
+ title: 'text-green-800 dark:text-green-400',
37
+ content: 'text-green-700 dark:text-green-300',
38
+ },
39
+ }
40
+
41
+ function createCallout(type: keyof typeof calloutStyles, icon: React.ReactNode, defaultTitle: string) {
42
+ return function Callout({ children, title }: CalloutProps) {
43
+ const styles = calloutStyles[type]
44
+ return (
45
+ <div className={cn('my-6 rounded-lg border p-4', styles.container)}>
46
+ <div className="flex gap-3">
47
+ <div className={cn('mt-0.5 shrink-0', styles.icon)}>{icon}</div>
48
+ <div className="flex-1 min-w-0">
49
+ {(title || defaultTitle) && (
50
+ <p className={cn('font-semibold mb-1', styles.title)}>
51
+ {title || defaultTitle}
52
+ </p>
53
+ )}
54
+ <div className={cn('text-sm [&>p]:m-0', styles.content)}>
55
+ {children}
56
+ </div>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ )
61
+ }
62
+ }
63
+
64
+ export const Info = createCallout(
65
+ 'info',
66
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
67
+ <path strokeLinecap="round" strokeLinejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
68
+ </svg>,
69
+ 'Info'
70
+ )
71
+
72
+ export const Tip = createCallout(
73
+ 'tip',
74
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
75
+ <path strokeLinecap="round" strokeLinejoin="round" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
76
+ </svg>,
77
+ 'Tip'
78
+ )
79
+
80
+ export const Warning = createCallout(
81
+ 'warning',
82
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
83
+ <path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
84
+ </svg>,
85
+ 'Warning'
86
+ )
87
+
88
+ export const Note = createCallout(
89
+ 'note',
90
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
91
+ <path strokeLinecap="round" strokeLinejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
92
+ </svg>,
93
+ 'Note'
94
+ )
95
+
96
+ export const Check = createCallout(
97
+ 'check',
98
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
99
+ <path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
100
+ </svg>,
101
+ ''
102
+ )
@@ -0,0 +1,110 @@
1
+ import Link from 'next/link'
2
+ import Image from 'next/image'
3
+ import { cn } from '@/lib/utils'
4
+
5
+ interface CardProps {
6
+ title: string
7
+ icon?: string
8
+ image?: string
9
+ href?: string
10
+ children?: React.ReactNode
11
+ }
12
+
13
+ const iconMap: Record<string, React.ReactNode> = {
14
+ rocket: (
15
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
16
+ <path strokeLinecap="round" strokeLinejoin="round" d="M15.59 14.37a6 6 0 01-5.84 7.38v-4.8m5.84-2.58a14.98 14.98 0 006.16-12.12A14.98 14.98 0 009.631 8.41m5.96 5.96a14.926 14.926 0 01-5.841 2.58m-.119-8.54a6 6 0 00-7.381 5.84h4.8m2.581-5.84a14.927 14.927 0 00-2.58 5.84m2.699 2.7c-.103.021-.207.041-.311.06a15.09 15.09 0 01-2.448-2.448 14.9 14.9 0 01.06-.312m-2.24 2.39a4.493 4.493 0 00-1.757 4.306 4.493 4.493 0 004.306-1.758M16.5 9a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0z" />
17
+ </svg>
18
+ ),
19
+ code: (
20
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
21
+ <path strokeLinecap="round" strokeLinejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5" />
22
+ </svg>
23
+ ),
24
+ book: (
25
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
26
+ <path strokeLinecap="round" strokeLinejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" />
27
+ </svg>
28
+ ),
29
+ gear: (
30
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
31
+ <path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
32
+ <path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
33
+ </svg>
34
+ ),
35
+ plug: (
36
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
37
+ <path strokeLinecap="round" strokeLinejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
38
+ </svg>
39
+ ),
40
+ terminal: (
41
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
42
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z" />
43
+ </svg>
44
+ ),
45
+ }
46
+
47
+ export function Card({ title, icon, image, href, children }: CardProps) {
48
+ const IconComponent = icon ? iconMap[icon] : null
49
+
50
+ const content = (
51
+ <div
52
+ className={cn(
53
+ 'group block h-full p-6 rounded-xl bg-gray-100 dark:bg-gray-800/50',
54
+ 'hover:bg-gray-200/80 dark:hover:bg-gray-800 transition-all duration-200',
55
+ href && 'cursor-pointer'
56
+ )}
57
+ >
58
+ {image ? (
59
+ <div className="mb-3 w-6 h-6 relative">
60
+ <Image
61
+ src={image}
62
+ alt=""
63
+ width={24}
64
+ height={24}
65
+ className="object-contain"
66
+ />
67
+ </div>
68
+ ) : IconComponent && (
69
+ <div className="mb-3 text-[var(--accent)]">
70
+ {IconComponent}
71
+ </div>
72
+ )}
73
+ <h3 className="font-semibold text-foreground mb-2">
74
+ {title}
75
+ </h3>
76
+ {children && (
77
+ <div className="text-sm text-muted-foreground leading-relaxed [&>p]:m-0">
78
+ {children}
79
+ </div>
80
+ )}
81
+ </div>
82
+ )
83
+
84
+ if (href) {
85
+ return <Link href={href} className="block h-full">{content}</Link>
86
+ }
87
+
88
+ return content
89
+ }
90
+
91
+ interface CardGroupProps {
92
+ cols?: number
93
+ children: React.ReactNode
94
+ }
95
+
96
+ export function CardGroup({ cols = 2, children }: CardGroupProps) {
97
+ return (
98
+ <div
99
+ className={cn(
100
+ 'grid gap-4 my-6 auto-rows-fr',
101
+ cols === 1 && 'grid-cols-1',
102
+ cols === 2 && 'grid-cols-1 sm:grid-cols-2',
103
+ cols === 3 && 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
104
+ cols === 4 && 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4'
105
+ )}
106
+ >
107
+ {children}
108
+ </div>
109
+ )
110
+ }
@@ -0,0 +1,42 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { cn } from '@/lib/utils'
5
+
6
+ interface CodeBlockProps {
7
+ children: React.ReactNode
8
+ title?: string
9
+ className?: string
10
+ }
11
+
12
+ export function CodeBlock({ children, title, className }: CodeBlockProps) {
13
+ const [copied, setCopied] = useState(false)
14
+
15
+ const handleCopy = async () => {
16
+ const code = document.querySelector('.code-block-content')?.textContent
17
+ if (code) {
18
+ await navigator.clipboard.writeText(code)
19
+ setCopied(true)
20
+ setTimeout(() => setCopied(false), 2000)
21
+ }
22
+ }
23
+
24
+ return (
25
+ <div className={cn('my-6 rounded-lg overflow-hidden border border-border', className)}>
26
+ {title && (
27
+ <div className="flex items-center justify-between px-4 py-2 bg-gray-100 dark:bg-gray-800 border-b border-border">
28
+ <span className="text-sm font-medium text-muted-foreground">{title}</span>
29
+ <button
30
+ onClick={handleCopy}
31
+ className="text-xs text-muted-foreground hover:text-foreground transition-colors"
32
+ >
33
+ {copied ? 'Copied!' : 'Copy'}
34
+ </button>
35
+ </div>
36
+ )}
37
+ <div className="code-block-content bg-[#fafafa] dark:bg-[#1a1a1f] overflow-x-auto">
38
+ {children}
39
+ </div>
40
+ </div>
41
+ )
42
+ }
@@ -0,0 +1,14 @@
1
+ import { cn } from '@/lib/utils'
2
+
3
+ interface FrameProps {
4
+ children: React.ReactNode
5
+ className?: string
6
+ }
7
+
8
+ export function Frame({ children, className }: FrameProps) {
9
+ return (
10
+ <div className={cn('my-6 rounded-lg overflow-hidden border border-border', className)}>
11
+ {children}
12
+ </div>
13
+ )
14
+ }