├── .eslintrc.js ├── .gitignore ├── .npmrc ├── README.md ├── apps └── web │ ├── app │ ├── (app) │ │ └── og │ │ │ ├── image │ │ │ └── [url] │ │ │ │ └── route.tsx │ │ │ └── route.tsx │ ├── icon.tsx │ ├── layout.tsx │ └── page.tsx │ ├── assets │ ├── Geist-Medium.ttf │ └── Geist-Regular.ttf │ ├── components.json │ ├── components │ ├── .gitkeep │ ├── directory-list.tsx │ ├── og-image.tsx │ ├── providers.tsx │ └── search-bar.tsx │ ├── eslint.config.js │ ├── hooks │ └── .gitkeep │ ├── lib │ ├── .gitkeep │ ├── types.ts │ └── utils.ts │ ├── next-env.d.ts │ ├── next.config.mjs │ ├── package.json │ ├── postcss.config.mjs │ ├── public │ └── registries.json │ └── tsconfig.json ├── package.json ├── packages ├── eslint-config │ ├── README.md │ ├── base.js │ ├── next.js │ ├── package.json │ └── react-internal.js ├── typescript-config │ ├── README.md │ ├── base.json │ ├── nextjs.json │ ├── package.json │ └── react-library.json └── ui │ ├── components.json │ ├── eslint.config.js │ ├── package.json │ ├── postcss.config.mjs │ ├── src │ ├── components │ │ ├── .gitkeep │ │ ├── button.tsx │ │ └── card.tsx │ ├── hooks │ │ └── .gitkeep │ ├── lib │ │ └── utils.ts │ └── styles │ │ └── globals.css │ ├── tsconfig.json │ └── tsconfig.lint.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── registries.json ├── tsconfig.json └── turbo.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // This configuration only applies to the package manager root. 2 | /** @type {import("eslint").Linter.Config} */ 3 | module.exports = { 4 | ignorePatterns: ["apps/**", "packages/**"], 5 | extends: ["@workspace/eslint-config/library.js"], 6 | parser: "@typescript-eslint/parser", 7 | parserOptions: { 8 | project: true, 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # Local env files 9 | .env 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | # Testing 16 | coverage 17 | 18 | # Turbo 19 | .turbo 20 | 21 | # Vercel 22 | .vercel 23 | 24 | # Build Outputs 25 | .next/ 26 | out/ 27 | build 28 | dist 29 | 30 | 31 | # Debug 32 | npm-debug.log* 33 | 34 | # Misc 35 | .DS_Store 36 | *.pem 37 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbadillap/registry.directory/5552509df924944313f17d40b6778ec0e7336dca/.npmrc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![screenshot](https://registry.directory/og) 2 | 3 | ### How to suggest a registry? 4 | 5 | 1. Make sure the registry is not already listed in the directory. 6 | 2. Ensure the registry uses **shadcn** as its distribution method 7 | (e.g., `npx shadcn@latest https://custom-registry.com/{component}`). 8 | 3. Add the registry entry at the bottom of the [`registries.json`](https://github.com/rbadillap/registry.directory/blob/main/apps/web/public/registries.json) file. 9 | 10 | -------------------------------------------------------------------------------- /apps/web/app/(app)/og/image/[url]/route.tsx: -------------------------------------------------------------------------------- 1 | import { readFile } from "node:fs/promises" 2 | import { join } from "node:path" 3 | import { ImageResponse } from "next/og" 4 | import { ImageResponseOptions, type NextRequest } from "next/server" 5 | import { getHostname, isValidUrl } from "@/lib/utils" 6 | import type { DirectoryEntry } from "@/lib/types" 7 | import urlMetadata from "url-metadata" 8 | import { OGImage } from "@/components/og-image" 9 | import sharp from "sharp" 10 | 11 | // Force static generation 12 | export const dynamic = 'force-static' 13 | 14 | // registry.directory uses a custom user agent to fetch metadata 15 | const USER_AGENT = 'Mozilla/5.0 (compatible; registry-directory/1.0; +https://registry.directory)' 16 | 17 | // Unsupported image formats 18 | const UNSUPPORTED_IMAGE_FORMATS = ['webp'] 19 | 20 | // Configuration constants 21 | const METADATA_TIMEOUT = 10000 22 | const MAX_REDIRECTS = 5 23 | const DESCRIPTION_LENGTH = 200 24 | const IMAGE_TIMEOUT = 10000 25 | const OG_IMAGE_WIDTH = 1200 26 | const OG_IMAGE_HEIGHT = 630 27 | const CACHE_MAX_AGE = 3600 28 | const CACHE_STALE_WHILE_REVALIDATE = 86400 29 | 30 | // Load registries data for validation 31 | async function getRegistries() { 32 | try { 33 | const filePath = join(process.cwd(), "public/registries.json"); 34 | const fileContents = await readFile(filePath, "utf8"); 35 | return JSON.parse(fileContents); 36 | } catch (error) { 37 | console.error("Error reading registries.json:", error); 38 | return []; 39 | } 40 | } 41 | 42 | 43 | // Fetch and extract metadata from URL using url-metadata 44 | async function fetchRegistryMetadata(url: string): Promise<{ 45 | image: string | null; 46 | title: string | null; 47 | description: string | null; 48 | favicon: string | null; 49 | siteName: string | null; 50 | }> { 51 | try { 52 | const metadata = await urlMetadata(url, { 53 | timeout: METADATA_TIMEOUT, 54 | maxRedirects: MAX_REDIRECTS, 55 | descriptionLength: DESCRIPTION_LENGTH, 56 | ensureSecureImageRequest: true, 57 | requestHeaders: { 58 | 'User-Agent': USER_AGENT 59 | } 60 | }); 61 | 62 | return { 63 | image: metadata['og:image'] || metadata['twitter:image'] || null, 64 | title: metadata['og:title'] || metadata.title || null, 65 | description: metadata['og:description'] || metadata.description || null, 66 | favicon: metadata.favicons?.[0]?.href || null, 67 | siteName: metadata['og:site_name'] || null 68 | }; 69 | } catch (error) { 70 | console.error('Error fetching metadata for URL:', url, error); 71 | return { 72 | image: null, 73 | title: null, 74 | description: null, 75 | favicon: null, 76 | siteName: null 77 | }; 78 | } 79 | } 80 | 81 | async function convertUnsupportedImageToPng(imageUrl: string): Promise { 82 | try { 83 | // Since we already filtered by extension, we can directly fetch and convert 84 | // But let's still verify the content-type to be safe 85 | const imageResponse = await fetch(imageUrl, { 86 | headers: { 'User-Agent': USER_AGENT }, 87 | signal: AbortSignal.timeout(IMAGE_TIMEOUT) 88 | }); 89 | 90 | if (!imageResponse.ok) { 91 | console.error(`Failed to fetch image: ${imageResponse.status}`); 92 | return null; 93 | } 94 | 95 | const contentType = imageResponse.headers.get('content-type'); 96 | 97 | // Double-check: if content-type doesn't indicate unsupported format, return original URL 98 | if (!contentType || !UNSUPPORTED_IMAGE_FORMATS.some(format => contentType.includes(format))) { 99 | return imageUrl; 100 | } 101 | 102 | const imageBuffer = await imageResponse.arrayBuffer(); 103 | 104 | // Convert to PNG using Sharp 105 | const convertedBuffer = await sharp(Buffer.from(imageBuffer)) 106 | .png() 107 | .toBuffer(); 108 | 109 | // Return as data URL 110 | return `data:image/png;base64,${convertedBuffer.toString('base64')}`; 111 | } catch (error) { 112 | console.error('Error converting unsupported image:', error); 113 | return null; 114 | } 115 | } 116 | 117 | // Validate if URL belongs to a known registry 118 | async function isKnownRegistry(url: string): Promise { 119 | const registries = await getRegistries(); 120 | return registries.some((registry: DirectoryEntry) => { 121 | try { 122 | const registryUrl = new URL(registry.url); 123 | const inputUrl = new URL(url); 124 | return registryUrl.hostname === inputUrl.hostname; 125 | } catch { 126 | return false; 127 | } 128 | }); 129 | } 130 | 131 | // Process image: convert unsupported formats if needed 132 | async function processImage(imageUrl: string | null): Promise { 133 | if (!imageUrl) return null; 134 | 135 | const fileExtension = imageUrl.split('.').pop() || ''; 136 | if (UNSUPPORTED_IMAGE_FORMATS.includes(fileExtension)) { 137 | return await convertUnsupportedImageToPng(imageUrl); 138 | } 139 | 140 | return imageUrl; 141 | } 142 | 143 | // Load fonts for ImageResponse 144 | async function loadFonts() { 145 | return Promise.all([ 146 | readFile(join(process.cwd(), "assets/Geist-Regular.ttf")), 147 | readFile(join(process.cwd(), "assets/Geist-Medium.ttf")), 148 | ]); 149 | } 150 | 151 | // Create ImageResponse options 152 | function createImageResponseOptions(fonts: [Buffer, Buffer]): ImageResponseOptions { 153 | const [geistSans, geistSansMedium] = fonts; 154 | 155 | return { 156 | width: OG_IMAGE_WIDTH, 157 | height: OG_IMAGE_HEIGHT, 158 | fonts: [ 159 | { 160 | name: "Geist", 161 | data: geistSans, 162 | style: "normal", 163 | weight: 400, 164 | }, 165 | { 166 | name: "Geist", 167 | data: geistSansMedium, 168 | style: "normal", 169 | weight: 500, 170 | }, 171 | ], 172 | headers: { 173 | 'Cache-Control': `public, max-age=${CACHE_MAX_AGE}, stale-while-revalidate=${CACHE_STALE_WHILE_REVALIDATE}`, 174 | }, 175 | }; 176 | } 177 | 178 | // Generate static params for all registries 179 | export async function generateStaticParams() { 180 | const registries = await getRegistries(); 181 | 182 | return registries.map((registry: DirectoryEntry) => ({ 183 | url: encodeURIComponent(registry.url) 184 | })); 185 | } 186 | 187 | export async function GET( 188 | request: NextRequest, 189 | { params }: { params: Promise<{ url: string }> } 190 | ) { 191 | try { 192 | // Decode and validate URL 193 | const resolvedParams = await params; 194 | const url = decodeURIComponent(resolvedParams.url); 195 | 196 | if (!url || !isValidUrl(url)) { 197 | return new Response(`Invalid URL provided`, { status: 400 }); 198 | } 199 | 200 | if (!(await isKnownRegistry(url))) { 201 | return new Response('Registry not found', { status: 404 }); 202 | } 203 | 204 | // Fetch metadata and process image 205 | const metadata = await fetchRegistryMetadata(url); 206 | const processedImage = await processImage(metadata.image); 207 | 208 | // Load fonts and create response 209 | const fonts = await loadFonts(); 210 | const options = createImageResponseOptions(fonts); 211 | 212 | return new ImageResponse( 213 | , 217 | options, 218 | ); 219 | } catch (error: unknown) { 220 | console.error(error); 221 | return new Response(`Failed to generate the image`, { status: 500 }); 222 | } 223 | } 224 | 225 | -------------------------------------------------------------------------------- /apps/web/app/(app)/og/route.tsx: -------------------------------------------------------------------------------- 1 | import { ImageResponse } from "next/og"; 2 | import { readFile } from "node:fs/promises"; 3 | import { join } from "node:path"; 4 | 5 | export async function GET() { 6 | const [geistSans, geistSansMedium] = await Promise.all([ 7 | readFile(join(process.cwd(), 'assets/Geist-Regular.ttf')), 8 | readFile(join(process.cwd(), 'assets/Geist-Medium.ttf')) 9 | ]); 10 | 11 | return new ImageResponse( 12 | ( 13 |
20 | {/* Decorative borders */} 21 |
22 |
23 |
24 |
25 | 26 | {/* Main content */} 27 |
28 |

32 | registry 33 | .directory 34 | 38 | beta 39 | 40 |

41 | 42 |

43 | the place where 44 | 45 | 46 | 47 | 48 | 49 | 50 | shadcn/ui 51 | registries live. 52 |

53 |
54 | 55 | {/* Subtle glow effect */} 56 |
62 |
63 | ), 64 | { 65 | width: 1200, 66 | height: 630, 67 | fonts: [ 68 | { 69 | name: "Geist", 70 | data: geistSans, 71 | style: "normal", 72 | weight: 400, 73 | }, 74 | { 75 | name: "Geist", 76 | data: geistSansMedium, 77 | style: "normal", 78 | weight: 500, 79 | }, 80 | ], 81 | } 82 | ); 83 | } -------------------------------------------------------------------------------- /apps/web/app/icon.tsx: -------------------------------------------------------------------------------- 1 | import { ImageResponse } from "next/og"; 2 | 3 | export const size = { 4 | width: 32, 5 | height: 32, 6 | }; 7 | 8 | export const contentType = 'image/png' 9 | 10 | export default function Icon() { 11 | return new ImageResponse( 12 | ( 13 |
r.d
25 | ), 26 | { 27 | ...size, 28 | } 29 | ) 30 | } -------------------------------------------------------------------------------- /apps/web/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata, Viewport } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google" 3 | import { Analytics } from "@vercel/analytics/next" 4 | 5 | import "@workspace/ui/globals.css" 6 | import { Providers } from "@/components/providers" 7 | 8 | const geistSans = Geist({ 9 | variable: "--font-geist-sans", 10 | subsets: ["latin"], 11 | }); 12 | 13 | const geistMono = Geist_Mono({ 14 | variable: "--font-geist-mono", 15 | subsets: ["latin"], 16 | }); 17 | 18 | export const metadata: Metadata = { 19 | title: "registry.directory - a collection of shadcn/ui registries", 20 | description: "The place where shadcn/ui registries live. Discover, Preview, Copy, and Paste components.", 21 | 22 | }; 23 | 24 | export const viewport: Viewport = { 25 | width: "device-width", 26 | initialScale: 1, 27 | maximumScale: 1, 28 | userScalable: false, 29 | themeColor: [ 30 | { media: "(prefers-color-scheme: light)", color: "var(--background)" }, 31 | { media: "(prefers-color-scheme: dark)", color: "var(--background)" }, 32 | ], 33 | }; 34 | 35 | export default function RootLayout({ 36 | children, 37 | }: Readonly<{ 38 | children: React.ReactNode; 39 | }>) { 40 | return ( 41 | 42 | 45 | {children} 46 | 47 | 48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /apps/web/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { readFile } from "node:fs/promises"; 2 | import { join } from "node:path"; 3 | import { Metadata } from "next"; 4 | import { DirectoryList } from "@/components/directory-list"; 5 | import type { DirectoryEntry } from "@/lib/types"; 6 | 7 | // Enable static generation 8 | export const dynamic = 'force-static' 9 | 10 | export const metadata: Metadata = { 11 | // metadataBase: new URL(process.env.VERCEL_URL || "http://localhost:3000"), 12 | title: "registry.directory - a collection of shadcn/ui registries", 13 | description: 14 | "The place where shadcn/ui registries live. Discover, Preview, Copy, and Paste components.", 15 | openGraph: { 16 | images: [ 17 | { 18 | url: "/og", 19 | }, 20 | ], 21 | }, 22 | twitter: { 23 | card: "summary_large_image", 24 | images: [ 25 | { 26 | url: "/og", 27 | }, 28 | ], 29 | }, 30 | }; 31 | 32 | async function getRegistries(): Promise { 33 | try { 34 | const filePath = join(process.cwd(), "public/registries.json"); 35 | const fileContents = await readFile(filePath, "utf8"); 36 | const registries = JSON.parse(fileContents); 37 | 38 | // Transform to DirectoryEntry format 39 | return registries.map((registry: DirectoryEntry) => ({ 40 | name: registry.name, 41 | description: registry.description, 42 | url: registry.url, 43 | })); 44 | } catch (error) { 45 | console.error("Error reading registries.json:", error); 46 | // Fallback to empty array or default registries 47 | return []; 48 | } 49 | } 50 | 51 | export default async function Home() { 52 | const entries = await getRegistries(); 53 | 54 | return ( 55 |
56 |
57 |

58 | registry.directory{" "} 59 | 60 | beta 61 | 62 |

63 |
64 | 65 |
66 | discover, preview, copy 67 | 68 | 74 | 81 | 88 | 89 | 90 | shadcn/ui 91 | registries 92 |
93 | 94 | 95 |
96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /apps/web/assets/Geist-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbadillap/registry.directory/5552509df924944313f17d40b6778ec0e7336dca/apps/web/assets/Geist-Medium.ttf -------------------------------------------------------------------------------- /apps/web/assets/Geist-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbadillap/registry.directory/5552509df924944313f17d40b6778ec0e7336dca/apps/web/assets/Geist-Regular.ttf -------------------------------------------------------------------------------- /apps/web/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "../../packages/ui/src/styles/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true 11 | }, 12 | "iconLibrary": "lucide", 13 | "aliases": { 14 | "components": "@/components", 15 | "hooks": "@/hooks", 16 | "lib": "@/lib", 17 | "utils": "@workspace/ui/lib/utils", 18 | "ui": "@workspace/ui/components" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbadillap/registry.directory/5552509df924944313f17d40b6778ec0e7336dca/apps/web/components/.gitkeep -------------------------------------------------------------------------------- /apps/web/components/directory-list.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Image from "next/image"; 4 | import { ExternalLink as ExternalLinkIcon } from "lucide-react"; 5 | import { 6 | Card, 7 | CardHeader, 8 | CardTitle, 9 | CardDescription, 10 | CardContent, 11 | CardFooter 12 | } from "@workspace/ui/components/card"; 13 | import { 14 | Button, 15 | } from "@workspace/ui/components/button"; 16 | import { getHostname } from "@/lib/utils"; 17 | import type { DirectoryEntry } from "@/lib/types"; 18 | import { SearchBar } from "./search-bar"; 19 | import { useMemo, useState } from "react"; 20 | 21 | const addUtmReference = (url: string) => { 22 | try { 23 | const u = new URL(url) 24 | u.searchParams.set("utm_source", "registry.directory") 25 | u.searchParams.set("utm_medium", "directory") 26 | u.searchParams.set("utm_campaign", "registry_preview") 27 | return u.toString() 28 | } catch { 29 | // Fallback if URL is invalid 30 | return url 31 | } 32 | } 33 | 34 | export function DirectoryList({ entries }: { entries: DirectoryEntry[] }) { 35 | const [searchTerm, setSearchTerm] = useState(''); 36 | 37 | const filteredEntries = useMemo(() => { 38 | if (!searchTerm) return entries; 39 | 40 | const term = searchTerm.toLowerCase(); 41 | return entries.filter(entry => 42 | entry.name.toLowerCase().includes(term) || 43 | entry.description.toLowerCase().includes(term) || 44 | entry.url.toLowerCase().includes(term) 45 | ); 46 | }, [entries, searchTerm]); 47 | 48 | return ( 49 | <> 50 | 51 | 52 | {filteredEntries.length === 0 ? ( 53 |
54 |

55 | No registries found matching "{searchTerm}" 56 |

57 |
58 | ) : ( 59 |
60 | {filteredEntries.map((entry) => ( 61 |
62 | 63 |
64 | {`${entry.name} 73 |
74 | 75 | 88 | 104 | 105 | 106 | 107 | {entry.description} 108 | 109 | 110 | 111 | 112 | {getHostname(entry.url)} 113 | 114 | 115 |
116 |
117 | ))} 118 |
119 | )} 120 | 121 | ); 122 | } -------------------------------------------------------------------------------- /apps/web/components/og-image.tsx: -------------------------------------------------------------------------------- 1 | interface OGImageProps { 2 | img: string | null; 3 | hostname: string; 4 | } 5 | 6 | export function OGImage({ img, hostname }: OGImageProps) { 7 | return ( 8 |
14 | {/* top left logo r.d */} 15 |
r.d
16 | 17 | {/* Decorative borders */} 18 |
19 |
20 |
21 |
22 | 23 | {/* Main content */} 24 |
25 | {/* OG Image or Fallback */} 26 | {img ? ( 27 | Registry image 39 | ) : ( 40 |
41 | {/* Fallback message */} 42 |
Preview Unavailable
43 |
Click to visit the registry
44 |
45 | )} 46 | 47 | {/* Site info */} 48 |
49 |

50 | {hostname} 51 |

52 |
53 |
54 | 55 | {/* Subtle glow effect */} 56 |
62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /apps/web/components/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ThemeProvider as NextThemesProvider } from "next-themes" 5 | 6 | export function Providers({ children }: { children: React.ReactNode }) { 7 | return ( 8 | 15 | {children} 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /apps/web/components/search-bar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Search, Plus } from "lucide-react"; 4 | 5 | interface SearchBarProps { 6 | value: string; 7 | onChange: (value: string) => void; 8 | } 9 | 10 | export function SearchBar({ value, onChange }: SearchBarProps) { 11 | const handleClear = () => { 12 | onChange(''); 13 | }; 14 | 15 | return ( 16 |
17 |
18 |
19 | {/* Search Input */} 20 |
21 | 22 | onChange(e.target.value)} 26 | placeholder="Search registries by name, description, or url..." 27 | className="w-full pl-10 pr-10 py-2 bg-black border border-input rounded-none text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground outline-none transition-[color,box-shadow] font-mono text-sm focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50" 28 | autoComplete="off" 29 | spellCheck="false" 30 | /> 31 | {value && ( 32 | 46 | )} 47 |
48 | 49 | {/* Add Registry Button */} 50 | 57 | 58 | Add registry 59 | 60 |
61 |
62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /apps/web/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { nextJsConfig } from "@workspace/eslint-config/next-js" 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default nextJsConfig 5 | -------------------------------------------------------------------------------- /apps/web/hooks/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbadillap/registry.directory/5552509df924944313f17d40b6778ec0e7336dca/apps/web/hooks/.gitkeep -------------------------------------------------------------------------------- /apps/web/lib/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbadillap/registry.directory/5552509df924944313f17d40b6778ec0e7336dca/apps/web/lib/.gitkeep -------------------------------------------------------------------------------- /apps/web/lib/types.ts: -------------------------------------------------------------------------------- 1 | export type DirectoryEntry = { 2 | name: string; 3 | description: string; 4 | url: string; 5 | }; -------------------------------------------------------------------------------- /apps/web/lib/utils.ts: -------------------------------------------------------------------------------- 1 | export function getHostname(url: string): string { 2 | try { 3 | return new URL(url).hostname; 4 | } catch { 5 | return url; 6 | } 7 | } 8 | 9 | export function isValidUrl(url: string): boolean { 10 | try { 11 | new URL(url) 12 | return true 13 | } catch { 14 | return false 15 | } 16 | } -------------------------------------------------------------------------------- /apps/web/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/web/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | transpilePackages: ["@workspace/ui"], 4 | } 5 | 6 | export default nextConfig 7 | -------------------------------------------------------------------------------- /apps/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.0.1", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "dev": "next dev --turbopack", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint", 11 | "lint:fix": "next lint --fix", 12 | "typecheck": "tsc --noEmit" 13 | }, 14 | "dependencies": { 15 | "@vercel/analytics": "^1.5.0", 16 | "@workspace/ui": "workspace:*", 17 | "lucide-react": "^0.475.0", 18 | "next": "^15.2.3", 19 | "next-themes": "^0.4.4", 20 | "react": "^19.0.0", 21 | "react-dom": "^19.0.0", 22 | "sharp": "^0.34.4", 23 | "url-metadata": "^5.2.2" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "^20", 27 | "@types/react": "^19", 28 | "@types/react-dom": "^19", 29 | "@workspace/eslint-config": "workspace:^", 30 | "@workspace/typescript-config": "workspace:*", 31 | "typescript": "^5.7.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/web/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | export { default } from "@workspace/ui/postcss.config"; -------------------------------------------------------------------------------- /apps/web/public/registries.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "shadcn/ui", 4 | "description": "The official registry for shadcn/ui components.", 5 | "url": "https://ui.shadcn.com/" 6 | }, 7 | { 8 | "name": "ui.pub", 9 | "description": "Perfect tools to build next-gen UI", 10 | "url": "https://uipub.com" 11 | }, 12 | { 13 | "name": "Tweakcn", 14 | "description": "A powerful Theme Editor for shadcn/ui.", 15 | "url": "https://tweakcn.com/" 16 | }, 17 | { 18 | "name": "Origin UI", 19 | "description": "Beautiful UI components built with Tailwind CSS and React", 20 | "url": "https://originui.com/" 21 | }, 22 | { 23 | "name": "Aceternity UI", 24 | "description": "Professional Next.js, Tailwind CSS and Framer Motion components.", 25 | "url": "https://ui.aceternity.com/" 26 | }, 27 | { 28 | "name": "Shadcn UI Blocks", 29 | "description": "Customized Shadcn UI Blocks & Components | Preview & Copy", 30 | "url": "https://shadcnui-blocks.com/" 31 | }, 32 | { 33 | "name": "Shadcn Form Builder", 34 | "description": "Create forms with Shadcn, react-hook-form and zod within minutes.", 35 | "url": "https://shadcn-form.com/" 36 | }, 37 | { 38 | "name": "Shadcn Blocks", 39 | "description": "A collection of premium blocks for Shadcn UI + Tailwind", 40 | "url": "https://shadcnblocks.com" 41 | }, 42 | { 43 | "name": "Algolia SiteSearch", 44 | "description": "Opinionated search experiences for your website needs, powered by Algolia", 45 | "url": "https://sitesearch.algolia.com/" 46 | }, 47 | { 48 | "name": "StyleGlide", 49 | "description": "Generate color palettes and typography styles. Tailored to your project. Distributed on shadcn. Edit the results to make it your own.", 50 | "url": "https://www.styleglide.ai/" 51 | }, 52 | { 53 | "name": "Neobrutalism components", 54 | "description": "A collection of neobrutalism-styled, shadcn/ui based components.", 55 | "url": "https://neobrutalism.dev/" 56 | }, 57 | { 58 | "name": "kokonut/ui", 59 | "description": "100+ UI components built with Tailwind CSS and shadcn/ui for Next.js", 60 | "url": "https://kokonutui.com/" 61 | }, 62 | { 63 | "name": "Magic UI", 64 | "description": "UI library for Design Engineers", 65 | "url": "https://magicui.design/" 66 | }, 67 | { 68 | "name": "Cult UI", 69 | "description": "Components crafted for Design Engineers", 70 | "url": "https://cult-ui.com" 71 | }, 72 | { 73 | "name": "Kibo UI", 74 | "description": "A custom registry of composable, accessible and open source components", 75 | "url": "https://kibo-ui.com" 76 | }, 77 | { 78 | "name": "ReUI", 79 | "description": "UI components and fully functional apps built with React, Next.js and Tailwind", 80 | "url": "https://reui.io" 81 | }, 82 | { 83 | "name": "RetroUI", 84 | "description": "React based component library, inspired by neo-brutalism design system", 85 | "url": "https://retroui.dev" 86 | }, 87 | { 88 | "name": "Skiper UI", 89 | "description": "Skiper UI - Un-common Components for shadcn/ui | Skiper UI", 90 | "url": "https://skiper-ui.com/" 91 | }, 92 | { 93 | "name": "JollyUI", 94 | "description": "shadcn/ui compatible react aria components that you can copy and paste into your apps. Accessible. Customizable. Open Source.", 95 | "url": "https://www.jollyui.dev/" 96 | }, 97 | { 98 | "name": "React Bits", 99 | "description": "An open source collection of animated, interactive & fully customizable React components for building memorable websites.", 100 | "url": "https://reactbits.dev/" 101 | }, 102 | { 103 | "name": "WDS Shadcn Registry", 104 | "description": "A collection of accessible components that fit perfectly with Shadcn.", 105 | "url": "https://wds-shadcn-registry.netlify.app/" 106 | }, 107 | { 108 | "name": "Animate UI", 109 | "description": "Fully animated, open-source component distribution built with React, TypeScript, Tailwind CSS, Motion, and Shadcn CLI.", 110 | "url": "https://animate-ui.com/" 111 | }, 112 | { 113 | "name": "AI Elements", 114 | "description": "A component library to help you build AI-native applications faster. It provides pre-built components like conversations, messages and more.", 115 | "url": "https://ai-sdk.dev/elements/overview" 116 | }, 117 | { 118 | "name": "Shadcn.IO", 119 | "description": "Essential UI components, advanced patterns, and AI integrations.", 120 | "url": "https://shadcn.io/" 121 | }, 122 | { 123 | "name": "Dice UI", 124 | "description": "Accessible shadcn/ui components built with React, TypeScript, and Tailwind CSS. Copy-paste ready, and customizable.", 125 | "url": "https://www.diceui.com/" 126 | }, 127 | { 128 | "name": "ElevenLabs UI", 129 | "description": "A collection of components designed for agent & audio applications, including orbs, waveforms, voice agents, audio players, and more.", 130 | "url": "https://ui.elevenlabs.io/" 131 | }, 132 | { 133 | "name": "8bitcn", 134 | "description": "A set of 8-bit styled components for shadcn/ui. Works with your favorite frameworks. Open Source. Open Code.", 135 | "url": "https://www.8bitcn.com/" 136 | }, 137 | { 138 | "name": "pqoqubbw/icons", 139 | "description": "An open-source (MIT License) collection of smooth animated icons for your projects. Built with motion and lucide", 140 | "url": "https://icons.pqoqubbw.dev/" 141 | }, 142 | { 143 | "name": "Intent UI", 144 | "description": "Accessible React component library to copy, customize, and own your UI.", 145 | "url": "https://intentui.com/" 146 | }, 147 | { 148 | "name": "Design Intent UI", 149 | "description": "Build your project faster with 500+ ready-made UI blocks you can copy and customize to fit your needs.", 150 | "url": "https://design.intentui.com/" 151 | }, 152 | { 153 | "name": "Eldora UI", 154 | "description": "Eldora UI is a collection of re-usable components that you can copy and paste into your web apps. It primarily features components, blocks, and templates.", 155 | "url": "https://www.eldoraui.site/" 156 | }, 157 | { 158 | "name": "shadcn/studio", 159 | "description": "Accelerate your project development with ready-to-use, and fully customizable shadcn ui Components, Blocks, UI Kits, Boilerplates, Templates and Themes with AI Tools ", 160 | "url": "https://shadcnstudio.com/" 161 | }, 162 | { 163 | "name": "Shadix UI", 164 | "description": "Explore a custom registry of production-ready, animated components for shadcn/ui. Built with React, Tailwind CSS, and Framer Motion for easy integration into your React projects.", 165 | "url": "https://shadix-ui.vercel.app/" 166 | }, 167 | { 168 | "name": "HextaUI", 169 | "description": "Extended Components & Blocks for shadcn/ui", 170 | "url": "https://hextaui.com" 171 | } 172 | ] 173 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@workspace/typescript-config/nextjs.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["./*"], 7 | "@workspace/ui/*": ["../../packages/ui/src/*"] 8 | }, 9 | "plugins": [ 10 | { 11 | "name": "next" 12 | } 13 | ] 14 | }, 15 | "include": [ 16 | "next-env.d.ts", 17 | "next.config.ts", 18 | "**/*.ts", 19 | "**/*.tsx", 20 | ".next/types/**/*.ts" 21 | ], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "registrydir", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "build": "turbo build", 7 | "dev": "turbo dev", 8 | "lint": "turbo lint", 9 | "format": "prettier --write \"**/*.{ts,tsx,md}\"" 10 | }, 11 | "devDependencies": { 12 | "@workspace/eslint-config": "workspace:*", 13 | "@workspace/typescript-config": "workspace:*", 14 | "prettier": "^3.5.1", 15 | "turbo": "^2.4.2", 16 | "typescript": "5.7.3" 17 | }, 18 | "packageManager": "pnpm@10.8.0", 19 | "engines": { 20 | "node": ">=20" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/eslint-config/README.md: -------------------------------------------------------------------------------- 1 | # `@workspace/eslint-config` 2 | 3 | Shared eslint configuration for the workspace. 4 | -------------------------------------------------------------------------------- /packages/eslint-config/base.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js" 2 | import eslintConfigPrettier from "eslint-config-prettier" 3 | import onlyWarn from "eslint-plugin-only-warn" 4 | import turboPlugin from "eslint-plugin-turbo" 5 | import tseslint from "typescript-eslint" 6 | 7 | /** 8 | * A shared ESLint configuration for the repository. 9 | * 10 | * @type {import("eslint").Linter.Config} 11 | * */ 12 | export const config = [ 13 | js.configs.recommended, 14 | eslintConfigPrettier, 15 | ...tseslint.configs.recommended, 16 | { 17 | plugins: { 18 | turbo: turboPlugin, 19 | }, 20 | rules: { 21 | "turbo/no-undeclared-env-vars": "warn", 22 | }, 23 | }, 24 | { 25 | plugins: { 26 | onlyWarn, 27 | }, 28 | }, 29 | { 30 | ignores: ["dist/**"], 31 | }, 32 | ] 33 | -------------------------------------------------------------------------------- /packages/eslint-config/next.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js" 2 | import pluginNext from "@next/eslint-plugin-next" 3 | import eslintConfigPrettier from "eslint-config-prettier" 4 | import pluginReact from "eslint-plugin-react" 5 | import pluginReactHooks from "eslint-plugin-react-hooks" 6 | import globals from "globals" 7 | import tseslint from "typescript-eslint" 8 | 9 | import { config as baseConfig } from "./base.js" 10 | 11 | /** 12 | * A custom ESLint configuration for libraries that use Next.js. 13 | * 14 | * @type {import("eslint").Linter.Config} 15 | * */ 16 | export const nextJsConfig = [ 17 | ...baseConfig, 18 | js.configs.recommended, 19 | eslintConfigPrettier, 20 | ...tseslint.configs.recommended, 21 | { 22 | ...pluginReact.configs.flat.recommended, 23 | languageOptions: { 24 | ...pluginReact.configs.flat.recommended.languageOptions, 25 | globals: { 26 | ...globals.serviceworker, 27 | }, 28 | }, 29 | }, 30 | { 31 | plugins: { 32 | "@next/next": pluginNext, 33 | }, 34 | rules: { 35 | ...pluginNext.configs.recommended.rules, 36 | ...pluginNext.configs["core-web-vitals"].rules, 37 | }, 38 | }, 39 | { 40 | plugins: { 41 | "react-hooks": pluginReactHooks, 42 | }, 43 | settings: { react: { version: "detect" } }, 44 | rules: { 45 | ...pluginReactHooks.configs.recommended.rules, 46 | // React scope no longer necessary with new JSX transform. 47 | "react/react-in-jsx-scope": "off", 48 | "react/prop-types": "off", 49 | }, 50 | }, 51 | ] 52 | -------------------------------------------------------------------------------- /packages/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@workspace/eslint-config", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "private": true, 6 | "exports": { 7 | "./base": "./base.js", 8 | "./next-js": "./next.js", 9 | "./react-internal": "./react-internal.js" 10 | }, 11 | "devDependencies": { 12 | "@eslint/js": "^9.35.0", 13 | "@next/eslint-plugin-next": "^15.1.7", 14 | "@typescript-eslint/eslint-plugin": "^8.24.1", 15 | "@typescript-eslint/parser": "^8.24.1", 16 | "eslint": "^9.20.1", 17 | "eslint-config-prettier": "^9.1.0", 18 | "eslint-plugin-only-warn": "^1.1.0", 19 | "eslint-plugin-react": "^7.37.4", 20 | "eslint-plugin-react-hooks": "^5.1.0", 21 | "eslint-plugin-turbo": "^2.4.2", 22 | "globals": "^15.15.0", 23 | "typescript": "^5.7.3", 24 | "typescript-eslint": "^8.24.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/eslint-config/react-internal.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js" 2 | import eslintConfigPrettier from "eslint-config-prettier" 3 | import pluginReact from "eslint-plugin-react" 4 | import pluginReactHooks from "eslint-plugin-react-hooks" 5 | import globals from "globals" 6 | import tseslint from "typescript-eslint" 7 | 8 | import { config as baseConfig } from "./base.js" 9 | 10 | /** 11 | * A custom ESLint configuration for libraries that use React. 12 | * 13 | * @type {import("eslint").Linter.Config} */ 14 | export const config = [ 15 | ...baseConfig, 16 | js.configs.recommended, 17 | eslintConfigPrettier, 18 | ...tseslint.configs.recommended, 19 | pluginReact.configs.flat.recommended, 20 | { 21 | languageOptions: { 22 | ...pluginReact.configs.flat.recommended.languageOptions, 23 | globals: { 24 | ...globals.serviceworker, 25 | ...globals.browser, 26 | }, 27 | }, 28 | }, 29 | { 30 | plugins: { 31 | "react-hooks": pluginReactHooks, 32 | }, 33 | settings: { react: { version: "detect" } }, 34 | rules: { 35 | ...pluginReactHooks.configs.recommended.rules, 36 | // React scope no longer necessary with new JSX transform. 37 | "react/react-in-jsx-scope": "off", 38 | "react/prop-types": "off", 39 | }, 40 | }, 41 | ] 42 | -------------------------------------------------------------------------------- /packages/typescript-config/README.md: -------------------------------------------------------------------------------- 1 | # `@workspace/typescript-config` 2 | 3 | Shared typescript configuration for the workspace. 4 | -------------------------------------------------------------------------------- /packages/typescript-config/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "declaration": true, 6 | "declarationMap": true, 7 | "esModuleInterop": true, 8 | "incremental": false, 9 | "isolatedModules": true, 10 | "lib": ["es2022", "DOM", "DOM.Iterable"], 11 | "module": "NodeNext", 12 | "moduleDetection": "force", 13 | "moduleResolution": "NodeNext", 14 | "noUncheckedIndexedAccess": true, 15 | "resolveJsonModule": true, 16 | "skipLibCheck": true, 17 | "strict": true, 18 | "target": "ES2022" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/typescript-config/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "plugins": [{ "name": "next" }], 7 | "module": "ESNext", 8 | "moduleResolution": "Bundler", 9 | "allowJs": true, 10 | "jsx": "preserve", 11 | "noEmit": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/typescript-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@workspace/typescript-config", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "PROPRIETARY", 6 | "publishConfig": { 7 | "access": "public" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/typescript-config/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "React Library", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "jsx": "react-jsx", 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/ui/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true 11 | }, 12 | "iconLibrary": "lucide", 13 | "aliases": { 14 | "components": "@workspace/ui/components", 15 | "utils": "@workspace/ui/lib/utils", 16 | "hooks": "@workspace/ui/hooks", 17 | "lib": "@workspace/ui/lib", 18 | "ui": "@workspace/ui/components" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/ui/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { config } from "@workspace/eslint-config/react-internal" 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default config 5 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@workspace/ui", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "lint": "eslint . --max-warnings 0" 8 | }, 9 | "dependencies": { 10 | "@radix-ui/react-slot": "^1.1.2", 11 | "class-variance-authority": "^0.7.1", 12 | "clsx": "^2.1.1", 13 | "lucide-react": "^0.475.0", 14 | "next-themes": "^0.4.4", 15 | "react": "^19.0.0", 16 | "react-dom": "^19.0.0", 17 | "tailwind-merge": "^3.0.1", 18 | "tw-animate-css": "^1.2.4", 19 | "zod": "^3.24.2" 20 | }, 21 | "devDependencies": { 22 | "@tailwindcss/postcss": "^4.0.8", 23 | "@turbo/gen": "^2.4.2", 24 | "@types/node": "^20", 25 | "@types/react": "^19", 26 | "@types/react-dom": "^19", 27 | "@workspace/eslint-config": "workspace:*", 28 | "@workspace/typescript-config": "workspace:*", 29 | "tailwindcss": "^4.0.8", 30 | "typescript": "^5.7.3" 31 | }, 32 | "exports": { 33 | "./globals.css": "./src/styles/globals.css", 34 | "./postcss.config": "./postcss.config.mjs", 35 | "./lib/*": "./src/lib/*.ts", 36 | "./components/*": "./src/components/*.tsx", 37 | "./hooks/*": "./src/hooks/*.ts" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/ui/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { "@tailwindcss/postcss": {} }, 4 | }; 5 | 6 | export default config; 7 | -------------------------------------------------------------------------------- /packages/ui/src/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbadillap/registry.directory/5552509df924944313f17d40b6778ec0e7336dca/packages/ui/src/components/.gitkeep -------------------------------------------------------------------------------- /packages/ui/src/components/button.tsx: -------------------------------------------------------------------------------- 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 "@workspace/ui/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 16 | outline: 17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: 21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 22 | link: "text-primary underline-offset-4 hover:underline", 23 | }, 24 | size: { 25 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 28 | icon: "size-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ) 37 | 38 | function Button({ 39 | className, 40 | variant, 41 | size, 42 | asChild = false, 43 | ...props 44 | }: React.ComponentProps<"button"> & 45 | VariantProps & { 46 | asChild?: boolean 47 | }) { 48 | const Comp = asChild ? Slot : "button" 49 | 50 | return ( 51 | 56 | ) 57 | } 58 | 59 | export { Button, buttonVariants } 60 | -------------------------------------------------------------------------------- /packages/ui/src/components/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@workspace/ui/lib/utils" 4 | 5 | function Card({ className, ...props }: React.ComponentProps<"div">) { 6 | return ( 7 |
15 | ) 16 | } 17 | 18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) { 19 | return ( 20 |
28 | ) 29 | } 30 | 31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 32 | return ( 33 |
38 | ) 39 | } 40 | 41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 42 | return ( 43 |
48 | ) 49 | } 50 | 51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) { 52 | return ( 53 |
61 | ) 62 | } 63 | 64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) { 65 | return ( 66 |
71 | ) 72 | } 73 | 74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 75 | return ( 76 |
81 | ) 82 | } 83 | 84 | export { 85 | Card, 86 | CardHeader, 87 | CardFooter, 88 | CardTitle, 89 | CardAction, 90 | CardDescription, 91 | CardContent, 92 | } 93 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rbadillap/registry.directory/5552509df924944313f17d40b6778ec0e7336dca/packages/ui/src/hooks/.gitkeep -------------------------------------------------------------------------------- /packages/ui/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /packages/ui/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @source "../../../apps/**/*.{ts,tsx}"; 3 | @source "../../../components/**/*.{ts,tsx}"; 4 | @source "../**/*.{ts,tsx}"; 5 | 6 | @import "tw-animate-css"; 7 | 8 | @custom-variant dark (&:is(.dark *)); 9 | 10 | :root { 11 | --background: oklch(1 0 0); 12 | --foreground: oklch(0.145 0 0); 13 | --card: oklch(1 0 0); 14 | --card-foreground: oklch(0.145 0 0); 15 | --popover: oklch(1 0 0); 16 | --popover-foreground: oklch(0.145 0 0); 17 | --primary: oklch(0.205 0 0); 18 | --primary-foreground: oklch(0.985 0 0); 19 | --secondary: oklch(0.97 0 0); 20 | --secondary-foreground: oklch(0.205 0 0); 21 | --muted: oklch(0.97 0 0); 22 | --muted-foreground: oklch(0.556 0 0); 23 | --accent: oklch(0.97 0 0); 24 | --accent-foreground: oklch(0.205 0 0); 25 | --destructive: oklch(0.577 0.245 27.325); 26 | --destructive-foreground: oklch(0.577 0.245 27.325); 27 | --border: oklch(0.922 0 0); 28 | --input: oklch(0.922 0 0); 29 | --ring: oklch(0.708 0 0); 30 | --chart-1: oklch(0.646 0.222 41.116); 31 | --chart-2: oklch(0.6 0.118 184.704); 32 | --chart-3: oklch(0.398 0.07 227.392); 33 | --chart-4: oklch(0.828 0.189 84.429); 34 | --chart-5: oklch(0.769 0.188 70.08); 35 | --radius: 0.625rem; 36 | --sidebar: oklch(0.985 0 0); 37 | --sidebar-foreground: oklch(0.145 0 0); 38 | --sidebar-primary: oklch(0.205 0 0); 39 | --sidebar-primary-foreground: oklch(0.985 0 0); 40 | --sidebar-accent: oklch(0.97 0 0); 41 | --sidebar-accent-foreground: oklch(0.205 0 0); 42 | --sidebar-border: oklch(0.922 0 0); 43 | --sidebar-ring: oklch(0.708 0 0); 44 | } 45 | 46 | .dark { 47 | --background: oklch(0.145 0 0); 48 | --foreground: oklch(0.985 0 0); 49 | --card: oklch(0.145 0 0); 50 | --card-foreground: oklch(0.985 0 0); 51 | --popover: oklch(0.145 0 0); 52 | --popover-foreground: oklch(0.985 0 0); 53 | --primary: oklch(0.985 0 0); 54 | --primary-foreground: oklch(0.205 0 0); 55 | --secondary: oklch(0.269 0 0); 56 | --secondary-foreground: oklch(0.985 0 0); 57 | --muted: oklch(0.269 0 0); 58 | --muted-foreground: oklch(0.708 0 0); 59 | --accent: oklch(0.269 0 0); 60 | --accent-foreground: oklch(0.985 0 0); 61 | --destructive: oklch(0.396 0.141 25.723); 62 | --destructive-foreground: oklch(0.637 0.237 25.331); 63 | --border: oklch(1 0 0 / 10%); 64 | --input: oklch(1 0 0 / 15%); 65 | --ring: oklch(0.556 0 0); 66 | --chart-1: oklch(0.488 0.243 264.376); 67 | --chart-2: oklch(0.696 0.17 162.48); 68 | --chart-3: oklch(0.769 0.188 70.08); 69 | --chart-4: oklch(0.627 0.265 303.9); 70 | --chart-5: oklch(0.645 0.246 16.439); 71 | --sidebar: oklch(0.205 0 0); 72 | --sidebar-foreground: oklch(0.985 0 0); 73 | --sidebar-primary: oklch(0.488 0.243 264.376); 74 | --sidebar-primary-foreground: oklch(0.985 0 0); 75 | --sidebar-accent: oklch(0.269 0 0); 76 | --sidebar-accent-foreground: oklch(0.985 0 0); 77 | --sidebar-border: oklch(1 0 0 / 10%); 78 | --sidebar-ring: oklch(0.439 0 0); 79 | } 80 | 81 | @theme inline { 82 | --color-background: var(--background); 83 | --color-foreground: var(--foreground); 84 | --font-sans: var(--font-geist-sans); 85 | --font-mono: var(--font-geist-mono); 86 | --color-card: var(--card); 87 | --color-card-foreground: var(--card-foreground); 88 | --color-popover: var(--popover); 89 | --color-popover-foreground: var(--popover-foreground); 90 | --color-primary: var(--primary); 91 | --color-primary-foreground: var(--primary-foreground); 92 | --color-secondary: var(--secondary); 93 | --color-secondary-foreground: var(--secondary-foreground); 94 | --color-muted: var(--muted); 95 | --color-muted-foreground: var(--muted-foreground); 96 | --color-accent: var(--accent); 97 | --color-accent-foreground: var(--accent-foreground); 98 | --color-destructive: var(--destructive); 99 | --color-destructive-foreground: var(--destructive-foreground); 100 | --color-border: var(--border); 101 | --color-input: var(--input); 102 | --color-ring: var(--ring); 103 | --color-chart-1: var(--chart-1); 104 | --color-chart-2: var(--chart-2); 105 | --color-chart-3: var(--chart-3); 106 | --color-chart-4: var(--chart-4); 107 | --color-chart-5: var(--chart-5); 108 | --radius-sm: calc(var(--radius) - 4px); 109 | --radius-md: calc(var(--radius) - 2px); 110 | --radius-lg: var(--radius); 111 | --radius-xl: calc(var(--radius) + 4px); 112 | --color-sidebar: var(--sidebar); 113 | --color-sidebar-foreground: var(--sidebar-foreground); 114 | --color-sidebar-primary: var(--sidebar-primary); 115 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 116 | --color-sidebar-accent: var(--sidebar-accent); 117 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 118 | --color-sidebar-border: var(--sidebar-border); 119 | --color-sidebar-ring: var(--sidebar-ring); 120 | } 121 | 122 | @layer base { 123 | * { 124 | @apply border-border outline-ring/50; 125 | } 126 | 127 | html { 128 | @apply whitespace-pre-line antialiased scroll-smooth; 129 | } 130 | 131 | body { 132 | @apply bg-background text-foreground; 133 | font-feature-settings: "rlig" 0, "calt" 0; 134 | text-rendering: optimizeLegibility; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@workspace/typescript-config/react-library.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@workspace/ui/*": ["./src/*"] 7 | } 8 | }, 9 | "include": ["."], 10 | "exclude": ["node_modules", "dist"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.lint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@workspace/typescript-config/react-library.json", 3 | "compilerOptions": { 4 | "outDir": "dist" 5 | }, 6 | "include": ["src", "turbo"], 7 | "exclude": ["node_modules", "dist"] 8 | } 9 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - apps/* 3 | - packages/* 4 | 5 | onlyBuiltDependencies: 6 | - core-js-pure 7 | - sharp 8 | -------------------------------------------------------------------------------- /registries.json: -------------------------------------------------------------------------------- 1 | apps/web/public/registries.json -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@workspace/typescript-config/base.json" 3 | } 4 | 5 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "ui": "tui", 4 | "tasks": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "inputs": ["$TURBO_DEFAULT$", ".env*"], 8 | "outputs": [".next/**", "!.next/cache/**"] 9 | }, 10 | "lint": { 11 | "dependsOn": ["^lint"] 12 | }, 13 | "check-types": { 14 | "dependsOn": ["^check-types"] 15 | }, 16 | "dev": { 17 | "cache": false, 18 | "persistent": true 19 | } 20 | } 21 | } 22 | --------------------------------------------------------------------------------