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.
Files changed (100) hide show
  1. package/README.md +279 -0
  2. package/ai-chat.tsx +222 -0
  3. package/chat-api.ts +90 -0
  4. package/cn.ts +15 -0
  5. package/config.ts +29 -0
  6. package/dist/ai-chat.d.ts +12 -0
  7. package/dist/ai-chat.js +72 -0
  8. package/dist/ai-chat.js.map +1 -0
  9. package/dist/chat-api.d.ts +16 -0
  10. package/dist/chat-api.js +62 -0
  11. package/dist/chat-api.js.map +1 -0
  12. package/dist/cn.d.ts +4 -0
  13. package/dist/cn.js +14 -0
  14. package/dist/cn.js.map +1 -0
  15. package/dist/config.d.ts +14 -0
  16. package/dist/config.js +15 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/doc-pagination.d.ts +13 -0
  19. package/dist/doc-pagination.js +8 -0
  20. package/dist/doc-pagination.js.map +1 -0
  21. package/dist/docs-index.d.ts +7 -0
  22. package/dist/docs-index.js +11 -0
  23. package/dist/docs-index.js.map +1 -0
  24. package/dist/docs-page.d.ts +15 -0
  25. package/dist/docs-page.js +38 -0
  26. package/dist/docs-page.js.map +1 -0
  27. package/dist/docs-sidebar.d.ts +18 -0
  28. package/dist/docs-sidebar.d.ts.map +1 -0
  29. package/dist/docs-sidebar.js +27 -0
  30. package/dist/docs-sidebar.js.map +1 -0
  31. package/dist/documentation-layout.d.ts +15 -0
  32. package/dist/documentation-layout.js +20 -0
  33. package/dist/documentation-layout.js.map +1 -0
  34. package/dist/heading.d.ts +10 -0
  35. package/dist/heading.js +16 -0
  36. package/dist/heading.js.map +1 -0
  37. package/dist/index.d.ts +18 -0
  38. package/dist/index.js +19 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/mdx/callouts.d.ts +8 -0
  41. package/dist/mdx/callouts.js +8 -0
  42. package/dist/mdx/callouts.js.map +1 -0
  43. package/dist/mdx/code-block.d.ts +8 -0
  44. package/dist/mdx/code-block.js +29 -0
  45. package/dist/mdx/code-block.js.map +1 -0
  46. package/dist/mdx/components.d.ts +13 -0
  47. package/dist/mdx/components.js +21 -0
  48. package/dist/mdx/components.js.map +1 -0
  49. package/dist/mdx.d.ts +20 -0
  50. package/dist/mdx.js +109 -0
  51. package/dist/mdx.js.map +1 -0
  52. package/dist/search-index.d.ts +10 -0
  53. package/dist/search-index.js +38 -0
  54. package/dist/search-index.js.map +1 -0
  55. package/dist/search.d.ts +6 -0
  56. package/dist/search.js +142 -0
  57. package/dist/search.js.map +1 -0
  58. package/dist/table-of-contents-provider.d.ts +4 -0
  59. package/dist/table-of-contents-provider.js +30 -0
  60. package/dist/table-of-contents-provider.js.map +1 -0
  61. package/dist/table-of-contents.d.ts +11 -0
  62. package/dist/table-of-contents.js +9 -0
  63. package/dist/table-of-contents.js.map +1 -0
  64. package/dist/theme-context.d.ts +20 -0
  65. package/dist/theme-context.js +28 -0
  66. package/dist/theme-context.js.map +1 -0
  67. package/dist/tsconfig.tsbuildinfo +1 -0
  68. package/dist/ui/button.d.ts +12 -0
  69. package/dist/ui/button.js +34 -0
  70. package/dist/ui/button.js.map +1 -0
  71. package/dist/ui/dialog.d.ts +17 -0
  72. package/dist/ui/dialog.js +22 -0
  73. package/dist/ui/dialog.js.map +1 -0
  74. package/dist/ui/input.d.ts +4 -0
  75. package/dist/ui/input.js +9 -0
  76. package/dist/ui/input.js.map +1 -0
  77. package/dist/util.d.ts +59 -0
  78. package/dist/util.js +96 -0
  79. package/dist/util.js.map +1 -0
  80. package/doc-pagination.tsx +67 -0
  81. package/docs-index.tsx +17 -0
  82. package/docs-page.tsx +68 -0
  83. package/docs-sidebar.tsx +165 -0
  84. package/documentation-layout.tsx +99 -0
  85. package/heading.tsx +63 -0
  86. package/index.ts +28 -0
  87. package/mdx/callouts.tsx +29 -0
  88. package/mdx/code-block.tsx +89 -0
  89. package/mdx/components.tsx +55 -0
  90. package/mdx.ts +138 -0
  91. package/package.json +99 -0
  92. package/search-index.ts +52 -0
  93. package/search.tsx +273 -0
  94. package/table-of-contents-provider.tsx +43 -0
  95. package/table-of-contents.tsx +44 -0
  96. package/theme-context.tsx +57 -0
  97. package/ui/button.tsx +56 -0
  98. package/ui/dialog.tsx +108 -0
  99. package/ui/input.tsx +22 -0
  100. 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 };