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,278 @@
|
|
|
1
|
+
# Unmint
|
|
2
|
+
|
|
3
|
+
A free, open-source Mintlify-style documentation system built with Next.js and Fumadocs.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## What is Unmint?
|
|
8
|
+
|
|
9
|
+
Unmint provides a beautiful, customizable documentation experience similar to [Mintlify](https://mintlify.com), but completely free and open source. It's designed to be forked, customized, and self-hosted.
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
- **Beautiful out-of-the-box** - Professional styling without configuration
|
|
14
|
+
- **MDX Components** - Cards, callouts, tabs, steps, accordions, and more
|
|
15
|
+
- **Built-in Search** - Full-text search powered by Fumadocs
|
|
16
|
+
- **Dynamic OG Images** - Auto-generated social preview images
|
|
17
|
+
- **Dark Mode** - Seamless light/dark theme switching
|
|
18
|
+
- **Easy Theming** - Single config file for all customization
|
|
19
|
+
- **SEO Optimized** - Automatic meta tags and sitemap
|
|
20
|
+
|
|
21
|
+
### What Unmint is NOT
|
|
22
|
+
|
|
23
|
+
- A hosted SaaS (you deploy it yourself)
|
|
24
|
+
- A replacement for all Mintlify features (no built-in analytics, AI search, etc.)
|
|
25
|
+
- A no-code solution (basic Next.js knowledge helpful)
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
### Prerequisites
|
|
30
|
+
|
|
31
|
+
- Node.js 18+
|
|
32
|
+
- npm, yarn, or pnpm
|
|
33
|
+
|
|
34
|
+
### Installation
|
|
35
|
+
|
|
36
|
+
The fastest way to get started is with our CLI:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx create-unmint@latest my-docs
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
This will prompt you for project configuration and set up everything automatically.
|
|
43
|
+
|
|
44
|
+
#### CLI Options
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Create with all defaults (no prompts)
|
|
48
|
+
npx create-unmint@latest my-docs --yes
|
|
49
|
+
|
|
50
|
+
# Update an existing project to latest
|
|
51
|
+
cd my-docs
|
|
52
|
+
npx create-unmint@latest --update
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
#### Manual Installation
|
|
56
|
+
|
|
57
|
+
You can also clone and customize manually:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Clone the repository
|
|
61
|
+
git clone https://github.com/gregce/unmint.git my-docs
|
|
62
|
+
cd my-docs
|
|
63
|
+
|
|
64
|
+
# Install dependencies
|
|
65
|
+
npm install
|
|
66
|
+
|
|
67
|
+
# Start development server
|
|
68
|
+
npm run dev
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Open [http://localhost:3000](http://localhost:3000) to see your docs.
|
|
72
|
+
|
|
73
|
+
## Project Structure
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
my-docs/
|
|
77
|
+
├── app/
|
|
78
|
+
│ ├── docs/ # Documentation routes
|
|
79
|
+
│ │ ├── layout.tsx # Docs layout (header, sidebar, footer)
|
|
80
|
+
│ │ └── [[...slug]]/ # Dynamic page rendering
|
|
81
|
+
│ ├── api/
|
|
82
|
+
│ │ ├── search/ # Search API endpoint
|
|
83
|
+
│ │ └── og/ # OG image generation
|
|
84
|
+
│ └── components/
|
|
85
|
+
│ └── docs/ # Docs-specific components
|
|
86
|
+
│ └── mdx/ # MDX components (Card, Callout, etc.)
|
|
87
|
+
├── content/
|
|
88
|
+
│ └── docs/ # Your MDX documentation files
|
|
89
|
+
│ ├── index.mdx # Homepage
|
|
90
|
+
│ ├── meta.json # Navigation structure
|
|
91
|
+
│ └── *.mdx # Your pages
|
|
92
|
+
├── lib/
|
|
93
|
+
│ ├── theme-config.ts # ⭐ Customize your theme here
|
|
94
|
+
│ ├── docs-source.ts # Fumadocs source config
|
|
95
|
+
│ └── utils.ts # Utility functions
|
|
96
|
+
├── public/ # Static assets (logo, images)
|
|
97
|
+
├── source.config.ts # MDX/Fumadocs configuration
|
|
98
|
+
└── package.json
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Customization
|
|
102
|
+
|
|
103
|
+
### 1. Update Site Info
|
|
104
|
+
|
|
105
|
+
Edit `lib/theme-config.ts`:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
export const siteConfig = {
|
|
109
|
+
name: 'My Docs',
|
|
110
|
+
description: 'Documentation for My Project',
|
|
111
|
+
url: 'https://docs.example.com',
|
|
112
|
+
logo: {
|
|
113
|
+
src: '/logo.png',
|
|
114
|
+
alt: 'My Project',
|
|
115
|
+
width: 32,
|
|
116
|
+
height: 32,
|
|
117
|
+
},
|
|
118
|
+
links: {
|
|
119
|
+
github: 'https://github.com/your-org/your-repo',
|
|
120
|
+
discord: 'https://discord.gg/your-invite',
|
|
121
|
+
support: 'mailto:support@example.com',
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 2. Customize Colors
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
export const themeConfig = {
|
|
130
|
+
colors: {
|
|
131
|
+
light: {
|
|
132
|
+
accent: '#0891b2', // Your brand color
|
|
133
|
+
},
|
|
134
|
+
dark: {
|
|
135
|
+
accent: '#22d3ee', // Brighter for dark mode
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### 3. Add Your Logo
|
|
142
|
+
|
|
143
|
+
Place your logo in `public/logo.png` and update `siteConfig.logo.src`.
|
|
144
|
+
|
|
145
|
+
### 4. Write Documentation
|
|
146
|
+
|
|
147
|
+
Create MDX files in `content/docs/`:
|
|
148
|
+
|
|
149
|
+
```mdx
|
|
150
|
+
---
|
|
151
|
+
title: My Page
|
|
152
|
+
description: A description of my page
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
# Hello World
|
|
156
|
+
|
|
157
|
+
This is my documentation page!
|
|
158
|
+
|
|
159
|
+
<Tip>
|
|
160
|
+
You can use all the built-in components.
|
|
161
|
+
</Tip>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 5. Configure Navigation
|
|
165
|
+
|
|
166
|
+
Edit `content/docs/meta.json`:
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"title": "Documentation",
|
|
171
|
+
"pages": [
|
|
172
|
+
"index",
|
|
173
|
+
"quickstart",
|
|
174
|
+
"---Getting Started---",
|
|
175
|
+
"installation",
|
|
176
|
+
"configuration",
|
|
177
|
+
"---Guides---",
|
|
178
|
+
"deployment"
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Available Components
|
|
184
|
+
|
|
185
|
+
### Cards
|
|
186
|
+
|
|
187
|
+
```mdx
|
|
188
|
+
<CardGroup cols={2}>
|
|
189
|
+
<Card title="Quick Start" icon="rocket" href="/docs/quickstart">
|
|
190
|
+
Get started in 5 minutes
|
|
191
|
+
</Card>
|
|
192
|
+
<Card title="API Reference" icon="code" href="/docs/api">
|
|
193
|
+
Explore our API
|
|
194
|
+
</Card>
|
|
195
|
+
</CardGroup>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Callouts
|
|
199
|
+
|
|
200
|
+
```mdx
|
|
201
|
+
<Info>Informational message</Info>
|
|
202
|
+
<Tip>Helpful tip</Tip>
|
|
203
|
+
<Warning>Warning message</Warning>
|
|
204
|
+
<Note>Additional note</Note>
|
|
205
|
+
<Check>Success message</Check>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Steps
|
|
209
|
+
|
|
210
|
+
```mdx
|
|
211
|
+
<Steps>
|
|
212
|
+
<Step title="Install">
|
|
213
|
+
Run `npm install`
|
|
214
|
+
</Step>
|
|
215
|
+
<Step title="Configure">
|
|
216
|
+
Edit your config file
|
|
217
|
+
</Step>
|
|
218
|
+
</Steps>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Tabs
|
|
222
|
+
|
|
223
|
+
```mdx
|
|
224
|
+
<Tabs>
|
|
225
|
+
<Tab title="npm">npm install package</Tab>
|
|
226
|
+
<Tab title="yarn">yarn add package</Tab>
|
|
227
|
+
</Tabs>
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Accordions
|
|
231
|
+
|
|
232
|
+
```mdx
|
|
233
|
+
<AccordionGroup>
|
|
234
|
+
<Accordion title="Question 1">Answer 1</Accordion>
|
|
235
|
+
<Accordion title="Question 2">Answer 2</Accordion>
|
|
236
|
+
</AccordionGroup>
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Deployment
|
|
240
|
+
|
|
241
|
+
### Vercel (Recommended)
|
|
242
|
+
|
|
243
|
+
1. Push to GitHub
|
|
244
|
+
2. Import at [vercel.com/new](https://vercel.com/new)
|
|
245
|
+
3. Deploy!
|
|
246
|
+
|
|
247
|
+
### Netlify
|
|
248
|
+
|
|
249
|
+
1. Add `netlify.toml`:
|
|
250
|
+
```toml
|
|
251
|
+
[build]
|
|
252
|
+
command = "npm run build"
|
|
253
|
+
publish = ".next"
|
|
254
|
+
[[plugins]]
|
|
255
|
+
package = "@netlify/plugin-nextjs"
|
|
256
|
+
```
|
|
257
|
+
2. Connect and deploy
|
|
258
|
+
|
|
259
|
+
### Other Hosts
|
|
260
|
+
|
|
261
|
+
Any platform supporting Next.js works. Run `npm run build` and deploy the output.
|
|
262
|
+
|
|
263
|
+
## Contributing
|
|
264
|
+
|
|
265
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
266
|
+
|
|
267
|
+
## License
|
|
268
|
+
|
|
269
|
+
MIT License - feel free to use this for any project.
|
|
270
|
+
|
|
271
|
+
## Credits
|
|
272
|
+
|
|
273
|
+
Built with:
|
|
274
|
+
- [Next.js](https://nextjs.org)
|
|
275
|
+
- [Fumadocs](https://fumadocs.vercel.app)
|
|
276
|
+
- [Tailwind CSS](https://tailwindcss.com)
|
|
277
|
+
|
|
278
|
+
Inspired by [Mintlify](https://mintlify.com).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { Info, Tip, Warning, Note, Check } from '../../app/components/docs/mdx/callout'
|
|
4
|
+
|
|
5
|
+
describe('Callout Components', () => {
|
|
6
|
+
describe('Info', () => {
|
|
7
|
+
it('renders children content', () => {
|
|
8
|
+
render(<Info>Test information message</Info>)
|
|
9
|
+
expect(screen.getByText('Test information message')).toBeDefined()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('has accessible structure', () => {
|
|
13
|
+
render(<Info>Accessible content</Info>)
|
|
14
|
+
const callout = screen.getByText('Accessible content').closest('div')
|
|
15
|
+
expect(callout).toBeDefined()
|
|
16
|
+
})
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('Tip', () => {
|
|
20
|
+
it('renders children content', () => {
|
|
21
|
+
render(<Tip>Helpful tip content</Tip>)
|
|
22
|
+
expect(screen.getByText('Helpful tip content')).toBeDefined()
|
|
23
|
+
})
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('Warning', () => {
|
|
27
|
+
it('renders children content', () => {
|
|
28
|
+
render(<Warning>Warning message</Warning>)
|
|
29
|
+
expect(screen.getByText('Warning message')).toBeDefined()
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('Note', () => {
|
|
34
|
+
it('renders children content', () => {
|
|
35
|
+
render(<Note>Note content</Note>)
|
|
36
|
+
expect(screen.getByText('Note content')).toBeDefined()
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('Check', () => {
|
|
41
|
+
it('renders children content', () => {
|
|
42
|
+
render(<Check>Success message</Check>)
|
|
43
|
+
expect(screen.getByText('Success message')).toBeDefined()
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { render, screen } from '@testing-library/react'
|
|
3
|
+
import { Card, CardGroup } from '../../app/components/docs/mdx/card'
|
|
4
|
+
|
|
5
|
+
describe('Card Component', () => {
|
|
6
|
+
it('renders with title and children', () => {
|
|
7
|
+
render(
|
|
8
|
+
<Card title="Test Card">
|
|
9
|
+
Card content here
|
|
10
|
+
</Card>
|
|
11
|
+
)
|
|
12
|
+
expect(screen.getByText('Test Card')).toBeDefined()
|
|
13
|
+
expect(screen.getByText('Card content here')).toBeDefined()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('renders as a link when href is provided', () => {
|
|
17
|
+
render(
|
|
18
|
+
<Card title="Linked Card" href="/docs/test">
|
|
19
|
+
Click me
|
|
20
|
+
</Card>
|
|
21
|
+
)
|
|
22
|
+
const link = screen.getByRole('link')
|
|
23
|
+
expect(link).toBeDefined()
|
|
24
|
+
expect(link.getAttribute('href')).toBe('/docs/test')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('renders icon when provided', () => {
|
|
28
|
+
render(
|
|
29
|
+
<Card title="Card with Icon" icon="rocket">
|
|
30
|
+
Content
|
|
31
|
+
</Card>
|
|
32
|
+
)
|
|
33
|
+
// Icon should be rendered (as SVG)
|
|
34
|
+
expect(screen.getByText('Card with Icon')).toBeDefined()
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('CardGroup Component', () => {
|
|
39
|
+
it('renders children cards', () => {
|
|
40
|
+
render(
|
|
41
|
+
<CardGroup cols={2}>
|
|
42
|
+
<Card title="Card 1">Content 1</Card>
|
|
43
|
+
<Card title="Card 2">Content 2</Card>
|
|
44
|
+
</CardGroup>
|
|
45
|
+
)
|
|
46
|
+
expect(screen.getByText('Card 1')).toBeDefined()
|
|
47
|
+
expect(screen.getByText('Card 2')).toBeDefined()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('applies grid layout', () => {
|
|
51
|
+
const { container } = render(
|
|
52
|
+
<CardGroup cols={3}>
|
|
53
|
+
<Card title="Card">Content</Card>
|
|
54
|
+
</CardGroup>
|
|
55
|
+
)
|
|
56
|
+
const grid = container.firstChild
|
|
57
|
+
expect(grid).toBeDefined()
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent, cleanup } from '@testing-library/react'
|
|
3
|
+
import { Tabs, Tab } from '../../app/components/docs/mdx/tabs'
|
|
4
|
+
|
|
5
|
+
describe('Tabs Component', () => {
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
cleanup()
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('renders tab titles', () => {
|
|
11
|
+
render(
|
|
12
|
+
<Tabs>
|
|
13
|
+
<Tab title="First">First content</Tab>
|
|
14
|
+
<Tab title="Second">Second content</Tab>
|
|
15
|
+
</Tabs>
|
|
16
|
+
)
|
|
17
|
+
expect(screen.getAllByText('First')[0]).toBeDefined()
|
|
18
|
+
expect(screen.getAllByText('Second')[0]).toBeDefined()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('shows first tab content by default', () => {
|
|
22
|
+
render(
|
|
23
|
+
<Tabs>
|
|
24
|
+
<Tab title="First">First content</Tab>
|
|
25
|
+
<Tab title="Second">Second content</Tab>
|
|
26
|
+
</Tabs>
|
|
27
|
+
)
|
|
28
|
+
expect(screen.getAllByText('First content')[0]).toBeDefined()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('switches tab content when clicked', () => {
|
|
32
|
+
render(
|
|
33
|
+
<Tabs>
|
|
34
|
+
<Tab title="First">First content</Tab>
|
|
35
|
+
<Tab title="Second">Second content</Tab>
|
|
36
|
+
</Tabs>
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
// Click on second tab
|
|
40
|
+
const secondTabs = screen.getAllByText('Second')
|
|
41
|
+
fireEvent.click(secondTabs[0])
|
|
42
|
+
|
|
43
|
+
// Second content should now be visible
|
|
44
|
+
expect(screen.getAllByText('Second content')[0]).toBeDefined()
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('Tab Component', () => {
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
cleanup()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('renders children when active', () => {
|
|
54
|
+
render(
|
|
55
|
+
<Tabs>
|
|
56
|
+
<Tab title="Test">Tab content here</Tab>
|
|
57
|
+
</Tabs>
|
|
58
|
+
)
|
|
59
|
+
expect(screen.getByText('Tab content here')).toBeDefined()
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { siteConfig, themeConfig, getCSSVariables } from '../lib/theme-config'
|
|
3
|
+
|
|
4
|
+
describe('theme-config', () => {
|
|
5
|
+
describe('siteConfig', () => {
|
|
6
|
+
it('should have required fields', () => {
|
|
7
|
+
expect(siteConfig.name).toBeDefined()
|
|
8
|
+
expect(siteConfig.description).toBeDefined()
|
|
9
|
+
expect(siteConfig.url).toBeDefined()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('should have logo configuration', () => {
|
|
13
|
+
expect(siteConfig.logo).toBeDefined()
|
|
14
|
+
expect(siteConfig.logo.src).toBeDefined()
|
|
15
|
+
expect(siteConfig.logo.width).toBeGreaterThan(0)
|
|
16
|
+
expect(siteConfig.logo.height).toBeGreaterThan(0)
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('themeConfig', () => {
|
|
21
|
+
it('should have light and dark color schemes', () => {
|
|
22
|
+
expect(themeConfig.colors.light).toBeDefined()
|
|
23
|
+
expect(themeConfig.colors.dark).toBeDefined()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should have accent colors', () => {
|
|
27
|
+
expect(themeConfig.colors.light.accent).toBeDefined()
|
|
28
|
+
expect(themeConfig.colors.dark.accent).toBeDefined()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should have OG image configuration', () => {
|
|
32
|
+
expect(themeConfig.ogImage).toBeDefined()
|
|
33
|
+
expect(themeConfig.ogImage.gradient).toBeDefined()
|
|
34
|
+
expect(themeConfig.ogImage.titleColor).toBeDefined()
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('getCSSVariables', () => {
|
|
39
|
+
it('should return CSS variables for light mode', () => {
|
|
40
|
+
const vars = getCSSVariables('light')
|
|
41
|
+
expect(vars['--accent']).toBe(themeConfig.colors.light.accent)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('should return CSS variables for dark mode', () => {
|
|
45
|
+
const vars = getCSSVariables('dark')
|
|
46
|
+
expect(vars['--accent']).toBe(themeConfig.colors.dark.accent)
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { cn } from '../lib/utils'
|
|
3
|
+
|
|
4
|
+
describe('cn utility', () => {
|
|
5
|
+
it('should merge class names', () => {
|
|
6
|
+
expect(cn('foo', 'bar')).toBe('foo bar')
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('should handle conditional classes', () => {
|
|
10
|
+
expect(cn('foo', false && 'bar', 'baz')).toBe('foo baz')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('should merge Tailwind classes correctly', () => {
|
|
14
|
+
expect(cn('px-4', 'px-6')).toBe('px-6')
|
|
15
|
+
expect(cn('text-red-500', 'text-blue-500')).toBe('text-blue-500')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('should handle arrays', () => {
|
|
19
|
+
expect(cn(['foo', 'bar'])).toBe('foo bar')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should handle undefined and null', () => {
|
|
23
|
+
expect(cn('foo', undefined, null, 'bar')).toBe('foo bar')
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ImageResponse } from 'next/og'
|
|
2
|
+
import { NextRequest } from 'next/server'
|
|
3
|
+
import { themeConfig, siteConfig } from '@/lib/theme-config'
|
|
4
|
+
|
|
5
|
+
export const runtime = 'edge'
|
|
6
|
+
|
|
7
|
+
export async function GET(request: NextRequest) {
|
|
8
|
+
const { searchParams } = new URL(request.url)
|
|
9
|
+
let title = searchParams.get('title') || 'Documentation'
|
|
10
|
+
const section = searchParams.get('section') || siteConfig.name
|
|
11
|
+
|
|
12
|
+
// Truncate very long titles to prevent overflow
|
|
13
|
+
if (title.length > 60) {
|
|
14
|
+
title = title.slice(0, 57) + '...'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Determine font size based on title length
|
|
18
|
+
const fontSize = title.length > 40 ? 48 : title.length > 25 ? 56 : 64
|
|
19
|
+
|
|
20
|
+
return new ImageResponse(
|
|
21
|
+
(
|
|
22
|
+
<div
|
|
23
|
+
style={{
|
|
24
|
+
display: 'flex',
|
|
25
|
+
flexDirection: 'column',
|
|
26
|
+
width: '100%',
|
|
27
|
+
height: '100%',
|
|
28
|
+
background: themeConfig.ogImage.gradient,
|
|
29
|
+
padding: '60px',
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
{/* Logo placeholder */}
|
|
33
|
+
<div style={{ display: 'flex', alignItems: 'flex-start' }}>
|
|
34
|
+
<div
|
|
35
|
+
style={{
|
|
36
|
+
display: 'flex',
|
|
37
|
+
width: '48px',
|
|
38
|
+
height: '48px',
|
|
39
|
+
background: `linear-gradient(135deg, ${themeConfig.colors.dark.accent} 0%, ${themeConfig.colors.light.accent} 100%)`,
|
|
40
|
+
borderRadius: '12px',
|
|
41
|
+
alignItems: 'center',
|
|
42
|
+
justifyContent: 'center',
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
<span style={{ fontSize: '24px', color: '#fff', fontWeight: 700 }}>
|
|
46
|
+
{siteConfig.name.charAt(0)}
|
|
47
|
+
</span>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
{/* Section name */}
|
|
52
|
+
<div
|
|
53
|
+
style={{
|
|
54
|
+
display: 'flex',
|
|
55
|
+
marginTop: 'auto',
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
<span
|
|
59
|
+
style={{
|
|
60
|
+
color: themeConfig.ogImage.sectionColor,
|
|
61
|
+
fontSize: '28px',
|
|
62
|
+
fontWeight: 500,
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
{section}
|
|
66
|
+
</span>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
{/* Title */}
|
|
70
|
+
<div
|
|
71
|
+
style={{
|
|
72
|
+
fontSize: `${fontSize}px`,
|
|
73
|
+
fontWeight: 700,
|
|
74
|
+
color: themeConfig.ogImage.titleColor,
|
|
75
|
+
marginTop: '16px',
|
|
76
|
+
lineHeight: 1.1,
|
|
77
|
+
maxWidth: '100%',
|
|
78
|
+
overflow: 'hidden',
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
{title}
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
),
|
|
85
|
+
{
|
|
86
|
+
width: 1200,
|
|
87
|
+
height: 630,
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import Link from 'next/link'
|
|
2
|
+
|
|
3
|
+
interface DocsPagerProps {
|
|
4
|
+
previous?: { name: string; url: string }
|
|
5
|
+
next?: { name: string; url: string }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function DocsPager({ previous, next }: DocsPagerProps) {
|
|
9
|
+
if (!previous && !next) return null
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<nav className="flex justify-between mt-16 pt-8 border-t border-border">
|
|
13
|
+
{previous ? (
|
|
14
|
+
<Link
|
|
15
|
+
href={previous.url}
|
|
16
|
+
className="group flex flex-col gap-1 max-w-[45%]"
|
|
17
|
+
>
|
|
18
|
+
<span className="text-sm text-muted-foreground">Previous</span>
|
|
19
|
+
<span className="text-foreground font-medium group-hover:text-[var(--accent)] transition-colors">
|
|
20
|
+
← {previous.name}
|
|
21
|
+
</span>
|
|
22
|
+
</Link>
|
|
23
|
+
) : (
|
|
24
|
+
<div />
|
|
25
|
+
)}
|
|
26
|
+
{next ? (
|
|
27
|
+
<Link
|
|
28
|
+
href={next.url}
|
|
29
|
+
className="group flex flex-col gap-1 items-end text-right max-w-[45%]"
|
|
30
|
+
>
|
|
31
|
+
<span className="text-sm text-muted-foreground">Next</span>
|
|
32
|
+
<span className="text-foreground font-medium group-hover:text-[var(--accent)] transition-colors">
|
|
33
|
+
{next.name} →
|
|
34
|
+
</span>
|
|
35
|
+
</Link>
|
|
36
|
+
) : (
|
|
37
|
+
<div />
|
|
38
|
+
)}
|
|
39
|
+
</nav>
|
|
40
|
+
)
|
|
41
|
+
}
|