├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── apps └── web │ ├── app │ ├── (home) │ │ ├── blocks │ │ │ ├── [...categories] │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── api │ │ ├── og │ │ │ └── route.tsx │ │ └── search │ │ │ └── route.ts │ ├── blocks │ │ └── preview │ │ │ └── [blockId] │ │ │ └── page.tsx │ ├── docs │ │ ├── [[...slug]] │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── favicon.ico │ ├── layout.config.tsx │ ├── layout.tsx │ ├── opengraph-image.png │ └── sitemap.ts │ ├── components │ ├── announcement.tsx │ ├── block-display.tsx │ ├── block-viewer.tsx │ ├── blocks-nav.tsx │ ├── cli-commands.tsx │ ├── code-block.tsx │ ├── content │ │ ├── component-loader.tsx │ │ ├── component-preview-collapse.tsx │ │ ├── component-preview.tsx │ │ ├── component-source.tsx │ │ └── mdx-components.tsx │ ├── copy-button.tsx │ ├── hero.tsx │ ├── showcase-code-viewer.tsx │ ├── showcase-component.tsx │ └── v0-button.tsx │ ├── config │ └── site.ts │ ├── content │ ├── blog │ │ └── web-interface-guide.mdx │ └── docs │ │ ├── components │ │ └── button.mdx │ │ ├── index.mdx │ │ ├── installation.mdx │ │ ├── meta.json │ │ ├── props.ts │ │ └── texts │ │ ├── text-loop.mdx │ │ └── text-type.mdx │ ├── hooks │ ├── use-config.ts │ ├── use-copy-to-clipboard.ts │ └── use-media-query.ts │ ├── lib │ ├── blocks.ts │ ├── fonts.ts │ ├── metadata.ts │ ├── registry.ts │ ├── source.ts │ └── utils.ts │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.js │ ├── public │ ├── banner.png │ ├── fonts │ │ ├── Geist-Medium.otf │ │ └── Redaction_35-Regular.woff2 │ ├── images │ │ ├── 10x-deng.png │ │ ├── brush-line.png │ │ ├── currents-evangelion.jpg │ │ └── lines-top.svg │ ├── next.svg │ ├── r │ │ ├── action-search-input-demo.json │ │ ├── button-color.json │ │ ├── button-destructive.json │ │ ├── button-outline.json │ │ ├── button-primary.json │ │ ├── button.json │ │ ├── card.json │ │ ├── checkbox.json │ │ ├── family-wallets-demo.json │ │ ├── icons.json │ │ ├── input.json │ │ ├── label.json │ │ ├── login-03.json │ │ ├── login-04.json │ │ ├── login-05.json │ │ ├── paper-fold-demo.json │ │ ├── separator.json │ │ ├── sheet.json │ │ ├── sidebar.json │ │ ├── skeleton.json │ │ ├── stack.json │ │ ├── text-loop-demo.json │ │ ├── text-loop.json │ │ ├── text-type-demo.json │ │ ├── text-type.json │ │ ├── tooltip.json │ │ ├── use-debounce.json │ │ ├── use-mobile.json │ │ └── utils.json │ └── vercel.svg │ ├── registry.json │ ├── registry │ ├── blocks │ │ ├── login-03 │ │ │ ├── components │ │ │ │ └── login-form.tsx │ │ │ └── page.tsx │ │ ├── login-04 │ │ │ ├── components │ │ │ │ └── login-form.tsx │ │ │ └── page.tsx │ │ └── login-05 │ │ │ ├── components │ │ │ └── login-form.tsx │ │ │ └── page.tsx │ ├── components │ │ ├── action-search-input-demo.tsx │ │ ├── button-color.tsx │ │ ├── button-destructive.tsx │ │ ├── button-outline.tsx │ │ ├── button-primary.tsx │ │ ├── family-wallets-demo.tsx │ │ ├── icons.tsx │ │ ├── paper-fold-demo.tsx │ │ ├── text-loop-demo.tsx │ │ └── text-type-demo.tsx │ ├── hooks │ │ ├── use-debounce.ts │ │ └── use-mobile.ts │ ├── lib │ │ └── utils.ts │ └── ui │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── drawer.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── stack.tsx │ │ ├── text-effect.tsx │ │ ├── text-loop.tsx │ │ ├── text-morph.tsx │ │ ├── text-type.tsx │ │ └── tooltip.tsx │ ├── source.config.ts │ ├── styles │ └── globals.css │ ├── tsconfig.json │ └── types │ └── component.ts ├── biome.json ├── bun.lockb ├── package.json ├── packages ├── tsconfig │ ├── base.json │ ├── nextjs.json │ ├── package.json │ └── react-library.json └── ui │ ├── package.json │ ├── postcss.config.js │ ├── src │ ├── components │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── collapsible.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── glass-button.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── page-header.tsx │ │ ├── references.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── tabs.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ └── tooltip.tsx │ ├── hooks │ │ └── use-mobile.ts │ ├── index.tsx │ └── lib │ │ └── utils.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── tsconfig.json └── turbo.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | node_modules 5 | /.pnp 6 | .pnpm/ 7 | .pnp.js 8 | 9 | ##output directories 10 | .map.ts 11 | dist 12 | .source 13 | 14 | 15 | # testing 16 | /coverage 17 | 18 | # next.js 19 | /.next/ 20 | /out/ 21 | 22 | # production 23 | build 24 | 25 | # misc 26 | .DS_Store 27 | *.pem 28 | 29 | # debug 30 | npm-debug.log* 31 | yarn-debug.log* 32 | yarn-error.log* 33 | 34 | # local env files 35 | .env 36 | .env*.local 37 | 38 | # vercel 39 | .vercel 40 | 41 | # typescriptuild 42 | *.tsbuildinfo 43 | next-env.d.ts 44 | 45 | ## nx 46 | .nx 47 | .nx/cache 48 | .nx/workspace-data 49 | 50 | # Next.js 51 | .next 52 | out 53 | 54 | # Turbo 55 | .turbo 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Nur Cahya Wibawa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UI/TOPIA 2 | 3 | Welcome to **UI/TOPIA**—a curated collection of fine UI components built with **[Motion](https://motion.dev/)** and **Tailwind CSS**. 4 | This project extend showcases experimental designs from [cahyawibawa.com](https://cahyawibawa.com). 5 | 6 | 7 | ## Documentation 8 | 9 | Explore the full documentation at [uitopia.xyz/docs](http://uitopia.xyz/docs) to get started. 10 | 11 | 12 | ## Contributing 13 | 14 | Contributions are always welcome! 15 | Whether you’re improving existing components or adding something new, we’d love to have your input. 16 | Check out our [Contributing Guide](/CONTRIBUTING.md) for details—it’s a simple process and a great way to share your creativity with the community. 17 | 18 | 19 | ## License 20 | 21 | This project is licensed under the [MIT License](/LICENSE). 22 | 23 | 24 | ## Stats 25 | 26 | ![Repobeats Analytics](https://repobeats.axiom.co/api/embed/16b1e8de603fba86ba6461e4201037ff9ea9bb27.svg 'Repobeats analytics image') 27 | -------------------------------------------------------------------------------- /apps/web/app/(home)/blocks/[...categories]/page.tsx: -------------------------------------------------------------------------------- 1 | import { BlockDisplay } from "@/components/block-display"; 2 | import { getAllBlockIds, getBlockCategories } from "@/lib/blocks"; 3 | 4 | export const dynamicParams = false; 5 | 6 | export async function generateStaticParams() { 7 | const categories = await getBlockCategories(); 8 | return categories.map((category) => ({ 9 | categories: [category.slug], 10 | })); 11 | } 12 | 13 | type Params = Promise<{ categories?: string[] }>; 14 | type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>; 15 | 16 | export default async function Page(props: { 17 | params: Params; 18 | searchParams: SearchParams; 19 | }) { 20 | const params = await props.params; 21 | const _searchParams = await props.searchParams; 22 | 23 | const blocks = await getAllBlockIds( 24 | ["registry:block"], 25 | params.categories ?? [], 26 | ); 27 | 28 | return ( 29 |
30 | {blocks.map((name) => ( 31 |
32 | 33 |
34 | ))} 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /apps/web/app/(home)/blocks/layout.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { BlocksNav } from "@/components/blocks-nav"; 3 | import { createMetadata } from "@/lib/metadata"; 4 | import { cn } from "@/lib/utils"; 5 | import { buttonVariants } from "@/registry/ui/button"; 6 | import registry from "@/registry.json"; 7 | 8 | import { 9 | PageActions, 10 | PageHeader, 11 | PageHeaderDescription, 12 | PageHeaderHeading, 13 | } from "@/uitopia/page-header"; 14 | 15 | export const metadata = createMetadata({ 16 | title: "Building Blocks for the Web", 17 | description: 18 | "Clean, modern building blocks. Copy and paste into your apps. Works with all React frameworks. Open Source. Free forever.", 19 | }); 20 | 21 | // Extract unique categories from registry blocks only 22 | const categories = Array.from( 23 | new Set( 24 | registry.items 25 | .filter((item) => item.type === "registry:block") 26 | .flatMap((item) => item.categories || []) 27 | .filter(Boolean), 28 | ), 29 | ).map((category) => ({ 30 | name: category.charAt(0).toUpperCase() + category.slice(1), 31 | slug: category.toLowerCase(), 32 | hidden: false, 33 | })); 34 | 35 | interface BlocksLayoutProps { 36 | children: React.ReactNode; 37 | } 38 | 39 | export default function BlocksLayout({ children }: BlocksLayoutProps) { 40 | return ( 41 | 42 |
43 | 44 | Building blocks for the web 45 | 46 | 47 | Clean, modern building blocks. Copy and paste into your apps. Works 48 | with all React frameworks. Open Source. Free forever. 49 | 50 | 51 | 52 | 59 | Browse blocks 60 | 61 | 62 |
63 |
64 | 65 |
66 |
67 |
{children}
68 |
69 |
70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /apps/web/app/(home)/blocks/page.tsx: -------------------------------------------------------------------------------- 1 | import { BlockDisplay } from "@/components/block-display"; 2 | 3 | const FEATURED_BLOCKS = ["login-03", "login-05"]; 4 | 5 | export default async function BlocksPage() { 6 | return ( 7 |
8 | {FEATURED_BLOCKS.map((block) => ( 9 |
13 | 14 |
15 | ))} 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /apps/web/app/(home)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { HomeLayout } from "fumadocs-ui/layouts/home"; 2 | import type { ReactNode } from "react"; 3 | import { baseOptions } from "@/app/layout.config"; 4 | import { siteConfig } from "@/config/site"; 5 | 6 | export default function Layout({ 7 | children, 8 | }: { 9 | children: ReactNode; 10 | }): React.ReactElement { 11 | return {children}; 12 | } 13 | 14 | function _Footer(): React.ReactElement { 15 | const currentYear = new Date().getFullYear(); 16 | return ( 17 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /apps/web/app/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | import { Hero } from "@/components/hero"; 2 | import { ShowcaseComponent } from "@/components/showcase-component"; 3 | 4 | export default function Home() { 5 | return ( 6 |
7 |
8 | 9 | 10 | 14 | 15 | 16 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /apps/web/app/api/og/route.tsx: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "fs"; 2 | import { ImageResponse } from "next/og"; 3 | import type { NextRequest } from "next/server"; 4 | import { join } from "path"; 5 | import { Icons } from "@/registry/components/icons"; 6 | 7 | export async function GET(req: NextRequest) { 8 | try { 9 | const { searchParams } = req.nextUrl; 10 | const title = searchParams.get("title"); 11 | const description = searchParams.get("description"); 12 | 13 | const fontPath = join(process.cwd(), "public", "fonts", "Geist-Medium.otf"); 14 | const fontData = readFileSync(fontPath); 15 | 16 | return new ImageResponse( 17 |
30 |
39 | 40 | 46 | ui/topia 47 | 48 |
49 |
62 |
{title}
63 |
64 |
{description}
65 |
66 |
, 67 | { 68 | width: 1200, 69 | height: 630, 70 | fonts: [ 71 | { 72 | name: "Geist", 73 | data: fontData, 74 | style: "normal", 75 | }, 76 | ], 77 | }, 78 | ); 79 | } catch (error) { 80 | console.error("Failed to generate the image:", error); 81 | return new Response(`Failed to generate the image: ${error}`, { 82 | status: 500, 83 | }); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /apps/web/app/api/search/route.ts: -------------------------------------------------------------------------------- 1 | import { createFromSource } from "fumadocs-core/search/server"; 2 | import { source } from "@/lib/source"; 3 | 4 | export const { GET } = createFromSource(source); 5 | -------------------------------------------------------------------------------- /apps/web/app/blocks/preview/[blockId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { BlockDisplay } from "@/components/block-display"; 2 | 3 | export const dynamic = "force-static"; 4 | export const dynamicParams = false; 5 | 6 | type Params = Promise<{ blockId: string }>; 7 | type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>; 8 | 9 | export default async function Page(props: { 10 | params: Params; 11 | searchParams: SearchParams; 12 | }) { 13 | const params = await props.params; 14 | const _searchParams = await props.searchParams; 15 | 16 | return ( 17 |
18 | 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/app/docs/[[...slug]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getPageTreePeers } from "fumadocs-core/server"; 2 | import { Card, Cards } from "fumadocs-ui/components/card"; 3 | import { 4 | DocsBody, 5 | DocsDescription, 6 | DocsPage, 7 | DocsTitle, 8 | } from "fumadocs-ui/page"; 9 | import type { Metadata } from "next"; 10 | import { notFound } from "next/navigation"; 11 | import { useMDXComponents } from "@/components/content/mdx-components"; 12 | import { siteConfig } from "@/config/site"; 13 | import { createMetadata } from "@/lib/metadata"; 14 | import { source } from "@/lib/source"; 15 | 16 | export default async function Page(props: { 17 | params: Promise<{ slug?: string[] }>; 18 | }) { 19 | const params = await props.params; 20 | const page = source.getPage(params.slug); 21 | 22 | if (!page) notFound(); 23 | 24 | const path = `apps/web/content/docs/${page.file.path}`; 25 | const MDX = page.data.body; 26 | 27 | return ( 28 | 42 | {page.data.title} 43 | 44 | {page.data.description} 45 | 46 | 47 | 48 | 49 | 50 | ); 51 | } 52 | 53 | function _DocsCategory({ url }: { url: string }) { 54 | return ( 55 | 56 | {getPageTreePeers(source.pageTree, url).map((peer) => ( 57 | 58 | {peer.description} 59 | 60 | ))} 61 | 62 | ); 63 | } 64 | 65 | export async function generateStaticParams() { 66 | return source.generateParams(); 67 | } 68 | 69 | export async function generateMetadata(props: { 70 | params: Promise<{ slug?: string[] }>; 71 | }): Promise { 72 | const params = await props.params; 73 | const page = source.getPage(params.slug); 74 | 75 | if (page == null) notFound(); 76 | 77 | const description = page.data.description ?? siteConfig.description; 78 | 79 | const imageParams = new URLSearchParams(); 80 | imageParams.set("description", description); 81 | 82 | const image = { 83 | alt: "Banner", 84 | url: `/api/og?${imageParams.toString()}`, 85 | width: 1200, 86 | height: 630, 87 | }; 88 | 89 | return createMetadata({ 90 | title: page.data.title, 91 | description, 92 | openGraph: { 93 | url: `/docs/${page.slugs.join("/")}`, 94 | images: image, 95 | }, 96 | twitter: { 97 | images: image, 98 | }, 99 | }); 100 | } 101 | -------------------------------------------------------------------------------- /apps/web/app/docs/layout.tsx: -------------------------------------------------------------------------------- 1 | import { GithubInfo } from "fumadocs-ui/components/github-info"; 2 | import { DocsLayout, type DocsLayoutProps } from "fumadocs-ui/layouts/docs"; 3 | import type { ReactNode } from "react"; 4 | import { baseOptions } from "@/app/layout.config"; 5 | import { source } from "@/lib/source"; 6 | 7 | const docsOptions: DocsLayoutProps = { 8 | ...baseOptions, 9 | tree: source.pageTree, 10 | links: [ 11 | { 12 | type: "custom", 13 | children: , 14 | }, 15 | ], 16 | }; 17 | 18 | export default function Layout({ children }: { children: ReactNode }) { 19 | return ( 20 | 38 | {node.icon} 39 | 40 | ), 41 | }; 42 | }, 43 | }, 44 | }} 45 | {...docsOptions} 46 | > 47 | {children} 48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /apps/web/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cahyawibawa/ui-topia/121ef6e0d07db0558849e06036235349ce7dc5ad/apps/web/app/favicon.ico -------------------------------------------------------------------------------- /apps/web/app/layout.config.tsx: -------------------------------------------------------------------------------- 1 | import type { LinkItemType } from "fumadocs-ui/layouts/docs"; 2 | import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared"; 3 | import { siteConfig } from "@/config/site"; 4 | import { Icons } from "@/registry/components/icons"; 5 | 6 | export const linkItems: LinkItemType[] = [ 7 | { 8 | text: "Docs", 9 | icon: , 10 | url: "/docs", 11 | }, 12 | { 13 | text: "Blocks", 14 | icon: , 15 | url: "/blocks", 16 | }, 17 | { 18 | type: "icon", 19 | url: siteConfig.links.github, 20 | text: "GitHub", 21 | icon: ( 22 | 23 | 24 | 25 | ), 26 | external: true, 27 | }, 28 | ]; 29 | 30 | export const baseOptions: BaseLayoutProps = { 31 | nav: { 32 | transparentMode: "top", 33 | title: ( 34 |
35 | 36 |
37 | {siteConfig.name} 38 |
39 | 40 | beta 41 | 42 |
43 | ), 44 | }, 45 | links: [...linkItems], 46 | }; 47 | -------------------------------------------------------------------------------- /apps/web/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Analytics } from "@vercel/analytics/react"; 2 | import { RootProvider } from "fumadocs-ui/provider"; 3 | import type { Viewport } from "next"; 4 | import { geistMono, geistSans, Redaction } from "@/lib/fonts"; 5 | import { createMetadata } from "@/lib/metadata"; 6 | import "styles/globals.css"; 7 | 8 | export const metadata = createMetadata({}); 9 | 10 | export const viewport: Viewport = { 11 | colorScheme: "dark light", 12 | themeColor: [ 13 | { media: "(prefers-color-scheme: light)", color: "white" }, 14 | { media: "(prefers-color-scheme: dark)", color: "black" }, 15 | ], 16 | }; 17 | 18 | interface RootLayoutProps { 19 | children: React.ReactNode; 20 | } 21 | export default function RootLayout({ children }: RootLayoutProps) { 22 | return ( 23 | 28 | 29 | 30 |
31 | {children} 32 |
33 | 34 |
35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /apps/web/app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cahyawibawa/ui-topia/121ef6e0d07db0558849e06036235349ce7dc5ad/apps/web/app/opengraph-image.png -------------------------------------------------------------------------------- /apps/web/app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import type { MetadataRoute } from "next"; 2 | import { source } from "@/lib/source"; 3 | 4 | export const revalidate = false; 5 | 6 | export default function sitemap(): MetadataRoute.Sitemap { 7 | const url = (path: string): string => 8 | new URL(path, process.env.NEXT_PUBLIC_APP_URL).toString(); 9 | 10 | return [ 11 | { 12 | url: url("/"), 13 | changeFrequency: "monthly", 14 | priority: 1, 15 | }, 16 | { 17 | url: url("/docs"), 18 | changeFrequency: "monthly", 19 | priority: 0.8, 20 | }, 21 | ...source.getPages().map((page) => ({ 22 | url: url(page.url), 23 | lastModified: page.data.lastModified 24 | ? new Date(page.data.lastModified) 25 | : undefined, 26 | changeFrequency: "weekly", 27 | priority: 0.5, 28 | })), 29 | ]; 30 | } 31 | -------------------------------------------------------------------------------- /apps/web/components/announcement.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowRight, Blocks } from "lucide-react"; 2 | import Link from "next/link"; 3 | 4 | export function Announcement() { 5 | return ( 6 | 10 | 11 | 12 | New blocks components 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /apps/web/components/block-display.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { BlockViewer } from "@/components/block-viewer"; 3 | import { getComponentsByName } from "@/lib/registry"; 4 | 5 | const LoadingFallback = () => ( 6 |
7 |
8 | Loading component... 9 |
10 |
11 | ); 12 | 13 | export async function BlockDisplay({ name }: { name: string }) { 14 | const item = await getCachedComponentItem(name); 15 | 16 | if (!item) { 17 | return ( 18 |
19 |
20 | No component found with name: {name} 21 |
22 |
23 | ); 24 | } 25 | 26 | // Use the meta data from the registry for height 27 | const meta = { iframeHeight: item.meta?.iframeHeight || "350px" }; 28 | 29 | return ( 30 |
31 | }> 32 | 33 | 34 |
35 | ); 36 | } 37 | 38 | const getCachedComponentItem = React.cache(async (name: string) => { 39 | return getComponentsByName(name); 40 | }); 41 | -------------------------------------------------------------------------------- /apps/web/components/blocks-nav.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { usePathname } from "next/navigation"; 5 | import { ScrollArea, ScrollBar } from "@/uitopia/scroll-area"; 6 | 7 | interface Category { 8 | name: string; 9 | slug: string; 10 | hidden?: boolean; 11 | } 12 | 13 | interface BlocksNavProps { 14 | categories: Category[]; 15 | } 16 | 17 | export function BlocksNav({ categories }: BlocksNavProps) { 18 | const pathname = usePathname(); 19 | 20 | return ( 21 |
22 | 23 |
24 | 28 | {categories.map((category) => ( 29 | 34 | ))} 35 |
36 | 37 |
38 |
39 | ); 40 | } 41 | 42 | function BlocksNavLink({ 43 | category, 44 | isActive, 45 | }: { 46 | category: Category; 47 | isActive: boolean; 48 | }) { 49 | if (category.hidden) { 50 | return null; 51 | } 52 | 53 | return ( 54 | 59 | {category.name} 60 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /apps/web/components/cli-commands.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import CopyButton from "@/components/copy-button"; 4 | import { useConfig } from "@/hooks/use-config"; 5 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/uitopia/tabs"; 6 | 7 | export default function CliCommands({ name }: { name: string }) { 8 | const [config, setConfig] = useConfig(); 9 | const packageManager = config.packageManager || "pnpm"; 10 | 11 | const commands = { 12 | pnpm: `pnpm dlx shadcn@latest add https://uitopia.vercel.app/r/${name}.json`, 13 | npm: `npx shadcn@latest add https://uitopia.vercel.app/r/${name}.json`, 14 | yarn: `npx shadcn@latest add https://uitopia.vercel.app/r/${name}.json`, 15 | bun: `bunx --bun shadcn@latest add https://uitopia.vercel.app/r/${name}.json`, 16 | }; 17 | 18 | return ( 19 |
20 | { 23 | setConfig({ 24 | ...config, 25 | packageManager: value as "pnpm" | "npm" | "yarn" | "bun", 26 | }); 27 | }} 28 | className="rounded-lg bg-zinc-950 dark:bg-zinc-900" 29 | > 30 |
31 | 32 | 36 | pnpm 37 | 38 | 42 | npm 43 | 44 | 48 | yarn 49 | 50 | 54 | bun 55 | 56 | 57 |
58 | {Object.entries(commands).map(([pkg, command]) => ( 59 | 60 |
61 |               {command}
62 |             
63 |
64 | ))} 65 |
66 | 69 |
70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /apps/web/components/code-block.tsx: -------------------------------------------------------------------------------- 1 | import { DynamicCodeBlock } from "fumadocs-ui/components/dynamic-codeblock"; 2 | 3 | interface CodeBlockProps { 4 | code: string; 5 | language?: string; 6 | } 7 | 8 | export function CodeBlock({ code, language = "tsx" }: CodeBlockProps) { 9 | return ( 10 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/components/content/component-loader.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { V0Button } from "@/components/v0-button"; 3 | import { cn } from "@/lib/utils"; 4 | import { Icons } from "@/registry/components/icons"; 5 | import type { 6 | ComponentDisplayProps, 7 | ComponentLoaderProps, 8 | } from "@/types/component"; 9 | import { 10 | Tooltip, 11 | TooltipContent, 12 | TooltipProvider, 13 | TooltipTrigger, 14 | } from "@/uitopia/tooltip"; 15 | 16 | export function ComponentLoader({ 17 | name, 18 | hasReTrigger = false, 19 | classNameComponentContainer, 20 | showV0Button = true, 21 | }: ComponentLoaderProps & { showV0Button?: boolean }) { 22 | const [Component, setComponent] = useState(null); 23 | const [reTriggerKey, setReTriggerKey] = useState(() => Date.now()); 24 | 25 | useEffect(() => { 26 | async function loadComponent() { 27 | try { 28 | const importedComponent = await import(`@/registry/components/${name}`); 29 | setComponent(() => importedComponent.default); 30 | } catch (error) { 31 | console.error(`Failed to load component: ${name}`, error); 32 | } 33 | } 34 | loadComponent(); 35 | }, [name]); 36 | 37 | const handleReTrigger = () => { 38 | setReTriggerKey(Date.now()); 39 | }; 40 | 41 | if (!Component) { 42 | return ( 43 |
44 | 45 | Loading... 46 |
47 | ); 48 | } 49 | 50 | return ( 51 | } 53 | hasReTrigger={hasReTrigger} 54 | className={classNameComponentContainer} 55 | reTriggerKey={reTriggerKey} 56 | reTrigger={handleReTrigger} 57 | name={name} 58 | showV0Button={showV0Button} 59 | /> 60 | ); 61 | } 62 | 63 | function ComponentDisplay({ 64 | component, 65 | hasReTrigger, 66 | className, 67 | reTriggerKey, 68 | reTrigger, 69 | name, 70 | showV0Button = true, 71 | }: ComponentDisplayProps & { showV0Button?: boolean }) { 72 | const renderComponent = () => { 73 | if (!React.isValidElement(component)) { 74 | return null; 75 | } 76 | 77 | return hasReTrigger 78 | ? React.cloneElement(component, { key: reTriggerKey }) 79 | : component; 80 | }; 81 | 82 | return ( 83 |
89 |
90 | {showV0Button && ( 91 | 96 | )} 97 | 98 | 99 | 100 | {hasReTrigger && ( 101 | 109 | )} 110 | 111 | Refresh component 112 | 113 | 114 |
115 | {renderComponent()} 116 |
117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /apps/web/components/content/component-preview-collapse.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import type { RegistryItem } from "shadcn/registry"; 5 | import { CodeBlock } from "@/components/code-block"; 6 | import { ComponentLoader } from "@/components/content/component-loader"; 7 | import { convertRegistryPaths, getComponentsByName } from "@/lib/registry"; 8 | import { cn } from "@/lib/utils"; 9 | import { Icons } from "@/registry/components/icons"; 10 | import type { ComponentPreviewProps } from "@/types/component"; 11 | import { 12 | Collapsible, 13 | CollapsibleContent, 14 | CollapsibleTrigger, 15 | } from "@/uitopia/collapsible"; 16 | 17 | export function ComponentCollapse({ 18 | name, 19 | hasReTrigger = false, 20 | classNameComponentContainer, 21 | }: ComponentPreviewProps) { 22 | const [isOpen, setIsOpen] = useState(false); 23 | const [_component, setComponent] = useState(null); 24 | const [code, setCode] = useState(null); 25 | const [error, setError] = useState(null); 26 | 27 | useEffect(() => { 28 | const registryComponent = getComponentsByName(name); 29 | setComponent(registryComponent); 30 | 31 | async function loadComponentCode() { 32 | try { 33 | const response = await fetch(`/r/${name}.json`); 34 | const data = (await response.json()) as RegistryItem; 35 | const codeContent = data.files?.[0]?.content; 36 | 37 | if (!codeContent) { 38 | throw new Error("No code content found"); 39 | } 40 | 41 | setCode(convertRegistryPaths(codeContent)); 42 | setError(null); 43 | } catch (err) { 44 | console.error("Error loading component:", err); 45 | setCode(null); 46 | setError("Failed to load component code"); 47 | } 48 | } 49 | 50 | loadComponentCode(); 51 | }, [name]); 52 | 53 | return ( 54 | 59 |
60 |
61 | 66 |
67 | 68 |
69 | 75 | {isOpen ? "Hide" : "Show"} code 76 |
77 |
78 |
79 | 80 |
81 | {error ? ( 82 |
{error}
83 | ) : code ? ( 84 | 85 | ) : ( 86 |
87 | Loading code... 88 |
89 | )} 90 |
91 |
92 |
93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /apps/web/components/content/component-preview.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import type { RegistryItem } from "shadcn/registry"; 5 | import { CodeBlock } from "@/components/code-block"; 6 | import { ComponentLoader } from "@/components/content/component-loader"; 7 | import { convertRegistryPaths, getComponentsByName } from "@/lib/registry"; 8 | import type { ComponentPreviewProps } from "@/types/component"; 9 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/uitopia/tabs"; 10 | 11 | export function ComponentPreview({ 12 | name, 13 | hasReTrigger = false, 14 | classNameComponentContainer, 15 | }: ComponentPreviewProps) { 16 | const [activeTab, setActiveTab] = useState("preview"); 17 | const [_component, setComponent] = useState(null); 18 | const [code, setCode] = useState(null); 19 | const [error, setError] = useState(null); 20 | 21 | useEffect(() => { 22 | const registryComponent = getComponentsByName(name); 23 | setComponent(registryComponent); 24 | 25 | async function loadComponentCode() { 26 | try { 27 | const response = await fetch(`/r/${name}.json`); 28 | const data = (await response.json()) as RegistryItem; 29 | const codeContent = data.files?.[0]?.content; 30 | 31 | if (!codeContent) { 32 | throw new Error("No code content found"); 33 | } 34 | 35 | setCode(convertRegistryPaths(codeContent)); 36 | setError(null); 37 | } catch (err) { 38 | console.error("Error loading component:", err); 39 | setCode(null); 40 | setError("Failed to load component code"); 41 | } 42 | } 43 | 44 | loadComponentCode(); 45 | }, [name]); 46 | 47 | return ( 48 |
49 | 54 | 55 | 59 | Preview 60 | 61 | 65 | Code 66 | 67 | 68 | 69 |
70 | 75 |
76 |
77 | 78 | {error ? ( 79 |
{error}
80 | ) : code ? ( 81 | 82 | ) : ( 83 |
84 | Loading code... 85 |
86 | )} 87 |
88 |
89 |
90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /apps/web/components/content/component-source.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import type { RegistryItem } from "shadcn/registry"; 5 | import { CodeBlock } from "@/components/code-block"; 6 | import { convertRegistryPaths, getComponentsByName } from "@/lib/registry"; 7 | import { cn } from "@/lib/utils"; 8 | import type { ComponentSourceProps } from "@/types/component"; 9 | import { Button } from "@/uitopia/button"; 10 | import { 11 | Collapsible, 12 | CollapsibleContent, 13 | CollapsibleTrigger, 14 | } from "@/uitopia/collapsible"; 15 | 16 | export function ComponentSource({ 17 | name, 18 | expandButtonTitle = "Expand", 19 | defaultExpanded = false, 20 | maxHeight = "550px", 21 | className, 22 | ...props 23 | }: ComponentSourceProps) { 24 | const [isOpened, setIsOpened] = useState(defaultExpanded); 25 | const [_component, setComponent] = useState(null); 26 | const [code, setCode] = useState(null); 27 | const [error, setError] = useState(null); 28 | 29 | useEffect(() => { 30 | const registryComponent = getComponentsByName(name); 31 | setComponent(registryComponent); 32 | 33 | async function loadComponentCode() { 34 | try { 35 | const response = await fetch(`/r/${name}.json`); 36 | const data = (await response.json()) as RegistryItem; 37 | const codeContent = data.files?.[0]?.content; 38 | 39 | if (!codeContent) { 40 | throw new Error("No code content found"); 41 | } 42 | 43 | setCode(convertRegistryPaths(codeContent)); 44 | setError(null); 45 | } catch (err) { 46 | console.error("Error loading component:", err); 47 | setCode(null); 48 | setError("Failed to load component code"); 49 | } 50 | } 51 | 52 | loadComponentCode(); 53 | }, [name]); 54 | 55 | return ( 56 | 61 |
68 | 72 |
82 | {error ? ( 83 |
{error}
84 | ) : code ? ( 85 | 86 | ) : ( 87 |
88 | Loading code... 89 |
90 | )} 91 |
92 |
93 | 94 |
100 | 101 | 104 | 105 |
106 |
107 |
108 | ); 109 | } 110 | -------------------------------------------------------------------------------- /apps/web/components/copy-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { cn } from "@/lib/utils"; 5 | import { Icons } from "@/registry/components/icons"; 6 | import { Button } from "@/uitopia/button"; 7 | 8 | interface CopyButtonProps { 9 | componentSource: string; 10 | className?: string; 11 | duration?: number; 12 | } 13 | 14 | function CopyButton({ 15 | componentSource, 16 | className, 17 | duration = 2000, 18 | }: CopyButtonProps) { 19 | const [copied, setCopied] = useState(false); 20 | 21 | async function copy(text: string) { 22 | try { 23 | await navigator.clipboard.writeText(text); 24 | setCopied(true); 25 | setTimeout(() => setCopied(false), duration); 26 | return true; 27 | } catch (err) { 28 | console.error("Failed to copy text: ", err); 29 | return false; 30 | } 31 | } 32 | 33 | return ( 34 |
35 | 51 |
52 | ); 53 | } 54 | 55 | export default CopyButton; 56 | -------------------------------------------------------------------------------- /apps/web/components/hero.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { siteConfig } from "@/config/site"; 3 | import { cn } from "@/lib/utils"; 4 | import { buttonVariants } from "@/uitopia/button"; 5 | import { 6 | PageActions, 7 | PageHeader, 8 | PageHeaderDescription, 9 | PageHeaderHeading, 10 | } from "@/uitopia/page-header"; 11 | import { Announcement } from "./announcement"; 12 | 13 | export function Hero() { 14 | return ( 15 | 16 | 17 | 18 | Make your interface delightful 19 | effortlessly. 20 | 21 | 22 | Collection set of beautifully designed motions components. Easy 23 | copy-paste. Customizable. Open Source. 24 | 25 | 26 | 27 | 34 | Explore Now 35 | 36 | 45 | GitHub 46 | 47 | 48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /apps/web/components/showcase-code-viewer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type React from "react"; 4 | import { Suspense } from "react"; 5 | import type { RegistryItem } from "shadcn/registry"; 6 | import ComponentCli from "@/components/cli-commands"; 7 | import { V0Button } from "@/components/v0-button"; 8 | import { useMediaQuery } from "@/hooks/use-media-query"; 9 | import { cn } from "@/lib/utils"; 10 | import { Icons } from "@/registry/components/icons"; 11 | import { 12 | Drawer, 13 | DrawerContent, 14 | DrawerTitle, 15 | DrawerTrigger, 16 | } from "@/registry/ui/drawer"; 17 | import { Button } from "@/uitopia/button"; 18 | import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "@/uitopia/sheet"; 19 | 20 | interface CodeViewerProps { 21 | children: React.ReactNode; 22 | component: RegistryItem; 23 | } 24 | 25 | function Content({ children, component }: CodeViewerProps) { 26 | return ( 27 |
28 |
29 |

Installation

30 | 31 |
32 |
33 |
34 |

Code

35 | 38 |
39 | {children} 40 |
41 |
42 | ); 43 | } 44 | 45 | export function CodeViewer({ component, children }: CodeViewerProps) { 46 | const isDesktop = useMediaQuery("(min-width: 768px)"); 47 | 48 | if (!isDesktop) { 49 | return ( 50 | 51 | 52 | 59 | 60 | 61 | Code 62 |
63 | {children} 64 |
65 |
66 |
67 | ); 68 | } 69 | 70 | return ( 71 | 72 | 73 | 88 | 89 | 93 | Code 94 | 97 | 98 | Loading... 99 | 100 | } 101 | > 102 | {children} 103 | 104 | 105 | 106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /apps/web/components/showcase-component.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import type { RegistryItem } from "shadcn/registry"; 5 | import { CodeBlock } from "@/components/code-block"; 6 | import { ComponentLoader } from "@/components/content/component-loader"; 7 | import { convertRegistryPaths, getComponentsByName } from "@/lib/registry"; 8 | import { cn } from "@/lib/utils"; 9 | import { Badge } from "@/uitopia/badge"; 10 | import { CodeViewer } from "./showcase-code-viewer"; 11 | 12 | interface ShowcaseComponentProps { 13 | name: string; 14 | height?: string; 15 | showV0Button?: boolean; 16 | } 17 | 18 | export function ShowcaseComponent({ 19 | name, 20 | height = "h-[400px] md:h-[540px]", 21 | showV0Button = true, 22 | }: ShowcaseComponentProps) { 23 | const [code, setCode] = useState(null); 24 | const [error, setError] = useState(null); 25 | const [component, setComponent] = useState(null); 26 | 27 | useEffect(() => { 28 | const registryComponent = getComponentsByName(name); 29 | setComponent(registryComponent); 30 | 31 | async function loadComponentCode() { 32 | try { 33 | const response = await fetch(`/r/${name}.json`); 34 | const data = (await response.json()) as RegistryItem; 35 | const codeContent = data.files?.[0]?.content; 36 | 37 | if (!codeContent) { 38 | throw new Error("No code content found"); 39 | } 40 | 41 | setCode(convertRegistryPaths(codeContent)); 42 | setError(null); 43 | } catch (err) { 44 | console.error("Error loading component:", err); 45 | setCode(null); 46 | setError("Failed to load component code"); 47 | } 48 | } 49 | 50 | loadComponentCode(); 51 | }, [name]); 52 | 53 | if (!component) { 54 | return null; 55 | } 56 | 57 | return ( 58 |
59 | {/* Component Display */} 60 |
66 |
67 | 68 | {error ? ( 69 |
{error}
70 | ) : code ? ( 71 | 72 | ) : ( 73 |
74 | Loading code... 75 |
76 | )} 77 |
78 |
79 | 80 |
81 | 82 | {/* Component Preview */} 83 |
84 |
85 |

{component.title}

86 |

87 | {component.description} 88 |

89 |
90 |
91 | {component.categories?.map((category) => ( 92 | 93 | {category} 94 | 95 | ))} 96 |
97 |
98 |
99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /apps/web/components/v0-button.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { cn } from "@/lib/utils"; 3 | import { buttonVariants } from "@/uitopia/button"; 4 | import { 5 | Tooltip, 6 | TooltipContent, 7 | TooltipProvider, 8 | TooltipTrigger, 9 | } from "@/uitopia/tooltip"; 10 | 11 | interface V0ButtonProps { 12 | componentSource: string; 13 | className?: string; 14 | variant?: "default" | "icon"; 15 | } 16 | 17 | const V0Icon = () => ( 18 | 24 | 28 | 32 | 33 | ); 34 | 35 | export function V0Button({ 36 | componentSource, 37 | className, 38 | variant = "default", 39 | }: V0ButtonProps) { 40 | const href = `https://v0.dev/chat/api/open?url=${encodeURIComponent(componentSource)}`; 41 | 42 | if (variant === "icon") { 43 | return ( 44 | 45 | 46 | 47 | 54 | 55 | 56 | 57 | Open in v0 58 | 59 | 60 | ); 61 | } 62 | 63 | return ( 64 | 75 | Open in 76 | 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /apps/web/config/site.ts: -------------------------------------------------------------------------------- 1 | export const siteConfig = { 2 | name: "ui/topia", 3 | creator: "@kyuotaka", 4 | url: process.env.NEXT_PUBLIC_APP_URL ?? "https://uitopia.vercel.app", 5 | ogImage: `${process.env.NEXT_PUBLIC_APP_URL}/opengraph-image.png`, 6 | description: 7 | "ui/topia is, a growing collection of experimental components built with shadcn/ui, Motion and Tailwind CSS.", 8 | keywords: [ 9 | "Next.js", 10 | "React", 11 | "Tailwind CSS", 12 | "UI Library", 13 | "UI Kit", 14 | "UI Components", 15 | "UI Elements", 16 | "Open Source", 17 | "shadcn/ui", 18 | ], 19 | links: { 20 | portfolio: "https://cahyawibawa.com/", 21 | github: "https://git.new/ui/topia", 22 | }, 23 | }; 24 | 25 | export type SiteConfig = typeof siteConfig; 26 | -------------------------------------------------------------------------------- /apps/web/content/docs/components/button.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Button 3 | description: Custom shadcn/ui button. 4 | --- 5 | 6 | ### Color 7 | 8 | 12 | 13 | ### Primary 14 | 15 | 19 | 20 | ### Outline 21 | 22 | 26 | 27 | ### Destructive 28 | 29 | 33 | -------------------------------------------------------------------------------- /apps/web/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | description: Your Perfect Destination for Elegant, Accessible, and Customizable UI Components 4 | icon: Album 5 | --- 6 | 7 | ## What is ui/topia? 8 | 9 | **ui/topia** isn’t just a component library (yet)—it’s my personal playground for: 10 | 11 | - Exploring interaction design with [Motion](https://motion.dev), formerly known as **Framer Motion**. 12 | - Recreating and collecting UI ideas that inspire me. 13 | - Refining documentation writing skills. 14 | - Working towards becoming a Design Engineer. 15 | 16 | 24 | 25 | ## Motivation 26 | 27 | **ui/topia** was created to be a space where unique and accessible UI components could inspire creativity. It’s designed to help designers and developers find ideas and solutions for their projects. 28 | 29 | This project draws inspiration from [shadcn/ui](https://github.com/shadcn/ui), [origin/ui](https://originui.com), [motion primitives](https://motion-primitives.com), [magic ui](https://magicui.design), and other well-known libraries. 30 | 31 | ## FAQ 32 | 33 | 34 | 35 | Definitely! **ui/topia** is free for personal and commercial projects. 36 | No credit is needed, but I’d love to see what you build, so feel free to share! 37 | 38 | 39 | 40 | Absolutely! Adding your work is a great way to showcase your creativity and help other developers. 41 | Check out the [CONTRIBUTING.md](https://github.com/cahyawibawa/ui-topia/blob/main/CONTRIBUTING.md) file for all the details. It’s got everything you need to get started. 42 | 43 | 44 | 45 | Not quite—at least not yet. For now, it’s more of a learning and experimentation playground. 46 | 47 | 48 | 49 | Start exploring **ui/topia** today and take your UI design to new heights! 50 | -------------------------------------------------------------------------------- /apps/web/content/docs/installation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | description: A simple guide to set up ui/topia with the required dependencies and structure. 4 | icon: PackagePlus 5 | --- 6 | 7 | This project is built with **Next.js**, using **TypeScript** and **Tailwind CSS** for all components. Some components also rely on [shadcn/ui](https://ui.shadcn.com/docs/installation) and [Motion](https://motion.dev). 8 | 9 | For the best development experience, I recommend using **TypeScript**, but it’s not mandatory. 10 | 11 | 12 | 13 | ### Install Tailwind CSS 14 | 15 | The components are styled using **Tailwind CSS**. 16 | Follow the official [installation guide](https://tailwindcss.com/docs/installation) to set it up in your project. 17 | 18 | 19 | 20 | ### Install Motion 21 | 22 | To use components with animations, you’ll need to add **Motion** to your project. 23 | 24 | 25 | ```bash 26 | npm install motion 27 | ``` 28 | 29 | {/* 30 | ```bash tab="npm" 31 | npm install motion 32 | ``` 33 | ```bash tab="pnpm" 34 | pnpm add motion 35 | ``` 36 | ```bash tab="bun" 37 | bun add motion 38 | ``` 39 | */} 40 | 41 | 42 | 43 | ### Add a Utility Helper 44 | 45 | To make it easier to conditionally apply Tailwind CSS classes, add this `cn` helper function to your project. 46 | 47 | ```ts title="lib/utils.ts" 48 | import { clsx, type ClassValue } from 'clsx' 49 | import { twMerge } from 'tailwind-merge' 50 | 51 | export function cn(...inputs: ClassValue[]) { 52 | return twMerge(clsx(inputs)) 53 | } 54 | ``` 55 | 56 | 57 | 58 | ### Folder Structure 59 | 60 | Here’s a recommended folder structure to organize your components and utilities: 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | Feel free to adapt it based on your project’s needs. 74 | 75 | 76 | 77 | You're all set! You can now start enjoying the components. 78 | -------------------------------------------------------------------------------- /apps/web/content/docs/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "ui", 3 | "root": true, 4 | "pages": [ 5 | "---Getting Started---", 6 | "index", 7 | "installation", 8 | "---Components---", 9 | "...components", 10 | "---Text---", 11 | "...texts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /apps/web/content/docs/props.ts: -------------------------------------------------------------------------------- 1 | import type { TextEffectProps } from "@/registry/ui/text-effect"; 2 | import type { TextLoopProps } from "@/registry/ui/text-loop"; 3 | import type { TextTypeProps } from "@/registry/ui/text-type"; 4 | 5 | export type TextLoop = TextLoopProps; 6 | export type TextEffect = TextEffectProps; 7 | export type TextType = TextTypeProps; 8 | -------------------------------------------------------------------------------- /apps/web/content/docs/texts/text-loop.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Text Loop 3 | description: Text animation that transitions between multiple items, creating an engaging looping effect. 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | ## Installation 17 | 18 | 19 | 20 | 21 | CLI 22 | Manual 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | Install the following dependencies: 35 | 36 | 37 | 38 | Copy and paste the following code into your project. 39 | 40 | 41 | 42 | Update the import paths to match your project setup. 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | ## Props 51 | 52 | 53 | 54 | {/* void', 90 | default: '-', 91 | }, 92 | }} 93 | /> */} 94 | 95 | -------------------------------------------------------------------------------- /apps/web/content/docs/texts/text-type.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Typewriter 3 | description: Text animation that types out a text, one letter at a time. 4 | --- 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | ## Installation 17 | 18 | 19 | 20 | 21 | CLI 22 | Manual 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | Install the following dependencies: 35 | 36 | 37 | 38 | Copy and paste the following code into your project. 39 | 40 | 41 | 42 | Update the import paths to match your project setup. 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | ## Props 51 | 52 | 53 | 54 | {/* */} 124 | 125 | -------------------------------------------------------------------------------- /apps/web/hooks/use-config.ts: -------------------------------------------------------------------------------- 1 | import { useAtom } from "jotai"; 2 | import { atomWithStorage } from "jotai/utils"; 3 | 4 | type Config = { 5 | packageManager: "npm" | "yarn" | "pnpm" | "bun"; 6 | }; 7 | 8 | const configAtom = atomWithStorage("config", { 9 | packageManager: "pnpm", 10 | }); 11 | 12 | export function useConfig() { 13 | return useAtom(configAtom); 14 | } 15 | -------------------------------------------------------------------------------- /apps/web/hooks/use-copy-to-clipboard.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | 5 | export function useCopyToClipboard({ 6 | timeout = 2000, 7 | onCopy, 8 | }: { 9 | timeout?: number; 10 | onCopy?: () => void; 11 | } = {}) { 12 | const [isCopied, setIsCopied] = React.useState(false); 13 | 14 | const copyToClipboard = (value: string) => { 15 | if (typeof window === "undefined" || !navigator.clipboard.writeText) { 16 | return; 17 | } 18 | 19 | if (!value) return; 20 | 21 | navigator.clipboard.writeText(value).then(() => { 22 | setIsCopied(true); 23 | 24 | if (onCopy) { 25 | onCopy(); 26 | } 27 | 28 | setTimeout(() => { 29 | setIsCopied(false); 30 | }, timeout); 31 | }, console.error); 32 | }; 33 | 34 | return { isCopied, copyToClipboard }; 35 | } 36 | -------------------------------------------------------------------------------- /apps/web/hooks/use-media-query.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export function useMediaQuery(query: string) { 4 | const [value, setValue] = React.useState(false); 5 | 6 | React.useEffect(() => { 7 | function onChange(event: MediaQueryListEvent) { 8 | setValue(event.matches); 9 | } 10 | 11 | const result = matchMedia(query); 12 | result.addEventListener("change", onChange); 13 | setValue(result.matches); 14 | 15 | return () => result.removeEventListener("change", onChange); 16 | }, [query]); 17 | 18 | return value; 19 | } 20 | -------------------------------------------------------------------------------- /apps/web/lib/blocks.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import type { RegistryItem } from "shadcn/registry"; 4 | import { getComponents, getComponentsByName } from "./registry"; 5 | 6 | export async function getAllBlockIds( 7 | types: RegistryItem["type"][] = ["registry:block"], 8 | categories: string[] = [], 9 | ): Promise { 10 | try { 11 | // Get all components from local registry 12 | const registry = getComponents(); 13 | 14 | const blocks = registry 15 | .filter((item: RegistryItem) => types.includes(item.type)) 16 | .map((item: RegistryItem) => item.name); 17 | 18 | // If no categories specified, return all blocks 19 | if (categories.length === 0) { 20 | return blocks; 21 | } 22 | 23 | // Filter blocks by category 24 | const filteredBlocks = []; 25 | for (const name of blocks) { 26 | const blockData = getComponentsByName(name); 27 | 28 | if ( 29 | blockData?.categories?.some((category) => 30 | categories.includes(category.toLowerCase()), 31 | ) 32 | ) { 33 | filteredBlocks.push(name); 34 | } 35 | } 36 | 37 | return filteredBlocks; 38 | } catch (error) { 39 | console.error("Error getting blocks:", error); 40 | return []; 41 | } 42 | } 43 | 44 | export async function getBlockCategories(): Promise< 45 | Array<{ name: string; slug: string }> 46 | > { 47 | try { 48 | const registry = getComponents(); 49 | 50 | const categories = new Set(); 51 | 52 | // Get all block names first 53 | const blocks = registry.filter( 54 | (item: RegistryItem) => item.type === "registry:block", 55 | ); 56 | 57 | // Collect categories from each block 58 | for (const block of blocks) { 59 | if (block.categories) { 60 | for (const category of block.categories) { 61 | categories.add(category.toLowerCase()); 62 | } 63 | } 64 | } 65 | 66 | return Array.from(categories).map((category) => ({ 67 | name: category.charAt(0).toUpperCase() + category.slice(1), 68 | slug: category, 69 | })); 70 | } catch (error) { 71 | console.error("Error getting categories:", error); 72 | return []; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /apps/web/lib/fonts.ts: -------------------------------------------------------------------------------- 1 | import { Geist, Geist_Mono } from "next/font/google"; 2 | import localFont from "next/font/local"; 3 | 4 | export const geistSans = Geist({ 5 | variable: "--font-geist-sans", 6 | subsets: ["latin"], 7 | }); 8 | 9 | export const geistMono = Geist_Mono({ 10 | variable: "--font-geist-mono", 11 | subsets: ["latin"], 12 | }); 13 | 14 | export const Redaction = localFont({ 15 | src: "../public/fonts/Redaction_35-Regular.woff2", 16 | variable: "--font-redaction", 17 | }); 18 | -------------------------------------------------------------------------------- /apps/web/lib/metadata.ts: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { siteConfig } from "@/config/site"; 3 | 4 | export const defaultMetadata: Metadata = { 5 | title: { 6 | default: siteConfig.name, 7 | template: `%s - ${siteConfig.name}`, 8 | }, 9 | description: siteConfig.description, 10 | metadataBase: process.env.NEXT_PUBLIC_APP_URL 11 | ? new URL(process.env.NEXT_PUBLIC_APP_URL) 12 | : undefined, 13 | keywords: siteConfig.keywords, 14 | }; 15 | 16 | export const defaultSocialMetadata = { 17 | openGraph: { 18 | title: siteConfig.name, 19 | description: siteConfig.description, 20 | type: "website", 21 | images: ["/api/og"], 22 | url: siteConfig.url, 23 | siteName: siteConfig.name, 24 | }, 25 | twitter: { 26 | card: "summary_large_image", 27 | creator: siteConfig.creator, 28 | title: siteConfig.name, 29 | description: siteConfig.description, 30 | images: ["/api/og"], 31 | }, 32 | } satisfies Pick; 33 | 34 | export function createMetadata(override: Metadata): Metadata { 35 | return { 36 | ...defaultMetadata, 37 | ...override, 38 | openGraph: { 39 | ...defaultSocialMetadata.openGraph, 40 | title: override.title ?? defaultSocialMetadata.openGraph.title, 41 | description: 42 | override.description ?? defaultSocialMetadata.openGraph.description, 43 | ...override.openGraph, 44 | }, 45 | twitter: { 46 | ...defaultSocialMetadata.twitter, 47 | title: override.title ?? defaultSocialMetadata.twitter.title, 48 | description: 49 | override.description ?? defaultSocialMetadata.twitter.description, 50 | ...override.twitter, 51 | }, 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /apps/web/lib/registry.ts: -------------------------------------------------------------------------------- 1 | import type { RegistryItem } from "shadcn/registry"; 2 | import registry from "@/registry.json"; 3 | 4 | const components = registry.items as RegistryItem[]; 5 | 6 | const PATH_MAPPINGS = { 7 | "@/uitopia/": "@/components/ui/", 8 | "@/registry/ui": "@/components/ui", 9 | "@/registry/hooks": "@/hooks", 10 | "@/registry/lib": "@/lib", 11 | } as const; 12 | 13 | export interface FileTree { 14 | name: string; 15 | path: string; // Original path from registry.json 16 | displayPath: string; // Transformed path for display (e.g., app/...) 17 | children?: FileTree[]; 18 | } 19 | 20 | export function getComponents(): readonly RegistryItem[] { 21 | return components; 22 | } 23 | 24 | export function getComponentsByName(name: string): RegistryItem | null { 25 | return components.find((item) => item.name === name) ?? null; 26 | } 27 | 28 | export function convertRegistryPaths(content: string): string { 29 | let updatedContent = content; 30 | 31 | // 1. Handle specific block component imports 32 | const blockComponentRegex = 33 | /@\/registry\/blocks\/([\w-]+)\/components\/([\w-]+)/g; 34 | updatedContent = updatedContent.replace( 35 | blockComponentRegex, 36 | (_match, _blockName, componentName) => `@/components/${componentName}`, 37 | ); 38 | 39 | // 2. Handle general registry path mappings 40 | updatedContent = Object.entries(PATH_MAPPINGS).reduce( 41 | (acc, [from, to]) => acc.replace(new RegExp(from, "g"), to), 42 | updatedContent, // Start with the already updated content 43 | ); 44 | 45 | return updatedContent; 46 | } 47 | 48 | export function getFileTarget(file: { path: string; type?: string }) { 49 | if (!file.path) return ""; 50 | 51 | const pathParts = file.path.split("/"); 52 | const blocksIndex = pathParts.indexOf("blocks"); 53 | if (blocksIndex === -1) return file.path; 54 | 55 | const componentName = pathParts[blocksIndex + 1]; 56 | const fileName = pathParts[pathParts.length - 1]; 57 | const isComponent = pathParts.includes("components"); 58 | 59 | if (isComponent) { 60 | return `components/${fileName}`; 61 | } 62 | 63 | if (fileName === "page.tsx") { 64 | return `app/${componentName}/${fileName}`; 65 | } 66 | 67 | return fileName; 68 | } 69 | 70 | export function createFileTreeForRegistryItemFiles( 71 | files: Array<{ path: string; target?: string }>, 72 | ): FileTree[] { 73 | const root: FileTree[] = []; 74 | 75 | // First pass: collect all unique display paths and map them back to original paths 76 | const uniquePaths = new Map(); // Map 77 | for (const file of files) { 78 | const displayPath = file.target ?? getFileTarget(file); 79 | if (displayPath && !uniquePaths.has(displayPath)) { 80 | uniquePaths.set(displayPath, file.path); 81 | } 82 | } 83 | 84 | // Second pass: create tree structure using display paths 85 | for (const [displayPath, originalPath] of uniquePaths.entries()) { 86 | const parts = displayPath.split("/"); 87 | let currentLevel = root; 88 | 89 | for (let i = 0; i < parts.length; i++) { 90 | const part = parts[i]; 91 | if (!part) continue; 92 | 93 | const isFile = i === parts.length - 1; 94 | const existingNode = currentLevel.find((node) => node.name === part); 95 | 96 | if (existingNode) { 97 | if (isFile) { 98 | // This shouldn't happen if display paths are unique, but handle defensively 99 | existingNode.path = originalPath; 100 | existingNode.displayPath = displayPath; 101 | } else { 102 | // Ensure children array exists 103 | existingNode.children = existingNode.children || []; 104 | currentLevel = existingNode.children; 105 | } 106 | } else { 107 | const newNode: FileTree = { 108 | name: part, 109 | path: isFile ? originalPath : "", // Only files have a meaningful original path here 110 | displayPath: isFile ? displayPath : "", // Folders don't have a direct path 111 | children: isFile ? undefined : [], 112 | }; 113 | 114 | currentLevel.push(newNode); 115 | 116 | if (!isFile) { 117 | currentLevel = newNode.children!; 118 | } 119 | } 120 | } 121 | } 122 | 123 | return root; 124 | } 125 | -------------------------------------------------------------------------------- /apps/web/lib/source.ts: -------------------------------------------------------------------------------- 1 | import type { InferMetaType, InferPageType } from "fumadocs-core/source"; 2 | import { loader } from "fumadocs-core/source"; 3 | import { createMDXSource } from "fumadocs-mdx"; 4 | import { icons } from "lucide-react"; 5 | import { docs, meta } from "@/.source"; 6 | import { create } from "@/registry/components/icons"; 7 | 8 | export const source = loader({ 9 | baseUrl: "/docs", 10 | source: createMDXSource(docs, meta), 11 | icon(icon) { 12 | if (icon && icon in icons) 13 | return create({ icon: icons[icon as keyof typeof icons] }); 14 | }, 15 | }); 16 | 17 | export type Page = InferPageType; 18 | export type Meta = InferMetaType; 19 | -------------------------------------------------------------------------------- /apps/web/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | 8 | export function absoluteUrl(path: string) { 9 | return `${process.env.NEXT_PUBLIC_APP_URL}${path}`; 10 | } 11 | -------------------------------------------------------------------------------- /apps/web/next.config.ts: -------------------------------------------------------------------------------- 1 | import createBundleAnalyzer from "@next/bundle-analyzer"; 2 | import { createMDX } from "fumadocs-mdx/next"; 3 | import type { NextConfig } from "next"; 4 | 5 | const withAnalyzer = createBundleAnalyzer({ 6 | enabled: process.env.ANALYZE === "true", 7 | }); 8 | 9 | const nextConfig: NextConfig = { 10 | outputFileTracingIncludes: { 11 | registry: ["./registry/**/*"], 12 | }, 13 | }; 14 | 15 | const withMDX = createMDX(); 16 | 17 | export default withAnalyzer(withMDX(nextConfig)); 18 | -------------------------------------------------------------------------------- /apps/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "next dev --turbo", 8 | "build": "next build", 9 | "lint": "next lint", 10 | "analyze": "ANALYZE=true next build", 11 | "clean": "git clean -xdf .next .turbo node_modules", 12 | "format": "biome format --write .", 13 | "check": "biome check --fix .", 14 | "shadcn": "bun x shadcn@latest add", 15 | "registry:build": "shadcn build" 16 | }, 17 | "dependencies": { 18 | "@ui/topia": "workspace:*", 19 | "@vercel/analytics": "^1.4.1", 20 | "class-variance-authority": "^0.7.1", 21 | "clsx": "^2.1.1", 22 | "fumadocs-core": "^15.2.10", 23 | "fumadocs-docgen": "^2.0.0", 24 | "fumadocs-mdx": "^11.6.1", 25 | "fumadocs-twoslash": "^3.1.1", 26 | "fumadocs-typescript": "^4.0.3", 27 | "fumadocs-ui": "^15.2.10", 28 | "jotai": "^2.11.0", 29 | "motion": "^11.14.4", 30 | "next": "^15.3.1", 31 | "react": "^19.0.0", 32 | "react-dom": "^19.0.0", 33 | "react-wrap-balancer": "^1.1.0", 34 | "shadcn": "^2.4.0-canary.11", 35 | "sharp": "^0.33.3", 36 | "tailwind-merge": "^2.5.5", 37 | "tailwindcss-animate": "^1.0.7", 38 | "zod": "^3.23.8" 39 | }, 40 | "devDependencies": { 41 | "@next/bundle-analyzer": "^15.1.4", 42 | "@tailwindcss/postcss": "^4.0.3", 43 | "@types/mdx": "^2.0.13", 44 | "@types/node": "^22.10.5", 45 | "@types/react": "^19.0.4", 46 | "@types/react-dom": "^19.0.2", 47 | "@uitopia/tsconfig": "workspace:*", 48 | "postcss": "^8.5.3", 49 | "tailwindcss": "^4.1.4", 50 | "typescript": "^5.7.3" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /apps/web/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /apps/web/public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cahyawibawa/ui-topia/121ef6e0d07db0558849e06036235349ce7dc5ad/apps/web/public/banner.png -------------------------------------------------------------------------------- /apps/web/public/fonts/Geist-Medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cahyawibawa/ui-topia/121ef6e0d07db0558849e06036235349ce7dc5ad/apps/web/public/fonts/Geist-Medium.otf -------------------------------------------------------------------------------- /apps/web/public/fonts/Redaction_35-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cahyawibawa/ui-topia/121ef6e0d07db0558849e06036235349ce7dc5ad/apps/web/public/fonts/Redaction_35-Regular.woff2 -------------------------------------------------------------------------------- /apps/web/public/images/10x-deng.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cahyawibawa/ui-topia/121ef6e0d07db0558849e06036235349ce7dc5ad/apps/web/public/images/10x-deng.png -------------------------------------------------------------------------------- /apps/web/public/images/brush-line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cahyawibawa/ui-topia/121ef6e0d07db0558849e06036235349ce7dc5ad/apps/web/public/images/brush-line.png -------------------------------------------------------------------------------- /apps/web/public/images/currents-evangelion.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cahyawibawa/ui-topia/121ef6e0d07db0558849e06036235349ce7dc5ad/apps/web/public/images/currents-evangelion.jpg -------------------------------------------------------------------------------- /apps/web/public/images/lines-top.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /apps/web/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/public/r/button-color.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "button-color", 4 | "type": "registry:component", 5 | "title": "Button color", 6 | "description": "custom shadcn/ui button.", 7 | "dependencies": [ 8 | "@radix-ui/react-slot", 9 | "class-variance-authority", 10 | "clsx", 11 | "react", 12 | "tailwind-merge" 13 | ], 14 | "registryDependencies": [ 15 | "https://uitopia.vercel.app/r/button.json", 16 | "https://uitopia.vercel.app/r/stack.json" 17 | ], 18 | "files": [ 19 | { 20 | "path": "registry/components/button-color.tsx", 21 | "content": "\"use client\";\n\nimport { Button } from \"@/registry/ui/button\";\nimport { Stack } from \"@/registry/ui/stack\";\n\nexport default function ButtonColor() {\n return (\n \n \n\n \n\n \n \n );\n}\n", 22 | "type": "registry:component", 23 | "target": "components/button-color.tsx" 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /apps/web/public/r/button-destructive.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "button-destructive", 4 | "type": "registry:component", 5 | "title": "Button Destructive", 6 | "description": "custom shadcn/ui button.", 7 | "dependencies": [ 8 | "@radix-ui/react-slot", 9 | "class-variance-authority", 10 | "clsx", 11 | "react", 12 | "tailwind-merge" 13 | ], 14 | "registryDependencies": [ 15 | "https://uitopia.vercel.app/r/button.json", 16 | "https://uitopia.vercel.app/r/stack.json" 17 | ], 18 | "files": [ 19 | { 20 | "path": "registry/components/button-destructive.tsx", 21 | "content": "\"use client\";\n\nimport { Button } from \"@/registry/ui/button\";\nimport { Stack } from \"@/registry/ui/stack\";\n\nexport default function ButtonDestructive() {\n return (\n \n \n Destructive\n \n\n \n \n Destructive\n \n \n\n \n \n Destructive\n \n \n \n );\n}\n", 22 | "type": "registry:component", 23 | "target": "components/button-destructive.tsx" 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /apps/web/public/r/button-outline.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "button-outline", 4 | "type": "registry:component", 5 | "title": "Button Outline", 6 | "description": "custom shadcn/ui button.", 7 | "dependencies": [ 8 | "@radix-ui/react-slot", 9 | "class-variance-authority", 10 | "clsx", 11 | "react", 12 | "tailwind-merge" 13 | ], 14 | "registryDependencies": [ 15 | "https://uitopia.vercel.app/r/button.json", 16 | "https://uitopia.vercel.app/r/stack.json" 17 | ], 18 | "files": [ 19 | { 20 | "path": "registry/components/button-outline.tsx", 21 | "content": "\"use client\";\n\nimport { Button } from \"@/registry/ui/button\";\nimport { Stack } from \"@/registry/ui/stack\";\n\nexport default function ButtonOutline() {\n return (\n \n \n Outline\n \n\n \n Outline\n \n\n \n Outline\n \n \n );\n}\n", 22 | "type": "registry:component", 23 | "target": "components/button-outline.tsx" 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /apps/web/public/r/button-primary.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "button-primary", 4 | "type": "registry:component", 5 | "title": "Button Primary", 6 | "description": "custom shadcn/ui button.", 7 | "dependencies": [ 8 | "@radix-ui/react-slot", 9 | "class-variance-authority", 10 | "clsx", 11 | "react", 12 | "tailwind-merge" 13 | ], 14 | "registryDependencies": [ 15 | "https://uitopia.vercel.app/r/button.json", 16 | "https://uitopia.vercel.app/r/stack.json" 17 | ], 18 | "files": [ 19 | { 20 | "path": "registry/components/button-primary.tsx", 21 | "content": "\"use client\";\n\nimport { Button } from \"@/registry/ui/button\";\nimport { Stack } from \"@/registry/ui/stack\";\n\nexport default function ButtonPrimary() {\n return (\n \n \n\n \n\n \n \n );\n}\n", 22 | "type": "registry:component", 23 | "target": "components/button-primary.tsx" 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /apps/web/public/r/button.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "button", 4 | "type": "registry:ui", 5 | "dependencies": [ 6 | "@radix-ui/react-slot" 7 | ], 8 | "files": [ 9 | { 10 | "path": "registry/ui/button.tsx", 11 | "content": "import { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/registry/lib/utils\";\n\nconst buttonVariants = cva(\n \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n {\n variants: {\n variant: {\n default:\n \"bg-primary text-primary-foreground shadow hover:bg-primary/90 ring-1 ring-inset ring-white/20 ring-offset-primary/90\",\n destructive:\n \"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90\",\n outline:\n \"border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground\",\n secondary:\n \"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80\",\n ghost: \"hover:bg-accent hover:text-accent-foreground\",\n link: \"text-primary underline-offset-4 hover:underline\",\n },\n size: {\n default: \"h-9 px-4 py-2\",\n sm: \"h-8 rounded-md px-3 text-xs\",\n lg: \"h-10 rounded-md px-8\",\n icon: \"h-9 w-9\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n },\n);\n\nexport interface ButtonProps\n extends React.ButtonHTMLAttributes,\n VariantProps {\n asChild?: boolean;\n}\n\nconst Button = React.forwardRef(\n ({ className, variant, size, asChild = false, ...props }, ref) => {\n const Comp = asChild ? Slot : \"button\";\n return (\n \n );\n },\n);\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n", 12 | "type": "registry:ui", 13 | "target": "components/ui/button.tsx" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /apps/web/public/r/card.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "card", 4 | "type": "registry:ui", 5 | "files": [ 6 | { 7 | "path": "registry/ui/card.tsx", 8 | "content": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Card = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => (\n \n));\nCard.displayName = \"Card\";\n\nconst CardHeader = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => (\n \n));\nCardHeader.displayName = \"CardHeader\";\n\nconst CardTitle = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => (\n \n));\nCardTitle.displayName = \"CardTitle\";\n\nconst CardDescription = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => (\n \n));\nCardDescription.displayName = \"CardDescription\";\n\nconst CardContent = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => (\n
\n));\nCardContent.displayName = \"CardContent\";\n\nconst CardFooter = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes\n>(({ className, ...props }, ref) => (\n \n));\nCardFooter.displayName = \"CardFooter\";\n\nexport {\n Card,\n CardHeader,\n CardFooter,\n CardTitle,\n CardDescription,\n CardContent,\n};\n", 9 | "type": "registry:ui", 10 | "target": "components/ui/card.tsx" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /apps/web/public/r/checkbox.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "checkbox", 4 | "type": "registry:ui", 5 | "dependencies": [ 6 | "@radix-ui/react-checkbox" 7 | ], 8 | "files": [ 9 | { 10 | "path": "registry/ui/checkbox.tsx", 11 | "content": "\"use client\";\n\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\";\nimport { CheckIcon } from \"lucide-react\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction Checkbox({\n className,\n ...props\n}: React.ComponentProps) {\n return (\n \n \n \n \n \n );\n}\n\nexport { Checkbox };\n", 12 | "type": "registry:ui", 13 | "target": "components/ui/checkbox.tsx" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /apps/web/public/r/input.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "input", 4 | "type": "registry:ui", 5 | "files": [ 6 | { 7 | "path": "registry/ui/input.tsx", 8 | "content": "import * as React from \"react\";\nimport { cn } from \"@/registry/lib/utils\";\n\nconst Input = React.forwardRef>(\n ({ className, type, ...props }, ref) => {\n return (\n \n );\n },\n);\nInput.displayName = \"Input\";\n\nexport { Input };\n", 9 | "type": "registry:ui", 10 | "target": "components/ui/input.tsx" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /apps/web/public/r/label.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "label", 4 | "type": "registry:ui", 5 | "dependencies": [ 6 | "@radix-ui/react-label" 7 | ], 8 | "files": [ 9 | { 10 | "path": "registry/ui/label.tsx", 11 | "content": "\"use client\";\n\nimport * as LabelPrimitive from \"@radix-ui/react-label\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst labelVariants = cva(\n \"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\",\n);\n\nconst Label = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef &\n VariantProps\n>(({ className, ...props }, ref) => (\n \n));\nLabel.displayName = LabelPrimitive.Root.displayName;\n\nexport { Label };\n", 12 | "type": "registry:ui", 13 | "target": "components/ui/label.tsx" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /apps/web/public/r/login-03.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "login-03", 4 | "type": "registry:block", 5 | "title": "Login 03", 6 | "description": "A simple login form.", 7 | "registryDependencies": [ 8 | "https://uitopia.vercel.app/r/button.json", 9 | "https://uitopia.vercel.app/r/card.json", 10 | "https://uitopia.vercel.app/r/input.json", 11 | "https://uitopia.vercel.app/r/label.json" 12 | ], 13 | "files": [ 14 | { 15 | "path": "registry/blocks/login-03/page.tsx", 16 | "content": "import { Login03 } from \"@/registry/blocks/login-03/components/login-form\";\n\nexport default function Page() {\n return (\n
\n
\n \n
\n
\n );\n}\n", 17 | "type": "registry:page", 18 | "target": "app/login-03/page.tsx" 19 | }, 20 | { 21 | "path": "registry/blocks/login-03/components/login-form.tsx", 22 | "content": "import { cn } from \"@/lib/utils\";\nimport { Button } from \"@/registry/ui/button\";\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/registry/ui/card\";\nimport { Input } from \"@/registry/ui/input\";\nimport { Label } from \"@/registry/ui/label\";\n\nexport function Login03({\n className,\n ...props\n}: React.ComponentPropsWithoutRef<\"div\">) {\n return (\n
\n \n \n Login\n \n Enter your email below to login to your account\n \n \n \n
\n
\n
\n \n \n
\n
\n
\n \n \n Forgot your password?\n \n
\n \n
\n \n \n
\n
\n Don't have an account?{\" \"}\n \n Sign up\n \n
\n
\n
\n
\n
\n );\n}\n", 23 | "type": "registry:component" 24 | } 25 | ], 26 | "meta": { 27 | "iframeHeight": "auto" 28 | }, 29 | "categories": [ 30 | "authentication" 31 | ] 32 | } -------------------------------------------------------------------------------- /apps/web/public/r/paper-fold-demo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "paper-fold-demo", 4 | "type": "registry:component", 5 | "title": "Paper Fold", 6 | "description": "A paper folding animation.", 7 | "dependencies": [ 8 | "@remixicon/react", 9 | "clsx", 10 | "tailwind-merge" 11 | ], 12 | "registryDependencies": [ 13 | "https://uitopia.vercel.app/r/utils.json" 14 | ], 15 | "files": [ 16 | { 17 | "path": "registry/components/paper-fold-demo.tsx", 18 | "content": "\"use client\";\n\nimport { RiTranslateAi } from \"@remixicon/react\";\n\nimport { cn } from \"@/registry/lib/utils\";\n\nconst StickerCard = ({\n icon: Icon,\n title,\n children,\n}: {\n icon: React.ElementType;\n title: string;\n children: React.ReactNode;\n}) => {\n return (\n
\n \n
\n
\n
\n \n

\n {title}\n

\n
\n

\n {children}\n

\n
\n \n
\n );\n};\n\nconst cardData = [\n {\n icon: RiTranslateAi,\n title: \"Lost in translations\",\n description: \"Lorem ipsum dolor sit amet, consectetur adipiscing elit.\",\n },\n //more data here\n];\n\nexport default function PaperFoldDemo() {\n return (\n
\n {cardData.map((card, index) => (\n \n {card.description}\n \n ))}\n
\n );\n}\n", 19 | "type": "registry:component", 20 | "target": "components/paper-fold-demo.tsx" 21 | } 22 | ], 23 | "categories": [ 24 | "react", 25 | "tailwindcss" 26 | ] 27 | } -------------------------------------------------------------------------------- /apps/web/public/r/separator.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "separator", 4 | "type": "registry:ui", 5 | "dependencies": [ 6 | "@radix-ui/react-separator" 7 | ], 8 | "files": [ 9 | { 10 | "path": "registry/ui/separator.tsx", 11 | "content": "\"use client\";\n\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Separator = React.forwardRef<\n React.ElementRef,\n React.ComponentPropsWithoutRef\n>(\n (\n { className, orientation = \"horizontal\", decorative = true, ...props },\n ref,\n ) => (\n \n ),\n);\nSeparator.displayName = SeparatorPrimitive.Root.displayName;\n\nexport { Separator };\n", 12 | "type": "registry:ui", 13 | "target": "components/ui/separator.tsx" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /apps/web/public/r/skeleton.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "skeleton", 4 | "type": "registry:ui", 5 | "files": [ 6 | { 7 | "path": "registry/ui/skeleton.tsx", 8 | "content": "import { cn } from \"@/lib/utils\";\n\nfunction Skeleton({\n className,\n ...props\n}: React.HTMLAttributes) {\n return (\n \n );\n}\n\nexport { Skeleton };\n", 9 | "type": "registry:ui", 10 | "target": "components/ui/skeleton.tsx" 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /apps/web/public/r/stack.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "stack", 4 | "type": "registry:ui", 5 | "dependencies": [ 6 | "clsx", 7 | "tailwind-merge" 8 | ], 9 | "files": [ 10 | { 11 | "path": "registry/ui/stack.tsx", 12 | "content": "import type { ComponentProps } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype Direction = \"row\" | \"column\";\ntype ResponsiveDirection = {\n sm?: Direction;\n md?: Direction;\n};\n\ntype FlexAlignItems = \"stretch\" | \"start\" | \"end\" | \"center\";\ntype FlexJustifyContent =\n | \"stretch\"\n | \"start\"\n | \"end\"\n | \"space-between\"\n | \"space-around\"\n | \"space-evenly\"\n | \"center\";\n\ninterface StackProps extends ComponentProps<\"div\"> {\n children: React.ReactNode;\n direction?: ResponsiveDirection;\n gap?: number;\n padding?: number;\n grow?: boolean;\n shrink?: boolean;\n wrap?: boolean;\n align?: FlexAlignItems;\n justify?: FlexJustifyContent;\n className?: string;\n}\n\nexport function Stack({\n direction = { sm: \"column\", md: \"row\" },\n align = \"start\",\n justify = \"start\",\n wrap = false,\n shrink = false,\n grow = false,\n padding = 0,\n gap = 0,\n children,\n className,\n ...etc\n}: StackProps) {\n const directionClasses = [\n direction.sm === \"row\" ? \"flex-row\" : \"flex-col\",\n direction.md === \"row\" ? \"md:flex-row\" : \"md:flex-col\",\n ];\n\n const alignClasses = {\n stretch: \"items-stretch\",\n start: \"items-start\",\n end: \"items-end\",\n center: \"items-center\",\n }[align];\n\n const justifyClasses = {\n stretch: \"justify-stretch\",\n start: \"justify-start\",\n end: \"justify-end\",\n \"space-between\": \"justify-between\",\n \"space-around\": \"justify-around\",\n \"space-evenly\": \"justify-evenly\",\n center: \"justify-center\",\n }[justify];\n\n return (\n \n {children}\n
\n );\n}\n", 13 | "type": "registry:ui", 14 | "target": "components/ui/stack.tsx" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /apps/web/public/r/text-loop-demo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "text-loop-demo", 4 | "type": "registry:component", 5 | "title": "Text Loop", 6 | "description": "Text animation that transitions between multiple items, creating an engaging looping effect", 7 | "dependencies": [ 8 | "clsx", 9 | "motion", 10 | "react", 11 | "tailwind-merge" 12 | ], 13 | "registryDependencies": [ 14 | "https://uitopia.vercel.app/r/text-loop.json" 15 | ], 16 | "files": [ 17 | { 18 | "path": "registry/components/text-loop-demo.tsx", 19 | "content": "\"use client\";\n\nimport TextLoop from \"@/registry/ui/text-loop\";\n\nexport default function TextLoopDemo() {\n return (\n

\n Some days you're{\" \"}\n \n Healing\n Feeling\n Thriving\n Surviving\n \n

\n );\n}\n", 20 | "type": "registry:component", 21 | "target": "components/text-loop-demo.tsx" 22 | } 23 | ], 24 | "categories": [ 25 | "motion", 26 | "react", 27 | "tailwindcss" 28 | ] 29 | } -------------------------------------------------------------------------------- /apps/web/public/r/text-loop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "text-loop", 4 | "type": "registry:component", 5 | "title": "Text Loop", 6 | "description": "text loop base", 7 | "dependencies": [ 8 | "clsx", 9 | "motion", 10 | "react", 11 | "tailwind-merge" 12 | ], 13 | "files": [ 14 | { 15 | "path": "registry/ui/text-loop.tsx", 16 | "content": "\"use client\";\nimport {\n AnimatePresence,\n motion,\n type Transition,\n type Variants,\n} from \"motion/react\";\nimport { Children, useEffect, useState } from \"react\";\nimport { cn } from \"@/registry/lib/utils\";\n\nexport type TextLoopProps = {\n children: React.ReactNode[];\n className?: string;\n interval?: number;\n transition?: Transition;\n variants?: Variants;\n onIndexChange?: (index: number) => void;\n};\n\nexport default function TextLoop({\n children,\n className,\n interval = 2,\n transition = { duration: 0.3 },\n variants,\n onIndexChange,\n}: TextLoopProps) {\n const [currentIndex, setCurrentIndex] = useState(0);\n const items = Children.toArray(children);\n\n useEffect(() => {\n const intervalMs = interval * 1000;\n\n const timer = setInterval(() => {\n setCurrentIndex((current) => {\n const next = (current + 1) % items.length;\n onIndexChange?.(next);\n return next;\n });\n }, intervalMs);\n return () => clearInterval(timer);\n }, [items.length, interval, onIndexChange]);\n\n const motionVariants: Variants = {\n initial: { y: 20, opacity: 0 },\n animate: { y: 0, opacity: 1 },\n exit: { y: -20, opacity: 0 },\n };\n\n return (\n
\n \n \n {items[currentIndex]}\n \n \n
\n );\n}\n", 17 | "type": "registry:ui", 18 | "target": "components/ui/text-loop.tsx" 19 | } 20 | ], 21 | "categories": [ 22 | "motion", 23 | "react", 24 | "tailwindcss" 25 | ] 26 | } -------------------------------------------------------------------------------- /apps/web/public/r/text-type-demo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "text-type-demo", 4 | "type": "registry:component", 5 | "title": "Typewriter", 6 | "description": "text type", 7 | "dependencies": [ 8 | "clsx", 9 | "motion", 10 | "react", 11 | "tailwind-merge" 12 | ], 13 | "registryDependencies": [ 14 | "https://uitopia.vercel.app/r/text-type.json" 15 | ], 16 | "files": [ 17 | { 18 | "path": "registry/components/text-type-demo.tsx", 19 | "content": "\"use client\";\n\nimport TextType from \"@/registry/ui/text-type\";\n\nexport default function TextLoopDemo() {\n return (\n

\n {\"Don't forget how \"}\n \n {\" you are.\"}\n

\n );\n}\n", 20 | "type": "registry:component", 21 | "target": "components/text-type-demo.tsx" 22 | } 23 | ], 24 | "categories": [ 25 | "motion", 26 | "react", 27 | "tailwindcss" 28 | ] 29 | } -------------------------------------------------------------------------------- /apps/web/public/r/text-type.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "text-type", 4 | "type": "registry:component", 5 | "title": "Text Type", 6 | "description": "text type base", 7 | "dependencies": [ 8 | "clsx", 9 | "motion", 10 | "react", 11 | "tailwind-merge" 12 | ], 13 | "files": [ 14 | { 15 | "path": "registry/ui/text-type.tsx", 16 | "content": "\"use client\";\n\nimport { motion, type Variants } from \"motion/react\";\nimport { useEffect, useState } from \"react\";\nimport { cn } from \"@/registry/lib/utils\";\n\nexport type TextTypeProps = {\n text: string | string[];\n speed?: number;\n initialDelay?: number;\n waitTime?: number;\n deleteSpeed?: number;\n loop?: boolean;\n className?: string;\n showCursor?: boolean;\n hideCursorOnType?: boolean;\n cursorChar?: string | React.ReactNode;\n cursorAnimationVariants?: {\n initial: Variants[\"initial\"];\n animate: Variants[\"animate\"];\n };\n cursorClassName?: string;\n};\n\nexport default function Typewriter({\n text,\n speed = 50,\n initialDelay = 0,\n waitTime = 2000,\n deleteSpeed = 30,\n loop = true,\n className,\n showCursor = true,\n hideCursorOnType = false,\n cursorChar = \"|\",\n cursorClassName = \"ml-0.5\",\n cursorAnimationVariants = {\n initial: { opacity: 0 },\n animate: {\n opacity: 1,\n transition: {\n duration: 0.01,\n repeat: Number.POSITIVE_INFINITY,\n repeatDelay: 0.4,\n repeatType: \"reverse\",\n },\n },\n },\n}: TextTypeProps) {\n const [displayText, setDisplayText] = useState(\"\");\n const [currentIndex, setCurrentIndex] = useState(0);\n const [isDeleting, setIsDeleting] = useState(false);\n const [currentTextIndex, setCurrentTextIndex] = useState(0);\n\n const texts = Array.isArray(text) ? text : [text];\n\n useEffect(() => {\n let timeout: NodeJS.Timeout;\n\n const currentText = texts[currentTextIndex];\n\n const startTyping = () => {\n if (isDeleting) {\n if (displayText === \"\") {\n setIsDeleting(false);\n if (currentTextIndex === texts.length - 1 && !loop) {\n return;\n }\n setCurrentTextIndex((prev) => (prev + 1) % texts.length);\n setCurrentIndex(0);\n timeout = setTimeout(() => {}, waitTime);\n } else {\n timeout = setTimeout(() => {\n setDisplayText((prev) => prev.slice(0, -1));\n }, deleteSpeed);\n }\n } else {\n if (currentText && currentIndex < currentText.length) {\n timeout = setTimeout(() => {\n setDisplayText((prev) => prev + currentText[currentIndex]);\n setCurrentIndex((prev) => prev + 1);\n }, speed);\n } else if (texts.length > 1) {\n timeout = setTimeout(() => {\n setIsDeleting(true);\n }, waitTime);\n }\n }\n };\n\n // Apply initial delay only at the start\n if (currentIndex === 0 && !isDeleting && displayText === \"\") {\n timeout = setTimeout(startTyping, initialDelay);\n } else {\n startTyping();\n }\n\n return () => clearTimeout(timeout);\n }, [\n currentIndex,\n displayText,\n isDeleting,\n speed,\n deleteSpeed,\n waitTime,\n texts,\n currentTextIndex,\n loop,\n initialDelay,\n ]);\n\n return (\n
\n {displayText}\n {showCursor && (\n \n {cursorChar}\n \n )}\n
\n );\n}\n", 17 | "type": "registry:component", 18 | "target": "components/ui/text-type.tsx" 19 | } 20 | ], 21 | "categories": [ 22 | "motion", 23 | "react", 24 | "tailwindcss" 25 | ] 26 | } -------------------------------------------------------------------------------- /apps/web/public/r/tooltip.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "tooltip", 4 | "type": "registry:ui", 5 | "dependencies": [ 6 | "@radix-ui/react-tooltip" 7 | ], 8 | "files": [ 9 | { 10 | "path": "registry/ui/tooltip.tsx", 11 | "content": "\"use client\";\n\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\nimport type * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction TooltipProvider({\n delayDuration = 0,\n ...props\n}: React.ComponentProps) {\n return (\n \n );\n}\n\nfunction Tooltip({\n ...props\n}: React.ComponentProps) {\n return (\n \n \n \n );\n}\n\nfunction TooltipTrigger({\n ...props\n}: React.ComponentProps) {\n return ;\n}\n\nfunction TooltipContent({\n className,\n sideOffset = 0,\n children,\n ...props\n}: React.ComponentProps) {\n return (\n \n \n {children}\n \n \n \n );\n}\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };\n", 12 | "type": "registry:ui", 13 | "target": "components/ui/tooltip.tsx" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /apps/web/public/r/use-debounce.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "use-debounce", 4 | "type": "registry:hook", 5 | "files": [ 6 | { 7 | "path": "registry/hooks/use-debounce.ts", 8 | "content": "import { useEffect, useState } from \"react\";\n\nfunction useDebounce(value: T, delay = 500): T {\n const [debouncedValue, setDebouncedValue] = useState(value);\n\n useEffect(() => {\n const timer = setTimeout(() => {\n setDebouncedValue(value);\n }, delay);\n\n return () => {\n clearTimeout(timer);\n };\n }, [value, delay]);\n\n return debouncedValue;\n}\n\nexport default useDebounce;\n", 9 | "type": "registry:hook" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /apps/web/public/r/use-mobile.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "use-mobile", 4 | "type": "registry:hook", 5 | "files": [ 6 | { 7 | "path": "registry/hooks/use-mobile.ts", 8 | "content": "import * as React from \"react\";\n\nconst MOBILE_BREAKPOINT = 768;\n\nexport function useIsMobile() {\n const [isMobile, setIsMobile] = React.useState(\n undefined,\n );\n\n React.useEffect(() => {\n const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);\n const onChange = () => {\n setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\n };\n mql.addEventListener(\"change\", onChange);\n setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\n return () => mql.removeEventListener(\"change\", onChange);\n }, []);\n\n return !!isMobile;\n}\n", 9 | "type": "registry:hook" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /apps/web/public/r/utils.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "utils", 4 | "type": "registry:lib", 5 | "dependencies": [ 6 | "clsx", 7 | "tailwind-merge" 8 | ], 9 | "files": [ 10 | { 11 | "path": "registry/lib/utils.ts", 12 | "content": "import { type ClassValue, clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n", 13 | "type": "registry:lib", 14 | "target": "lib/utils.ts" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /apps/web/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/registry/blocks/login-03/components/login-form.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { Button } from "@/registry/ui/button"; 3 | import { 4 | Card, 5 | CardContent, 6 | CardDescription, 7 | CardHeader, 8 | CardTitle, 9 | } from "@/registry/ui/card"; 10 | import { Input } from "@/registry/ui/input"; 11 | import { Label } from "@/registry/ui/label"; 12 | 13 | export function Login03({ 14 | className, 15 | ...props 16 | }: React.ComponentPropsWithoutRef<"div">) { 17 | return ( 18 |
19 | 20 | 21 | Login 22 | 23 | Enter your email below to login to your account 24 | 25 | 26 | 27 |
28 |
29 |
30 | 31 | 37 |
38 |
39 |
40 | 41 | 45 | Forgot your password? 46 | 47 |
48 | 49 |
50 | 53 | 56 |
57 |
58 | Don't have an account?{" "} 59 | 60 | Sign up 61 | 62 |
63 |
64 |
65 |
66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /apps/web/registry/blocks/login-03/page.tsx: -------------------------------------------------------------------------------- 1 | import { Login03 } from "@/registry/blocks/login-03/components/login-form"; 2 | 3 | export default function Page() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/registry/blocks/login-04/page.tsx: -------------------------------------------------------------------------------- 1 | import { Login04 } from "@/registry/blocks/login-04/components/login-form"; 2 | 3 | export default function Page() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/registry/blocks/login-05/page.tsx: -------------------------------------------------------------------------------- 1 | import { Login05 } from "@/registry/blocks/login-05/components/login-form"; 2 | 3 | export default function Page() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/registry/components/button-color.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/registry/ui/button"; 4 | import { Stack } from "@/registry/ui/stack"; 5 | 6 | export default function ButtonColor() { 7 | return ( 8 | 14 | 17 | 18 | 21 | 22 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /apps/web/registry/components/button-destructive.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/registry/ui/button"; 4 | import { Stack } from "@/registry/ui/stack"; 5 | 6 | export default function ButtonDestructive() { 7 | return ( 8 | 14 | 20 | 21 | 29 | 30 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /apps/web/registry/components/button-outline.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/registry/ui/button"; 4 | import { Stack } from "@/registry/ui/stack"; 5 | 6 | export default function ButtonOutline() { 7 | return ( 8 | 14 | 20 | 21 | 27 | 28 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /apps/web/registry/components/button-primary.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/registry/ui/button"; 4 | import { Stack } from "@/registry/ui/stack"; 5 | 6 | export default function ButtonPrimary() { 7 | return ( 8 | 14 | 17 | 18 | 23 | 24 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /apps/web/registry/components/paper-fold-demo.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { RiTranslateAi } from "@remixicon/react"; 4 | 5 | import { cn } from "@/registry/lib/utils"; 6 | 7 | const StickerCard = ({ 8 | icon: Icon, 9 | title, 10 | children, 11 | }: { 12 | icon: React.ElementType; 13 | title: string; 14 | children: React.ReactNode; 15 | }) => { 16 | return ( 17 |
18 | 60 | 74 | ); 75 | }; 76 | 77 | const cardData = [ 78 | { 79 | icon: RiTranslateAi, 80 | title: "Lost in translations", 81 | description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", 82 | }, 83 | //more data here 84 | ]; 85 | 86 | export default function PaperFoldDemo() { 87 | return ( 88 |
89 | {cardData.map((card, index) => ( 90 | 91 | {card.description} 92 | 93 | ))} 94 |
95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /apps/web/registry/components/text-loop-demo.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import TextLoop from "@/registry/ui/text-loop"; 4 | 5 | export default function TextLoopDemo() { 6 | return ( 7 |

8 | Some days you're{" "} 9 | 39 | Healing 40 | Feeling 41 | Thriving 42 | Surviving 43 | 44 |

45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /apps/web/registry/components/text-type-demo.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import TextType from "@/registry/ui/text-type"; 4 | 5 | export default function TextLoopDemo() { 6 | return ( 7 |

8 | {"Don't forget how "} 9 | 30 | {" you are."} 31 |

32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /apps/web/registry/hooks/use-debounce.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | function useDebounce(value: T, delay = 500): T { 4 | const [debouncedValue, setDebouncedValue] = useState(value); 5 | 6 | useEffect(() => { 7 | const timer = setTimeout(() => { 8 | setDebouncedValue(value); 9 | }, delay); 10 | 11 | return () => { 12 | clearTimeout(timer); 13 | }; 14 | }, [value, delay]); 15 | 16 | return debouncedValue; 17 | } 18 | 19 | export default useDebounce; 20 | -------------------------------------------------------------------------------- /apps/web/registry/hooks/use-mobile.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const MOBILE_BREAKPOINT = 768; 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState( 7 | undefined, 8 | ); 9 | 10 | React.useEffect(() => { 11 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); 12 | const onChange = () => { 13 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 14 | }; 15 | mql.addEventListener("change", onChange); 16 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 17 | return () => mql.removeEventListener("change", onChange); 18 | }, []); 19 | 20 | return !!isMobile; 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/registry/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/registry/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "@radix-ui/react-slot"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | import * as React from "react"; 4 | 5 | import { cn } from "@/registry/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-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90 ring-1 ring-inset ring-white/20 ring-offset-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | }, 35 | ); 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean; 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button"; 46 | return ( 47 | 52 | ); 53 | }, 54 | ); 55 | Button.displayName = "Button"; 56 | 57 | export { Button, buttonVariants }; 58 | -------------------------------------------------------------------------------- /apps/web/registry/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )); 18 | Card.displayName = "Card"; 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )); 30 | CardHeader.displayName = "CardHeader"; 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
41 | )); 42 | CardTitle.displayName = "CardTitle"; 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLDivElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |
53 | )); 54 | CardDescription.displayName = "CardDescription"; 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |
61 | )); 62 | CardContent.displayName = "CardContent"; 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )); 74 | CardFooter.displayName = "CardFooter"; 75 | 76 | export { 77 | Card, 78 | CardHeader, 79 | CardFooter, 80 | CardTitle, 81 | CardDescription, 82 | CardContent, 83 | }; 84 | -------------------------------------------------------------------------------- /apps/web/registry/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 4 | import { CheckIcon } from "lucide-react"; 5 | import type * as React from "react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | function Checkbox({ 10 | className, 11 | ...props 12 | }: React.ComponentProps) { 13 | return ( 14 | 22 | 26 | 27 | 28 | 29 | ); 30 | } 31 | 32 | export { Checkbox }; 33 | -------------------------------------------------------------------------------- /apps/web/registry/ui/drawer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { Drawer as DrawerPrimitive } from "vaul"; 5 | 6 | import { cn } from "@/registry/lib/utils"; 7 | 8 | const Drawer = ({ 9 | shouldScaleBackground = true, 10 | ...props 11 | }: React.ComponentProps) => ( 12 | 16 | ); 17 | Drawer.displayName = "Drawer"; 18 | 19 | const DrawerTrigger = DrawerPrimitive.Trigger; 20 | 21 | const DrawerPortal = DrawerPrimitive.Portal; 22 | 23 | const DrawerClose = DrawerPrimitive.Close; 24 | 25 | const DrawerOverlay = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 34 | )); 35 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName; 36 | 37 | const DrawerContent = React.forwardRef< 38 | React.ElementRef, 39 | React.ComponentPropsWithoutRef 40 | >(({ className, children, ...props }, ref) => ( 41 | 42 | 43 | 51 |
52 | {children} 53 | 54 | 55 | )); 56 | DrawerContent.displayName = "DrawerContent"; 57 | 58 | const DrawerHeader = ({ 59 | className, 60 | ...props 61 | }: React.HTMLAttributes) => ( 62 |
66 | ); 67 | DrawerHeader.displayName = "DrawerHeader"; 68 | 69 | const DrawerFooter = ({ 70 | className, 71 | ...props 72 | }: React.HTMLAttributes) => ( 73 |
77 | ); 78 | DrawerFooter.displayName = "DrawerFooter"; 79 | 80 | const DrawerTitle = React.forwardRef< 81 | React.ElementRef, 82 | React.ComponentPropsWithoutRef 83 | >(({ className, ...props }, ref) => ( 84 | 92 | )); 93 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName; 94 | 95 | const DrawerDescription = React.forwardRef< 96 | React.ElementRef, 97 | React.ComponentPropsWithoutRef 98 | >(({ className, ...props }, ref) => ( 99 | 104 | )); 105 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName; 106 | 107 | export { 108 | Drawer, 109 | DrawerPortal, 110 | DrawerOverlay, 111 | DrawerTrigger, 112 | DrawerClose, 113 | DrawerContent, 114 | DrawerHeader, 115 | DrawerFooter, 116 | DrawerTitle, 117 | DrawerDescription, 118 | }; 119 | -------------------------------------------------------------------------------- /apps/web/registry/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cn } from "@/registry/lib/utils"; 3 | 4 | const Input = React.forwardRef>( 5 | ({ className, type, ...props }, ref) => { 6 | return ( 7 | 20 | ); 21 | }, 22 | ); 23 | Input.displayName = "Input"; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /apps/web/registry/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as LabelPrimitive from "@radix-ui/react-label"; 4 | import { cva, type VariantProps } from "class-variance-authority"; 5 | import * as React from "react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 11 | ); 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )); 24 | Label.displayName = LabelPrimitive.Root.displayName; 25 | 26 | export { Label }; 27 | -------------------------------------------------------------------------------- /apps/web/registry/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 4 | import * as React from "react"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref, 15 | ) => ( 16 | 27 | ), 28 | ); 29 | Separator.displayName = SeparatorPrimitive.Root.displayName; 30 | 31 | export { Separator }; 32 | -------------------------------------------------------------------------------- /apps/web/registry/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ); 13 | } 14 | 15 | export { Skeleton }; 16 | -------------------------------------------------------------------------------- /apps/web/registry/ui/stack.tsx: -------------------------------------------------------------------------------- 1 | import type { ComponentProps } from "react"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | type Direction = "row" | "column"; 5 | type ResponsiveDirection = { 6 | sm?: Direction; 7 | md?: Direction; 8 | }; 9 | 10 | type FlexAlignItems = "stretch" | "start" | "end" | "center"; 11 | type FlexJustifyContent = 12 | | "stretch" 13 | | "start" 14 | | "end" 15 | | "space-between" 16 | | "space-around" 17 | | "space-evenly" 18 | | "center"; 19 | 20 | interface StackProps extends ComponentProps<"div"> { 21 | children: React.ReactNode; 22 | direction?: ResponsiveDirection; 23 | gap?: number; 24 | padding?: number; 25 | grow?: boolean; 26 | shrink?: boolean; 27 | wrap?: boolean; 28 | align?: FlexAlignItems; 29 | justify?: FlexJustifyContent; 30 | className?: string; 31 | } 32 | 33 | export function Stack({ 34 | direction = { sm: "column", md: "row" }, 35 | align = "start", 36 | justify = "start", 37 | wrap = false, 38 | shrink = false, 39 | grow = false, 40 | padding = 0, 41 | gap = 0, 42 | children, 43 | className, 44 | ...etc 45 | }: StackProps) { 46 | const directionClasses = [ 47 | direction.sm === "row" ? "flex-row" : "flex-col", 48 | direction.md === "row" ? "md:flex-row" : "md:flex-col", 49 | ]; 50 | 51 | const alignClasses = { 52 | stretch: "items-stretch", 53 | start: "items-start", 54 | end: "items-end", 55 | center: "items-center", 56 | }[align]; 57 | 58 | const justifyClasses = { 59 | stretch: "justify-stretch", 60 | start: "justify-start", 61 | end: "justify-end", 62 | "space-between": "justify-between", 63 | "space-around": "justify-around", 64 | "space-evenly": "justify-evenly", 65 | center: "justify-center", 66 | }[justify]; 67 | 68 | return ( 69 |
84 | {children} 85 |
86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /apps/web/registry/ui/text-loop.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { 3 | AnimatePresence, 4 | motion, 5 | type Transition, 6 | type Variants, 7 | } from "motion/react"; 8 | import { Children, useEffect, useState } from "react"; 9 | import { cn } from "@/registry/lib/utils"; 10 | 11 | export type TextLoopProps = { 12 | children: React.ReactNode[]; 13 | className?: string; 14 | interval?: number; 15 | transition?: Transition; 16 | variants?: Variants; 17 | onIndexChange?: (index: number) => void; 18 | }; 19 | 20 | export default function TextLoop({ 21 | children, 22 | className, 23 | interval = 2, 24 | transition = { duration: 0.3 }, 25 | variants, 26 | onIndexChange, 27 | }: TextLoopProps) { 28 | const [currentIndex, setCurrentIndex] = useState(0); 29 | const items = Children.toArray(children); 30 | 31 | useEffect(() => { 32 | const intervalMs = interval * 1000; 33 | 34 | const timer = setInterval(() => { 35 | setCurrentIndex((current) => { 36 | const next = (current + 1) % items.length; 37 | onIndexChange?.(next); 38 | return next; 39 | }); 40 | }, intervalMs); 41 | return () => clearInterval(timer); 42 | }, [items.length, interval, onIndexChange]); 43 | 44 | const motionVariants: Variants = { 45 | initial: { y: 20, opacity: 0 }, 46 | animate: { y: 0, opacity: 1 }, 47 | exit: { y: -20, opacity: 0 }, 48 | }; 49 | 50 | return ( 51 |
52 | 53 | 61 | {items[currentIndex]} 62 | 63 | 64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /apps/web/registry/ui/text-morph.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { AnimatePresence, motion } from "motion/react"; 3 | import { useId, useMemo } from "react"; 4 | import { cn } from "@/registry/lib/utils"; 5 | 6 | type TextMorphProps = { 7 | children: string; 8 | as?: React.ElementType; 9 | className?: string; 10 | style?: React.CSSProperties; 11 | }; 12 | 13 | export function TextMorph({ 14 | children, 15 | as: Component = "p", 16 | className, 17 | style, 18 | }: TextMorphProps) { 19 | const uniqueId = useId(); 20 | 21 | const characters = useMemo(() => { 22 | const charCounts: Record = {}; 23 | 24 | return children.split("").map((char, index) => { 25 | const lowerChar = char.toLowerCase(); 26 | charCounts[lowerChar] = (charCounts[lowerChar] || 0) + 1; 27 | 28 | return { 29 | id: `${uniqueId}-${lowerChar}${charCounts[lowerChar]}`, 30 | label: index === 0 ? char.toUpperCase() : lowerChar, 31 | }; 32 | }); 33 | }, [children, uniqueId]); 34 | 35 | return ( 36 | 37 | 38 | {characters.map((character) => ( 39 | 56 | ))} 57 | 58 | 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /apps/web/registry/ui/text-type.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { motion, type Variants } from "motion/react"; 4 | import { useEffect, useState } from "react"; 5 | import { cn } from "@/registry/lib/utils"; 6 | 7 | export type TextTypeProps = { 8 | text: string | string[]; 9 | speed?: number; 10 | initialDelay?: number; 11 | waitTime?: number; 12 | deleteSpeed?: number; 13 | loop?: boolean; 14 | className?: string; 15 | showCursor?: boolean; 16 | hideCursorOnType?: boolean; 17 | cursorChar?: string | React.ReactNode; 18 | cursorAnimationVariants?: { 19 | initial: Variants["initial"]; 20 | animate: Variants["animate"]; 21 | }; 22 | cursorClassName?: string; 23 | }; 24 | 25 | export default function Typewriter({ 26 | text, 27 | speed = 50, 28 | initialDelay = 0, 29 | waitTime = 2000, 30 | deleteSpeed = 30, 31 | loop = true, 32 | className, 33 | showCursor = true, 34 | hideCursorOnType = false, 35 | cursorChar = "|", 36 | cursorClassName = "ml-0.5", 37 | cursorAnimationVariants = { 38 | initial: { opacity: 0 }, 39 | animate: { 40 | opacity: 1, 41 | transition: { 42 | duration: 0.01, 43 | repeat: Number.POSITIVE_INFINITY, 44 | repeatDelay: 0.4, 45 | repeatType: "reverse", 46 | }, 47 | }, 48 | }, 49 | }: TextTypeProps) { 50 | const [displayText, setDisplayText] = useState(""); 51 | const [currentIndex, setCurrentIndex] = useState(0); 52 | const [isDeleting, setIsDeleting] = useState(false); 53 | const [currentTextIndex, setCurrentTextIndex] = useState(0); 54 | 55 | const texts = Array.isArray(text) ? text : [text]; 56 | 57 | useEffect(() => { 58 | let timeout: NodeJS.Timeout; 59 | 60 | const currentText = texts[currentTextIndex]; 61 | 62 | const startTyping = () => { 63 | if (isDeleting) { 64 | if (displayText === "") { 65 | setIsDeleting(false); 66 | if (currentTextIndex === texts.length - 1 && !loop) { 67 | return; 68 | } 69 | setCurrentTextIndex((prev) => (prev + 1) % texts.length); 70 | setCurrentIndex(0); 71 | timeout = setTimeout(() => {}, waitTime); 72 | } else { 73 | timeout = setTimeout(() => { 74 | setDisplayText((prev) => prev.slice(0, -1)); 75 | }, deleteSpeed); 76 | } 77 | } else { 78 | if (currentText && currentIndex < currentText.length) { 79 | timeout = setTimeout(() => { 80 | setDisplayText((prev) => prev + currentText[currentIndex]); 81 | setCurrentIndex((prev) => prev + 1); 82 | }, speed); 83 | } else if (texts.length > 1) { 84 | timeout = setTimeout(() => { 85 | setIsDeleting(true); 86 | }, waitTime); 87 | } 88 | } 89 | }; 90 | 91 | // Apply initial delay only at the start 92 | if (currentIndex === 0 && !isDeleting && displayText === "") { 93 | timeout = setTimeout(startTyping, initialDelay); 94 | } else { 95 | startTyping(); 96 | } 97 | 98 | return () => clearTimeout(timeout); 99 | }, [ 100 | currentIndex, 101 | displayText, 102 | isDeleting, 103 | speed, 104 | deleteSpeed, 105 | waitTime, 106 | texts, 107 | currentTextIndex, 108 | loop, 109 | initialDelay, 110 | ]); 111 | 112 | return ( 113 |
114 | {displayText} 115 | {showCursor && ( 116 | 129 | {cursorChar} 130 | 131 | )} 132 |
133 | ); 134 | } 135 | -------------------------------------------------------------------------------- /apps/web/registry/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 4 | import type * as React from "react"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function TooltipProvider({ 9 | delayDuration = 0, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 18 | ); 19 | } 20 | 21 | function Tooltip({ 22 | ...props 23 | }: React.ComponentProps) { 24 | return ( 25 | 26 | 27 | 28 | ); 29 | } 30 | 31 | function TooltipTrigger({ 32 | ...props 33 | }: React.ComponentProps) { 34 | return ; 35 | } 36 | 37 | function TooltipContent({ 38 | className, 39 | sideOffset = 0, 40 | children, 41 | ...props 42 | }: React.ComponentProps) { 43 | return ( 44 | 45 | 54 | {children} 55 | 56 | 57 | 58 | ); 59 | } 60 | 61 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; 62 | -------------------------------------------------------------------------------- /apps/web/source.config.ts: -------------------------------------------------------------------------------- 1 | import { rehypeCodeDefaultOptions } from "fumadocs-core/mdx-plugins"; 2 | import { fileGenerator, remarkDocGen, remarkInstall } from "fumadocs-docgen"; 3 | import { 4 | defineConfig, 5 | defineDocs, 6 | frontmatterSchema, 7 | } from "fumadocs-mdx/config"; 8 | import { transformerTwoslash } from "fumadocs-twoslash"; 9 | 10 | export const { docs, meta } = defineDocs({ 11 | dir: "content/docs", 12 | docs: { 13 | schema: frontmatterSchema, 14 | }, 15 | }); 16 | 17 | export default defineConfig({ 18 | lastModifiedTime: "git", 19 | mdxOptions: { 20 | rehypeCodeOptions: { 21 | inline: "tailing-curly-colon", 22 | themes: { 23 | light: "github-light", 24 | dark: "github-dark", 25 | }, 26 | transformers: [ 27 | ...(rehypeCodeDefaultOptions.transformers ?? []), 28 | transformerTwoslash(), 29 | { 30 | name: "transformers:remove-notation-escape", 31 | code(hast) { 32 | for (const line of hast.children) { 33 | if (line.type !== "element") continue; 34 | 35 | const lastSpan = line.children.findLast( 36 | (v) => v.type === "element", 37 | ); 38 | 39 | const head = lastSpan?.children[0]; 40 | if (head?.type !== "text") return; 41 | 42 | head.value = head.value.replace(/\[\\!code/g, "[!code"); 43 | } 44 | }, 45 | }, 46 | ], 47 | }, 48 | remarkPlugins: [ 49 | [remarkInstall, { persist: { id: "package-manager" } }], 50 | [remarkDocGen, { generators: [fileGenerator()] }], 51 | ], 52 | }, 53 | }); 54 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@uitopia/tsconfig/nextjs.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["./*"], 7 | "@/uitopia/*": ["../../packages/ui/src/components/*"] 8 | }, 9 | "plugins": [ 10 | { 11 | "name": "next" 12 | } 13 | ], 14 | "target": "ES2017" 15 | }, 16 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 17 | "exclude": ["node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /apps/web/types/component.ts: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | 3 | export interface ComponentLoaderProps { 4 | name: string; 5 | hasReTrigger?: boolean; 6 | classNameComponentContainer?: string; 7 | } 8 | 9 | export interface ComponentDisplayProps { 10 | component: ReactNode; 11 | hasReTrigger: boolean; 12 | className?: string; 13 | reTriggerKey: number; 14 | reTrigger: () => void; 15 | name: string; 16 | } 17 | 18 | export interface ComponentPreviewProps { 19 | name: string; 20 | hasReTrigger?: boolean; 21 | classNameComponentContainer?: string; 22 | } 23 | 24 | export interface ComponentSourceProps { 25 | name: string; 26 | expandButtonTitle?: string; 27 | defaultExpanded?: boolean; 28 | maxHeight?: string; 29 | className?: string; 30 | } 31 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/2.0.0-beta.3/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "includes": [ 11 | "**", 12 | "!**/.next", 13 | "!**/.source", 14 | "!**/dist", 15 | "!**/node_modules", 16 | "!**/public" 17 | ] 18 | }, 19 | "formatter": { 20 | "enabled": true, 21 | "formatWithErrors": false, 22 | "indentStyle": "space", 23 | "indentWidth": 2, 24 | "lineEnding": "lf", 25 | "lineWidth": 80, 26 | "attributePosition": "auto" 27 | }, 28 | "assist": { "actions": { "source": { "organizeImports": "on" } } }, 29 | "linter": { 30 | "enabled": true, 31 | "domains": { 32 | "next": "recommended" 33 | }, 34 | "rules": { 35 | "recommended": true, 36 | "a11y": { 37 | "noSvgWithoutTitle": "off", 38 | "useKeyWithClickEvents": "off", 39 | "useButtonType": "off", 40 | "useAltText": "off", 41 | "useValidAnchor": "off" 42 | }, 43 | "nursery": { 44 | "useSortedClasses": { 45 | "level": "error", 46 | "fix": "safe", 47 | "options": {} 48 | } 49 | }, 50 | "style": { 51 | "noNonNullAssertion": "off", 52 | "useSelfClosingElements": "off", 53 | "useNodejsImportProtocol": "off", 54 | "useNumberNamespace": "off", 55 | "noParameterAssign": "error", 56 | "useAsConstAssertion": "error", 57 | "useDefaultParameterLast": "error", 58 | "useEnumInitializers": "error", 59 | "useConst": "error", 60 | "useSingleVarDeclarator": "error", 61 | "noUnusedTemplateLiteral": "error", 62 | "noInferrableTypes": "error", 63 | "noUselessElse": "error" 64 | }, 65 | "correctness": { 66 | "useExhaustiveDependencies": "off" 67 | }, 68 | "suspicious": { 69 | "noArrayIndexKey": "off", 70 | "noExplicitAny": "warn", 71 | "noRedeclare": "off" 72 | }, 73 | "security": { 74 | "noDangerouslySetInnerHtml": "off" 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cahyawibawa/ui-topia/121ef6e0d07db0558849e06036235349ce7dc5ad/bun.lockb -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uitopia", 3 | "version": "1.0.0", 4 | "devDependencies": { 5 | "@biomejs/biome": "^2.0.0-beta.3", 6 | "@uitopia/tsconfig": "workspace:*", 7 | "turbo": "^2.5.0" 8 | }, 9 | "engines": { 10 | "node": ">=20.14.0" 11 | }, 12 | "license": "MIT", 13 | "packageManager": "bun@1.2.12", 14 | "private": true, 15 | "scripts": { 16 | "dev": "turbo run dev", 17 | "clean": "turbo run clean", 18 | "lint": "turbo run lint", 19 | "build": "turbo run build", 20 | "format": "biome format --write .", 21 | "check": "biome check --fix ." 22 | }, 23 | "workspaces": [ 24 | "apps/*", 25 | "packages/*" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "esModuleInterop": true, 6 | "incremental": false, 7 | "isolatedModules": true, 8 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 9 | "module": "ESNext", 10 | "moduleDetection": "force", 11 | "moduleResolution": "Bundler", 12 | "noUncheckedIndexedAccess": true, 13 | "resolveJsonModule": true, 14 | "skipLibCheck": true, 15 | "strict": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/tsconfig/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/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uitopia/tsconfig", 3 | "version": "1.0.0", 4 | "private": true, 5 | "files": [ 6 | "base.json", 7 | "nextjs.json", 8 | "react-library.json" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/tsconfig/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 | "target": "ES6", 8 | "moduleResolution": "Node", 9 | "allowJs": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ui/topia", 3 | "version": "1.0.0", 4 | "description": "UI components for uitopia.vercel.app", 5 | "sideEffects": false, 6 | "main": "./dist/index.js", 7 | "module": "./dist/index.mjs", 8 | "types": "./dist/index.d.ts", 9 | "files": [ 10 | "dist" 11 | ], 12 | "scripts": { 13 | "build": "tsup", 14 | "dev": "tsup --watch", 15 | "check-types": "tsc --noEmit", 16 | "shadcn": "bun x --bun shadcn@latest add" 17 | }, 18 | "exports": { 19 | ".": { 20 | "types": "./dist/index.d.ts", 21 | "import": "./dist/index.mjs", 22 | "require": "./dist/index.js" 23 | } 24 | }, 25 | "dependencies": { 26 | "@radix-ui/react-accordion": "^1.2.2", 27 | "@radix-ui/react-alert-dialog": "^1.1.4", 28 | "@radix-ui/react-checkbox": "^1.1.5", 29 | "@radix-ui/react-collapsible": "^1.1.0", 30 | "@radix-ui/react-dialog": "^1.1.5", 31 | "@radix-ui/react-dropdown-menu": "^2.1.4", 32 | "@radix-ui/react-icons": "^1.3.0", 33 | "@radix-ui/react-label": "^2.1.0", 34 | "@radix-ui/react-scroll-area": "^1.2.1", 35 | "@radix-ui/react-separator": "^1.1.3", 36 | "@radix-ui/react-slot": "^1.1.1", 37 | "@radix-ui/react-tabs": "^1.1.0", 38 | "@radix-ui/react-toggle-group": "^1.1.3", 39 | "@radix-ui/react-tooltip": "^1.1.4", 40 | "@remixicon/react": "^4.5.0", 41 | "class-variance-authority": "^0.7.0", 42 | "clsx": "^2.1.0", 43 | "lucide-react": "^0.468.0", 44 | "motion": "^11.15.0", 45 | "react": "^19.0.0", 46 | "react-dom": "^19.0.0", 47 | "react-icons": "^5.2.1", 48 | "react-resizable-panels": "^2.1.7", 49 | "tailwind-merge": "^2.2.0", 50 | "tailwindcss-animate": "^1.0.7", 51 | "usehooks-ts": "^3.1.0", 52 | "vaul": "^1.1.2", 53 | "zod": "^3.23.8" 54 | }, 55 | "devDependencies": { 56 | "@tailwindcss/postcss": "^4.0.3", 57 | "@types/react": "^19.0.4", 58 | "@types/react-dom": "^19.0.2", 59 | "@uitopia/tsconfig": "workspace:*", 60 | "postcss": "^8.5.1", 61 | "tailwindcss": "^4.0.3", 62 | "tsup": "^8.3.6", 63 | "typescript": "^5.7.3" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/ui/src/components/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as AccordionPrimitive from "@radix-ui/react-accordion"; 4 | import { ChevronDown } from "lucide-react"; 5 | import * as React from "react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Accordion = AccordionPrimitive.Root; 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )); 21 | AccordionItem.displayName = "AccordionItem"; 22 | 23 | const AccordionTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | svg]:rotate-180", 32 | className, 33 | )} 34 | {...props} 35 | > 36 | {children} 37 | 38 | 39 | 40 | )); 41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; 42 | 43 | const AccordionContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, children, ...props }, ref) => ( 47 | 52 |
{children}
53 |
54 | )); 55 | 56 | AccordionContent.displayName = AccordionPrimitive.Content.displayName; 57 | 58 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; 59 | -------------------------------------------------------------------------------- /packages/ui/src/components/badge.tsx: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from "class-variance-authority"; 2 | import type * as React from "react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-normal transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | }, 24 | ); 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ); 34 | } 35 | 36 | export { Badge, badgeVariants }; 37 | -------------------------------------------------------------------------------- /packages/ui/src/components/button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "@radix-ui/react-slot"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | import * as React from "react"; 4 | 5 | import { cn } from "@/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-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90 ring-1 ring-inset ring-white/20 ring-offset-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | }, 35 | ); 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean; 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button"; 46 | return ( 47 | 52 | ); 53 | }, 54 | ); 55 | Button.displayName = "Button"; 56 | 57 | export { Button, buttonVariants }; 58 | -------------------------------------------------------------------------------- /packages/ui/src/components/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )); 18 | Card.displayName = "Card"; 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )); 30 | CardHeader.displayName = "CardHeader"; 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
41 | )); 42 | CardTitle.displayName = "CardTitle"; 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLDivElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |
53 | )); 54 | CardDescription.displayName = "CardDescription"; 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |
61 | )); 62 | CardContent.displayName = "CardContent"; 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )); 74 | CardFooter.displayName = "CardFooter"; 75 | 76 | export { 77 | Card, 78 | CardHeader, 79 | CardFooter, 80 | CardTitle, 81 | CardDescription, 82 | CardContent, 83 | }; 84 | -------------------------------------------------------------------------------- /packages/ui/src/components/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 4 | import { Check } from "lucide-react"; 5 | import * as React from "react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 24 | 25 | 26 | 27 | )); 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 29 | 30 | export { Checkbox }; 31 | -------------------------------------------------------------------------------- /packages/ui/src/components/collapsible.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; 4 | 5 | const Collapsible = CollapsiblePrimitive.Root; 6 | 7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; 8 | 9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; 10 | 11 | export { Collapsible, CollapsibleContent, CollapsibleTrigger }; 12 | -------------------------------------------------------------------------------- /packages/ui/src/components/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | const Input = React.forwardRef>( 5 | ({ className, type, ...props }, ref) => { 6 | return ( 7 | 20 | ); 21 | }, 22 | ); 23 | Input.displayName = "Input"; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /packages/ui/src/components/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as LabelPrimitive from "@radix-ui/react-label"; 4 | import { cva, type VariantProps } from "class-variance-authority"; 5 | import * as React from "react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 11 | ); 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )); 24 | Label.displayName = LabelPrimitive.Root.displayName; 25 | 26 | export { Label }; 27 | -------------------------------------------------------------------------------- /packages/ui/src/components/page-header.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | function PageHeader({ 4 | className, 5 | children, 6 | ...props 7 | }: React.HTMLAttributes) { 8 | return ( 9 |
10 |
11 |
12 | {children} 13 |
14 |
15 |
16 | ); 17 | } 18 | 19 | function PageHeaderHeading({ 20 | className, 21 | ...props 22 | }: React.HTMLAttributes) { 23 | return ( 24 |

31 | ); 32 | } 33 | 34 | function PageHeaderDescription({ 35 | className, 36 | ...props 37 | }: React.HTMLAttributes) { 38 | return ( 39 |

46 | ); 47 | } 48 | 49 | function PageActions({ 50 | className, 51 | ...props 52 | }: React.HTMLAttributes) { 53 | return ( 54 |

61 | ); 62 | } 63 | 64 | export { PageActions, PageHeader, PageHeaderDescription, PageHeaderHeading }; 65 | -------------------------------------------------------------------------------- /packages/ui/src/components/references.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import type React from "react"; 3 | import { cn } from "@/lib/utils"; 4 | import { ExternalLinkIcon } from "../../../../apps/web/registry/components/icons"; 5 | import { badgeVariants } from "./badge"; 6 | 7 | interface LinkItem { 8 | link: string; 9 | label: string; 10 | } 11 | 12 | export function References({ children }: React.PropsWithChildren) { 13 | return
{children}
; 14 | } 15 | 16 | export function Reference({ label, link }: LinkItem) { 17 | return ( 18 | 27 | {label} 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /packages/ui/src/components/resizable.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { GripVertical } from "lucide-react"; 4 | import * as ResizablePrimitive from "react-resizable-panels"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const ResizablePanelGroup = ({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) => ( 12 | 19 | ); 20 | 21 | const ResizablePanel = ResizablePrimitive.Panel; 22 | 23 | const ResizableHandle = ({ 24 | withHandle, 25 | className, 26 | ...props 27 | }: React.ComponentProps & { 28 | withHandle?: boolean; 29 | }) => ( 30 | div]:rotate-90", 33 | className, 34 | )} 35 | {...props} 36 | > 37 | {withHandle && ( 38 |
39 | 40 |
41 | )} 42 |
43 | ); 44 | 45 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; 46 | -------------------------------------------------------------------------------- /packages/ui/src/components/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; 4 | import * as React from "react"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | )); 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | 43 | 44 | 45 | )); 46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; 47 | 48 | export { ScrollArea, ScrollBar }; 49 | -------------------------------------------------------------------------------- /packages/ui/src/components/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 4 | import * as React from "react"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref, 15 | ) => ( 16 | 27 | ), 28 | ); 29 | Separator.displayName = SeparatorPrimitive.Root.displayName; 30 | 31 | export { Separator }; 32 | -------------------------------------------------------------------------------- /packages/ui/src/components/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ); 13 | } 14 | 15 | export { Skeleton }; 16 | -------------------------------------------------------------------------------- /packages/ui/src/components/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as TabsPrimitive from "@radix-ui/react-tabs"; 4 | import * as React from "react"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Tabs = TabsPrimitive.Root; 9 | 10 | const TabsList = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )); 23 | TabsList.displayName = TabsPrimitive.List.displayName; 24 | 25 | const TabsTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 37 | )); 38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; 39 | 40 | const TabsContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | )); 53 | TabsContent.displayName = TabsPrimitive.Content.displayName; 54 | 55 | export { Tabs, TabsList, TabsTrigger, TabsContent }; 56 | -------------------------------------------------------------------------------- /packages/ui/src/components/toggle-group.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"; 4 | import type { VariantProps } from "class-variance-authority"; 5 | import * as React from "react"; 6 | 7 | import { toggleVariants } from "@/components/toggle"; 8 | import { cn } from "@/lib/utils"; 9 | 10 | const ToggleGroupContext = React.createContext< 11 | VariantProps 12 | >({ 13 | size: "default", 14 | variant: "default", 15 | }); 16 | 17 | const ToggleGroup = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef & 20 | VariantProps 21 | >(({ className, variant, size, children, ...props }, ref) => ( 22 | 27 | 28 | {children} 29 | 30 | 31 | )); 32 | 33 | ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName; 34 | 35 | const ToggleGroupItem = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef & 38 | VariantProps 39 | >(({ className, children, variant, size, ...props }, ref) => { 40 | const context = React.useContext(ToggleGroupContext); 41 | 42 | return ( 43 | 54 | {children} 55 | 56 | ); 57 | }); 58 | 59 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName; 60 | 61 | export { ToggleGroup, ToggleGroupItem }; 62 | -------------------------------------------------------------------------------- /packages/ui/src/components/toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as TogglePrimitive from "@radix-ui/react-toggle"; 4 | import { cva, type VariantProps } from "class-variance-authority"; 5 | import * as React from "react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const toggleVariants = cva( 10 | "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 11 | { 12 | variants: { 13 | variant: { 14 | default: "bg-transparent", 15 | outline: 16 | "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground", 17 | }, 18 | size: { 19 | default: "h-9 px-2 min-w-9", 20 | sm: "h-8 px-1.5 min-w-8", 21 | lg: "h-10 px-2.5 min-w-10", 22 | }, 23 | }, 24 | defaultVariants: { 25 | variant: "default", 26 | size: "default", 27 | }, 28 | }, 29 | ); 30 | 31 | const Toggle = React.forwardRef< 32 | React.ElementRef, 33 | React.ComponentPropsWithoutRef & 34 | VariantProps 35 | >(({ className, variant, size, ...props }, ref) => ( 36 | 41 | )); 42 | 43 | Toggle.displayName = TogglePrimitive.Root.displayName; 44 | 45 | export { Toggle, toggleVariants }; 46 | -------------------------------------------------------------------------------- /packages/ui/src/components/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 4 | import type * as React from "react"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function TooltipProvider({ 9 | delayDuration = 0, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 18 | ); 19 | } 20 | 21 | function Tooltip({ 22 | ...props 23 | }: React.ComponentProps) { 24 | return ( 25 | 26 | 27 | 28 | ); 29 | } 30 | 31 | function TooltipTrigger({ 32 | ...props 33 | }: React.ComponentProps) { 34 | return ; 35 | } 36 | 37 | function TooltipContent({ 38 | className, 39 | sideOffset = 0, 40 | children, 41 | ...props 42 | }: React.ComponentProps) { 43 | return ( 44 | 45 | 54 | {children} 55 | 56 | 57 | 58 | ); 59 | } 60 | 61 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; 62 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/use-mobile.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const MOBILE_BREAKPOINT = 768; 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState( 7 | undefined, 8 | ); 9 | 10 | React.useEffect(() => { 11 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); 12 | const onChange = () => { 13 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 14 | }; 15 | mql.addEventListener("change", onChange); 16 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 17 | return () => mql.removeEventListener("change", onChange); 18 | }, []); 19 | 20 | return !!isMobile; 21 | } 22 | -------------------------------------------------------------------------------- /packages/ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | // base components shadcn/ui 2 | export * from "./components/accordion"; 3 | export * from "./components/alert-dialog"; 4 | export * from "./components/badge"; 5 | export * from "./components/button"; 6 | export * from "./components/card"; 7 | export * from "./components/checkbox"; 8 | export * from "./components/collapsible"; 9 | export * from "./components/dialog"; 10 | export * from "./components/dropdown-menu"; 11 | export * from "./components/input"; 12 | export * from "./components/label"; 13 | // misc 14 | export * from "./components/page-header"; 15 | export * from "./components/references"; 16 | export * from "./components/resizable"; 17 | export * from "./components/scroll-area"; 18 | export * from "./components/separator"; 19 | export * from "./components/sheet"; 20 | export * from "./components/sidebar"; 21 | export * from "./components/skeleton"; 22 | export * from "./components/tabs"; 23 | export * from "./components/toggle"; 24 | export * from "./components/toggle-group"; 25 | export * from "./components/tooltip"; 26 | -------------------------------------------------------------------------------- /packages/ui/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@uitopia/tsconfig/react-library.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["./src/*"], 8 | "@/components/*": ["./src/components/*"], 9 | "@/examples/*": ["src/components/elements/texts/*"], 10 | "@/lib/*": ["./src/lib/*"] 11 | } 12 | }, 13 | "exclude": ["dist", "node_modules"], 14 | "include": [".", "../../apps/web/registry/ui/sidebar.tsx"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/ui/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.tsx"], 5 | format: ["cjs", "esm"], 6 | dts: true, 7 | splitting: false, 8 | sourcemap: true, 9 | clean: true, 10 | external: ["react", "react-dom"], 11 | esbuildOptions(options) { 12 | options.resolveExtensions = [".tsx", ".ts", ".jsx", ".js"]; 13 | options.alias = { 14 | "@": "./src", 15 | "@/components": "./src/components", 16 | "@/lib": "./src/lib", 17 | "@/hooks": "./src/hooks", 18 | }; 19 | options.banner = { 20 | js: '"use client"', 21 | }; 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@uitopia/tsconfig/base.json" 3 | } 4 | -------------------------------------------------------------------------------- /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": ["dist/**", ".next/**", "!.next/cache/**", ".source/**"] 9 | }, 10 | "lint": { 11 | "dependsOn": ["^lint"] 12 | }, 13 | "dev": { 14 | "cache": false, 15 | "persistent": true 16 | } 17 | } 18 | } 19 | --------------------------------------------------------------------------------