├── bun.lockb ├── src ├── app │ ├── favicon.ico │ ├── not-found.tsx │ ├── dark-mode │ │ └── page.tsx │ ├── sitemap.ts │ ├── layout.tsx │ ├── changelog │ │ └── page.tsx │ ├── globals.css │ ├── installation │ │ └── page.tsx │ ├── components │ │ ├── info-card │ │ │ └── page.tsx │ │ └── unsave-popup │ │ │ └── page.tsx │ └── page.tsx ├── lib │ └── utils.ts ├── components │ ├── ui │ │ ├── skeleton.tsx │ │ ├── label.tsx │ │ ├── input.tsx │ │ ├── separator.tsx │ │ ├── tooltip.tsx │ │ ├── hover-card.tsx │ │ ├── scroll-area.tsx │ │ ├── button.tsx │ │ ├── accordion.tsx │ │ ├── prop-table.tsx │ │ ├── code-snippet.tsx │ │ ├── breadcrumb.tsx │ │ ├── steps.tsx │ │ ├── dialog.tsx │ │ ├── code-block.tsx │ │ ├── sheet.tsx │ │ ├── package-manager-tabs.tsx │ │ ├── command.tsx │ │ └── dropdown-menu.tsx │ ├── theme-proider.tsx │ ├── mode-toggle.tsx │ ├── open-in-v0.tsx │ ├── hover-card.tsx │ ├── root.tsx │ ├── navigation-menu.tsx │ ├── pqoqubbw │ │ ├── search.tsx │ │ ├── download.tsx │ │ ├── moon.tsx │ │ ├── book-text.tsx │ │ └── clock.tsx │ ├── demo-ui │ │ ├── info-card │ │ │ ├── preview-code-tabs.tsx │ │ │ └── installation-tabs.tsx │ │ └── unsave-popup │ │ │ ├── code.tsx │ │ │ ├── demo-code.tsx │ │ │ ├── installation.tsx │ │ │ └── demo.tsx │ ├── command-menu.tsx │ ├── breadcrumb-nav.tsx │ ├── tweet-card.tsx │ └── dark-mode-content.tsx └── hooks │ └── use-mobile.tsx ├── public ├── icons │ ├── myicon.png │ ├── twitter.svg │ ├── remix.svg │ ├── vite.svg │ ├── astro.svg │ └── next.svg ├── readme-cover.jpg ├── vercel.svg ├── robots.txt ├── window.svg ├── file.svg ├── globe.svg ├── twitter-tick.svg ├── next.svg └── r │ ├── unsave-popup-demo.json │ ├── info-card-demo.json │ └── unsave-popup.json ├── vercel.json ├── postcss.config.mjs ├── next-env.d.ts ├── next.config.ts ├── eslint.config.mjs ├── components.json ├── .gitignore ├── tsconfig.json ├── README.md ├── LICENSE ├── CONTRIBUTING.md ├── package.json ├── registry.json ├── tailwind.config.ts └── registry └── kl-ui ├── unsave-popup ├── unsave-popup-demo.tsx └── unsave-popup.tsx └── info-card └── info-card-demo.tsx /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarrixLee/KL-ui/HEAD/bun.lockb -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarrixLee/KL-ui/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /public/icons/myicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarrixLee/KL-ui/HEAD/public/icons/myicon.png -------------------------------------------------------------------------------- /public/readme-cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KarrixLee/KL-ui/HEAD/public/readme-cover.jpg -------------------------------------------------------------------------------- /src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | export default function NotFound() { 4 | redirect("/"); 5 | } 6 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/r/:path", 5 | "destination": "/r/:path.json" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | Allow: /components/unsave-popup 4 | Allow: /components/info-card 5 | Allow: /documentation 6 | 7 | Sitemap: https://karrix.dev/sitemap.xml -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | images: { 5 | remotePatterns: [ 6 | { 7 | hostname: "github.com", 8 | }, 9 | ], 10 | }, 11 | }; 12 | 13 | export default nextConfig; 14 | -------------------------------------------------------------------------------- /public/icons/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/components/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 | -------------------------------------------------------------------------------- /src/components/theme-proider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 | 6 | export function ThemeProvider({ 7 | children, 8 | ...props 9 | }: React.ComponentProps) { 10 | return {children}; 11 | } 12 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /public/icons/remix.svg: -------------------------------------------------------------------------------- 1 | Remix -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .pnpm-debug.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # turbo 33 | .turbo 34 | 35 | .contentlayer 36 | tsconfig.tsbuildinfo 37 | 38 | # ide 39 | .idea 40 | .fleet 41 | .vscode -------------------------------------------------------------------------------- /src/hooks/use-mobile.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | const MOBILE_BREAKPOINT = 768 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState(undefined) 7 | 8 | React.useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 | } 13 | mql.addEventListener("change", onChange) 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 | return () => mql.removeEventListener("change", onChange) 16 | }, []) 17 | 18 | return !!isMobile 19 | } 20 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 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 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"], 23 | "@/registry/*": ["./registry/*"] 24 | } 25 | }, 26 | "include": [ 27 | "next-env.d.ts", 28 | "**/*.ts", 29 | "**/*.tsx", 30 | ".next/types/**/*.ts", 31 | "registry/**/*.ts", 32 | "registry/**/*.tsx" 33 | ], 34 | "exclude": ["node_modules"] 35 | } 36 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ) 18 | } 19 | ) 20 | Input.displayName = "Input" 21 | 22 | export { Input } 23 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 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 | -------------------------------------------------------------------------------- /public/icons/vite.svg: -------------------------------------------------------------------------------- 1 | Vite -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/twitter-tick.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KL UI 2 | 3 | Beautiful animated UI components and effects built with shadcn/ui and Motion. 4 | 5 | **Demo - [karrix.dev](https://karrix.dev)** 6 | 7 | ![KL UI](public/readme-cover.jpg) 8 | 9 | ## Overview 10 | 11 | Open source animated UI components and effects built with [shadcn/ui](https://ui.shadcn.com/) and [Motion](https://motion.dev/). 12 | Feel free to use it in your projects, and share your feedback. Hope you like it! 13 | 14 | ## Contributing 15 | 16 | Contributions are welcome! Check out our [contribution guidelines](CONTRIBUTING.md) for details on how to get started. 17 | 18 | ## License 19 | 20 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 21 | 22 | ## Credits 23 | 24 | Special thanks to: 25 | - [@pqoqubbw](https://github.com/pqoqubbw) for the amazing animated icon library 26 | - [@aidenybai](https://github.com/aidenybai) for React Scan, which helped optimize the components 27 | 28 | ## Questions? 29 | 30 | Feel free to reach out: 31 | - Twitter: [@karrixthediv](https://twitter.com/karrixthediv) 32 | - Email: karrixlee1231@gmail.com -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 KarrixLee 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 | -------------------------------------------------------------------------------- /src/app/dark-mode/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next"; 2 | import { DarkModeContent } from "@/components/dark-mode-content"; 3 | 4 | export const metadata: Metadata = { 5 | title: "Dark Mode", 6 | description: "Dark mode support for KL UI.", 7 | keywords: [ 8 | "KL UI", 9 | "Dark Mode", 10 | "Dark Theme", 11 | "Dark Mode Support", 12 | "Dark Mode Toggle", 13 | "Releases", 14 | "Shadcn UI", 15 | "Motion", 16 | "Framer Motion", 17 | "React", 18 | "Next.js", 19 | "UI Library", 20 | "Components", 21 | "TypeScript", 22 | "Component Library", 23 | ], 24 | }; 25 | 26 | export default function DarkModePage() { 27 | return ( 28 |
29 | {/* Header Section */} 30 |
31 |

32 | Dark Mode 33 |

34 |

35 | Using Dark Mode for the components. 36 |

37 |
38 | 39 | {/* Mystery Section */} 40 | 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from "next"; 2 | 3 | export default function sitemap(): MetadataRoute.Sitemap { 4 | const baseUrl = "https://karrix.dev"; 5 | 6 | return [ 7 | { 8 | url: baseUrl, 9 | lastModified: new Date(), 10 | changeFrequency: "monthly", 11 | priority: 1, 12 | }, 13 | { 14 | url: `${baseUrl}/components/unsave-popup`, 15 | lastModified: new Date(), 16 | changeFrequency: "monthly", 17 | priority: 0.9, 18 | }, 19 | { 20 | url: `${baseUrl}/components/info-card`, 21 | lastModified: new Date(), 22 | changeFrequency: "monthly", 23 | priority: 0.9, 24 | }, 25 | { 26 | url: `${baseUrl}/installation`, 27 | lastModified: new Date(), 28 | changeFrequency: "monthly", 29 | priority: 0.8, 30 | }, 31 | { 32 | url: `${baseUrl}/changelog`, 33 | lastModified: new Date("2024-02-17"), 34 | changeFrequency: "weekly", 35 | priority: 0.7, 36 | }, 37 | { 38 | url: `${baseUrl}/dark-mode`, 39 | lastModified: new Date(), 40 | changeFrequency: "monthly", 41 | priority: 0.6, 42 | }, 43 | ]; 44 | } 45 | -------------------------------------------------------------------------------- /public/icons/astro.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const TooltipProvider = TooltipPrimitive.Provider 9 | 10 | const Tooltip = TooltipPrimitive.Root 11 | 12 | const TooltipTrigger = TooltipPrimitive.Trigger 13 | 14 | const TooltipContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, sideOffset = 4, ...props }, ref) => ( 18 | 27 | )) 28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName 29 | 30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } 31 | -------------------------------------------------------------------------------- /src/components/mode-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { Moon, Sun } from "lucide-react"; 5 | import { useTheme } from "next-themes"; 6 | import { Button } from "@/components/ui/button"; 7 | import { 8 | Tooltip, 9 | TooltipContent, 10 | TooltipProvider, 11 | TooltipTrigger, 12 | } from "@/components/ui/tooltip"; 13 | 14 | export function ModeToggle() { 15 | const { theme, setTheme } = useTheme(); 16 | 17 | return ( 18 | 19 | 20 | 21 | 31 | 32 | 33 |

Toggle theme

34 |
35 |
36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/components/ui/hover-card.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const HoverCard = HoverCardPrimitive.Root 9 | 10 | const HoverCardTrigger = HoverCardPrimitive.Trigger 11 | 12 | const HoverCardContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 26 | )) 27 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName 28 | 29 | export { HoverCard, HoverCardTrigger, HoverCardContent } 30 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to KL UI 2 | 3 | Thanks for taking the time to help improve KL UI! 4 | 5 | ## Reporting Issues 6 | 7 | If you find a bug or have a performance issue, please help by: 8 | 9 | 1. Checking if the issue already exists in [GitHub Issues](https://github.com/karrixlee/kl-ui/issues) 10 | 2. If not, open a new issue with: 11 | - A clear description of the problem 12 | - Steps to reproduce 13 | - Expected vs actual behavior 14 | - Your environment (browser, OS, etc.) 15 | - Screenshots if applicable 16 | 17 | ## Development Setup 18 | 19 | If you'd like to help fix bugs, here's how to get started: 20 | 21 | 1. Clone the repository 22 | 2. Install dependencies: 23 | ```bash 24 | bun install 25 | # or 26 | npm install 27 | # or 28 | pnpm install 29 | # or 30 | yarn install 31 | ``` 32 | 33 | 3. Start the development server: 34 | ```bash 35 | bun dev 36 | # or 37 | npm run dev 38 | # or 39 | pnpm dev 40 | # or 41 | yarn dev 42 | ``` 43 | 44 | 4. Open [http://localhost:3000](http://localhost:3000) to see the result 45 | 46 | ## Questions? 47 | 48 | If you run into any problems or have questions, feel free to: 49 | - Open an issue 50 | - Reach out on [Twitter](https://twitter.com/karrixlee) 51 | - Send an [email](mailto:karrixlee1231@gmail.com) 52 | 53 | Thanks for helping make KL UI better! -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "registry:build": "shadcn build" 11 | }, 12 | "dependencies": { 13 | "@radix-ui/react-accordion": "^1.2.3", 14 | "@radix-ui/react-dialog": "^1.1.6", 15 | "@radix-ui/react-dropdown-menu": "^2.1.6", 16 | "@radix-ui/react-hover-card": "^1.1.6", 17 | "@radix-ui/react-label": "^2.1.2", 18 | "@radix-ui/react-scroll-area": "^1.2.3", 19 | "@radix-ui/react-separator": "^1.1.2", 20 | "@radix-ui/react-slot": "^1.1.2", 21 | "@radix-ui/react-tooltip": "^1.1.8", 22 | "class-variance-authority": "^0.7.1", 23 | "clsx": "^2.1.1", 24 | "cmdk": "1.0.0", 25 | "lucide-react": "^0.475.0", 26 | "motion": "^12.4.3", 27 | "next": "15.1.7", 28 | "next-themes": "^0.4.4", 29 | "react": "^19.0.0", 30 | "react-dom": "^19.0.0", 31 | "shadcn": "^2.4.0-canary.6", 32 | "tailwind-merge": "^3.0.1", 33 | "tailwindcss-animate": "^1.0.7" 34 | }, 35 | "devDependencies": { 36 | "@eslint/eslintrc": "^3", 37 | "@types/node": "^20", 38 | "@types/react": "^19", 39 | "@types/react-dom": "^19", 40 | "eslint": "^9", 41 | "eslint-config-next": "15.1.7", 42 | "postcss": "^8", 43 | "shiki": "^2.3.2", 44 | "tailwindcss": "^3.4.1", 45 | "typescript": "^5" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/open-in-v0.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | 3 | export function OpenInV0Button({ url }: { url: string }) { 4 | return ( 5 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /public/icons/next.svg: -------------------------------------------------------------------------------- 1 | Next.js -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | import { RootPage } from "@/components/root"; 5 | import { ThemeProvider } from "@/components/theme-proider"; 6 | 7 | const geistSans = Geist({ 8 | variable: "--font-geist-sans", 9 | subsets: ["latin"], 10 | }); 11 | 12 | const geistMono = Geist_Mono({ 13 | variable: "--font-geist-mono", 14 | subsets: ["latin"], 15 | }); 16 | 17 | export const metadata: Metadata = { 18 | title: { 19 | default: "KL UI - Modern React Component Library", 20 | template: "%s - KL UI", 21 | }, 22 | description: 23 | "Animated UI components and effects with love. Build with shadcn/ui and Motion.", 24 | keywords: [ 25 | "React", 26 | "Next.js", 27 | "Shadcn UI", 28 | "NextUI", 29 | "UI Library", 30 | "Components", 31 | "TypeScript", 32 | "Component Library", 33 | "Motion", 34 | "Framer Motion", 35 | "Changelog", 36 | ], 37 | }; 38 | 39 | export default function RootLayout({ 40 | children, 41 | }: { 42 | children: React.ReactNode; 43 | }) { 44 | return ( 45 | 46 | 49 | 50 |
51 | {children} 52 |
53 |
54 | 55 | 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /src/components/hover-card.tsx: -------------------------------------------------------------------------------- 1 | import { Link as LinkIcon } from "lucide-react"; 2 | import Image from "next/image"; 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | HoverCard, 6 | HoverCardContent, 7 | HoverCardTrigger, 8 | } from "@/components/ui/hover-card"; 9 | import Link from "next/link"; 10 | 11 | export function HoverCardUser({ 12 | name, 13 | username, 14 | iconUrl, 15 | description, 16 | }: { 17 | name: string; 18 | username: string; 19 | iconUrl: string; 20 | description: string; 21 | }) { 22 | return ( 23 | 24 | 25 | 28 | 29 | 30 |
31 | {name} 38 |
39 | 40 |

{username}

41 | 42 |

{description}

43 |
44 | {" "} 45 | 46 | x.com/{username} 47 | 48 |
49 |
50 |
51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 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 | -------------------------------------------------------------------------------- /src/components/root.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { MySidebar, SidebarContext } from "@/components/sidebar"; 4 | import { BreadcrumbNav } from "@/components/breadcrumb-nav"; 5 | import { ScrollArea } from "@/components/ui/scroll-area"; 6 | import { Button } from "@/components/ui/button"; 7 | import { Menu } from "lucide-react"; 8 | import { useState } from "react"; 9 | import { cn } from "@/lib/utils"; 10 | 11 | export function RootPage({ children }: { children: React.ReactNode }) { 12 | const [openSideBar, setOpenSideBar] = useState(false); 13 | 14 | return ( 15 | 16 |
17 | 18 |
26 |
27 |
28 | 36 | 37 |
38 | 39 | {children} 40 | 41 |
42 |
43 |
44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 5 | import { ChevronDown } from "lucide-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 | -------------------------------------------------------------------------------- /registry.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry.json", 3 | "name": "karrix", 4 | "homepage": "https://karrix.dev", 5 | "items": [ 6 | { 7 | "name": "info-card", 8 | "type": "registry:block", 9 | "description": "Information cards can serve as callout cards, banners, toast notifications, or announcement boxes to deliver news, updates, and alerts to your users.", 10 | "dependencies": ["motion"], 11 | "files": [ 12 | { 13 | "name": "info-card.tsx", 14 | "path": "registry/kl-ui/info-card/info-card.tsx", 15 | "type": "registry:component", 16 | "target": "components/kl-ui/info-card.tsx" 17 | } 18 | ] 19 | }, 20 | { 21 | "name": "info-card-demo", 22 | "type": "registry:example", 23 | "description": "Example of the kl-ui info-card component.", 24 | "registryDependencies": ["https://karrix.dev/r/info-card"], 25 | "files": [ 26 | { 27 | "name": "info-card-demo.tsx", 28 | "path": "registry/kl-ui/info-card/info-card-demo.tsx", 29 | "type": "registry:example", 30 | "target": "components/info-card-demo.tsx" 31 | } 32 | ] 33 | }, 34 | { 35 | "name": "unsave-popup", 36 | "type": "registry:block", 37 | "description": "A popup component to confirm before discarding changes. Inspired by Discord, it will shake when the user tries to leave without saving.", 38 | "dependencies": ["motion"], 39 | "registryDependencies": ["button"], 40 | "files": [ 41 | { 42 | "name": "unsave-popup.tsx", 43 | "path": "registry/kl-ui/unsave-popup/unsave-popup.tsx", 44 | "type": "registry:component", 45 | "target": "components/kl-ui/unsave-popup.tsx" 46 | } 47 | ] 48 | }, 49 | { 50 | "name": "unsave-popup-demo", 51 | "type": "registry:example", 52 | "description": "Example of the kl-ui unsave-popup component.", 53 | "registryDependencies": ["https://karrix.dev/r/unsave-popup"], 54 | "files": [ 55 | { 56 | "name": "unsave-popup-demo.tsx", 57 | "path": "registry/kl-ui/unsave-popup/unsave-popup-demo.tsx", 58 | "type": "registry:example", 59 | "target": "components/unsave-popup-demo.tsx" 60 | } 61 | ] 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /src/components/ui/prop-table.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { motion, AnimatePresence } from "framer-motion"; 5 | import { cn } from "@/lib/utils"; 6 | 7 | export interface PropDefinition { 8 | prop: string; 9 | type: string; 10 | default?: string; 11 | description: string; 12 | } 13 | 14 | interface PropTableProps { 15 | title: string; 16 | props: PropDefinition[]; 17 | className?: string; 18 | } 19 | 20 | export function PropTable({ title, props, className }: PropTableProps) { 21 | const [showAllProps, setShowAllProps] = useState(true); 22 | 23 | return ( 24 |
25 |
setShowAllProps((prev) => !prev)} 28 | > 29 |

{title}

30 |
31 | 32 | {showAllProps && ( 33 | 40 |
41 | {props.map((prop) => ( 42 |
43 |
44 | 45 | {prop.prop} 46 | 47 | 48 | {prop.type} 49 | 50 |
51 | {prop.default && ( 52 |
53 | Default: {prop.default} 54 |
55 | )} 56 |

57 | {prop.description} 58 |

59 |
60 | ))} 61 |
62 |
63 | )} 64 |
65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /src/components/ui/code-snippet.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { motion, AnimatePresence } from "framer-motion"; 5 | import { Button } from "./button"; 6 | import { Copy, Check } from "lucide-react"; 7 | import { cn } from "@/lib/utils"; 8 | 9 | interface CodeSnippetProps { 10 | code: string; 11 | className?: string; 12 | layoutId?: string; 13 | } 14 | 15 | export function CodeSnippet({ code, className, layoutId }: CodeSnippetProps) { 16 | const [copied, setCopied] = React.useState(false); 17 | 18 | const handleCopy = React.useCallback(async () => { 19 | try { 20 | await navigator.clipboard.writeText(code); 21 | setCopied(true); 22 | setTimeout(() => setCopied(false), 2000); 23 | } catch (err) { 24 | console.error("Failed to copy:", err); 25 | } 26 | }, [code]); 27 | 28 | return ( 29 | 36 |
37 |         {code}
38 |       
39 | 40 | 70 |
71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/components/navigation-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Link from "next/link"; 4 | import { useEffect, useState } from "react"; 5 | import { cn } from "@/lib/utils"; 6 | 7 | export function NavigationMenu() { 8 | const [activeSection, setActiveSection] = useState("playground"); 9 | const navigationItems = ["playground", "installation", "props"] as const; 10 | 11 | useEffect(() => { 12 | const observer = new IntersectionObserver( 13 | (entries) => { 14 | entries.forEach((entry) => { 15 | if (entry.isIntersecting && entry.intersectionRatio > 0.4) { 16 | setActiveSection(entry.target.id); 17 | } 18 | }); 19 | }, 20 | { 21 | threshold: [0, 0.2, 0.4, 0.6, 0.8, 1.0], 22 | rootMargin: "-10% 0px -70% 0px", 23 | } 24 | ); 25 | 26 | const sections = ["playground", "installation", "props"] 27 | .map((id) => document.getElementById(id)) 28 | .filter((section): section is HTMLElement => section !== null); 29 | 30 | sections.forEach((section) => observer.observe(section)); 31 | return () => sections.forEach((section) => observer.unobserve(section)); 32 | }, []); 33 | 34 | const handleClick = (e: React.MouseEvent, id: string) => { 35 | e.preventDefault(); 36 | document.getElementById(id)?.scrollIntoView({ 37 | behavior: "smooth", 38 | block: "start", 39 | }); 40 | }; 41 | 42 | const getDisplayText = (id: string) => 43 | id === "props" 44 | ? "Props and Usage" 45 | : id.charAt(0).toUpperCase() + id.slice(1); 46 | 47 | const isActive = (id: string) => 48 | id === "props" 49 | ? activeSection === "props" || activeSection === "usage" 50 | : activeSection === id; 51 | 52 | return ( 53 | 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /src/app/changelog/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next"; 2 | 3 | export const metadata: Metadata = { 4 | title: "Changelog", 5 | description: "Latest updates and improvements to KL UI.", 6 | keywords: [ 7 | "KL UI", 8 | "Changelog", 9 | "Updates", 10 | "Improvements", 11 | "Releases", 12 | "Shadcn UI", 13 | "Motion", 14 | "Framer Motion", 15 | "React", 16 | "Next.js", 17 | "UI Library", 18 | "Components", 19 | "TypeScript", 20 | "Component Library", 21 | ], 22 | }; 23 | 24 | interface ChangelogEntry { 25 | version: string; 26 | date: string; 27 | title: string; 28 | changes: string[]; 29 | } 30 | 31 | const changelogData: ChangelogEntry[] = [ 32 | { 33 | version: "v0.1.1 - beta", 34 | date: "23 Feb 2025", 35 | title: "Dark mode support", 36 | changes: ["Added dark mode support to the components"], 37 | }, 38 | { 39 | version: "v0.1.0 - beta", 40 | date: "17 Feb 2025", 41 | title: "Initial beta release", 42 | changes: ["Added Info Card component", "Added Unsave Popup component"], 43 | }, 44 | ]; 45 | 46 | function VersionEntry({ version, date, title, changes }: ChangelogEntry) { 47 | return ( 48 |
49 |
50 |
51 |
52 |

{version}

53 | 54 | {date} 55 | 56 |
57 |
58 |

{title}

59 |
    60 | {changes.map((change, index) => ( 61 |
  • {change}
  • 62 | ))} 63 |
64 |
65 |
66 |
67 | ); 68 | } 69 | 70 | export default function ChangelogPage() { 71 | return ( 72 |
73 |
74 |

75 | Changelog 76 |

77 |

78 | Latest updates and improvements to KL UI. 79 |

80 |
81 | 82 |
83 | {changelogData.map((entry, index) => ( 84 | 85 | ))} 86 |
87 |
88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /src/components/pqoqubbw/search.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { motion, useAnimation } from "motion/react"; 4 | import type { HTMLAttributes } from "react"; 5 | import { forwardRef, useCallback, useImperativeHandle, useRef } from "react"; 6 | 7 | export interface SearchIconHandle { 8 | startAnimation: () => void; 9 | stopAnimation: () => void; 10 | } 11 | 12 | const SearchIcon = forwardRef>( 13 | ({ onMouseEnter, onMouseLeave, ...props }, ref) => { 14 | const controls = useAnimation(); 15 | const isControlledRef = useRef(false); 16 | 17 | useImperativeHandle(ref, () => { 18 | isControlledRef.current = true; 19 | 20 | return { 21 | startAnimation: () => controls.start("animate"), 22 | stopAnimation: () => controls.start("normal"), 23 | }; 24 | }); 25 | 26 | const handleMouseEnter = useCallback( 27 | (e: React.MouseEvent) => { 28 | if (!isControlledRef.current) { 29 | controls.start("animate"); 30 | } else { 31 | onMouseEnter?.(e); 32 | } 33 | }, 34 | [controls, onMouseEnter] 35 | ); 36 | 37 | const handleMouseLeave = useCallback( 38 | (e: React.MouseEvent) => { 39 | if (!isControlledRef.current) { 40 | controls.start("normal"); 41 | } else { 42 | onMouseLeave?.(e); 43 | } 44 | }, 45 | [controls, onMouseLeave] 46 | ); 47 | 48 | return ( 49 |
55 | 78 | 79 | 80 | 81 |
82 | ); 83 | } 84 | ); 85 | 86 | SearchIcon.displayName = "SearchIcon"; 87 | 88 | export { SearchIcon }; 89 | -------------------------------------------------------------------------------- /src/components/pqoqubbw/download.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { Variants } from "motion/react"; 4 | import { motion, useAnimation } from "motion/react"; 5 | import type { HTMLAttributes } from "react"; 6 | import { forwardRef, useCallback, useImperativeHandle, useRef } from "react"; 7 | 8 | export interface DownloadIconHandle { 9 | startAnimation: () => void; 10 | stopAnimation: () => void; 11 | } 12 | 13 | const arrowVariants: Variants = { 14 | normal: { y: 0 }, 15 | animate: { 16 | y: 2, 17 | transition: { 18 | type: "spring", 19 | stiffness: 200, 20 | damping: 10, 21 | mass: 1, 22 | }, 23 | }, 24 | }; 25 | 26 | const DownloadIcon = forwardRef< 27 | DownloadIconHandle, 28 | HTMLAttributes 29 | >(({ onMouseEnter, onMouseLeave, ...props }, ref) => { 30 | const controls = useAnimation(); 31 | const isControlledRef = useRef(false); 32 | 33 | useImperativeHandle(ref, () => { 34 | isControlledRef.current = true; 35 | 36 | return { 37 | startAnimation: () => controls.start("animate"), 38 | stopAnimation: () => controls.start("normal"), 39 | }; 40 | }); 41 | 42 | const handleMouseEnter = useCallback( 43 | (e: React.MouseEvent) => { 44 | if (!isControlledRef.current) { 45 | controls.start("animate"); 46 | } else { 47 | onMouseEnter?.(e); 48 | } 49 | }, 50 | [controls, onMouseEnter] 51 | ); 52 | 53 | const handleMouseLeave = useCallback( 54 | (e: React.MouseEvent) => { 55 | if (!isControlledRef.current) { 56 | controls.start("normal"); 57 | } else { 58 | onMouseLeave?.(e); 59 | } 60 | }, 61 | [controls, onMouseLeave] 62 | ); 63 | 64 | return ( 65 |
71 | 82 | 83 | 84 | 85 | 86 | 87 | 88 |
89 | ); 90 | }); 91 | 92 | DownloadIcon.displayName = "DownloadIcon"; 93 | 94 | export { DownloadIcon }; 95 | -------------------------------------------------------------------------------- /src/components/pqoqubbw/moon.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { Transition, Variants } from "motion/react"; 4 | import { motion, useAnimation } from "motion/react"; 5 | import type { HTMLAttributes } from "react"; 6 | import { forwardRef, useCallback, useImperativeHandle, useRef } from "react"; 7 | 8 | export interface MoonIconHandle { 9 | startAnimation: () => void; 10 | stopAnimation: () => void; 11 | } 12 | 13 | const svgVariants: Variants = { 14 | normal: { 15 | rotate: 0, 16 | }, 17 | animate: { 18 | rotate: [0, -10, 10, -5, 5, 0], 19 | }, 20 | }; 21 | 22 | const svgTransition: Transition = { 23 | duration: 1.2, 24 | ease: "easeInOut", 25 | }; 26 | 27 | const MoonIcon = forwardRef>( 28 | ({ onMouseEnter, onMouseLeave, ...props }, ref) => { 29 | const controls = useAnimation(); 30 | const isControlledRef = useRef(false); 31 | 32 | useImperativeHandle(ref, () => { 33 | isControlledRef.current = true; 34 | 35 | return { 36 | startAnimation: () => controls.start("animate"), 37 | stopAnimation: () => controls.start("normal"), 38 | }; 39 | }); 40 | 41 | const handleMouseEnter = useCallback( 42 | (e: React.MouseEvent) => { 43 | if (!isControlledRef.current) { 44 | controls.start("animate"); 45 | } else { 46 | onMouseEnter?.(e); 47 | } 48 | }, 49 | [controls, onMouseEnter] 50 | ); 51 | 52 | const handleMouseLeave = useCallback( 53 | (e: React.MouseEvent) => { 54 | if (!isControlledRef.current) { 55 | controls.start("normal"); 56 | } else { 57 | onMouseLeave?.(e); 58 | } 59 | }, 60 | [controls, onMouseLeave] 61 | ); 62 | return ( 63 |
69 | 83 | 84 | 85 |
86 | ); 87 | } 88 | ); 89 | 90 | MoonIcon.displayName = "MoonIcon"; 91 | 92 | export { MoonIcon }; 93 | -------------------------------------------------------------------------------- /src/components/pqoqubbw/book-text.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { motion, useAnimation } from "motion/react"; 4 | import type { HTMLAttributes } from "react"; 5 | import { forwardRef, useCallback, useImperativeHandle, useRef } from "react"; 6 | 7 | export interface BookTextIconHandle { 8 | startAnimation: () => void; 9 | stopAnimation: () => void; 10 | } 11 | 12 | const BookTextIcon = forwardRef< 13 | BookTextIconHandle, 14 | HTMLAttributes 15 | >(({ onMouseEnter, onMouseLeave, ...props }, ref) => { 16 | const controls = useAnimation(); 17 | const isControlledRef = useRef(false); 18 | 19 | useImperativeHandle(ref, () => { 20 | isControlledRef.current = true; 21 | 22 | return { 23 | startAnimation: () => controls.start("animate"), 24 | stopAnimation: () => controls.start("normal"), 25 | }; 26 | }); 27 | 28 | const handleMouseEnter = useCallback( 29 | (e: React.MouseEvent) => { 30 | if (!isControlledRef.current) { 31 | controls.start("animate"); 32 | } else { 33 | onMouseEnter?.(e); 34 | } 35 | }, 36 | [controls, onMouseEnter] 37 | ); 38 | 39 | const handleMouseLeave = useCallback( 40 | (e: React.MouseEvent) => { 41 | if (!isControlledRef.current) { 42 | controls.start("normal"); 43 | } else { 44 | onMouseLeave?.(e); 45 | } 46 | }, 47 | [controls, onMouseLeave] 48 | ); 49 | 50 | return ( 51 |
57 | 86 | 87 | 88 | 89 | 90 |
91 | ); 92 | }); 93 | 94 | BookTextIcon.displayName = "BookTextIcon"; 95 | 96 | export { BookTextIcon }; 97 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | export default { 4 | darkMode: ["class"], 5 | content: [ 6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 9 | "./registry/**/*.{js,ts,jsx,tsx,mdx}", 10 | ], 11 | theme: { 12 | extend: { 13 | colors: { 14 | background: 'hsl(var(--background))', 15 | foreground: 'hsl(var(--foreground))', 16 | card: { 17 | DEFAULT: 'hsl(var(--card))', 18 | foreground: 'hsl(var(--card-foreground))' 19 | }, 20 | popover: { 21 | DEFAULT: 'hsl(var(--popover))', 22 | foreground: 'hsl(var(--popover-foreground))' 23 | }, 24 | primary: { 25 | DEFAULT: 'hsl(var(--primary))', 26 | foreground: 'hsl(var(--primary-foreground))' 27 | }, 28 | secondary: { 29 | DEFAULT: 'hsl(var(--secondary))', 30 | foreground: 'hsl(var(--secondary-foreground))' 31 | }, 32 | muted: { 33 | DEFAULT: 'hsl(var(--muted))', 34 | foreground: 'hsl(var(--muted-foreground))' 35 | }, 36 | accent: { 37 | DEFAULT: 'hsl(var(--accent))', 38 | foreground: 'hsl(var(--accent-foreground))' 39 | }, 40 | destructive: { 41 | DEFAULT: 'hsl(var(--destructive))', 42 | foreground: 'hsl(var(--destructive-foreground))' 43 | }, 44 | border: 'hsl(var(--border))', 45 | input: 'hsl(var(--input))', 46 | ring: 'hsl(var(--ring))', 47 | chart: { 48 | '1': 'hsl(var(--chart-1))', 49 | '2': 'hsl(var(--chart-2))', 50 | '3': 'hsl(var(--chart-3))', 51 | '4': 'hsl(var(--chart-4))', 52 | '5': 'hsl(var(--chart-5))' 53 | }, 54 | sidebar: { 55 | DEFAULT: 'hsl(var(--sidebar-background))', 56 | foreground: 'hsl(var(--sidebar-foreground))', 57 | primary: 'hsl(var(--sidebar-primary))', 58 | 'primary-foreground': 'hsl(var(--sidebar-primary-foreground))', 59 | accent: 'hsl(var(--sidebar-accent))', 60 | 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', 61 | border: 'hsl(var(--sidebar-border))', 62 | ring: 'hsl(var(--sidebar-ring))' 63 | } 64 | }, 65 | borderRadius: { 66 | lg: 'var(--radius)', 67 | md: 'calc(var(--radius) - 2px)', 68 | sm: 'calc(var(--radius) - 4px)' 69 | }, 70 | fontSize: { 71 | '2xs': '0.625rem' 72 | }, 73 | keyframes: { 74 | 'accordion-down': { 75 | from: { 76 | height: '0' 77 | }, 78 | to: { 79 | height: 'var(--radix-accordion-content-height)' 80 | } 81 | }, 82 | 'accordion-up': { 83 | from: { 84 | height: 'var(--radix-accordion-content-height)' 85 | }, 86 | to: { 87 | height: '0' 88 | } 89 | } 90 | }, 91 | animation: { 92 | 'accordion-down': 'accordion-down 0.2s ease-out', 93 | 'accordion-up': 'accordion-up 0.2s ease-out' 94 | } 95 | } 96 | }, 97 | plugins: [require("tailwindcss-animate")], 98 | } satisfies Config; 99 | -------------------------------------------------------------------------------- /src/components/demo-ui/info-card/preview-code-tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { motion } from "motion/react"; 4 | import { useState } from "react"; 5 | import { 6 | MultiStepContent, 7 | SidebarDemo, 8 | } from "@/components/demo-ui/info-card/demo"; 9 | import { CodeBlock } from "@/components/ui/code-block"; 10 | import { Button } from "@/components/ui/button"; 11 | import { RotateCcw } from "lucide-react"; 12 | 13 | interface TabOption { 14 | id: "preview" | "code"; 15 | label: string; 16 | } 17 | 18 | export function PreviewCodeTabs({ 19 | layoutIdPrefix, 20 | code, 21 | }: { 22 | layoutIdPrefix: string; 23 | code: string; 24 | }) { 25 | const [selected, setSelected] = useState("preview"); 26 | const [key, setKey] = useState(0); 27 | 28 | const options: TabOption[] = [ 29 | { id: "preview", label: "Preview" }, 30 | { id: "code", label: "Code" }, 31 | ]; 32 | 33 | const refreshComponent = () => { 34 | setKey((prevKey) => prevKey + 1); 35 | }; 36 | 37 | return ( 38 |
39 | {/* Tab buttons */} 40 |
41 | {options.map((option) => ( 42 | 65 | ))} 66 |
67 | 68 | {/* Tab content */} 69 |
70 | {selected === "preview" ? ( 71 |
72 | 73 | 74 | 75 |
76 | 79 |
80 |
81 | ) : ( 82 | 83 | )} 84 |
85 |
86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /src/components/command-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { 5 | CommandDialog, 6 | CommandEmpty, 7 | CommandGroup, 8 | CommandInput, 9 | CommandItem, 10 | CommandList, 11 | } from "@/components/ui/command"; 12 | import { 13 | Book, 14 | Clock, 15 | Download, 16 | GalleryVerticalEnd, 17 | Moon, 18 | SaveOff, 19 | } from "lucide-react"; 20 | import { useRouter } from "next/navigation"; 21 | 22 | export function CommandMenu({ 23 | open, 24 | onOpenChange, 25 | }: { 26 | open: boolean; 27 | onOpenChange: (open: boolean) => void; 28 | }) { 29 | const router = useRouter(); 30 | 31 | const runCommand = React.useCallback( 32 | (command: () => unknown) => { 33 | onOpenChange(false); 34 | command(); 35 | }, 36 | [onOpenChange] 37 | ); 38 | 39 | React.useEffect(() => { 40 | const down = (e: KeyboardEvent) => { 41 | if (e.key === "k" && (e.metaKey || e.ctrlKey)) { 42 | e.preventDefault(); 43 | onOpenChange(!open); 44 | } 45 | }; 46 | document.addEventListener("keydown", down); 47 | return () => document.removeEventListener("keydown", down); 48 | }, [onOpenChange, open]); 49 | 50 | return ( 51 | 52 | 53 | 54 | No results found. 55 | 56 | 58 | runCommand(() => router.push("/components/info-card")) 59 | } 60 | > 61 | 62 | Info Card 63 | 64 | 66 | runCommand(() => router.push("/components/unsave-popup")) 67 | } 68 | > 69 | 70 | Unsave Popup 71 | 72 | 73 | 74 | runCommand(() => router.push("/"))}> 75 | 76 | Introduction 77 | 78 | runCommand(() => router.push("/installation"))} 80 | > 81 | 82 | Installation 83 | 84 | runCommand(() => router.push("/dark-mode"))} 86 | > 87 | 88 | Dark Mode 89 | 90 | runCommand(() => router.push("/changelog"))} 92 | > 93 | 94 | Changelog 95 | 96 | 97 | 98 | 99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /src/components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { ChevronRight, MoreHorizontal } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Breadcrumb = React.forwardRef< 8 | HTMLElement, 9 | React.ComponentPropsWithoutRef<"nav"> & { 10 | separator?: React.ReactNode 11 | } 12 | >(({ ...props }, ref) =>
\n
\n )}\n\n \n {popupContent}\n \n \n );\n}\n", 13 | "type": "registry:example", 14 | "target": "components/unsave-popup-demo.tsx" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | @layer base { 10 | :root { 11 | --background: 0 0% 100%; 12 | --foreground: 0 0% 3.9%; 13 | --card: 0 0% 100%; 14 | --card-foreground: 0 0% 3.9%; 15 | --popover: 0 0% 100%; 16 | --popover-foreground: 0 0% 3.9%; 17 | --primary: 0 0% 9%; 18 | --primary-foreground: 0 0% 98%; 19 | --secondary: 0 0% 96.1%; 20 | --secondary-foreground: 0 0% 9%; 21 | --muted: 0 0% 96.1%; 22 | --muted-foreground: 0 0% 45.1%; 23 | --accent: 0 0% 96.1%; 24 | --accent-foreground: 0 0% 9%; 25 | --destructive: 0 84.2% 60.2%; 26 | --destructive-foreground: 0 0% 98%; 27 | --border: 0 0% 89.8%; 28 | --input: 0 0% 89.8%; 29 | --ring: 0 0% 3.9%; 30 | --chart-1: 12 76% 61%; 31 | --chart-2: 173 58% 39%; 32 | --chart-3: 197 37% 24%; 33 | --chart-4: 43 74% 66%; 34 | --chart-5: 27 87% 67%; 35 | --radius: 0.5rem; 36 | --sidebar-background: 0 0% 98%; 37 | --sidebar-foreground: 240 5.3% 26.1%; 38 | --sidebar-primary: 240 5.9% 10%; 39 | --sidebar-primary-foreground: 0 0% 98%; 40 | --sidebar-accent: 240 4.8% 95.9%; 41 | --sidebar-accent-foreground: 240 5.9% 10%; 42 | --sidebar-border: 220 13% 91%; 43 | --sidebar-ring: 217.2 91.2% 59.8%; 44 | --transition-duration: 200ms; 45 | } 46 | .dark { 47 | --background: 0 0% 3.9%; 48 | --foreground: 0 0% 98%; 49 | --card: 0 0% 3.9%; 50 | --card-foreground: 0 0% 98%; 51 | --popover: 0 0% 3.9%; 52 | --popover-foreground: 0 0% 98%; 53 | --primary: 0 0% 98%; 54 | --primary-foreground: 0 0% 9%; 55 | --secondary: 0 0% 14.9%; 56 | --secondary-foreground: 0 0% 98%; 57 | --muted: 0 0% 14.9%; 58 | --muted-foreground: 0 0% 63.9%; 59 | --accent: 0 0% 14.9%; 60 | --accent-foreground: 0 0% 98%; 61 | --destructive: 0 62.8% 30.6%; 62 | --destructive-foreground: 0 0% 98%; 63 | --border: 0 0% 14.9%; 64 | --input: 0 0% 14.9%; 65 | --ring: 0 0% 83.1%; 66 | --chart-1: 220 70% 50%; 67 | --chart-2: 160 60% 45%; 68 | --chart-3: 30 80% 55%; 69 | --chart-4: 280 65% 60%; 70 | --chart-5: 340 75% 55%; 71 | --sidebar-background: 240 5.9% 10%; 72 | --sidebar-foreground: 240 4.8% 95.9%; 73 | --sidebar-primary: 224.3 76.3% 48%; 74 | --sidebar-primary-foreground: 0 0% 100%; 75 | --sidebar-accent: 240 3.7% 15.9%; 76 | --sidebar-accent-foreground: 240 4.8% 95.9%; 77 | --sidebar-border: 240 3.7% 15.9%; 78 | --sidebar-ring: 217.2 91.2% 59.8%; 79 | } 80 | 81 | html.dark { 82 | transition: background-color var(--transition-duration) ease, 83 | color var(--transition-duration) ease; 84 | } 85 | 86 | body { 87 | transition: background-color var(--transition-duration) ease; 88 | } 89 | 90 | /* Only apply transitions to specific properties that change with theme */ 91 | .transition-theme { 92 | transition: background-color var(--transition-duration) ease, 93 | border-color var(--transition-duration) ease, 94 | color var(--transition-duration) ease, 95 | fill var(--transition-duration) ease, 96 | stroke var(--transition-duration) ease; 97 | } 98 | } 99 | 100 | @layer base { 101 | * { 102 | @apply border-border; 103 | } 104 | body { 105 | @apply bg-background text-foreground; 106 | } 107 | } 108 | 109 | @layer base { 110 | * { 111 | @apply border-border outline-ring/50; 112 | } 113 | body { 114 | @apply bg-background text-foreground; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /registry/kl-ui/unsave-popup/unsave-popup-demo.tsx: -------------------------------------------------------------------------------- 1 | import { Label } from "@/components/ui/label"; 2 | import { X, Info } from "lucide-react"; 3 | import { Input } from "@/components/ui/input"; 4 | import { useState, useCallback, useMemo } from "react"; 5 | import { Button } from "@/components/ui/button"; 6 | // import { UnsavePopup } from "@/components/kl-ui/unsave-popup"; 7 | import { UnsavePopup } from "@/registry/kl-ui/unsave-popup/unsave-popup"; 8 | import { cn } from "@/lib/utils"; 9 | 10 | export function UnsavePopupDemo() { 11 | const [value, setValue] = useState(""); 12 | const [showPopup, setShowPopup] = useState(false); 13 | const [shouldBlockNav, setShouldBlockNav] = useState(false); 14 | const [closeForm, setCloseForm] = useState(false); 15 | const [isSaved, setIsSaved] = useState(false); 16 | 17 | const handleInputChange = useCallback( 18 | (e: React.ChangeEvent) => { 19 | setValue(e.target.value); 20 | setShowPopup(true); 21 | setIsSaved(false); 22 | }, 23 | [] 24 | ); 25 | 26 | const handleSave = useCallback(async () => { 27 | await new Promise((resolve) => setTimeout(resolve, 1000)); 28 | setShowPopup(false); 29 | setIsSaved(true); 30 | }, []); 31 | 32 | const handleReset = useCallback(() => { 33 | setValue(""); 34 | setShowPopup(false); 35 | setShouldBlockNav(false); 36 | }, []); 37 | 38 | const handleCloseFormClick = useCallback(() => { 39 | if (value && !isSaved) { 40 | setShouldBlockNav(true); 41 | setTimeout(() => setShouldBlockNav(false), 100); 42 | return; 43 | } 44 | setCloseForm(true); 45 | }, [value, isSaved]); 46 | 47 | const shouldBlockFn = useCallback(() => shouldBlockNav, [shouldBlockNav]); 48 | 49 | const formContent = useMemo( 50 | () => ( 51 |
52 | 55 | 61 |
62 | ), 63 | [value, handleInputChange] 64 | ); 65 | 66 | const popupContent = useMemo( 67 | () => ( 68 | <> 69 | Try to press the "x" to close it! 70 | 71 | ), 72 | [] 73 | ); 74 | 75 | return ( 76 | <> 77 | {!closeForm && ( 78 |
82 |
88 | {formContent} 89 |
90 | 98 |
99 |
100 |
101 | )} 102 | 103 | 110 | {popupContent} 111 | 112 | 113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /src/components/tweet-card.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Ellipsis, 5 | MessageCircle, 6 | Repeat2, 7 | Heart, 8 | ChartNoAxesColumn, 9 | Bookmark, 10 | Upload, 11 | } from "lucide-react"; 12 | import { motion } from "motion/react"; 13 | import Image from "next/image"; 14 | import Link from "next/link"; 15 | 16 | export function TweetCard() { 17 | return ( 18 | 33 | 37 |
38 | @shadcn 45 |
46 |
47 | shadcn 48 | Twitter 54 | @shadcn 55 | · 56 | Feb 14 57 | 60 |
61 |
Replying to @me
62 |
Amazing!
63 |
64 | 67 | 70 | 73 | 76 |
77 | 80 | 83 |
84 |
85 |
86 |
87 | 88 |
89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /src/components/demo-ui/unsave-popup/code.tsx: -------------------------------------------------------------------------------- 1 | import { codeToHtml } from "shiki"; 2 | 3 | export async function UnsavePopupDemoCode() { 4 | const html = await codeToHtml(unsavePopupCode, { 5 | lang: "tsx", 6 | theme: "min-dark", 7 | }); 8 | 9 | return html; 10 | } 11 | 12 | export const unsavePopupCode = `import { Label } from "@/components/ui/label"; 13 | import { X, Info } from "lucide-react"; 14 | import { Input } from "@/components/ui/input"; 15 | import { useState, useCallback, useMemo } from "react"; 16 | import { Button } from "@/components/ui/button"; 17 | import { UnsavePopup } from "@/components/kl-ui/unsave-popup"; 18 | import { cn } from "@/lib/utils"; 19 | 20 | export function UnsavePopupDemo() { 21 | const [value, setValue] = useState(""); 22 | const [showPopup, setShowPopup] = useState(false); 23 | const [shouldBlockNav, setShouldBlockNav] = useState(false); 24 | const [closeForm, setCloseForm] = useState(false); 25 | const [isSaved, setIsSaved] = useState(false); 26 | 27 | const handleInputChange = useCallback( 28 | (e: React.ChangeEvent) => { 29 | setValue(e.target.value); 30 | setShowPopup(true); 31 | setIsSaved(false); 32 | }, 33 | [] 34 | ); 35 | 36 | const handleSave = useCallback(async () => { 37 | await new Promise((resolve) => setTimeout(resolve, 1000)); 38 | setShowPopup(false); 39 | setIsSaved(true); 40 | }, []); 41 | 42 | const handleReset = useCallback(() => { 43 | setValue(""); 44 | setShowPopup(false); 45 | setShouldBlockNav(false); 46 | }, []); 47 | 48 | const handleCloseFormClick = useCallback(() => { 49 | if (value && !isSaved) { 50 | setShouldBlockNav(true); 51 | setTimeout(() => setShouldBlockNav(false), 100); 52 | return; 53 | } 54 | setCloseForm(true); 55 | }, [value, isSaved]); 56 | 57 | const shouldBlockFn = useCallback(() => shouldBlockNav, [shouldBlockNav]); 58 | 59 | const formContent = useMemo( 60 | () => ( 61 |
62 | 65 | 71 |
72 | ), 73 | [value, handleInputChange] 74 | ); 75 | 76 | const popupContent = useMemo( 77 | () => ( 78 | <> 79 | Try to press the "x" to close it! 80 | 81 | ), 82 | [] 83 | ); 84 | 85 | return ( 86 | <> 87 | {!closeForm && ( 88 |
92 |
98 | {formContent} 99 |
100 | 108 |
109 |
110 |
111 | )} 112 | 113 | 120 | {popupContent} 121 | 122 | 123 | ); 124 | } 125 | `; 126 | -------------------------------------------------------------------------------- /src/components/demo-ui/unsave-popup/demo-code.tsx: -------------------------------------------------------------------------------- 1 | import { Label } from "@/components/ui/label"; 2 | import { X, Info } from "lucide-react"; 3 | import { AnimatePresence, motion } from "motion/react"; 4 | import { Input } from "@/components/ui/input"; 5 | import { useState, useCallback, useMemo } from "react"; 6 | import { Button } from "@/components/ui/button"; 7 | import { UnsavePopup } from "@/registry/kl-ui/unsave-popup/unsave-popup"; 8 | import { cn } from "@/lib/utils"; 9 | 10 | export function UnsavePopupUI() { 11 | const [value, setValue] = useState(""); 12 | const [showPopup, setShowPopup] = useState(false); 13 | const [shouldBlockNav, setShouldBlockNav] = useState(false); 14 | const [closeForm, setCloseForm] = useState(false); 15 | const [isSaved, setIsSaved] = useState(false); 16 | 17 | const handleInputChange = useCallback( 18 | (e: React.ChangeEvent) => { 19 | setValue(e.target.value); 20 | setShowPopup(true); 21 | setIsSaved(false); 22 | }, 23 | [] 24 | ); 25 | 26 | const handleSave = useCallback(async () => { 27 | await new Promise((resolve) => setTimeout(resolve, 1000)); 28 | setShowPopup(false); 29 | setIsSaved(true); 30 | }, []); 31 | 32 | const handleReset = useCallback(() => { 33 | setValue(""); 34 | setShowPopup(false); 35 | setShouldBlockNav(false); 36 | }, []); 37 | 38 | const handleCloseFormClick = useCallback(() => { 39 | if (value && !isSaved) { 40 | setShouldBlockNav(true); 41 | setTimeout(() => setShouldBlockNav(false), 100); 42 | return; 43 | } 44 | setCloseForm(true); 45 | }, [value, isSaved]); 46 | 47 | const shouldBlockFn = useCallback(() => shouldBlockNav, [shouldBlockNav]); 48 | 49 | const formContent = useMemo( 50 | () => ( 51 |
52 | 55 | 61 |
62 | ), 63 | [value, handleInputChange] 64 | ); 65 | 66 | const popupContent = useMemo( 67 | () => ( 68 | <> 69 | Try to press the "x" to close it! 70 | 71 | ), 72 | [] 73 | ); 74 | 75 | return ( 76 | <> 77 | 78 | {!closeForm && ( 79 | 87 |
93 | {formContent} 94 |
95 | 103 |
104 |
105 |
106 | )} 107 |
108 | 109 | 116 | {popupContent} 117 | 118 | 119 | ); 120 | } 121 | -------------------------------------------------------------------------------- /src/components/dark-mode-content.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ModeToggle } from "@/components/mode-toggle"; 4 | import { useTheme } from "next-themes"; 5 | import { motion } from "framer-motion"; 6 | import Link from "next/link"; 7 | import { ChevronRight } from "lucide-react"; 8 | 9 | export function DarkModeContent() { 10 | const { theme } = useTheme(); 11 | const isDark = theme === "dark"; 12 | 13 | return ( 14 |
15 |
16 | {isDark ? ( 17 |

Welcome to the dark mode ...

18 | ) : ( 19 |

Click to enter the mystery ...

20 | )} 21 |
22 | 23 |
24 |
25 | 26 | {isDark ? ( 27 | 33 |

34 | I just realized that many of you are using dark mode in your site. 35 | Sorry that it came late, but late is better than never - I've 36 | added dark mode support to all components. 37 |

38 |

39 | And ngl, dark mode is actually pretty sick lol 🔥 40 |

41 | 42 |
43 |

44 | Installation Guide 45 |
46 |

47 |

48 | The installation is also not that hard, you can follow how shadcn 49 | ui did: 50 |

51 |
52 | {[ 53 | { 54 | name: "Next.js", 55 | href: "https://ui.shadcn.com/docs/dark-mode/next", 56 | icon: "/icons/next.svg", 57 | }, 58 | { 59 | name: "Vite", 60 | href: "https://ui.shadcn.com/docs/dark-mode/vite", 61 | icon: "/icons/vite.svg", 62 | }, 63 | { 64 | name: "Astro", 65 | href: "https://ui.shadcn.com/docs/dark-mode/astro", 66 | icon: "/icons/astro.svg", 67 | }, 68 | { 69 | name: "Remix", 70 | href: "https://ui.shadcn.com/docs/dark-mode/remix", 71 | icon: "/icons/remix.svg", 72 | }, 73 | ].map((framework) => ( 74 | 79 |
80 | {framework.name} 85 | {framework.name} 86 |
87 | 88 | 89 | 90 | 91 | ))} 92 |
93 |
94 |
95 | ) : ( 96 |
97 | Switch to dark mode to reveal the content... 98 |
99 | )} 100 |
101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /public/r/info-card-demo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "info-card-demo", 4 | "type": "registry:example", 5 | "description": "Example of the kl-ui info-card component.", 6 | "registryDependencies": [ 7 | "https://karrix.dev/r/info-card" 8 | ], 9 | "files": [ 10 | { 11 | "path": "registry/kl-ui/info-card/info-card-demo.tsx", 12 | "content": "import {\n InfoCard,\n InfoCardContent,\n InfoCardTitle,\n InfoCardDescription,\n InfoCardMedia,\n InfoCardFooter,\n InfoCardDismiss,\n InfoCardAction,\n} from \"@/components/kl-ui/info-card\";\nimport {\n Sidebar,\n SidebarProvider,\n SidebarContent,\n SidebarGroup,\n SidebarGroupLabel,\n SidebarGroupContent,\n SidebarMenu,\n SidebarMenuItem,\n SidebarMenuButton,\n SidebarFooter,\n SidebarTrigger,\n} from \"@/components/ui/sidebar\";\nimport {\n ExternalLink,\n User,\n ChevronsUpDown,\n Calendar,\n Home,\n Inbox,\n Search,\n Settings,\n} from \"lucide-react\";\nimport Link from \"next/link\";\n\n// Menu items.\nconst items = [\n {\n title: \"Home\",\n url: \"#\",\n icon: Home,\n },\n {\n title: \"Inbox\",\n url: \"#\",\n icon: Inbox,\n },\n {\n title: \"Calendar\",\n url: \"#\",\n icon: Calendar,\n },\n {\n title: \"Search\",\n url: \"#\",\n icon: Search,\n },\n {\n title: \"Settings\",\n url: \"#\",\n icon: Settings,\n },\n];\n\nexport function InfoCardDemo() {\n return (\n \n \n \n \n Application\n \n \n {items.map((item) => (\n \n \n \n \n {item.title}\n \n \n \n ))}\n \n \n \n \n \n \n \n Introducing New Dashboard\n \n New Feature. New Platform. Same Feel.\n \n \n \n Dismiss\n \n \n Try it out \n \n \n \n \n \n \n \n
\n \n
\n KL\n \n kl@example.com\n \n
\n
\n \n
\n
\n
\n
\n
\n \n
\n
\n );\n}\n", 13 | "type": "registry:example", 14 | "target": "components/info-card-demo.tsx" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogClose, 116 | DialogTrigger, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /registry/kl-ui/info-card/info-card-demo.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | InfoCard, 3 | InfoCardContent, 4 | InfoCardTitle, 5 | InfoCardDescription, 6 | InfoCardMedia, 7 | InfoCardFooter, 8 | InfoCardDismiss, 9 | InfoCardAction, 10 | } from "@/registry/kl-ui/info-card/info-card"; 11 | // } from "@/components/kl-ui/info-card"; 12 | import { 13 | Sidebar, 14 | SidebarProvider, 15 | SidebarContent, 16 | SidebarGroup, 17 | SidebarGroupLabel, 18 | SidebarGroupContent, 19 | SidebarMenu, 20 | SidebarMenuItem, 21 | SidebarMenuButton, 22 | SidebarFooter, 23 | SidebarTrigger, 24 | } from "@/components/ui/sidebar"; 25 | import { 26 | ExternalLink, 27 | User, 28 | ChevronsUpDown, 29 | Calendar, 30 | Home, 31 | Inbox, 32 | Search, 33 | Settings, 34 | } from "lucide-react"; 35 | import Link from "next/link"; 36 | 37 | // Menu items. 38 | const items = [ 39 | { 40 | title: "Home", 41 | url: "#", 42 | icon: Home, 43 | }, 44 | { 45 | title: "Inbox", 46 | url: "#", 47 | icon: Inbox, 48 | }, 49 | { 50 | title: "Calendar", 51 | url: "#", 52 | icon: Calendar, 53 | }, 54 | { 55 | title: "Search", 56 | url: "#", 57 | icon: Search, 58 | }, 59 | { 60 | title: "Settings", 61 | url: "#", 62 | icon: Settings, 63 | }, 64 | ]; 65 | 66 | export function InfoCardDemo() { 67 | return ( 68 | 69 | 70 | 71 | 72 | Application 73 | 74 | 75 | {items.map((item) => ( 76 | 77 | 78 | 79 | 80 | {item.title} 81 | 82 | 83 | 84 | ))} 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | Introducing New Dashboard 93 | 94 | New Feature. New Platform. Same Feel. 95 | 96 | 109 | 110 | Dismiss 111 | 112 | 116 | Try it out 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 |
125 | 126 |
127 | KL 128 | 129 | kl@example.com 130 | 131 |
132 |
133 | 134 |
135 |
136 |
137 |
138 |
139 | 140 |
141 |
142 | ); 143 | } 144 | -------------------------------------------------------------------------------- /src/components/demo-ui/info-card/installation-tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { motion } from "framer-motion"; 4 | import { useState } from "react"; 5 | import { PackageManagerTabs } from "@/components/ui/package-manager-tabs"; 6 | import { CodeBlock } from "@/components/ui/code-block"; 7 | import { Step, Steps } from "@/components/ui/steps"; 8 | import { CodeSnippet } from "@/components/ui/code-snippet"; 9 | 10 | interface TabOption { 11 | id: "cli" | "manual"; 12 | label: string; 13 | } 14 | 15 | interface InstallationTabsProps { 16 | layoutIdPrefix: string; 17 | cliCommand: string; 18 | codeHtml: string; 19 | importCode: string; 20 | } 21 | 22 | export function InstallationTabs({ 23 | layoutIdPrefix, 24 | cliCommand, 25 | codeHtml, 26 | importCode, 27 | }: InstallationTabsProps) { 28 | const [selected, setSelected] = useState("cli"); 29 | const [activeStep, setActiveStep] = useState(1); 30 | 31 | const options: TabOption[] = [ 32 | { id: "cli", label: "CLI" }, 33 | { id: "manual", label: "Manual" }, 34 | ]; 35 | 36 | return ( 37 |
38 | {/* Tab buttons */} 39 |
40 | {options.map((option) => ( 41 | 67 | ))} 68 |
69 | 70 | {/* Tab content */} 71 |
72 | {selected === "cli" ? ( 73 | 78 | ) : ( 79 | 80 | 1} 85 | isActive={activeStep === 1} 86 | onClick={() => setActiveStep(1)} 87 | > 88 | 93 | 94 | 95 | 2} 100 | isActive={activeStep === 2} 101 | onClick={() => setActiveStep(2)} 102 | > 103 | 104 | 105 | 106 | 3} 111 | isActive={activeStep === 3} 112 | onClick={() => setActiveStep(3)} 113 | > 114 |
115 | Import and use the component in your project: 116 | 121 |
122 |
123 |
124 | )} 125 |
126 |
127 | ); 128 | } 129 | -------------------------------------------------------------------------------- /src/components/ui/code-block.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "./button"; 4 | import { Copy, Check, ChevronDown, ChevronUp } from "lucide-react"; 5 | import { useState } from "react"; 6 | import { motion, AnimatePresence } from "framer-motion"; 7 | import { cn } from "@/lib/utils"; 8 | import { ScrollArea } from "./scroll-area"; 9 | 10 | interface CodeBlockProps { 11 | html: string; 12 | className?: string; 13 | maxHeight?: number; 14 | expandedHeight?: number; 15 | } 16 | 17 | export function CodeBlock({ 18 | html, 19 | className, 20 | maxHeight, 21 | expandedHeight, 22 | }: CodeBlockProps) { 23 | const [copied, setCopied] = useState(false); 24 | const [isExpanded, setIsExpanded] = useState(false); 25 | 26 | const handleCopy = async () => { 27 | // Extract text content from HTML string 28 | const temp = document.createElement("div"); 29 | temp.innerHTML = html; 30 | const text = temp.textContent || temp.innerText; 31 | 32 | try { 33 | await navigator.clipboard.writeText(text); 34 | setCopied(true); 35 | setTimeout(() => setCopied(false), 2000); 36 | } catch (err) { 37 | console.error("Failed to copy:", err); 38 | } 39 | }; 40 | 41 | const showExpandButton = maxHeight && expandedHeight; 42 | 43 | return ( 44 |
45 | {/* Copy button */} 46 | 76 | 77 | {/* Code content */} 78 | 88 | 98 |
102 | 103 | 104 | {/* Gradient overlay */} 105 | {showExpandButton && ( 106 | 110 | )} 111 | 112 | {/* Expand/Collapse button */} 113 | {showExpandButton && ( 114 |
115 | 131 |
132 | )} 133 |
134 | ); 135 | } 136 | -------------------------------------------------------------------------------- /src/components/ui/sheet.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SheetPrimitive from "@radix-ui/react-dialog" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | import { X } from "lucide-react" 7 | 8 | import { cn } from "@/lib/utils" 9 | 10 | const Sheet = SheetPrimitive.Root 11 | 12 | const SheetTrigger = SheetPrimitive.Trigger 13 | 14 | const SheetClose = SheetPrimitive.Close 15 | 16 | const SheetPortal = SheetPrimitive.Portal 17 | 18 | const SheetOverlay = React.forwardRef< 19 | React.ElementRef, 20 | React.ComponentPropsWithoutRef 21 | >(({ className, ...props }, ref) => ( 22 | 30 | )) 31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName 32 | 33 | const sheetVariants = cva( 34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", 35 | { 36 | variants: { 37 | side: { 38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", 39 | bottom: 40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", 41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", 42 | right: 43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", 44 | }, 45 | }, 46 | defaultVariants: { 47 | side: "right", 48 | }, 49 | } 50 | ) 51 | 52 | interface SheetContentProps 53 | extends React.ComponentPropsWithoutRef, 54 | VariantProps {} 55 | 56 | const SheetContent = React.forwardRef< 57 | React.ElementRef, 58 | SheetContentProps 59 | >(({ side = "right", className, children, ...props }, ref) => ( 60 | 61 | 62 | 67 | {children} 68 | 69 | 70 | Close 71 | 72 | 73 | 74 | )) 75 | SheetContent.displayName = SheetPrimitive.Content.displayName 76 | 77 | const SheetHeader = ({ 78 | className, 79 | ...props 80 | }: React.HTMLAttributes) => ( 81 |
88 | ) 89 | SheetHeader.displayName = "SheetHeader" 90 | 91 | const SheetFooter = ({ 92 | className, 93 | ...props 94 | }: React.HTMLAttributes) => ( 95 |
102 | ) 103 | SheetFooter.displayName = "SheetFooter" 104 | 105 | const SheetTitle = React.forwardRef< 106 | React.ElementRef, 107 | React.ComponentPropsWithoutRef 108 | >(({ className, ...props }, ref) => ( 109 | 114 | )) 115 | SheetTitle.displayName = SheetPrimitive.Title.displayName 116 | 117 | const SheetDescription = React.forwardRef< 118 | React.ElementRef, 119 | React.ComponentPropsWithoutRef 120 | >(({ className, ...props }, ref) => ( 121 | 126 | )) 127 | SheetDescription.displayName = SheetPrimitive.Description.displayName 128 | 129 | export { 130 | Sheet, 131 | SheetPortal, 132 | SheetOverlay, 133 | SheetTrigger, 134 | SheetClose, 135 | SheetContent, 136 | SheetHeader, 137 | SheetFooter, 138 | SheetTitle, 139 | SheetDescription, 140 | } 141 | -------------------------------------------------------------------------------- /src/app/installation/page.tsx: -------------------------------------------------------------------------------- 1 | import { PackageManagerTabs } from "@/components/ui/package-manager-tabs"; 2 | import { Button } from "@/components/ui/button"; 3 | import { ArrowRight, ExternalLink } from "lucide-react"; 4 | import Link from "next/link"; 5 | import { Metadata } from "next"; 6 | 7 | export const metadata: Metadata = { 8 | title: "Installation", 9 | description: "How to install dependencies and structure your app.", 10 | keywords: [ 11 | "React", 12 | "Next.js", 13 | "Shadcn UI", 14 | "NextUI", 15 | "Installation", 16 | "KL UI", 17 | "Component Library", 18 | "Motion", 19 | "Framer Motion", 20 | ], 21 | }; 22 | 23 | export default function Installation() { 24 | return ( 25 |
26 | {/* Header */} 27 |
28 |

Installation

29 | 30 | How to install dependencies and structure your app. 31 | 32 |
33 | 34 | {/* Prerequisites */} 35 |
36 |

Prerequisites

37 |
38 | {/* Shadcn Installation */} 39 |
40 |
41 |
42 |

Shadcn UI

43 |

44 | Install Shadcn UI CLI to add components 45 |

46 |
47 | 48 | 52 | 53 |
54 |
55 | 61 |
62 |
63 | 64 | {/* Motion Installation */} 65 |
66 |
67 |
68 |

Motion

69 |

70 | Install Motion for animations 71 |

72 |
73 | 74 | 78 | 79 |
80 |
81 | 87 |
88 |
89 |
90 |
91 | 92 |
93 | ... And you are good to go! 94 |
95 | 96 | {/* Next Steps */} 97 |
98 |

Next Steps

99 |
100 |
101 | 105 |
106 |

107 | Check out the components 108 |

109 |

110 | Learn how to use the components in your project 111 |

112 |
113 | 116 | 117 |
118 |
119 |
120 |
121 | ); 122 | } 123 | -------------------------------------------------------------------------------- /src/components/ui/package-manager-tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { motion, AnimatePresence } from "framer-motion"; 4 | import { useState } from "react"; 5 | import { cn } from "@/lib/utils"; 6 | import { Button } from "./button"; 7 | import { Copy, Check } from "lucide-react"; 8 | 9 | type PackageManager = "pnpm" | "npm" | "yarn" | "bun"; 10 | type CommandVariant = "add" | "install" | "dlx"; 11 | 12 | interface PackageManagerTabsProps { 13 | command: string; 14 | variant?: CommandVariant; 15 | className?: string; 16 | layoutId?: string; 17 | } 18 | 19 | // Move this outside the component to be shared 20 | const STORAGE_KEY = "preferred-package-manager"; 21 | 22 | function getStoredManager(): PackageManager { 23 | if (typeof window === "undefined") return "pnpm"; 24 | return (localStorage.getItem(STORAGE_KEY) as PackageManager) || "pnpm"; 25 | } 26 | 27 | export function PackageManagerTabs({ 28 | command, 29 | variant = "dlx", 30 | className, 31 | layoutId = "package-manager-tab", 32 | }: PackageManagerTabsProps) { 33 | const [selected, setSelected] = useState(getStoredManager); 34 | const [copied, setCopied] = useState(false); 35 | 36 | const options: { id: PackageManager; label: string }[] = [ 37 | { id: "pnpm", label: "pnpm" }, 38 | { id: "npm", label: "npm" }, 39 | { id: "yarn", label: "yarn" }, 40 | { id: "bun", label: "bun" }, 41 | ]; 42 | 43 | // Update localStorage when selection changes 44 | const handleSelect = (manager: PackageManager) => { 45 | setSelected(manager); 46 | localStorage.setItem(STORAGE_KEY, manager); 47 | }; 48 | 49 | const getCommand = (manager: PackageManager) => { 50 | const prefixMap = { 51 | dlx: { 52 | pnpm: "pnpm dlx", 53 | npm: "npx", 54 | yarn: "npx", 55 | bun: "bunx --bun", 56 | }, 57 | add: { 58 | pnpm: "pnpm add", 59 | npm: "npm install", 60 | yarn: "yarn add", 61 | bun: "bun add", 62 | }, 63 | install: { 64 | pnpm: "pnpm install", 65 | npm: "npm install", 66 | yarn: "yarn install", 67 | bun: "bun install", 68 | }, 69 | }; 70 | 71 | const prefix = prefixMap[variant][manager]; 72 | return `${prefix} ${command}`; 73 | }; 74 | 75 | const handleCopy = async () => { 76 | try { 77 | await navigator.clipboard.writeText(getCommand(selected)); 78 | setCopied(true); 79 | setTimeout(() => setCopied(false), 2000); 80 | } catch (err) { 81 | console.error("Failed to copy:", err); 82 | } 83 | }; 84 | 85 | return ( 86 |
87 | {/* Tab buttons */} 88 |
89 | {options.map((option) => ( 90 | 113 | ))} 114 |
115 | 116 | {/* Command display */} 117 |
118 |
119 | {getCommand(selected)} 120 |
121 | 150 |
151 |
152 | ); 153 | } 154 | -------------------------------------------------------------------------------- /src/components/ui/command.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { type DialogProps } from "@radix-ui/react-dialog" 5 | import { Command as CommandPrimitive } from "cmdk" 6 | import { Search } from "lucide-react" 7 | 8 | import { cn } from "@/lib/utils" 9 | import { Dialog, DialogContent } from "@/components/ui/dialog" 10 | 11 | const Command = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 23 | )) 24 | Command.displayName = CommandPrimitive.displayName 25 | 26 | const CommandDialog = ({ children, ...props }: DialogProps) => { 27 | return ( 28 | 29 | 30 | 31 | {children} 32 | 33 | 34 | 35 | ) 36 | } 37 | 38 | const CommandInput = React.forwardRef< 39 | React.ElementRef, 40 | React.ComponentPropsWithoutRef 41 | >(({ className, ...props }, ref) => ( 42 |
43 | 44 | 52 |
53 | )) 54 | 55 | CommandInput.displayName = CommandPrimitive.Input.displayName 56 | 57 | const CommandList = React.forwardRef< 58 | React.ElementRef, 59 | React.ComponentPropsWithoutRef 60 | >(({ className, ...props }, ref) => ( 61 | 66 | )) 67 | 68 | CommandList.displayName = CommandPrimitive.List.displayName 69 | 70 | const CommandEmpty = React.forwardRef< 71 | React.ElementRef, 72 | React.ComponentPropsWithoutRef 73 | >((props, ref) => ( 74 | 79 | )) 80 | 81 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName 82 | 83 | const CommandGroup = React.forwardRef< 84 | React.ElementRef, 85 | React.ComponentPropsWithoutRef 86 | >(({ className, ...props }, ref) => ( 87 | 95 | )) 96 | 97 | CommandGroup.displayName = CommandPrimitive.Group.displayName 98 | 99 | const CommandSeparator = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName 110 | 111 | const CommandItem = React.forwardRef< 112 | React.ElementRef, 113 | React.ComponentPropsWithoutRef 114 | >(({ className, ...props }, ref) => ( 115 | 123 | )) 124 | 125 | CommandItem.displayName = CommandPrimitive.Item.displayName 126 | 127 | const CommandShortcut = ({ 128 | className, 129 | ...props 130 | }: React.HTMLAttributes) => { 131 | return ( 132 | 139 | ) 140 | } 141 | CommandShortcut.displayName = "CommandShortcut" 142 | 143 | export { 144 | Command, 145 | CommandDialog, 146 | CommandInput, 147 | CommandList, 148 | CommandEmpty, 149 | CommandGroup, 150 | CommandItem, 151 | CommandShortcut, 152 | CommandSeparator, 153 | } 154 | -------------------------------------------------------------------------------- /public/r/unsave-popup.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json", 3 | "name": "unsave-popup", 4 | "type": "registry:block", 5 | "description": "A popup component to confirm before discarding changes. Inspired by Discord, it will shake when the user tries to leave without saving.", 6 | "dependencies": [ 7 | "motion" 8 | ], 9 | "registryDependencies": [ 10 | "button" 11 | ], 12 | "files": [ 13 | { 14 | "path": "registry/kl-ui/unsave-popup/unsave-popup.tsx", 15 | "content": "\"use client\";\n\nimport { Loader2, Save } from \"lucide-react\";\nimport { AnimatePresence, easeOut, motion, useAnimation } from \"motion/react\";\nimport { Button } from \"@/components/ui/button\";\nimport { useState, useEffect, useCallback, useMemo, memo } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport React from \"react\";\n\nconst UnsavePopupDescription = memo(\n ({ children }: { children: React.ReactNode }) => {\n return
{children}
;\n }\n);\nUnsavePopupDescription.displayName = \"UnsavePopupDescription\";\n\nconst UnsavePopupAction = memo(\n ({\n children,\n isLoading,\n onClick,\n }: {\n children: React.ReactNode;\n isLoading?: boolean;\n onClick?: () => Promise;\n }) => {\n return (\n \n );\n }\n);\nUnsavePopupAction.displayName = \"UnsavePopupAction\";\n\nconst UnsavePopupDismiss = memo(\n ({\n children,\n onClick,\n }: {\n children: React.ReactNode;\n onClick?: () => void;\n }) => {\n return (\n \n );\n }\n);\nUnsavePopupDismiss.displayName = \"UnsavePopupDismiss\";\n\n// Main component\nconst UnsavePopup = memo(function UnsavePopup({\n children,\n onSave,\n onReset,\n shouldBlockFn,\n show,\n className,\n}: {\n children: React.ReactNode;\n className?: string;\n onSave?: () => Promise;\n onReset?: () => void;\n shouldBlockFn?: () => boolean;\n show: boolean;\n}) {\n const controls = useAnimation();\n const [isLoading, setIsLoading] = useState(false);\n\n const shakeAnimation = useCallback(\n () => ({\n x: [0, -8, 12, -15, 8, -10, 5, -3, 2, -1, 0],\n y: [0, 4, -9, 6, -12, 8, -3, 5, -2, 1, 0],\n filter: [\n \"blur(0px)\",\n \"blur(2px)\",\n \"blur(2px)\",\n \"blur(3px)\",\n \"blur(2px)\",\n \"blur(2px)\",\n \"blur(1px)\",\n \"blur(2px)\",\n \"blur(1px)\",\n \"blur(1px)\",\n \"blur(0px)\",\n ],\n \"--warning-opacity\": [0, 0.5, 0.3, 0.1, 0], \n transition: {\n duration: 0.4,\n ease: easeOut,\n },\n }),\n []\n );\n\n const triggerShake = useCallback(async () => {\n await controls.start(shakeAnimation());\n }, [controls, shakeAnimation]);\n\n const handleSave = useCallback(async () => {\n setIsLoading(true);\n await onSave?.();\n setIsLoading(false);\n }, [onSave]);\n\n const handleReset = useCallback(() => {\n onReset?.();\n }, [onReset]);\n\n const { hasCompoundComponents, hasValidComponents } = useMemo(() => {\n const childrenArray = React.Children.toArray(children);\n const hasCompound = childrenArray.some(\n (child) =>\n React.isValidElement(child) &&\n (child.type === UnsavePopupDescription ||\n child.type === UnsavePopupAction ||\n child.type === UnsavePopupDismiss)\n );\n\n if (!hasCompound) {\n return { hasCompoundComponents: false, hasValidComponents: true };\n }\n\n const hasDescription = childrenArray.some(\n (child) =>\n React.isValidElement(child) && child.type === UnsavePopupDescription\n );\n const hasAction = childrenArray.some(\n (child) => React.isValidElement(child) && child.type === UnsavePopupAction\n );\n const hasDismiss = childrenArray.some(\n (child) =>\n React.isValidElement(child) && child.type === UnsavePopupDismiss\n );\n\n return {\n hasCompoundComponents: true,\n hasValidComponents: hasDescription && hasAction && hasDismiss,\n };\n }, [children]);\n\n useEffect(() => {\n if (hasCompoundComponents && !hasValidComponents) {\n throw new Error(\n cn(\n \"When using UnsavePopupDescription, UnsavePopupAction, or UnsavePopupDismiss, \",\n \"you must use all three components together. Check out the docs for more info.\"\n )\n );\n }\n }, [hasCompoundComponents, hasValidComponents]);\n\n const defaultButtons = useCallback(\n () => (\n
\n \n \n {isLoading ? (\n \n \n Saving...\n \n ) : (\n \n \n Save\n \n )}\n \n
\n ),\n [isLoading, handleReset, handleSave]\n );\n\n useEffect(() => {\n if (shouldBlockFn && shouldBlockFn()) {\n triggerShake();\n }\n }, [shouldBlockFn, triggerShake]);\n\n return (\n \n {show && (\n \n \n {hasCompoundComponents ? (\n children\n ) : (\n <>\n
\n {children}\n
\n {defaultButtons()}\n \n )}\n \n \n )}\n
\n );\n});\n\nUnsavePopup.displayName = \"UnsavePopup\";\n\nexport {\n UnsavePopup,\n UnsavePopupDescription,\n UnsavePopupAction,\n UnsavePopupDismiss,\n};\n", 16 | "type": "registry:component", 17 | "target": "components/kl-ui/unsave-popup.tsx" 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /src/app/components/info-card/page.tsx: -------------------------------------------------------------------------------- 1 | import { InfoCardDemo } from "@/components/demo-ui/info-card/demo"; 2 | import { 3 | InfoCardDemoCode, 4 | basicUsageRawCode, 5 | stepsUsageRawCode, 6 | } from "@/components/demo-ui/info-card/code"; 7 | import { InfoCardInstallationCode } from "@/components/demo-ui/info-card/installation"; 8 | import { NavigationMenu } from "@/components/navigation-menu"; 9 | import { Info } from "lucide-react"; 10 | import { PropTable, PropDefinition } from "@/components/ui/prop-table"; 11 | import { codeToHtml } from "shiki"; 12 | import { CodeBlock } from "@/components/ui/code-block"; 13 | import { Metadata } from "next"; 14 | import { OpenInV0Button } from "@/components/open-in-v0"; 15 | import { PreviewCodeTabs } from "@/components/demo-ui/info-card/preview-code-tabs"; 16 | 17 | const infoCardProps: PropDefinition[] = [ 18 | { 19 | prop: "storageKey", 20 | type: "string", 21 | description: 22 | "Required when using dismissType='forever'. Used to store the dismissed state in localStorage.", 23 | }, 24 | { 25 | prop: "dismissType", 26 | type: "'once' | 'forever'", 27 | default: "'once'", 28 | description: 29 | "Controls whether the card should be dismissed temporarily or permanently using localStorage.", 30 | }, 31 | { 32 | prop: "children", 33 | type: "React.ReactNode", 34 | description: "The content of the card.", 35 | }, 36 | ]; 37 | 38 | const mediaProps: PropDefinition[] = [ 39 | { 40 | prop: "media", 41 | type: "MediaItem[]", 42 | description: 43 | "Array of media items to display. Each item can be an image or video. At most 3 items are supported.", 44 | }, 45 | { 46 | prop: "shrinkHeight", 47 | type: "number", 48 | default: "75", 49 | description: "Height of the media container when not hovered (in pixels).", 50 | }, 51 | { 52 | prop: "expandHeight", 53 | type: "number", 54 | default: "150", 55 | description: "Height of the media container when hovered (in pixels).", 56 | }, 57 | { 58 | prop: "loading", 59 | type: "'eager' | 'lazy'", 60 | description: "Controls the loading behavior of images.", 61 | }, 62 | ]; 63 | 64 | const mediaItemProps: PropDefinition[] = [ 65 | { 66 | prop: "type", 67 | type: "'image' | 'video'", 68 | default: "'image'", 69 | description: "The type of media to display.", 70 | }, 71 | { 72 | prop: "src", 73 | type: "string", 74 | description: "The URL of the media resource.", 75 | }, 76 | { 77 | prop: "alt", 78 | type: "string", 79 | description: "Alt text for images (accessibility).", 80 | }, 81 | { 82 | prop: "className", 83 | type: "string", 84 | description: "Additional CSS classes to apply to the media element.", 85 | }, 86 | { 87 | prop: "props", 88 | type: "React.HTMLAttributes", 89 | description: "Additional props to pass to the media element.", 90 | }, 91 | ]; 92 | 93 | export const metadata: Metadata = { 94 | title: "Info Card", 95 | description: 96 | "A versatile information card component for displaying content in an organized and visually appealing way.", 97 | keywords: [ 98 | "React", 99 | "Next.js", 100 | "Shadcn UI", 101 | "NextUI", 102 | "Info Card", 103 | "Card", 104 | "KL UI", 105 | "Component Library", 106 | "Motion", 107 | "Framer Motion", 108 | ], 109 | }; 110 | 111 | // Make the page component async 112 | export default async function InfoCardPage() { 113 | const codeComponent = await InfoCardDemoCode(); 114 | const basicUsageCode = await codeToHtml(basicUsageRawCode, { 115 | lang: "tsx", 116 | theme: "min-dark", 117 | }); 118 | const stepsUsageCode = await codeToHtml(stepsUsageRawCode, { 119 | lang: "tsx", 120 | theme: "min-dark", 121 | }); 122 | 123 | return ( 124 |
125 |
126 | {/* title */} 127 |
128 |

Information Card

129 | 130 | Information cards can serve as callout cards, banners, toast 131 | notifications, or announcement boxes to deliver news, updates, and 132 | alerts to your users. 133 | 134 | {/* Quote block */} 135 |
136 | 137 |
138 |

139 | It works especially well in sidebars, but you can use it 140 | anywhere too! 141 |

142 |
143 |
144 |
145 | 146 | {/* content */} 147 |
148 |
149 |

150 | Playground 151 |

152 | 153 |
154 | 155 |
156 | 157 |
158 | 159 | {/* Installation */} 160 |
161 |

162 | Installation 163 |

164 | 165 |
166 | 167 |
168 | 169 | {/* Props and Usage */} 170 |
171 |

172 | Props and Usage 173 |

174 | 175 | {/* Basic usage example */} 176 |
177 |
178 |

179 | Basic Usage 180 |

181 | 186 |
187 | 188 | {/* Steps usage example */} 189 |
190 |

191 | Steps Usage 192 |

193 | 197 |
198 | 199 | {/* Props tables */} 200 |
201 | 202 | 203 | 204 | 205 | 206 |
207 |
208 |
209 |
210 | 211 | {/* Right-side navigation */} 212 |
213 | 214 |
215 |
216 | ); 217 | } 218 | -------------------------------------------------------------------------------- /registry/kl-ui/unsave-popup/unsave-popup.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Loader2, Save } from "lucide-react"; 4 | import { AnimatePresence, easeOut, motion, useAnimation } from "motion/react"; 5 | import { Button } from "@/components/ui/button"; 6 | import { useState, useEffect, useCallback, useMemo, memo } from "react"; 7 | import { cn } from "@/lib/utils"; 8 | import React from "react"; 9 | 10 | const UnsavePopupDescription = memo( 11 | ({ children }: { children: React.ReactNode }) => { 12 | return
{children}
; 13 | } 14 | ); 15 | UnsavePopupDescription.displayName = "UnsavePopupDescription"; 16 | 17 | const UnsavePopupAction = memo( 18 | ({ 19 | children, 20 | isLoading, 21 | onClick, 22 | }: { 23 | children: React.ReactNode; 24 | isLoading?: boolean; 25 | onClick?: () => Promise; 26 | }) => { 27 | return ( 28 | 38 | ); 39 | } 40 | ); 41 | UnsavePopupAction.displayName = "UnsavePopupAction"; 42 | 43 | const UnsavePopupDismiss = memo( 44 | ({ 45 | children, 46 | onClick, 47 | }: { 48 | children: React.ReactNode; 49 | onClick?: () => void; 50 | }) => { 51 | return ( 52 | 55 | ); 56 | } 57 | ); 58 | UnsavePopupDismiss.displayName = "UnsavePopupDismiss"; 59 | 60 | // Main component 61 | const UnsavePopup = memo(function UnsavePopup({ 62 | children, 63 | onSave, 64 | onReset, 65 | shouldBlockFn, 66 | show, 67 | className, 68 | }: { 69 | children: React.ReactNode; 70 | className?: string; 71 | onSave?: () => Promise; 72 | onReset?: () => void; 73 | shouldBlockFn?: () => boolean; 74 | show: boolean; 75 | }) { 76 | const controls = useAnimation(); 77 | const [isLoading, setIsLoading] = useState(false); 78 | 79 | const shakeAnimation = useCallback( 80 | () => ({ 81 | x: [0, -8, 12, -15, 8, -10, 5, -3, 2, -1, 0], 82 | y: [0, 4, -9, 6, -12, 8, -3, 5, -2, 1, 0], 83 | filter: [ 84 | "blur(0px)", 85 | "blur(2px)", 86 | "blur(2px)", 87 | "blur(3px)", 88 | "blur(2px)", 89 | "blur(2px)", 90 | "blur(1px)", 91 | "blur(2px)", 92 | "blur(1px)", 93 | "blur(1px)", 94 | "blur(0px)", 95 | ], 96 | "--warning-opacity": [0, 0.5, 0.3, 0.1, 0], 97 | transition: { 98 | duration: 0.4, 99 | ease: easeOut, 100 | }, 101 | }), 102 | [] 103 | ); 104 | 105 | const triggerShake = useCallback(async () => { 106 | await controls.start(shakeAnimation()); 107 | }, [controls, shakeAnimation]); 108 | 109 | const handleSave = useCallback(async () => { 110 | setIsLoading(true); 111 | await onSave?.(); 112 | setIsLoading(false); 113 | }, [onSave]); 114 | 115 | const handleReset = useCallback(() => { 116 | onReset?.(); 117 | }, [onReset]); 118 | 119 | const { hasCompoundComponents, hasValidComponents } = useMemo(() => { 120 | const childrenArray = React.Children.toArray(children); 121 | const hasCompound = childrenArray.some( 122 | (child) => 123 | React.isValidElement(child) && 124 | (child.type === UnsavePopupDescription || 125 | child.type === UnsavePopupAction || 126 | child.type === UnsavePopupDismiss) 127 | ); 128 | 129 | if (!hasCompound) { 130 | return { hasCompoundComponents: false, hasValidComponents: true }; 131 | } 132 | 133 | const hasDescription = childrenArray.some( 134 | (child) => 135 | React.isValidElement(child) && child.type === UnsavePopupDescription 136 | ); 137 | const hasAction = childrenArray.some( 138 | (child) => React.isValidElement(child) && child.type === UnsavePopupAction 139 | ); 140 | const hasDismiss = childrenArray.some( 141 | (child) => 142 | React.isValidElement(child) && child.type === UnsavePopupDismiss 143 | ); 144 | 145 | return { 146 | hasCompoundComponents: true, 147 | hasValidComponents: hasDescription && hasAction && hasDismiss, 148 | }; 149 | }, [children]); 150 | 151 | useEffect(() => { 152 | if (hasCompoundComponents && !hasValidComponents) { 153 | throw new Error( 154 | cn( 155 | "When using UnsavePopupDescription, UnsavePopupAction, or UnsavePopupDismiss, ", 156 | "you must use all three components together. Check out the docs for more info." 157 | ) 158 | ); 159 | } 160 | }, [hasCompoundComponents, hasValidComponents]); 161 | 162 | const defaultButtons = useCallback( 163 | () => ( 164 |
165 | 168 | 185 |
186 | ), 187 | [isLoading, handleReset, handleSave] 188 | ); 189 | 190 | useEffect(() => { 191 | if (shouldBlockFn && shouldBlockFn()) { 192 | triggerShake(); 193 | } 194 | }, [shouldBlockFn, triggerShake]); 195 | 196 | return ( 197 | 198 | {show && ( 199 | 206 | 220 | {hasCompoundComponents ? ( 221 | children 222 | ) : ( 223 | <> 224 |
225 | {children} 226 |
227 | {defaultButtons()} 228 | 229 | )} 230 |
231 |
232 | )} 233 |
234 | ); 235 | }); 236 | 237 | UnsavePopup.displayName = "UnsavePopup"; 238 | 239 | export { 240 | UnsavePopup, 241 | UnsavePopupDescription, 242 | UnsavePopupAction, 243 | UnsavePopupDismiss, 244 | }; 245 | -------------------------------------------------------------------------------- /src/components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 5 | import { Check, ChevronRight, Circle } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const DropdownMenu = DropdownMenuPrimitive.Root 10 | 11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger 12 | 13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group 14 | 15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal 16 | 17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub 18 | 19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup 20 | 21 | const DropdownMenuSubTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef & { 24 | inset?: boolean 25 | } 26 | >(({ className, inset, children, ...props }, ref) => ( 27 | 36 | {children} 37 | 38 | 39 | )) 40 | DropdownMenuSubTrigger.displayName = 41 | DropdownMenuPrimitive.SubTrigger.displayName 42 | 43 | const DropdownMenuSubContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, ...props }, ref) => ( 47 | 55 | )) 56 | DropdownMenuSubContent.displayName = 57 | DropdownMenuPrimitive.SubContent.displayName 58 | 59 | const DropdownMenuContent = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, sideOffset = 4, ...props }, ref) => ( 63 | 64 | 73 | 74 | )) 75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName 76 | 77 | const DropdownMenuItem = React.forwardRef< 78 | React.ElementRef, 79 | React.ComponentPropsWithoutRef & { 80 | inset?: boolean 81 | } 82 | >(({ className, inset, ...props }, ref) => ( 83 | 92 | )) 93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName 94 | 95 | const DropdownMenuCheckboxItem = React.forwardRef< 96 | React.ElementRef, 97 | React.ComponentPropsWithoutRef 98 | >(({ className, children, checked, ...props }, ref) => ( 99 | 108 | 109 | 110 | 111 | 112 | 113 | {children} 114 | 115 | )) 116 | DropdownMenuCheckboxItem.displayName = 117 | DropdownMenuPrimitive.CheckboxItem.displayName 118 | 119 | const DropdownMenuRadioItem = React.forwardRef< 120 | React.ElementRef, 121 | React.ComponentPropsWithoutRef 122 | >(({ className, children, ...props }, ref) => ( 123 | 131 | 132 | 133 | 134 | 135 | 136 | {children} 137 | 138 | )) 139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName 140 | 141 | const DropdownMenuLabel = React.forwardRef< 142 | React.ElementRef, 143 | React.ComponentPropsWithoutRef & { 144 | inset?: boolean 145 | } 146 | >(({ className, inset, ...props }, ref) => ( 147 | 156 | )) 157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName 158 | 159 | const DropdownMenuSeparator = React.forwardRef< 160 | React.ElementRef, 161 | React.ComponentPropsWithoutRef 162 | >(({ className, ...props }, ref) => ( 163 | 168 | )) 169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName 170 | 171 | const DropdownMenuShortcut = ({ 172 | className, 173 | ...props 174 | }: React.HTMLAttributes) => { 175 | return ( 176 | 180 | ) 181 | } 182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut" 183 | 184 | export { 185 | DropdownMenu, 186 | DropdownMenuTrigger, 187 | DropdownMenuContent, 188 | DropdownMenuItem, 189 | DropdownMenuCheckboxItem, 190 | DropdownMenuRadioItem, 191 | DropdownMenuLabel, 192 | DropdownMenuSeparator, 193 | DropdownMenuShortcut, 194 | DropdownMenuGroup, 195 | DropdownMenuPortal, 196 | DropdownMenuSub, 197 | DropdownMenuSubContent, 198 | DropdownMenuSubTrigger, 199 | DropdownMenuRadioGroup, 200 | } 201 | -------------------------------------------------------------------------------- /src/components/demo-ui/unsave-popup/installation.tsx: -------------------------------------------------------------------------------- 1 | import { codeToHtml } from "shiki"; 2 | import { InstallationTabs } from "../info-card/installation-tabs"; 3 | 4 | const installationCode = `"use client"; 5 | 6 | import { Loader2, Save } from "lucide-react"; 7 | import { AnimatePresence, easeOut, motion, useAnimation } from "motion/react"; 8 | import { Button } from "@/components/ui/button"; 9 | import { useState, useEffect, useCallback, useMemo, memo } from "react"; 10 | import { cn } from "@/lib/utils"; 11 | import React from "react"; 12 | 13 | const UnsavePopupDescription = memo( 14 | ({ children }: { children: React.ReactNode }) => { 15 | return
{children}
; 16 | } 17 | ); 18 | UnsavePopupDescription.displayName = "UnsavePopupDescription"; 19 | 20 | const UnsavePopupAction = memo( 21 | ({ 22 | children, 23 | isLoading, 24 | onClick, 25 | }: { 26 | children: React.ReactNode; 27 | isLoading?: boolean; 28 | onClick?: () => Promise; 29 | }) => { 30 | return ( 31 | 41 | ); 42 | } 43 | ); 44 | UnsavePopupAction.displayName = "UnsavePopupAction"; 45 | 46 | const UnsavePopupDismiss = memo( 47 | ({ 48 | children, 49 | onClick, 50 | }: { 51 | children: React.ReactNode; 52 | onClick?: () => void; 53 | }) => { 54 | return ( 55 | 58 | ); 59 | } 60 | ); 61 | UnsavePopupDismiss.displayName = "UnsavePopupDismiss"; 62 | 63 | // Main component 64 | const UnsavePopup = memo(function UnsavePopup({ 65 | children, 66 | onSave, 67 | onReset, 68 | shouldBlockFn, 69 | show, 70 | className, 71 | }: { 72 | children: React.ReactNode; 73 | className?: string; 74 | onSave?: () => Promise; 75 | onReset?: () => void; 76 | shouldBlockFn?: () => boolean; 77 | show: boolean; 78 | }) { 79 | const controls = useAnimation(); 80 | const [isLoading, setIsLoading] = useState(false); 81 | 82 | const shakeAnimation = useCallback( 83 | () => ({ 84 | x: [0, -8, 12, -15, 8, -10, 5, -3, 2, -1, 0], 85 | y: [0, 4, -9, 6, -12, 8, -3, 5, -2, 1, 0], 86 | filter: [ 87 | "blur(0px)", 88 | "blur(2px)", 89 | "blur(2px)", 90 | "blur(3px)", 91 | "blur(2px)", 92 | "blur(2px)", 93 | "blur(1px)", 94 | "blur(2px)", 95 | "blur(1px)", 96 | "blur(1px)", 97 | "blur(0px)", 98 | ], 99 | "--warning-opacity": [0, 0.5, 0.3, 0.1, 0], 100 | transition: { 101 | duration: 0.4, 102 | ease: easeOut, 103 | }, 104 | }), 105 | [] 106 | ); 107 | 108 | const triggerShake = useCallback(async () => { 109 | await controls.start(shakeAnimation()); 110 | }, [controls, shakeAnimation]); 111 | 112 | const handleSave = useCallback(async () => { 113 | setIsLoading(true); 114 | await onSave?.(); 115 | setIsLoading(false); 116 | }, [onSave]); 117 | 118 | const handleReset = useCallback(() => { 119 | onReset?.(); 120 | }, [onReset]); 121 | 122 | const { hasCompoundComponents, hasValidComponents } = useMemo(() => { 123 | const childrenArray = React.Children.toArray(children); 124 | const hasCompound = childrenArray.some( 125 | (child) => 126 | React.isValidElement(child) && 127 | (child.type === UnsavePopupDescription || 128 | child.type === UnsavePopupAction || 129 | child.type === UnsavePopupDismiss) 130 | ); 131 | 132 | if (!hasCompound) { 133 | return { hasCompoundComponents: false, hasValidComponents: true }; 134 | } 135 | 136 | const hasDescription = childrenArray.some( 137 | (child) => 138 | React.isValidElement(child) && child.type === UnsavePopupDescription 139 | ); 140 | const hasAction = childrenArray.some( 141 | (child) => React.isValidElement(child) && child.type === UnsavePopupAction 142 | ); 143 | const hasDismiss = childrenArray.some( 144 | (child) => 145 | React.isValidElement(child) && child.type === UnsavePopupDismiss 146 | ); 147 | 148 | return { 149 | hasCompoundComponents: true, 150 | hasValidComponents: hasDescription && hasAction && hasDismiss, 151 | }; 152 | }, [children]); 153 | 154 | useEffect(() => { 155 | if (hasCompoundComponents && !hasValidComponents) { 156 | throw new Error( 157 | cn( 158 | "When using UnsavePopupDescription, UnsavePopupAction, or UnsavePopupDismiss, ", 159 | "you must use all three components together. Check out the docs for more info." 160 | ) 161 | ); 162 | } 163 | }, [hasCompoundComponents, hasValidComponents]); 164 | 165 | const defaultButtons = useCallback( 166 | () => ( 167 |
168 | 171 | 188 |
189 | ), 190 | [isLoading, handleReset, handleSave] 191 | ); 192 | 193 | useEffect(() => { 194 | if (shouldBlockFn && shouldBlockFn()) { 195 | triggerShake(); 196 | } 197 | }, [shouldBlockFn, triggerShake]); 198 | 199 | return ( 200 | 201 | {show && ( 202 | 209 | 223 | {hasCompoundComponents ? ( 224 | children 225 | ) : ( 226 | <> 227 |
228 | {children} 229 |
230 | {defaultButtons()} 231 | 232 | )} 233 |
234 |
235 | )} 236 |
237 | ); 238 | }); 239 | 240 | UnsavePopup.displayName = "UnsavePopup"; 241 | 242 | export { 243 | UnsavePopup, 244 | UnsavePopupDescription, 245 | UnsavePopupAction, 246 | UnsavePopupDismiss, 247 | }; 248 | `; 249 | 250 | export async function UnsavePopupInstallationCode() { 251 | const html = await codeToHtml(installationCode, { 252 | lang: "bash", 253 | theme: "min-dark", 254 | }); 255 | 256 | return ( 257 | 263 | ); 264 | } 265 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next"; 2 | import { ArrowRight, Coffee, Github } from "lucide-react"; 3 | import Link from "next/link"; 4 | import { Button } from "@/components/ui/button"; 5 | import { TweetCard } from "@/components/tweet-card"; 6 | import { HoverCardUser } from "@/components/hover-card"; 7 | import { 8 | Accordion, 9 | AccordionContent, 10 | AccordionItem, 11 | AccordionTrigger, 12 | } from "@/components/ui/accordion"; 13 | 14 | export const metadata: Metadata = { 15 | title: "KL UI - Animated UI components with React", 16 | description: 17 | "A modern, customizable, and accessible React component library built with best practices and developer experience in mind.", 18 | keywords: [ 19 | "React", 20 | "Next.js", 21 | "Shadcn UI", 22 | "NextUI", 23 | "UI Library", 24 | "Components", 25 | "TypeScript", 26 | "Component Library", 27 | "Motion", 28 | "Framer Motion", 29 | ], 30 | openGraph: { 31 | images: ["/readme-cover.jpg"], 32 | }, 33 | twitter: { 34 | card: "summary_large_image", 35 | images: ["/readme-cover.jpg"], 36 | }, 37 | authors: [{ name: "Karrix Lee", url: "https://github.com/karrixlee" }], 38 | }; 39 | 40 | export default function Home() { 41 | return ( 42 |
43 | {/* Hero Section */} 44 |
45 |
46 |

KL

47 | 48 | UI 49 | 50 |
51 |

52 | Animated UI components and effects with love. Build with{" "} 53 | shadcn/ui and{" "} 54 | Motion. 55 |

56 |
57 | 58 | 62 | 63 | 64 | 68 | 69 |
70 |
71 | 72 | 73 | 74 |
75 |

76 | So here's the thing - I randomly posted some of my UI work on X 77 | one day and went off to grab dinner. When I came back, my phone was 78 | going crazy with notifications 79 |

80 |

81 | And get this - shadcn himself 82 | had commented on my tweet lol. 83 |

84 | 85 |

86 | That moment got me thinking - "Hey, why not share these UI 87 | components with everyone?" I mean, I'm still learning and 88 | growing in design, but I feel like that's the motivation for me 89 | to do that. 90 |

91 | 92 |

93 | Btw I've got to mention my buddy{" "} 94 | {" "} 100 | - this guy's been my best friend, always pushing me to try new 101 | things. Shoutout to him! 102 |

103 | 104 |

105 | Last thing - This is my first repo on GitHub, your support means a lot 106 | to me.{" "} 107 | 108 | I am Karrix - please enjoy the UI components! 109 | 110 |

111 |
112 | 113 |
114 |

FAQ

115 | 116 | 117 | Can I use this in my project? 118 | 119 | Absolutely! This is an MIT licensed open-source project. Feel free 120 | to use it in any project, commercial or personal. Your feedback 121 | means a lot to me - I'd love to hear about your experience 122 | using these components. 123 | 124 | 125 | 126 | 127 | How's the animation performance? 128 | 129 | 130 |

131 | All complex animations are powered by Motion, while simpler UI 132 | interactions use shadcn's built-in transitions. I've 133 | extensively tested each component to ensure optimal performance 134 | and ease of use. 135 |

136 |

137 | If you encounter any performance issues, please don't 138 | hesitate to report them - I'm committed to providing the 139 | best possible experience. 140 |

141 |
142 |
143 | 144 | Why is there so little UI here? 145 | 146 |

147 | Simple — because I made this with love and interest. I'm 148 | not just throwing in random components for the sake of it. 149 | Everything here is something I genuinely think is the best 150 | I've ever made. 151 |

152 |

153 | I'll add more when I feel it's right — when I create 154 | something that truly deserves a spot here. Until then, enjoy 155 | what's here, knowing each piece is built with real care. 156 |

157 |
158 |
159 | 160 | 161 |
162 | Please let me credit{" "} 163 | 164 |
165 |
166 | 167 |

168 | Special thanks to{" "} 169 | {" "} 175 | for the amazing animated icon library that brings life to our 176 | navigation. 177 |

178 |

179 | Also, a huge shoutout to{" "} 180 | {" "} 186 | 's React Scan, which has been invaluable for performance 187 | testing and optimization. These tools have helped me a lot. 188 |

189 |
190 |
191 |
192 |
193 |
194 | ); 195 | } 196 | -------------------------------------------------------------------------------- /src/components/demo-ui/unsave-popup/demo.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Fragment, useEffect, useState } from "react"; 4 | import { motion, AnimatePresence } from "framer-motion"; 5 | import { Check, Code, Copy, Play, RotateCcw } from "lucide-react"; 6 | import { Button } from "@/components/ui/button"; 7 | import { 8 | Tooltip, 9 | TooltipContent, 10 | TooltipProvider, 11 | TooltipTrigger, 12 | } from "@/components/ui/tooltip"; 13 | import { CodeDisplay } from "../info-card/code"; 14 | import { UnsavePopupUI } from "@/components/demo-ui/unsave-popup/demo-code"; 15 | import { unsavePopupCode } from "./code"; 16 | 17 | interface TabOption { 18 | id: "form" | "code"; 19 | label: string; 20 | icon: React.ReactNode; 21 | } 22 | 23 | export function UnsavePopupDemo({ 24 | initialCodeHtml, 25 | }: { 26 | initialCodeHtml: string; 27 | }) { 28 | const [selected, setSelected] = useState<"form" | "code">("form"); 29 | const [key, setKey] = useState(0); 30 | const [copied, setCopied] = useState(false); 31 | const [isInPlayground, setIsInPlayground] = useState(true); 32 | 33 | const options: TabOption[] = [ 34 | { id: "form", label: "Form", icon: }, 35 | { id: "code", label: "Code", icon: }, 36 | ]; 37 | 38 | const handleReload = () => { 39 | setKey((prev) => prev + 1); 40 | }; 41 | 42 | const handleCopy = async () => { 43 | try { 44 | if (copied) return; 45 | 46 | await navigator.clipboard.writeText(unsavePopupCode); 47 | setCopied(true); 48 | setTimeout(() => { 49 | setCopied(false); 50 | }, 2000); // Reset after 2 seconds 51 | } catch (err) { 52 | console.error("Failed to copy:", err); 53 | } 54 | }; 55 | 56 | useEffect(() => { 57 | const observer = new IntersectionObserver( 58 | (entries) => { 59 | entries.forEach((entry) => { 60 | if (entry.target.id === "playground") { 61 | setIsInPlayground(entry.isIntersecting); 62 | } 63 | }); 64 | }, 65 | { threshold: 0.2 } 66 | ); 67 | 68 | const playground = document.getElementById("playground"); 69 | if (playground) { 70 | observer.observe(playground); 71 | } 72 | 73 | return () => observer.disconnect(); 74 | }, []); 75 | 76 | return ( 77 |
78 | 79 | {selected === "code" ? ( 80 | 87 | 88 | 89 | ) : ( 90 |
91 | 92 |
93 | )} 94 |
95 | 96 | {/* Bottom tab bar */} 97 | 98 | {isInPlayground && ( 99 |
100 | 106 |
107 |
108 | {options.map((option, index) => ( 109 | 110 | {index > 0 && ( 111 |
112 | )} 113 |
114 | 138 |
139 | 140 | ))} 141 |
142 | 143 |
144 | 145 | 146 | 147 | 155 | 156 | 157 |

Reset demo

158 |
159 |
160 |
161 | 162 |
163 | 164 | 165 | 166 | 167 | 184 | 185 | 186 |

{copied ? "Copied!" : "Copy code"}

187 |
188 |
189 |
190 |
191 |
192 | 193 |
194 | )} 195 | 196 |
197 | ); 198 | } 199 | -------------------------------------------------------------------------------- /src/app/components/unsave-popup/page.tsx: -------------------------------------------------------------------------------- 1 | import { UnsavePopupDemo } from "@/components/demo-ui/unsave-popup/demo"; 2 | import { PropTable, PropDefinition } from "@/components/ui/prop-table"; 3 | import { UnsavePopupDemoCode } from "@/components/demo-ui/unsave-popup/code"; 4 | import { NavigationMenu } from "@/components/navigation-menu"; 5 | import { UnsavePopupInstallationCode } from "@/components/demo-ui/unsave-popup/installation"; 6 | import { CodeBlock } from "@/components/ui/code-block"; 7 | import { codeToHtml } from "shiki"; 8 | import { AlertTriangle } from "lucide-react"; 9 | import { Metadata } from "next"; 10 | import { OpenInV0Button } from "@/components/open-in-v0"; 11 | 12 | const unsavePopupProps: PropDefinition[] = [ 13 | { 14 | prop: "show", 15 | type: "boolean", 16 | description: "Controls the visibility of the popup", 17 | }, 18 | { 19 | prop: "children", 20 | type: "React.ReactNode", 21 | description: 22 | "The content to be displayed in the popup. Can be plain text for default usage, or compound components for custom usage", 23 | }, 24 | { 25 | prop: "onSave", 26 | type: "() => Promise", 27 | description: 28 | "Callback function when save button is clicked (used in default mode)", 29 | }, 30 | { 31 | prop: "onReset", 32 | type: "() => void", 33 | description: 34 | "Callback function when reset button is clicked (used in default mode)", 35 | }, 36 | { 37 | prop: "shouldBlockFn", 38 | type: "() => boolean", 39 | description: 40 | "Function to determine if the popup should trigger the block animation", 41 | }, 42 | { 43 | prop: "className", 44 | type: "string", 45 | description: "Additional CSS classes to apply to the popup container", 46 | }, 47 | ]; 48 | 49 | const defaultUsageCode = `// Default usage 50 | 55 | You have unsaved changes 56 | `; 57 | 58 | const customUsageCode = `// Customized usage with compound components 59 | 60 | 61 | 🔴 You have unsaved changes 62 | 63 | 64 | Reset 65 | 66 | 67 | Save Changes 68 | 69 | `; 70 | 71 | const descriptionProps: PropDefinition[] = [ 72 | { 73 | prop: "children", 74 | type: "React.ReactNode", 75 | description: "The content to be displayed in the description area", 76 | }, 77 | ]; 78 | 79 | const actionProps: PropDefinition[] = [ 80 | { 81 | prop: "children", 82 | type: "React.ReactNode", 83 | description: "The content of the action button", 84 | }, 85 | { 86 | prop: "isLoading", 87 | type: "boolean", 88 | description: "Controls the loading state of the button", 89 | }, 90 | { 91 | prop: "onClick", 92 | type: "() => Promise", 93 | description: "Callback function when the action button is clicked", 94 | }, 95 | ]; 96 | 97 | const dismissProps: PropDefinition[] = [ 98 | { 99 | prop: "children", 100 | type: "React.ReactNode", 101 | description: "The content of the dismiss button", 102 | }, 103 | { 104 | prop: "onClick", 105 | type: "() => void", 106 | description: "Callback function when the dismiss button is clicked", 107 | }, 108 | ]; 109 | 110 | export const metadata: Metadata = { 111 | title: "Unsave Popup", 112 | description: 113 | "A customizable unsave popup component that helps manage unsaved changes in your forms and applications.", 114 | keywords: [ 115 | "React", 116 | "Next.js", 117 | "Shadcn UI", 118 | "NextUI", 119 | "Popup", 120 | "Unsave", 121 | "Dialog", 122 | "KL UI", 123 | "Component Library", 124 | "Motion", 125 | "Framer Motion", 126 | ], 127 | }; 128 | 129 | export default async function UnsavePopupPage() { 130 | const unsavePopupCodeHtml = await UnsavePopupDemoCode(); 131 | 132 | const defaultUsageHtml = await codeToHtml(defaultUsageCode, { 133 | lang: "tsx", 134 | theme: "min-dark", 135 | }); 136 | 137 | const customUsageHtml = await codeToHtml(customUsageCode, { 138 | lang: "tsx", 139 | theme: "min-dark", 140 | }); 141 | 142 | return ( 143 |
144 |
145 | {/* title */} 146 |
147 |

Unsave Popup

148 | 149 | A popup component to confirm before discarding changes. Inspired by 150 | Discord, it will shake when the user tries to leave without saving. 151 | 152 |
153 | 154 | {/* content */} 155 |
156 |
157 |

158 | Playground 159 |

160 | 161 |
162 | 163 |
164 | 165 |
166 | 167 | {/* Installation */} 168 |
169 |

170 | Installation 171 |

172 | 173 |
174 | 175 |
176 | 177 | {/* Usage Examples */} 178 |
179 |

180 | Usage Examples 181 |

182 | 183 | {/* Default Usage */} 184 |
185 |

Default Usage

186 | 187 |
188 | 189 | {/* Notice for compound components */} 190 |
191 | 192 |
193 |

194 | When using compound components: 195 |

196 |
    197 |
  • 198 | 199 | UnsavePopupDescription 200 | 201 |
  • 202 |
  • 203 | 204 | UnsavePopupAction 205 | 206 |
  • 207 |
  • 208 | 209 | UnsavePopupDismiss 210 | 211 |
  • 212 |
213 |

214 | You must include all three components together. Using them 215 | individually will throw an error. 216 |

217 |
218 |
219 | 220 | {/* Custom Usage */} 221 |
222 |

Customized Usage

223 | 224 |
225 |
226 | 227 |
228 | 229 | {/* Props and Usage */} 230 |
231 |

232 | Props 233 |

234 | 235 | 239 | 240 | 241 |
242 |
243 | 244 | {/* Right-side navigation */} 245 |
246 | 247 |
248 |
249 | ); 250 | } 251 | --------------------------------------------------------------------------------