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.
- package/README.md +57 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +499 -0
- package/package.json +48 -0
- package/template/LICENSE +21 -0
- package/template/README.md +278 -0
- package/template/__tests__/components/callout.test.tsx +46 -0
- package/template/__tests__/components/card.test.tsx +59 -0
- package/template/__tests__/components/tabs.test.tsx +61 -0
- package/template/__tests__/theme-config.test.ts +49 -0
- package/template/__tests__/utils.test.ts +25 -0
- package/template/app/api/og/route.tsx +90 -0
- package/template/app/api/search/route.ts +6 -0
- package/template/app/components/docs/docs-pager.tsx +41 -0
- package/template/app/components/docs/docs-sidebar.tsx +143 -0
- package/template/app/components/docs/docs-toc.tsx +61 -0
- package/template/app/components/docs/mdx/accordion.tsx +54 -0
- package/template/app/components/docs/mdx/callout.tsx +102 -0
- package/template/app/components/docs/mdx/card.tsx +110 -0
- package/template/app/components/docs/mdx/code-block.tsx +42 -0
- package/template/app/components/docs/mdx/frame.tsx +14 -0
- package/template/app/components/docs/mdx/index.tsx +167 -0
- package/template/app/components/docs/mdx/pre.tsx +82 -0
- package/template/app/components/docs/mdx/steps.tsx +59 -0
- package/template/app/components/docs/mdx/tabs.tsx +60 -0
- package/template/app/components/docs/mdx/youtube.tsx +18 -0
- package/template/app/components/docs/search-dialog.tsx +281 -0
- package/template/app/components/docs/theme-toggle.tsx +35 -0
- package/template/app/docs/[[...slug]]/page.tsx +139 -0
- package/template/app/docs/layout.tsx +98 -0
- package/template/app/globals.css +151 -0
- package/template/app/layout.tsx +33 -0
- package/template/app/page.tsx +5 -0
- package/template/app/providers/theme-provider.tsx +8 -0
- package/template/content/docs/components.mdx +82 -0
- package/template/content/docs/customization.mdx +34 -0
- package/template/content/docs/deployment.mdx +28 -0
- package/template/content/docs/index.mdx +91 -0
- package/template/content/docs/meta.json +13 -0
- package/template/content/docs/quickstart.mdx +110 -0
- package/template/content/docs/theming.mdx +41 -0
- package/template/lib/docs-source.ts +7 -0
- package/template/lib/theme-config.ts +89 -0
- package/template/lib/utils.ts +6 -0
- package/template/next.config.mjs +10 -0
- package/template/package-lock.json +10695 -0
- package/template/package.json +45 -0
- package/template/postcss.config.mjs +7 -0
- package/template/public/logo.png +0 -0
- package/template/public/logo.svg +9 -0
- package/template/public/logo.txt +1 -0
- package/template/source.config.ts +22 -0
- package/template/tailwind.config.ts +34 -0
- package/template/tsconfig.json +33 -0
- 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
|
+
}
|