react-docs-module 0.1.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 +279 -0
- package/ai-chat.tsx +222 -0
- package/chat-api.ts +90 -0
- package/cn.ts +15 -0
- package/config.ts +29 -0
- package/dist/ai-chat.d.ts +12 -0
- package/dist/ai-chat.js +72 -0
- package/dist/ai-chat.js.map +1 -0
- package/dist/chat-api.d.ts +16 -0
- package/dist/chat-api.js +62 -0
- package/dist/chat-api.js.map +1 -0
- package/dist/cn.d.ts +4 -0
- package/dist/cn.js +14 -0
- package/dist/cn.js.map +1 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +15 -0
- package/dist/config.js.map +1 -0
- package/dist/doc-pagination.d.ts +13 -0
- package/dist/doc-pagination.js +8 -0
- package/dist/doc-pagination.js.map +1 -0
- package/dist/docs-index.d.ts +7 -0
- package/dist/docs-index.js +11 -0
- package/dist/docs-index.js.map +1 -0
- package/dist/docs-page.d.ts +15 -0
- package/dist/docs-page.js +38 -0
- package/dist/docs-page.js.map +1 -0
- package/dist/docs-sidebar.d.ts +18 -0
- package/dist/docs-sidebar.d.ts.map +1 -0
- package/dist/docs-sidebar.js +27 -0
- package/dist/docs-sidebar.js.map +1 -0
- package/dist/documentation-layout.d.ts +15 -0
- package/dist/documentation-layout.js +20 -0
- package/dist/documentation-layout.js.map +1 -0
- package/dist/heading.d.ts +10 -0
- package/dist/heading.js +16 -0
- package/dist/heading.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/mdx/callouts.d.ts +8 -0
- package/dist/mdx/callouts.js +8 -0
- package/dist/mdx/callouts.js.map +1 -0
- package/dist/mdx/code-block.d.ts +8 -0
- package/dist/mdx/code-block.js +29 -0
- package/dist/mdx/code-block.js.map +1 -0
- package/dist/mdx/components.d.ts +13 -0
- package/dist/mdx/components.js +21 -0
- package/dist/mdx/components.js.map +1 -0
- package/dist/mdx.d.ts +20 -0
- package/dist/mdx.js +109 -0
- package/dist/mdx.js.map +1 -0
- package/dist/search-index.d.ts +10 -0
- package/dist/search-index.js +38 -0
- package/dist/search-index.js.map +1 -0
- package/dist/search.d.ts +6 -0
- package/dist/search.js +142 -0
- package/dist/search.js.map +1 -0
- package/dist/table-of-contents-provider.d.ts +4 -0
- package/dist/table-of-contents-provider.js +30 -0
- package/dist/table-of-contents-provider.js.map +1 -0
- package/dist/table-of-contents.d.ts +11 -0
- package/dist/table-of-contents.js +9 -0
- package/dist/table-of-contents.js.map +1 -0
- package/dist/theme-context.d.ts +20 -0
- package/dist/theme-context.js +28 -0
- package/dist/theme-context.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/ui/button.d.ts +12 -0
- package/dist/ui/button.js +34 -0
- package/dist/ui/button.js.map +1 -0
- package/dist/ui/dialog.d.ts +17 -0
- package/dist/ui/dialog.js +22 -0
- package/dist/ui/dialog.js.map +1 -0
- package/dist/ui/input.d.ts +4 -0
- package/dist/ui/input.js +9 -0
- package/dist/ui/input.js.map +1 -0
- package/dist/util.d.ts +59 -0
- package/dist/util.js +96 -0
- package/dist/util.js.map +1 -0
- package/doc-pagination.tsx +67 -0
- package/docs-index.tsx +17 -0
- package/docs-page.tsx +68 -0
- package/docs-sidebar.tsx +165 -0
- package/documentation-layout.tsx +99 -0
- package/heading.tsx +63 -0
- package/index.ts +28 -0
- package/mdx/callouts.tsx +29 -0
- package/mdx/code-block.tsx +89 -0
- package/mdx/components.tsx +55 -0
- package/mdx.ts +138 -0
- package/package.json +99 -0
- package/search-index.ts +52 -0
- package/search.tsx +273 -0
- package/table-of-contents-provider.tsx +43 -0
- package/table-of-contents.tsx +44 -0
- package/theme-context.tsx +57 -0
- package/ui/button.tsx +56 -0
- package/ui/dialog.tsx +108 -0
- package/ui/input.tsx +22 -0
- package/util.ts +169 -0
package/search.tsx
ADDED
@@ -0,0 +1,273 @@
|
|
1
|
+
"use client";
|
2
|
+
import React, { useEffect, useState } from "react";
|
3
|
+
import { useRouter } from "next/navigation";
|
4
|
+
import { Button } from "./ui/button";
|
5
|
+
import { Input } from "./ui/input";
|
6
|
+
import { Dialog, DialogContent, DialogTitle } from "./ui/dialog";
|
7
|
+
import type { SearchResult } from "./search-index";
|
8
|
+
import { AIChat, AIChatOption } from "./ai-chat";
|
9
|
+
import { ReactDocsConfig } from "./config";
|
10
|
+
import { useDocsColors } from "./theme-context";
|
11
|
+
|
12
|
+
// Singleton to cache search index
|
13
|
+
class SearchIndexCache {
|
14
|
+
private static instances: Map<string, SearchIndexCache> = new Map();
|
15
|
+
private searchIndex: SearchResult[] | null = null;
|
16
|
+
private loading = false;
|
17
|
+
private callbacks: ((index: SearchResult[]) => void)[] = [];
|
18
|
+
|
19
|
+
constructor(private config: ReactDocsConfig) { }
|
20
|
+
|
21
|
+
static getInstance(config: ReactDocsConfig): SearchIndexCache {
|
22
|
+
const key = config.searchApiPath;
|
23
|
+
if (!SearchIndexCache.instances.has(key)) {
|
24
|
+
SearchIndexCache.instances.set(key, new SearchIndexCache(config));
|
25
|
+
}
|
26
|
+
return SearchIndexCache.instances.get(key)!;
|
27
|
+
}
|
28
|
+
|
29
|
+
async getSearchIndex(): Promise<SearchResult[]> {
|
30
|
+
if (this.searchIndex) {
|
31
|
+
return this.searchIndex;
|
32
|
+
}
|
33
|
+
|
34
|
+
if (this.loading) {
|
35
|
+
return new Promise((resolve) => {
|
36
|
+
this.callbacks.push(resolve);
|
37
|
+
});
|
38
|
+
}
|
39
|
+
|
40
|
+
this.loading = true;
|
41
|
+
try {
|
42
|
+
const response = await fetch(this.config.searchApiPath);
|
43
|
+
const data = await response.json();
|
44
|
+
this.searchIndex = data;
|
45
|
+
this.callbacks.forEach(callback => callback(data));
|
46
|
+
this.callbacks = [];
|
47
|
+
return data;
|
48
|
+
} finally {
|
49
|
+
this.loading = false;
|
50
|
+
}
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
interface SearchResultWithScore extends SearchResult {
|
55
|
+
score: number;
|
56
|
+
snippet: string;
|
57
|
+
}
|
58
|
+
|
59
|
+
function highlightText(text: string, query: string) {
|
60
|
+
if (!query) return text;
|
61
|
+
const parts = text.split(new RegExp(`(${query})`, "gi"));
|
62
|
+
return parts.map((part, i) =>
|
63
|
+
part.toLowerCase() === query.toLowerCase() ?
|
64
|
+
<mark key={i} className="bg-yellow-500/20 text-yellow-200 rounded px-0.5">{part}</mark> :
|
65
|
+
part
|
66
|
+
);
|
67
|
+
}
|
68
|
+
|
69
|
+
export interface SearchProps {
|
70
|
+
config: ReactDocsConfig;
|
71
|
+
}
|
72
|
+
|
73
|
+
export function Search({ config }: SearchProps) {
|
74
|
+
const [open, setOpen] = useState(false);
|
75
|
+
const [isMac, setIsMac] = useState(false);
|
76
|
+
const [query, setQuery] = useState("");
|
77
|
+
const [results, setResults] = useState<SearchResultWithScore[]>([]);
|
78
|
+
const [searchIndex, setSearchIndex] = useState<SearchResult[]>([]);
|
79
|
+
const [isLoading, setIsLoading] = useState(false);
|
80
|
+
const [showAIChat, setShowAIChat] = useState(false);
|
81
|
+
const router = useRouter();
|
82
|
+
const colors = useDocsColors();
|
83
|
+
|
84
|
+
useEffect(() => {
|
85
|
+
// Detect if user is on macOS
|
86
|
+
setIsMac(navigator.platform.toUpperCase().indexOf('MAC') >= 0);
|
87
|
+
|
88
|
+
// Load search index using cache
|
89
|
+
const cache = SearchIndexCache.getInstance(config);
|
90
|
+
cache.getSearchIndex().then((data) => setSearchIndex(data));
|
91
|
+
|
92
|
+
const down = (e: KeyboardEvent) => {
|
93
|
+
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
94
|
+
e.preventDefault();
|
95
|
+
setOpen(true);
|
96
|
+
}
|
97
|
+
|
98
|
+
if (e.key === "Escape") {
|
99
|
+
setOpen(false);
|
100
|
+
}
|
101
|
+
};
|
102
|
+
|
103
|
+
document.addEventListener("keydown", down);
|
104
|
+
return () => document.removeEventListener("keydown", down);
|
105
|
+
}, [config]);
|
106
|
+
|
107
|
+
useEffect(() => {
|
108
|
+
const search = async () => {
|
109
|
+
setIsLoading(true);
|
110
|
+
// Only search if query has 2 or more characters
|
111
|
+
if (!query.trim() || query.length < 2) {
|
112
|
+
setResults([]);
|
113
|
+
setIsLoading(false);
|
114
|
+
return;
|
115
|
+
}
|
116
|
+
|
117
|
+
const searchQuery = query.toLowerCase();
|
118
|
+
const filtered = searchIndex
|
119
|
+
.map(item => {
|
120
|
+
const titleMatch = item.title.toLowerCase().includes(searchQuery);
|
121
|
+
const contentMatch = item.content.toLowerCase().includes(searchQuery);
|
122
|
+
|
123
|
+
if (!titleMatch && !contentMatch) return null;
|
124
|
+
|
125
|
+
// Find the best content snippet
|
126
|
+
let snippet = item.content;
|
127
|
+
if (contentMatch) {
|
128
|
+
const index = item.content.toLowerCase().indexOf(searchQuery);
|
129
|
+
const start = Math.max(0, index - 100);
|
130
|
+
const end = Math.min(item.content.length, index + 100);
|
131
|
+
snippet = (start > 0 ? "..." : "") +
|
132
|
+
item.content.slice(start, end) +
|
133
|
+
(end < item.content.length ? "..." : "");
|
134
|
+
}
|
135
|
+
|
136
|
+
return {
|
137
|
+
...item,
|
138
|
+
snippet,
|
139
|
+
score: titleMatch ? 2 : 1,
|
140
|
+
};
|
141
|
+
})
|
142
|
+
.filter((item): item is SearchResultWithScore => item !== null)
|
143
|
+
.sort((a, b) => b.score - a.score);
|
144
|
+
|
145
|
+
setResults(filtered);
|
146
|
+
setIsLoading(false);
|
147
|
+
};
|
148
|
+
|
149
|
+
const debounce = setTimeout(search, 200);
|
150
|
+
return () => clearTimeout(debounce);
|
151
|
+
}, [query, searchIndex]);
|
152
|
+
|
153
|
+
const handleSelect = (result: SearchResultWithScore) => {
|
154
|
+
router.push(result.url);
|
155
|
+
setOpen(false);
|
156
|
+
};
|
157
|
+
|
158
|
+
const handleAIChat = () => {
|
159
|
+
setShowAIChat(true);
|
160
|
+
};
|
161
|
+
|
162
|
+
return (
|
163
|
+
<div className="relative w-full">
|
164
|
+
<Button
|
165
|
+
variant="outline"
|
166
|
+
className="relative w-full justify-start text-sm text-muted-foreground bg-gray-800/50 hover:bg-gray-800 border-0 hover:text-gray-100"
|
167
|
+
style={{
|
168
|
+
'--ring-color': colors.primary,
|
169
|
+
} as React.CSSProperties}
|
170
|
+
onClick={() => setOpen(true)}
|
171
|
+
>
|
172
|
+
<span className="inline-flex items-center">
|
173
|
+
<svg
|
174
|
+
className="mr-2 h-4 w-4"
|
175
|
+
fill="none"
|
176
|
+
viewBox="0 0 24 24"
|
177
|
+
stroke="currentColor"
|
178
|
+
>
|
179
|
+
<path
|
180
|
+
strokeLinecap="round"
|
181
|
+
strokeLinejoin="round"
|
182
|
+
strokeWidth={2}
|
183
|
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
184
|
+
/>
|
185
|
+
</svg>
|
186
|
+
Search documentation or ask AI...
|
187
|
+
</span>
|
188
|
+
<kbd className="pointer-events-none absolute right-2 top-[50%] translate-y-[-50%] inline-flex h-5 select-none items-center gap-1 rounded border border-gray-700 bg-gray-800 px-1.5 font-mono text-[10px] font-medium text-gray-400">
|
189
|
+
{isMac ? (
|
190
|
+
<span className="text-xs">⌘</span>
|
191
|
+
) : (
|
192
|
+
<span className="text-xs">Ctrl</span>
|
193
|
+
)}
|
194
|
+
+ K
|
195
|
+
</kbd>
|
196
|
+
</Button>
|
197
|
+
|
198
|
+
<Dialog open={open} onOpenChange={(isOpen) => {
|
199
|
+
setOpen(isOpen);
|
200
|
+
if (!isOpen) {
|
201
|
+
setShowAIChat(false);
|
202
|
+
setQuery("");
|
203
|
+
}
|
204
|
+
}}>
|
205
|
+
<DialogContent
|
206
|
+
className="fixed top-20 left-1/2 -translate-x-1/2 w-full max-w-2xl shadow-2xl backdrop-blur-sm bg-gray-900/95 border border-gray-800 rounded-lg overflow-hidden"
|
207
|
+
style={{ maxHeight: 'calc(100vh - 6rem)' }}
|
208
|
+
>
|
209
|
+
<DialogTitle className="absolute w-px h-px p-0 -m-px overflow-hidden whitespace-nowrap border-0" style={{ clip: 'rect(0, 0, 0, 0)' }}>
|
210
|
+
Search Documentation
|
211
|
+
</DialogTitle>
|
212
|
+
{!showAIChat ? (
|
213
|
+
<div className="flex flex-col" style={{ maxHeight: 'calc(100vh - 6rem)' }}>
|
214
|
+
<div className="shrink-0 p-4 border-b border-gray-800">
|
215
|
+
<Input
|
216
|
+
placeholder="Search documentation or ask AI..."
|
217
|
+
className="w-full bg-gray-800/50 border-gray-700"
|
218
|
+
value={query}
|
219
|
+
onChange={(e) => setQuery(e.target.value)}
|
220
|
+
autoFocus
|
221
|
+
/>
|
222
|
+
</div>
|
223
|
+
|
224
|
+
<div className="min-h-0 overflow-y-auto">
|
225
|
+
<div className="p-4 space-y-4">
|
226
|
+
{isLoading ? (
|
227
|
+
<div className="text-sm text-gray-400">Searching...</div>
|
228
|
+
) : query.length < 2 ? (
|
229
|
+
<div className="text-sm text-gray-400">Enter at least 2 characters to search...</div>
|
230
|
+
) : (
|
231
|
+
<>
|
232
|
+
{query.length >= 2 && <AIChatOption query={query} onClick={handleAIChat} />}
|
233
|
+
{results.length > 0 ? (
|
234
|
+
<div className="space-y-4">
|
235
|
+
{results.map((result) => (
|
236
|
+
<button
|
237
|
+
key={result.url}
|
238
|
+
className="block w-full text-left p-4 rounded-lg hover:bg-gray-800/50 transition-colors"
|
239
|
+
onClick={() => handleSelect(result)}
|
240
|
+
>
|
241
|
+
<h3 className="text-sm font-medium text-white mb-1">
|
242
|
+
{highlightText(result.title, query)}
|
243
|
+
</h3>
|
244
|
+
<p className="text-sm text-gray-400 line-clamp-2">
|
245
|
+
{highlightText(result.snippet, query)}
|
246
|
+
</p>
|
247
|
+
</button>
|
248
|
+
))}
|
249
|
+
</div>
|
250
|
+
) : (
|
251
|
+
<div className="text-sm text-gray-400">No documentation results found</div>
|
252
|
+
)}
|
253
|
+
</>
|
254
|
+
)}
|
255
|
+
</div>
|
256
|
+
</div>
|
257
|
+
</div>
|
258
|
+
) : (
|
259
|
+
<div className="overflow-y-auto" style={{ maxHeight: 'calc(100vh - 6rem)' }}>
|
260
|
+
<div className="p-4">
|
261
|
+
<AIChat
|
262
|
+
query={query}
|
263
|
+
onBack={() => setShowAIChat(false)}
|
264
|
+
config={config}
|
265
|
+
/>
|
266
|
+
</div>
|
267
|
+
</div>
|
268
|
+
)}
|
269
|
+
</DialogContent>
|
270
|
+
</Dialog>
|
271
|
+
</div>
|
272
|
+
);
|
273
|
+
}
|
@@ -0,0 +1,43 @@
|
|
1
|
+
"use client";
|
2
|
+
import { useEffect } from "react";
|
3
|
+
|
4
|
+
export function TableOfContentsProvider({
|
5
|
+
children
|
6
|
+
}: {
|
7
|
+
children: React.ReactNode
|
8
|
+
}) {
|
9
|
+
useEffect(() => {
|
10
|
+
const observer = new IntersectionObserver(
|
11
|
+
(entries) => {
|
12
|
+
entries.forEach((entry) => {
|
13
|
+
if (entry.isIntersecting) {
|
14
|
+
// Remove active class from all links
|
15
|
+
document.querySelectorAll('[data-heading-id]').forEach(el => {
|
16
|
+
el.classList.remove('text-white');
|
17
|
+
el.classList.add('text-gray-400');
|
18
|
+
});
|
19
|
+
|
20
|
+
// Add active class to current heading's link
|
21
|
+
const link = document.querySelector(
|
22
|
+
`[data-heading-id="${entry.target.id}"]`
|
23
|
+
);
|
24
|
+
if (link) {
|
25
|
+
link.classList.remove('text-gray-400');
|
26
|
+
link.classList.add('text-white');
|
27
|
+
}
|
28
|
+
}
|
29
|
+
});
|
30
|
+
},
|
31
|
+
{ rootMargin: "-80px 0px -80% 0px" }
|
32
|
+
);
|
33
|
+
|
34
|
+
// Observe all headings
|
35
|
+
document.querySelectorAll("h1, h2, h3").forEach((heading) => {
|
36
|
+
observer.observe(heading);
|
37
|
+
});
|
38
|
+
|
39
|
+
return () => observer.disconnect();
|
40
|
+
}, []);
|
41
|
+
|
42
|
+
return children;
|
43
|
+
}
|
@@ -0,0 +1,44 @@
|
|
1
|
+
import { TableOfContentsProvider } from "./table-of-contents-provider";
|
2
|
+
|
3
|
+
interface Heading {
|
4
|
+
id: string;
|
5
|
+
text: string;
|
6
|
+
level: number;
|
7
|
+
}
|
8
|
+
|
9
|
+
interface Props {
|
10
|
+
headings: Heading[];
|
11
|
+
}
|
12
|
+
|
13
|
+
function TableOfContentsList({ headings }: Props) {
|
14
|
+
return (
|
15
|
+
<nav className="sticky top-8 z-0 max-h-[calc(100vh-4rem)] overflow-y-auto">
|
16
|
+
<h4 className="text-sm font-semibold mb-4 text-gray-400">On this page</h4>
|
17
|
+
<ul className="space-y-2.5">
|
18
|
+
{headings.map(({ id, text, level }) => (
|
19
|
+
<li
|
20
|
+
key={id}
|
21
|
+
style={{ paddingLeft: `${(level - 1) * 16}px` }}
|
22
|
+
className="text-sm"
|
23
|
+
>
|
24
|
+
<a
|
25
|
+
href={`#${id}`}
|
26
|
+
className="hover:text-white transition-colors text-gray-400 block"
|
27
|
+
data-heading-id={id}
|
28
|
+
>
|
29
|
+
{text}
|
30
|
+
</a>
|
31
|
+
</li>
|
32
|
+
))}
|
33
|
+
</ul>
|
34
|
+
</nav>
|
35
|
+
);
|
36
|
+
}
|
37
|
+
|
38
|
+
export function TableOfContents({ headings }: Props) {
|
39
|
+
return (
|
40
|
+
<TableOfContentsProvider>
|
41
|
+
<TableOfContentsList headings={headings} />
|
42
|
+
</TableOfContentsProvider>
|
43
|
+
);
|
44
|
+
}
|
@@ -0,0 +1,57 @@
|
|
1
|
+
"use client";
|
2
|
+
import React, { createContext, useContext } from "react";
|
3
|
+
import type { DocsJsonConfig } from "./util";
|
4
|
+
|
5
|
+
export interface ThemeColors {
|
6
|
+
primary: string;
|
7
|
+
light?: string;
|
8
|
+
dark?: string;
|
9
|
+
}
|
10
|
+
|
11
|
+
export interface DocsTheme {
|
12
|
+
colors: ThemeColors;
|
13
|
+
name: string;
|
14
|
+
theme: string;
|
15
|
+
}
|
16
|
+
|
17
|
+
const DocsThemeContext = createContext<DocsTheme | null>(null);
|
18
|
+
|
19
|
+
export interface DocsThemeProviderProps {
|
20
|
+
config: DocsJsonConfig;
|
21
|
+
children: React.ReactNode;
|
22
|
+
}
|
23
|
+
|
24
|
+
export function DocsThemeProvider({ config, children }: DocsThemeProviderProps) {
|
25
|
+
const theme: DocsTheme = {
|
26
|
+
colors: config.colors,
|
27
|
+
name: config.name,
|
28
|
+
theme: config.theme,
|
29
|
+
};
|
30
|
+
|
31
|
+
return (
|
32
|
+
<DocsThemeContext.Provider value={theme}>
|
33
|
+
<div
|
34
|
+
style={{
|
35
|
+
'--docs-primary': theme.colors.primary,
|
36
|
+
'--docs-primary-light': theme.colors.light || theme.colors.primary,
|
37
|
+
'--docs-primary-dark': theme.colors.dark || theme.colors.primary,
|
38
|
+
} as React.CSSProperties}
|
39
|
+
>
|
40
|
+
{children}
|
41
|
+
</div>
|
42
|
+
</DocsThemeContext.Provider>
|
43
|
+
);
|
44
|
+
}
|
45
|
+
|
46
|
+
export function useDocsTheme(): DocsTheme {
|
47
|
+
const context = useContext(DocsThemeContext);
|
48
|
+
if (!context) {
|
49
|
+
throw new Error('useDocsTheme must be used within a DocsThemeProvider');
|
50
|
+
}
|
51
|
+
return context;
|
52
|
+
}
|
53
|
+
|
54
|
+
export function useDocsColors(): ThemeColors {
|
55
|
+
const theme = useDocsTheme();
|
56
|
+
return theme.colors;
|
57
|
+
}
|
package/ui/button.tsx
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
import * as React from "react"
|
2
|
+
import { Slot } from "@radix-ui/react-slot"
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
4
|
+
|
5
|
+
import { cn } from "../cn"
|
6
|
+
|
7
|
+
const buttonVariants = cva(
|
8
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
9
|
+
{
|
10
|
+
variants: {
|
11
|
+
variant: {
|
12
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
13
|
+
destructive:
|
14
|
+
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
15
|
+
outline:
|
16
|
+
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
17
|
+
secondary:
|
18
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
19
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
20
|
+
link: "text-primary underline-offset-4 hover:underline",
|
21
|
+
},
|
22
|
+
size: {
|
23
|
+
default: "h-10 px-4 py-2",
|
24
|
+
sm: "h-9 rounded-md px-3",
|
25
|
+
lg: "h-11 rounded-md px-8",
|
26
|
+
icon: "h-10 w-10",
|
27
|
+
},
|
28
|
+
},
|
29
|
+
defaultVariants: {
|
30
|
+
variant: "default",
|
31
|
+
size: "default",
|
32
|
+
},
|
33
|
+
}
|
34
|
+
)
|
35
|
+
|
36
|
+
export interface ButtonProps
|
37
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
38
|
+
VariantProps<typeof buttonVariants> {
|
39
|
+
asChild?: boolean
|
40
|
+
}
|
41
|
+
|
42
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
43
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
44
|
+
const Comp = asChild ? Slot : "button"
|
45
|
+
return (
|
46
|
+
<Comp
|
47
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
48
|
+
ref={ref}
|
49
|
+
{...props}
|
50
|
+
/>
|
51
|
+
)
|
52
|
+
}
|
53
|
+
)
|
54
|
+
Button.displayName = "Button"
|
55
|
+
|
56
|
+
export { Button, buttonVariants }
|
package/ui/dialog.tsx
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
"use client";
|
2
|
+
|
3
|
+
import * as React from "react";
|
4
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
5
|
+
import { cn } from "../cn";
|
6
|
+
|
7
|
+
const Dialog = DialogPrimitive.Root;
|
8
|
+
|
9
|
+
const DialogTrigger = DialogPrimitive.Trigger;
|
10
|
+
|
11
|
+
const DialogPortal = DialogPrimitive.Portal;
|
12
|
+
|
13
|
+
const DialogOverlay = React.forwardRef<
|
14
|
+
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
15
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
16
|
+
>(({ className, ...props }, ref) => (
|
17
|
+
<DialogPrimitive.Overlay
|
18
|
+
ref={ref}
|
19
|
+
className={cn(
|
20
|
+
"fixed inset-0 z-50 bg-black/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
21
|
+
className
|
22
|
+
)}
|
23
|
+
{...props}
|
24
|
+
/>
|
25
|
+
));
|
26
|
+
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
27
|
+
|
28
|
+
const DialogContent = React.forwardRef<
|
29
|
+
React.ElementRef<typeof DialogPrimitive.Content>,
|
30
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
31
|
+
>(({ className, children, ...props }, ref) => (
|
32
|
+
<DialogPortal>
|
33
|
+
<DialogOverlay />
|
34
|
+
<DialogPrimitive.Content
|
35
|
+
ref={ref}
|
36
|
+
className={cn(
|
37
|
+
"fixed left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] gap-4 border border-gray-700 bg-gray-900 p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 rounded-lg",
|
38
|
+
className
|
39
|
+
)}
|
40
|
+
{...props}
|
41
|
+
>
|
42
|
+
{children}
|
43
|
+
</DialogPrimitive.Content>
|
44
|
+
</DialogPortal>
|
45
|
+
));
|
46
|
+
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
47
|
+
|
48
|
+
const DialogHeader = ({
|
49
|
+
className,
|
50
|
+
...props
|
51
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
52
|
+
<div
|
53
|
+
className={cn(
|
54
|
+
"flex flex-col space-y-1.5 text-center sm:text-left",
|
55
|
+
className
|
56
|
+
)}
|
57
|
+
{...props}
|
58
|
+
/>
|
59
|
+
);
|
60
|
+
DialogHeader.displayName = "DialogHeader";
|
61
|
+
|
62
|
+
const DialogFooter = ({
|
63
|
+
className,
|
64
|
+
...props
|
65
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
66
|
+
<div
|
67
|
+
className={cn(
|
68
|
+
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
69
|
+
className
|
70
|
+
)}
|
71
|
+
{...props}
|
72
|
+
/>
|
73
|
+
);
|
74
|
+
DialogFooter.displayName = "DialogFooter";
|
75
|
+
|
76
|
+
const DialogTitle = React.forwardRef<
|
77
|
+
React.ElementRef<typeof DialogPrimitive.Title>,
|
78
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
79
|
+
>(({ className, ...props }, ref) => (
|
80
|
+
<DialogPrimitive.Title
|
81
|
+
ref={ref}
|
82
|
+
className={cn("text-lg font-semibold leading-none", className)}
|
83
|
+
{...props}
|
84
|
+
/>
|
85
|
+
));
|
86
|
+
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
87
|
+
|
88
|
+
const DialogDescription = React.forwardRef<
|
89
|
+
React.ElementRef<typeof DialogPrimitive.Description>,
|
90
|
+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
91
|
+
>(({ className, ...props }, ref) => (
|
92
|
+
<DialogPrimitive.Description
|
93
|
+
ref={ref}
|
94
|
+
className={cn("text-sm text-gray-400", className)}
|
95
|
+
{...props}
|
96
|
+
/>
|
97
|
+
));
|
98
|
+
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
99
|
+
|
100
|
+
export {
|
101
|
+
Dialog,
|
102
|
+
DialogTrigger,
|
103
|
+
DialogContent,
|
104
|
+
DialogHeader,
|
105
|
+
DialogFooter,
|
106
|
+
DialogTitle,
|
107
|
+
DialogDescription,
|
108
|
+
};
|
package/ui/input.tsx
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
import * as React from "react";
|
2
|
+
import { cn } from "../cn";
|
3
|
+
|
4
|
+
const Input = React.forwardRef<
|
5
|
+
HTMLInputElement,
|
6
|
+
React.InputHTMLAttributes<HTMLInputElement>
|
7
|
+
>(({ className, type, ...props }, ref) => {
|
8
|
+
return (
|
9
|
+
<input
|
10
|
+
type={type}
|
11
|
+
className={cn(
|
12
|
+
"flex h-10 w-full rounded-md border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-100 ring-offset-gray-900 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-700 focus:ring-offset-2",
|
13
|
+
className
|
14
|
+
)}
|
15
|
+
ref={ref}
|
16
|
+
{...props}
|
17
|
+
/>
|
18
|
+
);
|
19
|
+
});
|
20
|
+
Input.displayName = "Input";
|
21
|
+
|
22
|
+
export { Input };
|