├── .eslintrc.json
├── bun.lockb
├── postcss.config.js
├── lib
├── utils.ts
└── themes.ts
├── next.config.js
├── hooks
└── use-theme.ts
├── app
├── components
│ ├── layout.tsx
│ ├── checkbox
│ │ └── page.tsx
│ ├── button
│ │ └── page.tsx
│ ├── select
│ │ └── page.tsx
│ ├── theme
│ │ └── page.tsx
│ ├── switch
│ │ └── page.tsx
│ ├── input
│ │ └── page.tsx
│ ├── radio
│ │ └── page.tsx
│ ├── toggle
│ │ └── page.tsx
│ ├── colorpicker
│ │ └── page.tsx
│ └── datepicker
│ │ └── page.tsx
├── layout.tsx
├── globals.css
├── page.tsx
└── manifest
│ └── page.tsx
├── components
├── layouts
│ └── components-layout.tsx
├── ui
│ ├── aspect-ratio.tsx
│ ├── toggle.tsx
│ ├── radio-group.tsx
│ ├── container.tsx
│ ├── space.tsx
│ ├── textarea.tsx
│ ├── breadcrumb.tsx
│ ├── skeleton.tsx
│ ├── grid.tsx
│ ├── checkbox.tsx
│ ├── radio.tsx
│ ├── spinner.tsx
│ ├── switch.tsx
│ ├── stack.tsx
│ ├── divider.tsx
│ ├── toast-provider.tsx
│ ├── badge.tsx
│ ├── input.tsx
│ ├── button.tsx
│ ├── alert.tsx
│ ├── stats.tsx
│ ├── tree.tsx
│ ├── form.tsx
│ ├── progress.tsx
│ ├── modal.tsx
│ ├── stepper.tsx
│ ├── timeline.tsx
│ ├── list.tsx
│ ├── toast.tsx
│ ├── pagination.tsx
│ ├── select.tsx
│ ├── color-picker.tsx
│ ├── calendar.tsx
│ ├── upload.tsx
│ ├── tabs.tsx
│ ├── table.tsx
│ └── date-picker.tsx
├── feature-card.tsx
├── background-pattern.tsx
├── code-block.tsx
├── theme-provider.tsx
└── sidebar.tsx
├── components.json
├── .gitignore
├── tsconfig.json
├── package.json
└── tailwind.config.ts
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ddoemonn/parrot_ui/HEAD/bun.lockb
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx";
2 | import { twMerge } from "tailwind-merge";
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs));
6 | }
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | output: 'export',
4 | eslint: {
5 | ignoreDuringBuilds: true,
6 | },
7 | images: { unoptimized: true },
8 | };
9 |
10 | module.exports = nextConfig;
11 |
--------------------------------------------------------------------------------
/hooks/use-theme.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { ThemeContext } from '@/components/theme-provider';
3 |
4 | export function useTheme() {
5 | const context = useContext(ThemeContext);
6 | if (context === undefined) {
7 | throw new Error('useTheme must be used within a ThemeProvider');
8 | }
9 | return context;
10 | }
--------------------------------------------------------------------------------
/app/components/layout.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Sidebar } from '@/components/sidebar';
4 |
5 | export default function ComponentsLayout({
6 | children,
7 | }: {
8 | children: React.ReactNode;
9 | }) {
10 | return (
11 |
12 |
13 |
14 | {children}
15 |
16 |
17 | );
18 | }
--------------------------------------------------------------------------------
/components/layouts/components-layout.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Sidebar } from '@/components/sidebar';
4 |
5 | export function ComponentsLayout({
6 | children,
7 | }: {
8 | children: React.ReactNode;
9 | }) {
10 | return (
11 |
12 |
13 |
14 | {children}
15 |
16 |
17 | );
18 | }
--------------------------------------------------------------------------------
/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": "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 | }
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 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
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 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------
/components/ui/aspect-ratio.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { cn } from '@/lib/utils';
5 | import { useTheme } from '@/hooks/use-theme';
6 |
7 | interface AspectRatioProps {
8 | ratio?: number;
9 | children: React.ReactNode;
10 | className?: string;
11 | }
12 |
13 | export function AspectRatio({
14 | ratio = 16/9,
15 | children,
16 | className
17 | }: AspectRatioProps) {
18 | const { theme } = useTheme();
19 |
20 | return (
21 |
30 |
31 | {children}
32 |
33 |
34 | );
35 | }
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import './globals.css';
2 | import type { Metadata } from 'next';
3 | import { Inter } from 'next/font/google';
4 | import { ThemeProvider } from '@/components/theme-provider';
5 | import { ToastProvider } from '@/components/ui/toast-provider';
6 |
7 | const inter = Inter({ subsets: ['latin'] });
8 |
9 | export const metadata: Metadata = {
10 | title: 'ParrotUI - Modern React Component Library',
11 | description: 'A beautiful and feature-rich React component library',
12 | };
13 |
14 | export default function RootLayout({
15 | children,
16 | }: {
17 | children: React.ReactNode;
18 | }) {
19 | return (
20 |
21 |
22 |
23 |
24 | {children}
25 |
26 |
27 |
28 |
29 | );
30 | }
--------------------------------------------------------------------------------
/components/feature-card.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { motion } from 'framer-motion';
4 | import { ReactNode } from 'react';
5 |
6 | interface FeatureCardProps {
7 | icon: ReactNode;
8 | title: string;
9 | description: string;
10 | }
11 |
12 | export function FeatureCard({ icon, title, description }: FeatureCardProps) {
13 | return (
14 |
18 |
19 |
20 | {icon}
21 |
22 |
23 |
{title}
24 |
{description}
25 |
26 |
27 |
28 | );
29 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@next/swc-wasm-nodejs": "13.5.1",
13 | "autoprefixer": "^10.4.17",
14 | "class-variance-authority": "^0.7.0",
15 | "clsx": "^2.1.1",
16 | "framer-motion": "^10.18.0",
17 | "lucide-react": "^0.344.0",
18 | "next": "13.5.1",
19 | "next-themes": "^0.3.0",
20 | "postcss": "^8.4.35",
21 | "react": "18.2.0",
22 | "react-dom": "18.2.0",
23 | "react-syntax-highlighter": "^15.5.0",
24 | "tailwind-merge": "^2.2.1",
25 | "tailwindcss": "3.3.3",
26 | "tailwindcss-animate": "^1.0.7"
27 | },
28 | "devDependencies": {
29 | "@types/node": "22.9.1",
30 | "@types/react": "18.3.12",
31 | "@types/react-syntax-highlighter": "^15.5.11",
32 | "typescript": "5.6.3"
33 | }
34 | }
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss';
2 |
3 | const config: Config = {
4 | darkMode: ['class'],
5 | content: [
6 | './pages/**/*.{js,ts,jsx,tsx,mdx}',
7 | './components/**/*.{js,ts,jsx,tsx,mdx}',
8 | './app/**/*.{js,ts,jsx,tsx,mdx}',
9 | ],
10 | theme: {
11 | extend: {
12 | colors: {
13 | primary: 'var(--primary)',
14 | secondary: 'var(--secondary)',
15 | accent: 'var(--accent)',
16 | background: 'var(--background)',
17 | text: 'var(--text)',
18 | muted: 'var(--muted)',
19 | border: 'var(--border)',
20 | error: 'var(--error)',
21 | success: 'var(--success)',
22 | warning: 'var(--warning)',
23 | info: 'var(--info)',
24 | },
25 | borderRadius: {
26 | lg: 'var(--radius)',
27 | md: 'calc(var(--radius) - 2px)',
28 | sm: 'calc(var(--radius) - 4px)',
29 | },
30 | },
31 | },
32 | plugins: [require('tailwindcss-animate')],
33 | };
34 |
35 | export default config;
--------------------------------------------------------------------------------
/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { motion } from 'framer-motion';
5 | import { cn } from '@/lib/utils';
6 |
7 | interface ToggleProps {
8 | pressed?: boolean;
9 | onPressedChange?: (pressed: boolean) => void;
10 | disabled?: boolean;
11 | className?: string;
12 | children?: React.ReactNode;
13 | }
14 |
15 | export function Toggle({
16 | pressed = false,
17 | onPressedChange,
18 | disabled = false,
19 | className,
20 | children,
21 | }: ToggleProps) {
22 | return (
23 | !disabled && onPressedChange?.(!pressed)}
26 | className={cn(
27 | "inline-flex items-center justify-center rounded-xl px-4 py-2",
28 | "transition-all duration-200 text-sm font-medium",
29 | "bg-black/20 backdrop-blur-xl border border-white/20",
30 | pressed && "bg-gradient-to-r from-[#FF6B6B] to-[#FF8E53] border-transparent",
31 | disabled && "opacity-50 cursor-not-allowed",
32 | !disabled && "hover:bg-white/5",
33 | className
34 | )}
35 | >
36 | {children}
37 |
38 | );
39 | }
--------------------------------------------------------------------------------
/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { Radio } from './radio';
5 | import { cn } from '@/lib/utils';
6 |
7 | interface RadioOption {
8 | label: string;
9 | value: string;
10 | disabled?: boolean;
11 | }
12 |
13 | interface RadioGroupProps {
14 | options: RadioOption[];
15 | value?: string;
16 | onChange?: (value: string) => void;
17 | name: string;
18 | className?: string;
19 | orientation?: 'horizontal' | 'vertical';
20 | }
21 |
22 | export function RadioGroup({
23 | options,
24 | value,
25 | onChange,
26 | name,
27 | className,
28 | orientation = 'vertical'
29 | }: RadioGroupProps) {
30 | return (
31 |
36 | {options.map((option) => (
37 | onChange?.(option.value)}
44 | disabled={option.disabled}
45 | />
46 | ))}
47 |
48 | );
49 | }
--------------------------------------------------------------------------------
/components/background-pattern.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { motion } from 'framer-motion';
4 |
5 | export function BackgroundPattern() {
6 | return (
7 |
8 |
25 |
42 |
43 | );
44 | }
--------------------------------------------------------------------------------
/components/code-block.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
4 | import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
5 |
6 | interface CodeBlockProps {
7 | code: string;
8 | language: string;
9 | }
10 |
11 | export function CodeBlock({ code, language }: CodeBlockProps) {
12 | return (
13 |
14 |
21 |
22 |
33 | {code.trim()}
34 |
35 |
36 |
37 | );
38 | }
--------------------------------------------------------------------------------
/components/ui/container.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { motion } from 'framer-motion';
5 | import { useTheme } from '@/hooks/use-theme';
6 | import { cn } from '@/lib/utils';
7 |
8 | interface ContainerProps extends React.HTMLAttributes {
9 | size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
10 | padding?: 'none' | 'sm' | 'md' | 'lg';
11 | center?: boolean;
12 | as?: React.ElementType;
13 | }
14 |
15 | export function Container({
16 | children,
17 | className,
18 | size = 'lg',
19 | padding = 'md',
20 | center = true,
21 | as: Component = 'div',
22 | ...props
23 | }: ContainerProps) {
24 | const { theme } = useTheme();
25 |
26 | const sizeClasses = {
27 | sm: 'max-w-2xl',
28 | md: 'max-w-4xl',
29 | lg: 'max-w-6xl',
30 | xl: 'max-w-7xl',
31 | full: 'max-w-full'
32 | };
33 |
34 | const paddingClasses = {
35 | none: 'px-0',
36 | sm: 'px-4',
37 | md: 'px-6',
38 | lg: 'px-8'
39 | };
40 |
41 | return (
42 |
56 | {children}
57 |
58 | );
59 | }
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --primary: #FF6B6B;
7 | --secondary: #4ECDC4;
8 | --accent: #45B7D1;
9 | --background: #0A0A0A;
10 | --text: #FFFFFF;
11 | --muted: #666666;
12 | --border: #2A2A2A;
13 | --error: #FF4D4D;
14 | --success: #4CAF50;
15 | --warning: #FFA726;
16 | --info: #29B6F6;
17 | --radius: 1rem;
18 | }
19 |
20 | body {
21 | background-color: var(--background);
22 | color: var(--text);
23 | }
24 |
25 | /* Hide scrollbar for Chrome, Safari and Opera */
26 | ::-webkit-scrollbar {
27 | display: none;
28 | }
29 |
30 | /* Hide scrollbar for IE, Edge and Firefox */
31 | * {
32 | -ms-overflow-style: none; /* IE and Edge */
33 | scrollbar-width: none; /* Firefox */
34 | }
35 |
36 | /* Ensure content is still scrollable */
37 | html, body {
38 | overflow-y: auto;
39 | overflow-x: hidden;
40 | }
41 |
42 | /* Add smooth scrolling for a better user experience */
43 | html {
44 | scroll-behavior: smooth;
45 | }
46 |
47 |
48 | @keyframes progress-indeterminate {
49 | 0% {
50 | transform: translateX(-100%);
51 | }
52 | 100% {
53 | transform: translateX(100%);
54 | }
55 | }
56 |
57 | @keyframes progress-stripes {
58 | 0% {
59 | background-position: 1rem 0;
60 | }
61 | 100% {
62 | background-position: 0 0;
63 | }
64 | }
65 |
66 | .animate-progress-indeterminate {
67 | animation: progress-indeterminate 1.5s infinite linear;
68 | }
69 |
70 | .animate-progress {
71 | animation: progress-stripes 1s linear infinite;
72 | }
--------------------------------------------------------------------------------
/lib/themes.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue } from 'clsx';
2 |
3 | export type ThemeColors = {
4 | primary: string;
5 | secondary: string;
6 | accent: string;
7 | background: string;
8 | text: string;
9 | muted: string;
10 | border: string;
11 | error: string;
12 | success: string;
13 | warning: string;
14 | info: string;
15 | };
16 |
17 | export type ThemeGradients = {
18 | primary: string;
19 | secondary: string;
20 | accent: string;
21 | };
22 |
23 | export type ThemeConfig = {
24 | colors: ThemeColors;
25 | gradients: ThemeGradients;
26 | borderRadius: string;
27 | fontFamily: string;
28 | };
29 |
30 | export const defaultTheme: ThemeConfig = {
31 | colors: {
32 | primary: '#FF6B6B',
33 | secondary: '#4ECDC4',
34 | accent: '#45B7D1',
35 | background: '#0A0A0A',
36 | text: '#FFFFFF',
37 | muted: '#666666',
38 | border: '#2A2A2A',
39 | error: '#FF4D4D',
40 | success: '#4CAF50',
41 | warning: '#FFA726',
42 | info: '#29B6F6'
43 | },
44 | gradients: {
45 | primary: 'from-[#FF6B6B] to-[#FF8E53]',
46 | secondary: 'from-[#4ECDC4] to-[#45B7D1]',
47 | accent: 'from-[#A8E6CF] to-[#3EECAC]'
48 | },
49 | borderRadius: '0.5rem',
50 | fontFamily: 'Inter, sans-serif'
51 | };
52 |
53 | export const darkTheme: ThemeConfig = {
54 | ...defaultTheme,
55 | colors: {
56 | ...defaultTheme.colors,
57 | background: '#000000',
58 | text: '#FFFFFF',
59 | muted: '#888888',
60 | border: '#333333'
61 | }
62 | };
63 |
64 | export const lightTheme: ThemeConfig = {
65 | ...defaultTheme,
66 | colors: {
67 | ...defaultTheme.colors,
68 | background: '#FFFFFF',
69 | text: '#000000',
70 | muted: '#666666',
71 | border: '#E0E0E0'
72 | }
73 | };
--------------------------------------------------------------------------------
/components/ui/space.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { cn } from '@/lib/utils';
5 | import { useTheme } from '@/hooks/use-theme';
6 |
7 | interface SpaceProps {
8 | size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl';
9 | direction?: 'horizontal' | 'vertical';
10 | className?: string;
11 | children?: React.ReactNode;
12 | }
13 |
14 | const spaceValues = {
15 | xs: '0.25rem',
16 | sm: '0.5rem',
17 | md: '1rem',
18 | lg: '1.5rem',
19 | xl: '2rem',
20 | '2xl': '3rem',
21 | '3xl': '4rem',
22 | '4xl': '6rem',
23 | };
24 |
25 | export function Space({
26 | size = 'md',
27 | direction = 'vertical',
28 | className,
29 | children
30 | }: SpaceProps) {
31 | const { theme } = useTheme();
32 |
33 | const style = {
34 | '--space-size': spaceValues[size],
35 | } as React.CSSProperties;
36 |
37 | if (children) {
38 | return (
39 |
47 | {React.Children.map(children, (child, index) => {
48 | if (index === React.Children.count(children) - 1) return child;
49 | return (
50 | <>
51 | {child}
52 |
58 | >
59 | );
60 | })}
61 |
62 | );
63 | }
64 |
65 | return (
66 |
73 | );
74 | }
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { cn } from '@/lib/utils';
5 | import { useTheme } from '@/hooks/use-theme';
6 |
7 | interface TextareaProps extends React.TextareaHTMLAttributes {
8 | error?: string;
9 | helperText?: string;
10 | resize?: 'none' | 'vertical' | 'horizontal' | 'both';
11 | variant?: 'default' | 'filled' | 'outline' | 'ghost';
12 | }
13 |
14 | export function Textarea({
15 | className,
16 | error,
17 | helperText,
18 | resize = 'vertical',
19 | variant = 'default',
20 | disabled,
21 | ...props
22 | }: TextareaProps) {
23 | const { theme } = useTheme();
24 |
25 | const variantStyles = {
26 | default: 'bg-white/5 border border-white/10',
27 | filled: 'bg-white/10 border-transparent',
28 | outline: 'bg-transparent border border-white/20',
29 | ghost: 'bg-transparent border-transparent hover:bg-white/5'
30 | };
31 |
32 | return (
33 |
34 |
56 | {(error || helperText) && (
57 |
61 | {error || helperText}
62 |
63 | )}
64 |
65 | );
66 | }
--------------------------------------------------------------------------------
/components/ui/breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { ChevronRight } from 'lucide-react';
5 | import { cn } from '@/lib/utils';
6 | import { useTheme } from '@/hooks/use-theme';
7 |
8 | export interface BreadcrumbItem {
9 | label: string;
10 | href?: string;
11 | icon?: React.ReactNode;
12 | }
13 |
14 | interface BreadcrumbProps {
15 | items: BreadcrumbItem[];
16 | separator?: React.ReactNode;
17 | className?: string;
18 | }
19 |
20 | export function Breadcrumb({
21 | items,
22 | separator = ,
23 | className
24 | }: BreadcrumbProps) {
25 | const { theme } = useTheme();
26 |
27 | return (
28 |
67 | );
68 | }
--------------------------------------------------------------------------------
/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { motion, HTMLMotionProps } from 'framer-motion';
5 | import { cn } from '@/lib/utils';
6 | import { useTheme } from '@/hooks/use-theme';
7 |
8 | export interface SkeletonProps extends HTMLMotionProps<"div"> {
9 | variant?: 'default' | 'circular' | 'rounded';
10 | width?: string | number;
11 | height?: string | number;
12 | animated?: boolean;
13 | }
14 |
15 | export function Skeleton({
16 | variant = 'default',
17 | width,
18 | height,
19 | animated = true,
20 | className,
21 | ...props
22 | }: SkeletonProps) {
23 | const { theme } = useTheme();
24 |
25 | const baseStyles = {
26 | backgroundColor: `${theme.colors.border}`,
27 | width: width || '100%',
28 | height: height || '1rem',
29 | };
30 |
31 | const variants = {
32 | default: 'rounded-md',
33 | circular: 'rounded-full',
34 | rounded: 'rounded-lg',
35 | };
36 |
37 | const animation = {
38 | initial: { opacity: 0.5 },
39 | animate: { opacity: 1 },
40 | transition: {
41 | duration: 1.5,
42 | repeat: Infinity,
43 | repeatType: 'reverse' as const,
44 | ease: 'easeInOut',
45 | },
46 | };
47 |
48 | return (
49 |
55 | );
56 | }
57 |
58 | export function SkeletonText({
59 | lines = 3,
60 | lastLineWidth = '70%',
61 | spacing = 'md',
62 | ...props
63 | }: {
64 | lines?: number;
65 | lastLineWidth?: string;
66 | spacing?: 'sm' | 'md' | 'lg';
67 | } & Omit) {
68 | const spacingMap = {
69 | sm: 'space-y-1',
70 | md: 'space-y-2',
71 | lg: 'space-y-3',
72 | };
73 |
74 | return (
75 |
76 | {Array.from({ length: lines }).map((_, i) => (
77 |
82 | ))}
83 |
84 | );
85 | }
--------------------------------------------------------------------------------
/components/ui/grid.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { useTheme } from '@/hooks/use-theme';
5 | import { cn } from '@/lib/utils';
6 |
7 | interface GridProps extends React.HTMLAttributes {
8 | cols?: 1 | 2 | 3 | 4 | 5 | 6 | 12;
9 | gap?: 'none' | 'sm' | 'md' | 'lg';
10 | responsive?: boolean;
11 | alignItems?: 'start' | 'center' | 'end' | 'stretch';
12 | justifyItems?: 'start' | 'center' | 'end' | 'stretch';
13 | flow?: 'row' | 'col' | 'dense';
14 | }
15 |
16 | export function Grid({
17 | children,
18 | className,
19 | cols = 3,
20 | gap = 'md',
21 | responsive = true,
22 | alignItems = 'stretch',
23 | justifyItems = 'stretch',
24 | flow = 'row',
25 | ...props
26 | }: GridProps) {
27 | const { theme } = useTheme();
28 |
29 | const gapClasses = {
30 | none: 'gap-0',
31 | sm: 'gap-4',
32 | md: 'gap-6',
33 | lg: 'gap-8'
34 | };
35 |
36 | const responsiveClasses = responsive
37 | ? {
38 | 1: 'grid-cols-1',
39 | 2: 'grid-cols-1 sm:grid-cols-2',
40 | 3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
41 | 4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
42 | 5: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5',
43 | 6: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6',
44 | 12: 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-12'
45 | }
46 | : {
47 | 1: 'grid-cols-1',
48 | 2: 'grid-cols-2',
49 | 3: 'grid-cols-3',
50 | 4: 'grid-cols-4',
51 | 5: 'grid-cols-5',
52 | 6: 'grid-cols-6',
53 | 12: 'grid-cols-12'
54 | };
55 |
56 | return (
57 |
74 | {children}
75 |
76 | );
77 | }
--------------------------------------------------------------------------------
/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { motion, AnimatePresence } from 'framer-motion';
5 | import { Check } from 'lucide-react';
6 | import { cn } from '@/lib/utils';
7 | import { useTheme } from '@/hooks/use-theme';
8 |
9 | interface CheckboxProps {
10 | checked?: boolean;
11 | onChange?: (checked: boolean) => void;
12 | label?: string;
13 | disabled?: boolean;
14 | className?: string;
15 | }
16 |
17 | export function Checkbox({
18 | checked = false,
19 | onChange,
20 | label,
21 | disabled = false,
22 | className,
23 | }: CheckboxProps) {
24 | const { theme } = useTheme();
25 |
26 | return (
27 |
68 | );
69 | }
--------------------------------------------------------------------------------
/components/ui/radio.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { motion } from 'framer-motion';
5 | import { cn } from '@/lib/utils';
6 | import { useTheme } from '@/hooks/use-theme';
7 |
8 | interface RadioProps {
9 | checked?: boolean;
10 | onChange?: (checked: boolean) => void;
11 | label?: string;
12 | disabled?: boolean;
13 | className?: string;
14 | value: string;
15 | name: string;
16 | }
17 |
18 | export function Radio({
19 | checked = false,
20 | onChange,
21 | label,
22 | disabled = false,
23 | className,
24 | value,
25 | name,
26 | }: RadioProps) {
27 | const { theme } = useTheme();
28 |
29 | return (
30 |
77 | );
78 | }
--------------------------------------------------------------------------------
/components/ui/spinner.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { motion } from 'framer-motion';
5 | import { useTheme } from '@/hooks/use-theme';
6 | import { cn } from '@/lib/utils';
7 |
8 | export interface SpinnerProps extends React.HTMLAttributes {
9 | size?: 'sm' | 'md' | 'lg' | 'xl';
10 | variant?: 'default' | 'gradient' | 'dots';
11 | color?: string;
12 | speed?: 'slow' | 'normal' | 'fast';
13 | }
14 |
15 | export function Spinner({
16 | size = 'md',
17 | variant = 'default',
18 | color,
19 | speed = 'normal',
20 | className,
21 | ...props
22 | }: SpinnerProps) {
23 | const { theme } = useTheme();
24 |
25 | const sizes = {
26 | sm: 'w-4 h-4',
27 | md: 'w-6 h-6',
28 | lg: 'w-8 h-8',
29 | xl: 'w-12 h-12',
30 | };
31 |
32 | const speeds = {
33 | slow: 2,
34 | normal: 1.5,
35 | fast: 1,
36 | };
37 |
38 | if (variant === 'dots') {
39 | return (
40 |
41 | {[1, 2, 3].map((i) => (
42 |
59 | ))}
60 |
61 | );
62 | }
63 |
64 | return (
65 |
90 | );
91 | }
--------------------------------------------------------------------------------
/components/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { createContext, useContext, useState, useEffect } from 'react';
4 | import { ThemeConfig, defaultTheme } from '@/lib/themes';
5 |
6 | type ThemeContextType = {
7 | theme: ThemeConfig;
8 | setTheme: (theme: ThemeConfig) => void;
9 | toggleColorMode: () => void;
10 | };
11 |
12 | export const ThemeContext = createContext({
13 | theme: defaultTheme,
14 | setTheme: () => {},
15 | toggleColorMode: () => {},
16 | });
17 |
18 | export function useTheme() {
19 | const context = useContext(ThemeContext);
20 | if (!context) {
21 | throw new Error('useTheme must be used within a ThemeProvider');
22 | }
23 | return context;
24 | }
25 |
26 | export function ThemeProvider({
27 | children,
28 | initialTheme = defaultTheme
29 | }: {
30 | children: React.ReactNode;
31 | initialTheme?: ThemeConfig;
32 | }) {
33 | const [theme, setTheme] = useState(initialTheme);
34 | const [mounted, setMounted] = useState(false);
35 |
36 | useEffect(() => {
37 | setMounted(true);
38 | }, []);
39 |
40 | const toggleColorMode = () => {
41 | setTheme(prevTheme => ({
42 | ...prevTheme,
43 | colors: {
44 | ...prevTheme.colors,
45 | background: prevTheme.colors.background === '#FFFFFF' ? '#000000' : '#FFFFFF',
46 | text: prevTheme.colors.text === '#000000' ? '#FFFFFF' : '#000000'
47 | }
48 | }));
49 | };
50 |
51 | const value = {
52 | theme,
53 | setTheme,
54 | toggleColorMode
55 | };
56 |
57 | if (!mounted) {
58 | return null;
59 | }
60 |
61 | return (
62 |
63 |
81 | {children}
82 |
83 |
84 | );
85 | }
--------------------------------------------------------------------------------
/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { motion } from 'framer-motion';
5 | import { cn } from '@/lib/utils';
6 | import { useTheme } from '@/hooks/use-theme';
7 |
8 | interface SwitchProps {
9 | checked?: boolean;
10 | onChange?: (checked: boolean) => void;
11 | label?: string;
12 | disabled?: boolean;
13 | className?: string;
14 | size?: 'sm' | 'md' | 'lg';
15 | }
16 |
17 | export function Switch({
18 | checked = false,
19 | onChange,
20 | label,
21 | disabled = false,
22 | className,
23 | size = 'md',
24 | }: SwitchProps) {
25 | const { theme } = useTheme();
26 |
27 | const sizes = {
28 | sm: { track: 'w-8 h-4', thumb: 'w-3 h-3', translate: 'translate-x-4' },
29 | md: { track: 'w-10 h-5', thumb: 'w-4 h-4', translate: 'translate-x-5' },
30 | lg: { track: 'w-12 h-6', thumb: 'w-5 h-5', translate: 'translate-x-6' },
31 | };
32 |
33 | return (
34 |
80 | );
81 | }
--------------------------------------------------------------------------------
/components/ui/stack.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { useTheme } from '@/hooks/use-theme';
5 | import { cn } from '@/lib/utils';
6 |
7 | export interface StackProps extends React.HTMLAttributes {
8 | children: React.ReactNode;
9 | direction?: 'horizontal' | 'vertical';
10 | spacing?: 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
11 | align?: 'start' | 'center' | 'end' | 'stretch';
12 | justify?: 'start' | 'center' | 'end' | 'between' | 'around';
13 | wrap?: boolean;
14 | dividers?: boolean;
15 | className?: string;
16 | }
17 |
18 | export function Stack({
19 | children,
20 | direction = 'vertical',
21 | spacing = 'md',
22 | align = 'stretch',
23 | justify = 'start',
24 | wrap = false,
25 | dividers = false,
26 | className,
27 | ...props
28 | }: StackProps) {
29 | const { theme } = useTheme();
30 |
31 | const getSpacing = () => {
32 | switch (spacing) {
33 | case 'none': return '0';
34 | case 'xs': return '0.5rem';
35 | case 'sm': return '1rem';
36 | case 'md': return '1.5rem';
37 | case 'lg': return '2rem';
38 | case 'xl': return '3rem';
39 | default: return '1.5rem';
40 | }
41 | };
42 |
43 | const stackStyles = {
44 | display: 'flex',
45 | flexDirection: direction === 'vertical' ? 'column' : 'row',
46 | alignItems: direction === 'vertical'
47 | ? align
48 | : { start: 'flex-start', center: 'center', end: 'flex-end', stretch: 'stretch' }[align],
49 | justifyContent: {
50 | start: 'flex-start',
51 | center: 'center',
52 | end: 'flex-end',
53 | between: 'space-between',
54 | around: 'space-around'
55 | }[justify],
56 | flexWrap: wrap ? 'wrap' : 'nowrap',
57 | gap: getSpacing(),
58 | } as React.CSSProperties;
59 |
60 | const childrenArray = React.Children.toArray(children).filter(Boolean);
61 |
62 | return (
63 |
68 | {childrenArray.map((child, index) => (
69 |
70 | {child}
71 | {dividers && index < childrenArray.length - 1 && (
72 |
82 | )}
83 |
84 | ))}
85 |
86 | );
87 | }
--------------------------------------------------------------------------------
/components/ui/divider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { useTheme } from '@/hooks/use-theme';
5 | import { cn } from '@/lib/utils';
6 |
7 | export interface DividerProps {
8 | orientation?: 'horizontal' | 'vertical';
9 | variant?: 'solid' | 'dashed' | 'dotted';
10 | size?: 'thin' | 'medium' | 'thick';
11 | label?: React.ReactNode;
12 | className?: string;
13 | }
14 |
15 | export function Divider({
16 | orientation = 'horizontal',
17 | variant = 'solid',
18 | size = 'medium',
19 | label,
20 | className
21 | }: DividerProps) {
22 | const { theme } = useTheme();
23 |
24 | const sizeStyles = {
25 | thin: '1px',
26 | medium: '2px',
27 | thick: '4px'
28 | };
29 |
30 | const baseStyles = {
31 | backgroundColor: theme.colors.border,
32 | borderStyle: variant,
33 | borderColor: theme.colors.border
34 | };
35 |
36 | if (orientation === 'vertical') {
37 | return (
38 |
50 | );
51 | }
52 |
53 | if (label) {
54 | return (
55 |
56 |
68 |
{label}
69 |
81 |
82 | );
83 | }
84 |
85 | return (
86 |
98 | );
99 | }
--------------------------------------------------------------------------------
/components/ui/toast-provider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useState, useCallback, createContext, useContext } from 'react';
4 | import type { ToastProps } from '@/components/ui/toast';
5 | import { Toast } from '@/components/ui/toast';
6 | import { AnimatePresence, motion } from 'framer-motion';
7 |
8 | interface ToastContextType {
9 | toasts: ToastProps[];
10 | toast: (options: Omit) => string;
11 | dismiss: (id: string) => void;
12 | dismissAll: () => void;
13 | }
14 |
15 | const ToastContext = createContext(undefined);
16 |
17 | export function useToast() {
18 | const context = useContext(ToastContext);
19 | if (!context) {
20 | throw new Error('useToast must be used within a ToastProvider');
21 | }
22 | return context;
23 | }
24 |
25 | export function ToastProvider({ children }: { children: React.ReactNode }) {
26 | const { toasts, toast, dismiss, dismissAll } = useToastProvider();
27 |
28 | return (
29 |
30 | {children}
31 |
32 |
33 | {toasts.map((toast) => (
34 |
41 |
42 |
43 | ))}
44 |
45 |
46 |
47 | );
48 | }
49 |
50 | function useToastProvider() {
51 | const [toasts, setToasts] = useState([]);
52 |
53 | const toast = useCallback((options: Omit) => {
54 | const id = Math.random().toString(36).substring(2, 9);
55 | const newToast: ToastProps = {
56 | ...options,
57 | id,
58 | onDismiss: () => dismiss(id),
59 | };
60 |
61 | setToasts((prev) => [...prev, newToast]);
62 |
63 | // Auto dismiss after duration
64 | if (options.duration !== 0) {
65 | setTimeout(() => {
66 | dismiss(id);
67 | }, options.duration || 3000);
68 | }
69 |
70 | return id;
71 | }, []);
72 |
73 | const dismiss = useCallback((id: string) => {
74 | setToasts((prev) => prev.filter((toast) => toast.id !== id));
75 | }, []);
76 |
77 | const dismissAll = useCallback(() => {
78 | setToasts([]);
79 | }, []);
80 |
81 | return {
82 | toasts,
83 | toast,
84 | dismiss,
85 | dismissAll,
86 | };
87 | }
88 |
89 | export type { ToastContextType };
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import { cva, type VariantProps } from 'class-variance-authority';
5 | import { cn } from '@/lib/utils';
6 |
7 | const badgeVariants = cva(
8 | 'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
9 | {
10 | variants: {
11 | variant: {
12 | default: 'bg-primary text-primary-foreground hover:bg-primary/80',
13 | secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
14 | success: 'bg-success text-white hover:bg-success/80',
15 | error: 'bg-error text-white hover:bg-error/80',
16 | warning: 'bg-warning text-white hover:bg-warning/80',
17 | info: 'bg-info text-white hover:bg-info/80',
18 | outline: 'text-foreground border border-input hover:bg-accent hover:text-accent-foreground',
19 | ghost: 'hover:bg-accent hover:text-accent-foreground',
20 | gradient: 'bg-gradient-to-r from-primary to-secondary text-white hover:opacity-90',
21 | },
22 | size: {
23 | sm: 'px-2 py-0.5 text-xs',
24 | md: 'px-2.5 py-0.5 text-sm',
25 | lg: 'px-3 py-1 text-base',
26 | },
27 | rounded: {
28 | default: 'rounded-full',
29 | sm: 'rounded-md',
30 | lg: 'rounded-xl',
31 | },
32 | removable: {
33 | true: 'pr-1',
34 | },
35 | },
36 | defaultVariants: {
37 | variant: 'default',
38 | size: 'md',
39 | rounded: 'default',
40 | removable: false,
41 | },
42 | }
43 | );
44 |
45 | export interface BadgeProps
46 | extends React.HTMLAttributes,
47 | VariantProps {
48 | onRemove?: () => void;
49 | }
50 |
51 | function Badge({
52 | className,
53 | variant,
54 | size,
55 | rounded,
56 | removable,
57 | onRemove,
58 | children,
59 | ...props
60 | }: BadgeProps) {
61 | return (
62 |
66 | {children}
67 | {removable && (
68 |
86 | )}
87 |
88 | );
89 | }
90 |
91 | export { Badge, badgeVariants };
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { cn } from '@/lib/utils';
5 | import { useTheme } from '@/hooks/use-theme';
6 |
7 | interface InputProps extends React.InputHTMLAttributes {
8 | variant?: 'default' | 'filled' | 'outline' | 'ghost';
9 | inputSize?: 'sm' | 'md' | 'lg';
10 | leftIcon?: React.ReactNode;
11 | rightIcon?: React.ReactNode;
12 | error?: string;
13 | success?: boolean;
14 | label?: string;
15 | helperText?: string;
16 | clearable?: boolean;
17 | onClear?: () => void;
18 | }
19 |
20 | export const Input = React.forwardRef(
21 | ({
22 | className,
23 | variant = 'default',
24 | inputSize = 'md',
25 | leftIcon,
26 | rightIcon,
27 | error,
28 | success,
29 | label,
30 | helperText,
31 | clearable,
32 | onClear,
33 | ...props
34 | }, ref) => {
35 | const { theme } = useTheme();
36 |
37 | const baseStyles = "w-full rounded-lg transition-all focus:outline-none focus:ring-2 focus:ring-offset-2";
38 |
39 | const variants = {
40 | default: "border border-border bg-white/5",
41 | filled: "bg-white/10 border-transparent",
42 | outline: "bg-transparent border-2 border-border",
43 | ghost: "bg-transparent border-transparent hover:bg-white/5",
44 | };
45 |
46 | const sizes = {
47 | sm: "px-3 py-1.5 text-sm",
48 | md: "px-4 py-2 text-base",
49 | lg: "px-6 py-3 text-lg",
50 | };
51 |
52 | const states = {
53 | error: "border-error focus:ring-error/50",
54 | success: "border-success focus:ring-success/50",
55 | };
56 |
57 | return (
58 |
59 | {label && (
60 |
63 | )}
64 |
65 | {leftIcon && (
66 |
67 | {leftIcon}
68 |
69 | )}
70 |
84 | {rightIcon && (
85 |
86 | {rightIcon}
87 |
88 | )}
89 |
90 | {helperText && (
91 |
{helperText}
92 | )}
93 | {error && (
94 |
{error}
95 | )}
96 |
97 | );
98 | }
99 | );
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { cn } from '@/lib/utils';
5 | import { useTheme } from '@/hooks/use-theme';
6 |
7 | interface ButtonProps extends React.ButtonHTMLAttributes {
8 | variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'link' | 'gradient';
9 | size?: 'sm' | 'md' | 'lg' | 'xl' | 'icon';
10 | isLoading?: boolean;
11 | leftIcon?: React.ReactNode;
12 | rightIcon?: React.ReactNode;
13 | fullWidth?: boolean;
14 | gradientFrom?: string;
15 | gradientTo?: string;
16 | }
17 |
18 | export const Button = React.forwardRef(
19 | ({
20 | className,
21 | variant = 'primary',
22 | size = 'md',
23 | isLoading,
24 | leftIcon,
25 | rightIcon,
26 | fullWidth,
27 | gradientFrom,
28 | gradientTo,
29 | children,
30 | ...props
31 | }, ref) => {
32 | const { theme } = useTheme();
33 |
34 | const baseStyles = "inline-flex items-center justify-center rounded-full font-medium focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed transition-transform duration-100 ease-in-out active:scale-95";
35 |
36 | const variants = {
37 | primary: "bg-primary text-white hover:opacity-90",
38 | secondary: "bg-secondary text-white hover:opacity-90",
39 | outline: "border-2 border-primary text-primary hover:bg-primary hover:text-white",
40 | ghost: "text-primary hover:bg-primary/10",
41 | link: "text-primary underline-offset-4 hover:underline",
42 | gradient: `bg-gradient-to-r from-[${gradientFrom || theme.colors.primary}] to-[${gradientTo || theme.colors.secondary}] text-white hover:opacity-90`,
43 | };
44 |
45 | const sizes = {
46 | sm: "px-3 py-1.5 text-sm",
47 | md: "px-4 py-2 text-base",
48 | lg: "px-6 py-3 text-lg",
49 | xl: "px-8 py-4 text-xl",
50 | icon: "p-2",
51 | };
52 |
53 | return (
54 |
79 | );
80 | }
81 | );
82 |
83 | Button.displayName = 'Button';
84 |
--------------------------------------------------------------------------------
/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { useTheme } from '@/hooks/use-theme';
5 | import { cn } from '@/lib/utils';
6 | import { AlertCircle, CheckCircle2, Info, XCircle, X } from 'lucide-react';
7 | import { motion, AnimatePresence } from 'framer-motion';
8 |
9 | export interface AlertProps {
10 | type?: 'info' | 'success' | 'warning' | 'error';
11 | title?: string;
12 | children: React.ReactNode;
13 | dismissible?: boolean;
14 | onDismiss?: () => void;
15 | variant?: 'filled' | 'outlined' | 'light';
16 | icon?: React.ReactNode;
17 | className?: string;
18 | }
19 |
20 | const icons = {
21 | info: Info,
22 | success: CheckCircle2,
23 | warning: AlertCircle,
24 | error: XCircle,
25 | };
26 |
27 | export function Alert({
28 | type = 'info',
29 | title,
30 | children,
31 | dismissible = false,
32 | onDismiss,
33 | variant = 'filled',
34 | icon,
35 | className,
36 | }: AlertProps) {
37 | const { theme } = useTheme();
38 |
39 | const getTypeColor = () => {
40 | switch (type) {
41 | case 'success':
42 | return theme.colors.success;
43 | case 'warning':
44 | return theme.colors.warning;
45 | case 'error':
46 | return theme.colors.error;
47 | default:
48 | return theme.colors.info;
49 | }
50 | };
51 |
52 | const getStyles = () => {
53 | const color = getTypeColor();
54 |
55 | switch (variant) {
56 | case 'outlined':
57 | return {
58 | backgroundColor: 'transparent',
59 | borderColor: color,
60 | color: color,
61 | };
62 | case 'light':
63 | return {
64 | backgroundColor: `${color}20`,
65 | borderColor: 'transparent',
66 | color: color,
67 | };
68 | default:
69 | return {
70 | backgroundColor: color,
71 | borderColor: 'transparent',
72 | color: theme.colors.background,
73 | };
74 | }
75 | };
76 |
77 |
78 |
79 | return (
80 |
81 |
91 | {icon || (
92 |
93 | {React.createElement(icons[type], {
94 | size: 20,
95 | color: getTypeColor(),
96 | })}
97 |
98 | )}
99 |
100 |
101 | {title && (
102 |
103 | {title}
104 |
105 | )}
106 |
107 | {children}
108 |
109 |
110 |
111 | {dismissible && (
112 |
118 | )}
119 |
120 |
121 | );
122 | }
--------------------------------------------------------------------------------
/components/ui/stats.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { motion } from 'framer-motion';
5 | import { useTheme } from '@/hooks/use-theme';
6 | import { cn } from '@/lib/utils';
7 |
8 | export interface StatProps {
9 | title: string;
10 | value: string | number;
11 | description?: string;
12 | icon?: React.ReactNode;
13 | trend?: {
14 | value: number;
15 | label?: string;
16 | direction: 'up' | 'down';
17 | };
18 | variant?: 'default' | 'gradient' | 'outline';
19 | size?: 'sm' | 'md' | 'lg';
20 | className?: string;
21 | }
22 |
23 | export interface StatsProps {
24 | items: StatProps[];
25 | columns?: 1 | 2 | 3 | 4;
26 | className?: string;
27 | }
28 |
29 | export function Stats({ items, columns = 4, className }: StatsProps) {
30 | const { theme } = useTheme();
31 |
32 | return (
33 |
43 | {items.map((stat, index) => (
44 |
45 | ))}
46 |
47 | );
48 | }
49 |
50 | export function Stat({
51 | title,
52 | value,
53 | description,
54 | icon,
55 | trend,
56 | variant = 'default',
57 | size = 'md',
58 | className,
59 | }: StatProps) {
60 | const { theme } = useTheme();
61 |
62 | const variants = {
63 | default: 'bg-white/5 border border-white/10',
64 | gradient: 'bg-gradient-to-br from-primary/20 to-secondary/20 border border-white/10',
65 | outline: 'border border-white/20 bg-transparent',
66 | };
67 |
68 | const sizes = {
69 | sm: 'p-4',
70 | md: 'p-6',
71 | lg: 'p-8',
72 | };
73 |
74 | return (
75 |
86 |
87 | {icon && (
88 |
92 | {icon}
93 |
94 | )}
95 | {trend && (
96 |
100 | {trend.direction === 'up' ? '↑' : '↓'}
101 | {trend.value}%
102 | {trend.label && (
103 |
104 | {trend.label}
105 |
106 | )}
107 |
108 | )}
109 |
110 |
111 |
112 |
{title}
113 |
{value}
114 | {description && (
115 |
{description}
116 | )}
117 |
118 |
119 | );
120 | }
--------------------------------------------------------------------------------
/components/ui/tree.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useState } from 'react';
4 | import { motion, AnimatePresence } from 'framer-motion';
5 | import { ChevronRight, Folder, File } from 'lucide-react';
6 | import { cn } from '@/lib/utils';
7 | import { useTheme } from '@/hooks/use-theme';
8 |
9 | export interface TreeNode {
10 | id: string;
11 | label: string;
12 | icon?: React.ReactNode;
13 | children?: TreeNode[];
14 | }
15 |
16 | interface TreeProps {
17 | data: TreeNode[];
18 | defaultExpanded?: boolean;
19 | onNodeClick?: (node: TreeNode) => void;
20 | className?: string;
21 | }
22 |
23 | interface TreeItemProps extends TreeNode {
24 | level: number;
25 | defaultExpanded?: boolean;
26 | onNodeClick?: (node: TreeNode) => void;
27 | }
28 |
29 | const TreeItem = ({ id, label, icon, children, level, defaultExpanded, onNodeClick }: TreeItemProps) => {
30 | const [isExpanded, setIsExpanded] = useState(defaultExpanded);
31 | const { theme } = useTheme();
32 | const hasChildren = children && children.length > 0;
33 |
34 | const handleClick = () => {
35 | if (hasChildren) {
36 | setIsExpanded(!isExpanded);
37 | }
38 | onNodeClick?.({ id, label, icon, children });
39 | };
40 |
41 | return (
42 |
43 |
0 && 'ml-6'
48 | )}
49 | onClick={handleClick}
50 | whileTap={{ scale: 0.98 }}
51 | >
52 | {hasChildren && (
53 |
58 |
59 |
60 | )}
61 | {!hasChildren && }
62 |
63 | {icon || (hasChildren ? : )}
64 |
65 | {label}
66 |
67 |
68 |
69 | {isExpanded && children && (
70 |
76 | {children.map((child) => (
77 |
84 | ))}
85 |
86 | )}
87 |
88 |
89 | );
90 | };
91 |
92 | export function Tree({ data, defaultExpanded = false, onNodeClick, className }: TreeProps) {
93 | return (
94 |
95 | {data.map((node) => (
96 |
103 | ))}
104 |
105 | );
106 | }
--------------------------------------------------------------------------------
/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { useTheme } from '@/hooks/use-theme';
5 | import { cn } from '@/lib/utils';
6 | import { motion, AnimatePresence } from 'framer-motion';
7 | import { AlertCircle, CheckCircle } from 'lucide-react';
8 |
9 | export interface FormProps extends React.FormHTMLAttributes {
10 | onSubmit?: (e: React.FormEvent) => void;
11 | loading?: boolean;
12 | error?: string;
13 | success?: string;
14 | className?: string;
15 | }
16 |
17 | export interface FormFieldProps {
18 | label?: string;
19 | error?: string;
20 | required?: boolean;
21 | className?: string;
22 | children: React.ReactNode;
23 | }
24 |
25 | export interface FormMessageProps {
26 | children: React.ReactNode;
27 | type?: 'error' | 'success';
28 | className?: string;
29 | }
30 |
31 | export function Form({ children, onSubmit, loading, error, success, className, ...props }: FormProps) {
32 | const { theme } = useTheme();
33 |
34 | const handleSubmit = (e: React.FormEvent) => {
35 | e.preventDefault();
36 | if (!loading && onSubmit) {
37 | onSubmit(e);
38 | }
39 | };
40 |
41 | return (
42 |
63 | );
64 | }
65 |
66 | export function FormField({ label, error, required, className, children }: FormFieldProps) {
67 | const { theme } = useTheme();
68 |
69 | return (
70 |
71 | {label && (
72 |
76 | )}
77 | {children}
78 |
79 | {error && (
80 |
85 | {error}
86 |
87 | )}
88 |
89 |
90 | );
91 | }
92 |
93 | export function FormMessage({ children, type = 'error', className }: FormMessageProps) {
94 | const { theme } = useTheme();
95 |
96 | return (
97 |
105 | {type === 'error' ? (
106 |
107 | ) : (
108 |
109 | )}
110 | {children}
111 |
112 | );
113 | }
--------------------------------------------------------------------------------
/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { motion } from 'framer-motion';
5 | import { useTheme } from '@/hooks/use-theme';
6 | import { cn } from '@/lib/utils';
7 |
8 | interface ProgressProps {
9 | value?: number;
10 | variant?: 'default' | 'gradient' | 'striped' | 'indeterminate';
11 | size?: 'sm' | 'md' | 'lg' | 'xl';
12 | color?: 'primary' | 'success' | 'warning' | 'error';
13 | gradientFrom?: string;
14 | gradientTo?: string;
15 | showValue?: boolean;
16 | animated?: boolean;
17 | className?: string;
18 | }
19 |
20 | export function Progress({
21 | value = 0,
22 | variant = 'default',
23 | size = 'md',
24 | color = 'primary',
25 | gradientFrom,
26 | gradientTo,
27 | showValue = false,
28 | animated = false,
29 | className,
30 | }: ProgressProps) {
31 | const { theme } = useTheme();
32 |
33 | const sizes = {
34 | sm: 'h-1',
35 | md: 'h-2',
36 | lg: 'h-3',
37 | xl: 'h-4',
38 | };
39 |
40 | const colors = {
41 | primary: theme.colors.primary,
42 | success: theme.colors.success,
43 | warning: theme.colors.warning,
44 | error: theme.colors.error,
45 | };
46 |
47 | const getBackgroundStyle = () => {
48 | if (variant === 'gradient') {
49 | return {
50 | background: `linear-gradient(to right, ${gradientFrom || theme.colors.primary}, ${gradientTo || theme.colors.secondary})`,
51 | };
52 | }
53 |
54 | if (variant === 'striped') {
55 | return {
56 | background: `linear-gradient(45deg,
57 | ${colors[color]}88 25%,
58 | ${colors[color]} 25%,
59 | ${colors[color]} 50%,
60 | ${colors[color]}88 50%,
61 | ${colors[color]}88 75%,
62 | ${colors[color]} 75%,
63 | ${colors[color]})`,
64 | backgroundSize: '1rem 1rem',
65 | };
66 | }
67 |
68 | return {
69 | backgroundColor: colors[color],
70 | };
71 | };
72 |
73 | return (
74 |
75 |
82 | {variant === 'indeterminate' ? (
83 |
97 | ) : (
98 |
111 | )}
112 |
113 | {showValue && variant !== 'indeterminate' && (
114 |
115 | {value}%
116 |
117 | )}
118 |
119 | );
120 | }
121 |
122 | export type { ProgressProps };
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import Link from 'next/link';
4 | import { ChevronRight, Feather, Package, Palette, Sparkles, Zap, Github } from 'lucide-react';
5 | import { BackgroundPattern } from '@/components/background-pattern';
6 | import { FeatureCard } from '@/components/feature-card';
7 | import { CodeBlock } from '@/components/code-block';
8 | import { Button } from '@/components/ui/button';
9 |
10 | const features = [
11 | {
12 | icon: ,
13 | title: "Next.js First",
14 | description: "Built specifically for Next.js App Router, offering seamless integration and optimal performance."
15 | },
16 | {
17 | icon: ,
18 | title: "Best of Both Worlds",
19 | description: "Combines Mantine's flexibility with shadcn/ui's simplicity and customization approach."
20 | },
21 | {
22 | icon: ,
23 | title: "Theme System",
24 | description: "Powerful theming with CSS variables, inspired by Mantine's theme system but more streamlined."
25 | },
26 | {
27 | icon: ,
28 | title: "Modern Design",
29 | description: "Beautiful, accessible components with smooth animations and modern aesthetics."
30 | }
31 | ];
32 |
33 | const exampleCode = `// Install ParrotUI
34 | npm install @parrot-ui/react
35 |
36 | // Import and use components
37 | import { Button } from '@parrot-ui/react'
38 |
39 | function App() {
40 | return (
41 |
44 | )
45 | }`;
46 |
47 | export default function Home() {
48 | return (
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | ParrotUI
58 |
59 |
60 |
61 | A beautiful React component library built specifically for Next.js App Router,
62 | combining the best features of Mantine and shadcn/ui.
63 |
64 |
65 |
69 | Get Started
70 |
71 |
72 |
76 | Learn More
77 |
78 |
79 |
80 |
81 |
82 | {features.map((feature, i) => (
83 |
84 | ))}
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | );
95 | }
--------------------------------------------------------------------------------
/components/ui/modal.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useEffect, useCallback } from 'react';
4 | import { motion, AnimatePresence } from 'framer-motion';
5 | import { X } from 'lucide-react';
6 | import { useTheme } from '@/hooks/use-theme';
7 | import { cn } from '@/lib/utils';
8 |
9 | export interface ModalProps {
10 | isOpen: boolean;
11 | onClose: () => void;
12 | children: React.ReactNode;
13 | title?: string;
14 | size?: 'sm' | 'md' | 'lg' | 'full';
15 | header?: React.ReactNode;
16 | footer?: React.ReactNode;
17 | closeOnOverlayClick?: boolean;
18 | closeOnEsc?: boolean;
19 | className?: string;
20 | }
21 |
22 | export function Modal({
23 | isOpen,
24 | onClose,
25 | children,
26 | title,
27 | size = 'md',
28 | header,
29 | footer,
30 | closeOnOverlayClick = true,
31 | closeOnEsc = true,
32 | className,
33 | }: ModalProps) {
34 | const { theme } = useTheme();
35 |
36 | const handleEscapeKey = useCallback((event: KeyboardEvent) => {
37 | if (event.key === 'Escape' && closeOnEsc) {
38 | onClose();
39 | }
40 | }, [closeOnEsc, onClose]);
41 |
42 | useEffect(() => {
43 | if (isOpen && closeOnEsc) {
44 | document.addEventListener('keydown', handleEscapeKey);
45 | return () => document.removeEventListener('keydown', handleEscapeKey);
46 | }
47 | }, [isOpen, closeOnEsc, handleEscapeKey]);
48 |
49 | const sizeClasses = {
50 | sm: 'max-w-sm',
51 | md: 'max-w-md',
52 | lg: 'max-w-lg',
53 | full: 'max-w-full m-4',
54 | };
55 |
56 | return (
57 |
58 | {isOpen && (
59 |
60 | {/* Overlay */}
61 |
69 |
70 | {/* Modal */}
71 |
85 | {/* Default Header */}
86 | {!header && title && (
87 |
88 |
89 | {title}
90 |
91 |
97 |
98 | )}
99 |
100 | {/* Custom Header */}
101 | {header}
102 |
103 | {/* Content */}
104 |
105 | {children}
106 |
107 |
108 | {/* Footer */}
109 | {footer}
110 |
111 |
112 | )}
113 |
114 | );
115 | }
--------------------------------------------------------------------------------
/components/ui/stepper.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { cn } from '@/lib/utils';
5 | import { Check } from 'lucide-react';
6 | import { motion } from 'framer-motion';
7 | import { useTheme } from '@/hooks/use-theme';
8 |
9 | export interface StepperProps {
10 | steps: {
11 | title: string;
12 | description?: string;
13 | icon?: React.ReactNode;
14 | }[];
15 | currentStep: number;
16 | orientation?: 'horizontal' | 'vertical';
17 | variant?: 'default' | 'outlined' | 'numbered';
18 | size?: 'sm' | 'md' | 'lg';
19 | className?: string;
20 | }
21 |
22 | export function Stepper({
23 | steps,
24 | currentStep,
25 | orientation = 'horizontal',
26 | variant = 'default',
27 | size = 'md',
28 | className
29 | }: StepperProps) {
30 | const { theme } = useTheme();
31 |
32 | const sizeClasses = {
33 | sm: 'h-8 w-8 text-sm',
34 | md: 'h-10 w-10 text-base',
35 | lg: 'h-12 w-12 text-lg'
36 | };
37 |
38 | const contentSizeClasses = {
39 | sm: 'space-y-0.5',
40 | md: 'space-y-1',
41 | lg: 'space-y-2'
42 | };
43 |
44 | const lineClasses = {
45 | horizontal: 'h-[2px] w-full',
46 | vertical: 'w-[2px] h-full'
47 | };
48 |
49 | return (
50 |
57 | {steps.map((step, index) => {
58 | const isCompleted = currentStep > index + 1;
59 | const isCurrent = currentStep === index + 1;
60 | const isLast = index === steps.length - 1;
61 |
62 | return (
63 |
70 |
71 |
84 | {isCompleted ? (
85 |
86 | ) : variant === 'numbered' ? (
87 | index + 1
88 | ) : step.icon || index + 1}
89 |
90 |
91 | {!isLast && (
92 |
99 | )}
100 |
101 |
102 |
108 |
{step.title}
109 | {step.description && (
110 |
111 | {step.description}
112 |
113 | )}
114 |
115 |
116 | );
117 | })}
118 |
119 | );
120 | }
--------------------------------------------------------------------------------
/components/ui/timeline.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { useTheme } from '@/hooks/use-theme';
5 | import { cn } from '@/lib/utils';
6 | import { Circle } from 'lucide-react';
7 |
8 | export interface TimelineItem {
9 | id: string;
10 | title: string;
11 | description?: string;
12 | date?: string;
13 | icon?: React.ReactNode;
14 | status?: 'complete' | 'current' | 'upcoming';
15 | }
16 |
17 | interface TimelineProps {
18 | items: TimelineItem[];
19 | orientation?: 'vertical' | 'horizontal';
20 | variant?: 'default' | 'alternate' | 'compact';
21 | showConnectors?: boolean;
22 | className?: string;
23 | }
24 |
25 | export function Timeline({
26 | items,
27 | orientation = 'vertical',
28 | variant = 'default',
29 | showConnectors = true,
30 | className,
31 | }: TimelineProps) {
32 | const { theme } = useTheme();
33 |
34 | const getStatusColor = (status?: TimelineItem['status']) => {
35 | switch (status) {
36 | case 'complete':
37 | return theme.colors.success;
38 | case 'current':
39 | return theme.colors.primary;
40 | case 'upcoming':
41 | return theme.colors.muted;
42 | default:
43 | return theme.colors.border;
44 | }
45 | };
46 |
47 | const timelineClasses = cn(
48 | 'relative',
49 | orientation === 'horizontal' ? 'flex' : 'flex flex-col',
50 | className
51 | );
52 |
53 | const itemClasses = cn(
54 | 'relative flex',
55 | orientation === 'horizontal' ? 'flex-col items-center px-4' : 'items-start',
56 | variant === 'compact' && 'gap-2',
57 | variant !== 'compact' && 'gap-4'
58 | );
59 |
60 | const renderConnector = (index: number) => {
61 | if (!showConnectors || index === items.length - 1) return null;
62 |
63 | return (
64 |
73 | );
74 | };
75 |
76 | return (
77 |
78 | {items.map((item, index) => (
79 |
92 |
93 |
97 | {item.icon || }
98 |
99 | {renderConnector(index)}
100 |
101 |
102 |
103 |
104 |
105 | {item.title}
106 |
107 | {item.date && (
108 |
109 | {item.date}
110 |
111 | )}
112 | {item.description && (
113 |
117 | {item.description}
118 |
119 | )}
120 |
121 |
122 |
123 | ))}
124 |
125 | );
126 | }
--------------------------------------------------------------------------------
/components/ui/list.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { cn } from '@/lib/utils';
5 | import { motion } from 'framer-motion';
6 | import { useTheme } from '@/hooks/use-theme';
7 | import { Check, Circle, ChevronRight } from 'lucide-react';
8 |
9 | export interface ListProps extends React.HTMLAttributes {
10 | variant?: 'unordered' | 'ordered' | 'checked' | 'arrow';
11 | spacing?: 'compact' | 'normal' | 'relaxed';
12 | marker?: React.ReactNode;
13 | hoverable?: boolean;
14 | animated?: boolean;
15 | children: React.ReactNode;
16 | }
17 |
18 | export interface ListItemProps extends React.HTMLAttributes {
19 | icon?: React.ReactNode;
20 | active?: boolean;
21 | disabled?: boolean;
22 | children: React.ReactNode;
23 | }
24 |
25 | const ListItem = React.forwardRef(
26 | ({ icon, active, disabled, children, className, ...props }, ref) => {
27 | const { theme } = useTheme();
28 |
29 | return (
30 |
40 | {icon && (
41 |
42 | {icon}
43 |
44 | )}
45 | {children}
46 |
47 | );
48 | }
49 | );
50 |
51 | ListItem.displayName = 'ListItem';
52 |
53 | const List = React.forwardRef(
54 | ({
55 | variant = 'unordered',
56 | spacing = 'normal',
57 | marker,
58 | hoverable = false,
59 | animated = false,
60 | className,
61 | children,
62 | ...props
63 | }, ref) => {
64 | const { theme } = useTheme();
65 |
66 | const spacingClasses = {
67 | compact: 'space-y-1',
68 | normal: 'space-y-2',
69 | relaxed: 'space-y-4'
70 | };
71 |
72 | const defaultMarkers = {
73 | unordered: ,
74 | checked: ,
75 | arrow:
76 | };
77 |
78 | const listVariant = variant === 'ordered' ? 'ol' : 'ul';
79 | const ListComponent = listVariant as keyof JSX.IntrinsicElements;
80 |
81 | const items = React.Children.map(children, (child, index) => {
82 | if (!React.isValidElement(child)) return null;
83 |
84 | const itemProps: any = {
85 | ...child.props,
86 | className: cn(
87 | hoverable && 'transition-colors duration-200 hover:bg-white/5 rounded-lg px-2',
88 | child.props.className
89 | )
90 | };
91 |
92 | if (variant !== 'ordered' && !child.props.icon) {
93 | itemProps.icon = marker || defaultMarkers[variant as keyof typeof defaultMarkers];
94 | }
95 |
96 | if (animated) {
97 | return (
98 |
103 | {React.cloneElement(child, itemProps)}
104 |
105 | );
106 | }
107 |
108 | return React.cloneElement(child, itemProps);
109 | });
110 |
111 | return React.createElement(
112 | ListComponent,
113 | {
114 | ref,
115 | className: cn(
116 | spacingClasses[spacing],
117 | variant === 'ordered' && 'list-decimal pl-4',
118 | className
119 | ),
120 | style: {
121 | '--list-marker-color': theme.colors.primary
122 | } as React.CSSProperties,
123 | ...props
124 | },
125 | items
126 | );
127 | }
128 | );
129 |
130 | List.displayName = 'List';
131 |
132 | export { List, ListItem };
--------------------------------------------------------------------------------
/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useEffect } from 'react';
4 | import { motion, AnimatePresence } from 'framer-motion';
5 | import { useTheme } from '@/hooks/use-theme';
6 | import { X, CheckCircle2, AlertCircle, AlertTriangle, Info } from 'lucide-react';
7 | import { cn } from '@/lib/utils';
8 |
9 | export type ToastType = 'success' | 'error' | 'warning' | 'info';
10 | export type ToastPosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
11 |
12 | export interface ToastProps {
13 | id: string;
14 | title?: string;
15 | description?: string;
16 | type?: ToastType;
17 | duration?: number;
18 | position?: ToastPosition;
19 | icon?: React.ReactNode;
20 | dismissible?: boolean;
21 | onDismiss?: () => void;
22 | }
23 |
24 | const icons = {
25 | success: CheckCircle2,
26 | error: AlertCircle,
27 | warning: AlertTriangle,
28 | info: Info,
29 | };
30 |
31 | const positions = {
32 | 'top-right': 'top-4 right-4',
33 | 'top-left': 'top-4 left-4',
34 | 'bottom-right': 'bottom-4 right-4',
35 | 'bottom-left': 'bottom-4 left-4',
36 | };
37 |
38 | export function Toast({
39 | id,
40 | title,
41 | description,
42 | type = 'info',
43 | duration = 3000,
44 | position = 'top-right',
45 | icon,
46 | dismissible = true,
47 | onDismiss,
48 | }: ToastProps) {
49 | const { theme } = useTheme();
50 | const Icon = icons[type];
51 |
52 | useEffect(() => {
53 | if (duration > 0) {
54 | const timer = setTimeout(() => {
55 | onDismiss?.();
56 | }, duration);
57 |
58 | return () => clearTimeout(timer);
59 | }
60 | }, [duration, onDismiss]);
61 |
62 | const getTypeStyles = () => {
63 | switch (type) {
64 | case 'success':
65 | return {
66 | background: theme.colors.success,
67 | text: '#FFFFFF',
68 | };
69 | case 'error':
70 | return {
71 | background: theme.colors.error,
72 | text: '#FFFFFF',
73 | };
74 | case 'warning':
75 | return {
76 | background: theme.colors.warning,
77 | text: '#FFFFFF',
78 | };
79 | case 'info':
80 | default:
81 | return {
82 | background: theme.colors.info,
83 | text: '#FFFFFF',
84 | };
85 | }
86 | };
87 |
88 | const styles = getTypeStyles();
89 |
90 | return (
91 |
102 |
103 | {(icon ||
) && (
104 |
105 | {icon || }
106 |
107 | )}
108 |
109 | {title && (
110 |
111 | {title}
112 |
113 | )}
114 | {description && (
115 |
116 | {description}
117 |
118 | )}
119 |
120 | {dismissible && (
121 |
127 | )}
128 |
129 |
130 | );
131 | }
132 |
133 | export function ToastContainer({ children }: { children: React.ReactNode }) {
134 | return (
135 |
136 | {children}
137 |
138 | );
139 | }
--------------------------------------------------------------------------------
/components/ui/pagination.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
5 | import { cn } from '@/lib/utils';
6 | import { useTheme } from '@/hooks/use-theme';
7 |
8 | interface PaginationProps {
9 | currentPage: number;
10 | totalPages: number;
11 | onPageChange: (page: number) => void;
12 | siblingCount?: number;
13 | showFirstLast?: boolean;
14 | size?: 'sm' | 'md' | 'lg';
15 | variant?: 'default' | 'outline' | 'ghost';
16 | className?: string;
17 | }
18 |
19 | export function Pagination({
20 | currentPage,
21 | totalPages,
22 | onPageChange,
23 | siblingCount = 1,
24 | showFirstLast = true,
25 | size = 'md',
26 | variant = 'default',
27 | className
28 | }: PaginationProps) {
29 | const { theme } = useTheme();
30 |
31 | const getPageNumbers = () => {
32 | const pageNumbers = [];
33 | const leftSibling = Math.max(currentPage - siblingCount, 1);
34 | const rightSibling = Math.min(currentPage + siblingCount, totalPages);
35 |
36 | for (let i = leftSibling; i <= rightSibling; i++) {
37 | pageNumbers.push(i);
38 | }
39 |
40 | if (leftSibling > 1) {
41 | if (leftSibling > 2) {
42 | pageNumbers.unshift('...');
43 | }
44 | pageNumbers.unshift(1);
45 | }
46 |
47 | if (rightSibling < totalPages) {
48 | if (rightSibling < totalPages - 1) {
49 | pageNumbers.push('...');
50 | }
51 | pageNumbers.push(totalPages);
52 | }
53 |
54 | return pageNumbers;
55 | };
56 |
57 | const sizeClasses = {
58 | sm: 'h-8 w-8 text-sm',
59 | md: 'h-10 w-10 text-base',
60 | lg: 'h-12 w-12 text-lg'
61 | };
62 |
63 | const variantClasses = {
64 | default: 'bg-white/10 hover:bg-white/20',
65 | outline: 'border border-white/20 hover:bg-white/10',
66 | ghost: 'hover:bg-white/10'
67 | };
68 |
69 | const baseButtonClasses = cn(
70 | 'flex items-center justify-center rounded-lg transition-colors duration-200',
71 | 'disabled:opacity-50 disabled:cursor-not-allowed',
72 | sizeClasses[size],
73 | variantClasses[variant]
74 | );
75 |
76 | const activeButtonClasses = cn(
77 | 'bg-gradient-to-r',
78 | 'from-[#FF6B6B] to-[#4ECDC4]',
79 | 'text-white'
80 | );
81 |
82 | return (
83 |
143 | );
144 | }
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useState, useRef, useEffect } from 'react';
4 | import { motion, AnimatePresence } from 'framer-motion';
5 | import { ChevronDown } from 'lucide-react';
6 | import { cn } from '@/lib/utils';
7 | import { useTheme } from '@/hooks/use-theme';
8 |
9 | export interface SelectOption {
10 | value: string;
11 | label: string;
12 | }
13 |
14 | interface SelectProps {
15 | options: SelectOption[];
16 | value?: string;
17 | onChange?: (value: string) => void;
18 | placeholder?: string;
19 | className?: string;
20 | disabled?: boolean;
21 | }
22 |
23 | export function Select({
24 | options,
25 | value,
26 | onChange,
27 | placeholder = 'Select option',
28 | className,
29 | disabled = false,
30 | }: SelectProps) {
31 | const { theme } = useTheme();
32 | const [isOpen, setIsOpen] = useState(false);
33 | const [selectedOption, setSelectedOption] = useState(
34 | options.find(opt => opt.value === value) || null
35 | );
36 | const selectRef = useRef(null);
37 |
38 | useEffect(() => {
39 | const handleClickOutside = (event: MouseEvent) => {
40 | if (selectRef.current && !selectRef.current.contains(event.target as Node)) {
41 | setIsOpen(false);
42 | }
43 | };
44 |
45 | document.addEventListener('mousedown', handleClickOutside);
46 | return () => document.removeEventListener('mousedown', handleClickOutside);
47 | }, []);
48 |
49 | const handleSelect = (option: SelectOption) => {
50 | setSelectedOption(option);
51 | setIsOpen(false);
52 | onChange?.(option.value);
53 | };
54 |
55 | return (
56 |
57 |
!disabled && setIsOpen(!isOpen)}
60 | style={{
61 | borderColor: theme.colors.border,
62 | backgroundColor: `${theme.colors.background}40`,
63 | color: theme.colors.text,
64 | }}
65 | className={cn(
66 | "w-full flex items-center justify-between px-4 py-2 rounded-xl",
67 | "backdrop-blur-xl border",
68 | "text-sm focus:outline-none",
69 | "transition-all duration-200",
70 | disabled && "opacity-50 cursor-not-allowed",
71 | className
72 | )}
73 | >
74 |
75 | {selectedOption ? selectedOption.label : placeholder}
76 |
77 |
81 |
82 |
83 |
84 |
85 |
86 | {isOpen && (
87 |
98 |
99 | {options.map((option) => (
100 |
handleSelect(option)}
104 | style={{
105 | color: theme.colors.text,
106 | backgroundColor: selectedOption?.value === option.value ? `${theme.colors.primary}20` : 'transparent',
107 | }}
108 | className={cn(
109 | "w-full px-4 py-2 text-sm text-left",
110 | "transition-colors duration-150 flex items-center space-x-2"
111 | )}
112 | >
113 |
117 | {option.label}
118 |
119 | ))}
120 |
121 |
122 | )}
123 |
124 |
125 | );
126 | }
--------------------------------------------------------------------------------
/components/ui/color-picker.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useState, useRef, useEffect } from 'react';
4 | import { useTheme } from '@/hooks/use-theme';
5 | import { cn } from '@/lib/utils';
6 | import { motion, AnimatePresence } from 'framer-motion';
7 | import { Check, ChevronDown } from 'lucide-react';
8 |
9 | export interface ColorPickerProps {
10 | value?: string;
11 | onChange?: (color: string) => void;
12 | presetColors?: string[];
13 | disabled?: boolean;
14 | className?: string;
15 | showInput?: boolean;
16 | showPresets?: boolean;
17 | }
18 |
19 | export function ColorPicker({
20 | value = '#000000',
21 | onChange,
22 | presetColors = [
23 | '#FF6B6B', '#4ECDC4', '#45B7D1', '#A8E6CF', '#FF8E53',
24 | '#F472B6', '#8B5CF6', '#3B82F6', '#2DD4BF', '#F59E0B'
25 | ],
26 | disabled = false,
27 | className,
28 | showInput = true,
29 | showPresets = true,
30 | }: ColorPickerProps) {
31 | const { theme } = useTheme();
32 | const [isOpen, setIsOpen] = useState(false);
33 | const [inputValue, setInputValue] = useState(value);
34 | const containerRef = useRef(null);
35 |
36 | useEffect(() => {
37 | setInputValue(value);
38 | }, [value]);
39 |
40 | useEffect(() => {
41 | const handleClickOutside = (event: MouseEvent) => {
42 | if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
43 | setIsOpen(false);
44 | }
45 | };
46 |
47 | document.addEventListener('mousedown', handleClickOutside);
48 | return () => document.removeEventListener('mousedown', handleClickOutside);
49 | }, []);
50 |
51 | const handleInputChange = (e: React.ChangeEvent) => {
52 | const newValue = e.target.value;
53 | setInputValue(newValue);
54 | if (newValue.match(/^#[0-9A-Fa-f]{6}$/)) {
55 | onChange?.(newValue);
56 | }
57 | };
58 |
59 | const handlePresetClick = (color: string) => {
60 | onChange?.(color);
61 | setIsOpen(false);
62 | };
63 |
64 | return (
65 |
73 |
!disabled && setIsOpen(!isOpen)}
79 | style={{ borderColor: isOpen ? theme.colors.primary : undefined }}
80 | >
81 |
85 | {showInput && (
86 |
e.stopPropagation()}
92 | />
93 | )}
94 |
100 |
101 |
102 |
103 | {isOpen && (
104 |
111 | {showPresets && (
112 |
113 | {presetColors.map((color) => (
114 |
124 | ))}
125 |
126 | )}
127 |
128 | )}
129 |
130 |
131 | );
132 | }
--------------------------------------------------------------------------------
/app/components/checkbox/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useState } from 'react';
4 | import { motion } from 'framer-motion';
5 | import { Checkbox } from '@/components/ui/checkbox';
6 | import { CodeBlock } from '@/components/code-block';
7 |
8 | const installCode = `npm install @parrot-ui/react`;
9 |
10 | const usageCode = `import { Checkbox } from '@parrot-ui/react';
11 |
12 | function App() {
13 | const [checked, setChecked] = useState(false);
14 |
15 | return (
16 |
21 | );
22 | }`;
23 |
24 | export default function CheckboxPage() {
25 | const [checked1, setChecked1] = useState(false);
26 | const [checked2, setChecked2] = useState(true);
27 | const [checked3, setChecked3] = useState(false);
28 |
29 | return (
30 |
31 |
36 |
37 | Checkbox
38 |
39 |
40 | A customizable checkbox component with smooth animations.
41 |
42 |
43 | {/* Installation */}
44 |
45 | Installation
46 |
47 |
48 |
49 | {/* Usage */}
50 |
54 |
55 | {/* Examples */}
56 |
57 | Examples
58 |
59 | {/* Basic */}
60 |
61 |
Basic Checkbox
62 |
63 |
68 |
69 |
70 |
71 | {/* Checked by default */}
72 |
73 |
Checked by Default
74 |
75 |
80 |
81 |
82 |
83 | {/* Disabled */}
84 |
85 |
Disabled State
86 |
87 |
93 |
94 |
95 |
96 |
97 | {/* Props */}
98 |
99 | Props
100 |
101 |
102 |
103 |
104 | | Prop |
105 | Type |
106 | Default |
107 | Description |
108 |
109 |
110 |
111 |
112 | | checked |
113 | boolean |
114 | false |
115 | Whether the checkbox is checked |
116 |
117 |
118 | | onChange |
119 | (checked: boolean) ={'>'} void |
120 | undefined |
121 | Called when the checkbox state changes |
122 |
123 |
124 | | label |
125 | string |
126 | undefined |
127 | Label text for the checkbox |
128 |
129 |
130 | | disabled |
131 | boolean |
132 | false |
133 | Whether the checkbox is disabled |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 | );
142 | }
--------------------------------------------------------------------------------
/app/manifest/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { motion } from 'framer-motion';
5 | import { CodeBlock } from '@/components/code-block';
6 | import { Bot, Users, Sparkles, Github } from 'lucide-react';
7 |
8 | const installCode = `npm install @parrot-ui/react`;
9 |
10 | export default function ManifestPage() {
11 | return (
12 |
13 |
18 |
19 | ParrotUI Manifest
20 |
21 |
22 | Our vision for a modern React component library that combines the best of Mantine and shadcn/ui.
23 |
24 |
25 |
26 |
27 | {/* Rest of the manifest content remains the same */}
28 | {/* Core Principles */}
29 |
30 | Core Principles
31 |
32 |
33 | -
34 | Next.js First: Built specifically for Next.js App Router, ensuring optimal performance and seamless integration.
35 |
36 | -
37 | Best of Both Worlds: Combines Mantine's powerful features and flexibility with shadcn/ui's simplicity and copy-paste approach.
38 |
39 | -
40 | Modern DX: TypeScript-first development with excellent autocompletion and type safety.
41 |
42 | -
43 | Flexible Theming: CSS variables-based theme system that's powerful yet easy to customize.
44 |
45 |
46 |
47 |
48 |
49 | {/* Key Features */}
50 |
51 | Key Features
52 |
53 |
54 |
Mantine-Inspired Features
55 |
56 | - • Powerful theme system with CSS variables
57 | - • Rich set of hooks for common use cases
58 | - • Flexible component APIs
59 | - • Built-in dark mode support
60 |
61 |
62 |
63 |
shadcn/ui-Inspired Features
64 |
65 | - • Copy-paste component approach
66 | - • Tailwind CSS integration
67 | - • Beautifully designed components
68 | - • Full source code access
69 |
70 |
71 |
72 |
73 |
74 | {/* Installation */}
75 |
76 | Installation
77 |
78 |
79 | ParrotUI is designed to work exclusively with Next.js App Router projects, ensuring optimal performance and integration.
80 |
81 |
82 |
83 | {/* Design Decisions */}
84 |
85 | Design Decisions
86 |
87 |
88 | -
89 | Next.js Exclusivity: By focusing solely on Next.js App Router, we can provide better optimizations and integrations.
90 |
91 | -
92 | CSS Variables: Using CSS variables for theming provides better performance and easier customization than CSS-in-JS.
93 |
94 | -
95 | Tailwind Integration: Leveraging Tailwind CSS for utility classes while maintaining component-specific styles.
96 |
97 | -
98 | Framer Motion: Using Framer Motion for smooth, performant animations that enhance user experience.
99 |
100 |
101 |
102 |
103 |
104 | {/* Future Plans */}
105 |
106 | Future Plans
107 |
108 |
109 | - Expanding component library with more advanced components
110 | - Adding more hooks for common use cases
111 | - Improving documentation and examples
112 | - Building a community around ParrotUI
113 | - Creating templates and starter kits
114 | - Fostering AI and human collaboration in open source
115 |
116 |
117 |
118 |
119 | {/* AI and Contributions */}
120 |
121 |
122 |
123 | );
124 | }
--------------------------------------------------------------------------------
/app/components/button/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { motion } from 'framer-motion';
5 | import { ArrowRight, Heart, Mail, Plus } from 'lucide-react';
6 | import { Button } from '@/components/ui/button';
7 | import { CodeBlock } from '@/components/code-block';
8 |
9 | const variants = ['primary', 'secondary', 'outline', 'ghost', 'link', 'gradient'] as const;
10 | const sizes = ['sm', 'md', 'lg', 'xl'] as const;
11 |
12 | const installCode = `npm install @parrot-ui/react`;
13 |
14 | const usageCode = `import { Button } from '@parrot-ui/react';
15 |
16 | function App() {
17 | return (
18 |
19 | {/* Basic usage */}
20 |
21 |
22 | {/* With variants */}
23 |
24 |
25 |
26 |
27 | {/* With sizes */}
28 |
29 |
30 |
31 | {/* With icons */}
32 | }>Send Email
33 | }>Next
34 |
35 | {/* States */}
36 |
37 |
38 |
39 | );
40 | }`;
41 |
42 | export default function ButtonDocs() {
43 | return (
44 |
45 |
50 |
51 | Button
52 |
53 |
54 | A versatile button component with multiple variants, sizes, and states.
55 |
56 |
57 | {/* Installation */}
58 |
59 | Installation
60 |
61 |
62 |
63 | {/* Usage */}
64 |
68 |
69 | {/* Basic Example */}
70 |
71 | Basic Usage
72 |
73 |
74 | }>With Icon
75 |
76 |
77 |
78 |
79 | {/* Variants */}
80 |
81 | Variants
82 |
83 | {variants.map((variant) => (
84 |
87 | ))}
88 |
89 |
90 |
91 | {/* Props Table */}
92 |
93 | Props
94 |
95 |
96 |
97 |
98 | | Prop |
99 | Type |
100 | Default |
101 | Description |
102 |
103 |
104 |
105 |
106 | | variant |
107 | primary | secondary | outline | ghost | link | gradient |
108 | primary |
109 | The visual style of the button |
110 |
111 |
112 | | size |
113 | sm | md | lg | xl | icon |
114 | md |
115 | The size of the button |
116 |
117 |
118 | | leftIcon |
119 | ReactNode |
120 | undefined |
121 | Icon to show before the button text |
122 |
123 |
124 | | rightIcon |
125 | ReactNode |
126 | undefined |
127 | Icon to show after the button text |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 | );
136 | }
--------------------------------------------------------------------------------
/app/components/select/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useState } from 'react';
4 | import { motion } from 'framer-motion';
5 | import { Select } from '@/components/ui/select';
6 | import { CodeBlock } from '@/components/code-block';
7 |
8 | const installCode = `npm install @parrot-ui/react`;
9 |
10 | const usageCode = `import { Select } from '@parrot-ui/react';
11 |
12 | function App() {
13 | const options = [
14 | { value: 'react', label: 'React' },
15 | { value: 'vue', label: 'Vue' },
16 | { value: 'svelte', label: 'Svelte' },
17 | ];
18 |
19 | const [value, setValue] = useState('');
20 |
21 | return (
22 |
28 | );
29 | }`;
30 |
31 | export default function SelectPage() {
32 | const [value, setValue] = useState('');
33 | const [multiValue, setMultiValue] = useState('');
34 |
35 | const options = [
36 | { value: 'react', label: 'React' },
37 | { value: 'vue', label: 'Vue' },
38 | { value: 'svelte', label: 'Svelte' },
39 | { value: 'angular', label: 'Angular' },
40 | { value: 'nextjs', label: 'Next.js' },
41 | ];
42 |
43 | return (
44 |
45 |
50 |
51 | Select
52 |
53 |
54 | A customizable select component with search and keyboard navigation.
55 |
56 |
57 | {/* Installation */}
58 |
59 | Installation
60 |
61 |
62 |
63 | {/* Usage */}
64 |
68 |
69 | {/* Examples */}
70 |
71 | Examples
72 |
73 | {/* Basic */}
74 |
75 |
Basic Select
76 |
77 |
83 |
84 |
85 |
86 | {/* Disabled */}
87 |
88 |
Disabled State
89 |
90 |
97 |
98 |
99 |
100 |
101 | {/* Props */}
102 |
103 | Props
104 |
105 |
106 |
107 |
108 | | Prop |
109 | Type |
110 | Default |
111 | Description |
112 |
113 |
114 |
115 |
116 | | options |
117 | SelectOption[] |
118 | required |
119 | Array of options to display |
120 |
121 |
122 | | value |
123 | string |
124 | undefined |
125 | Currently selected value |
126 |
127 |
128 | | onChange |
129 | (value: string) ={'>'} void |
130 | undefined |
131 | Called when selection changes |
132 |
133 |
134 | | placeholder |
135 | string |
136 | "Select option" |
137 | Placeholder text |
138 |
139 |
140 | | disabled |
141 | boolean |
142 | false |
143 | Whether the select is disabled |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 | );
152 | }
--------------------------------------------------------------------------------
/app/components/theme/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { motion } from 'framer-motion';
5 | import { useTheme } from '@/hooks/use-theme';
6 | import { Button } from '@/components/ui/button';
7 | import { Input } from '@/components/ui/input';
8 | import { Palette, Type, Maximize, Grid } from 'lucide-react';
9 | import { CodeBlock } from '@/components/code-block';
10 |
11 | const exampleCode = `// 1. Import the ThemeProvider
12 | import { ThemeProvider } from '@parrot-ui/react';
13 |
14 | // 2. Create your custom theme
15 | const myTheme = {
16 | colors: {
17 | primary: '#FF6B6B',
18 | secondary: '#4ECDC4',
19 | // ... other colors
20 | },
21 | gradients: {
22 | primary: 'from-[#FF6B6B] to-[#FF8E53]',
23 | // ... other gradients
24 | },
25 | borderRadius: '0.5rem',
26 | fontFamily: 'Inter, sans-serif'
27 | };
28 |
29 | // 3. Wrap your app with ThemeProvider
30 | function App() {
31 | return (
32 |
33 |
34 |
35 | );
36 | }`;
37 |
38 | export default function ThemeDocs() {
39 | const { theme, setTheme } = useTheme();
40 |
41 | return (
42 |
43 |
48 |
49 | Theme System
50 |
51 |
52 | A comprehensive guide to ParrotUI's theming system and customization options.
53 |
54 |
55 | {/* Quick Links */}
56 |
57 | {[
58 | { icon:
, title: 'Colors', href: '/components/colors' },
59 | { icon:
, title: 'Typography', href: '/components/typography' },
60 | { icon:
, title: 'Spacing', href: '/components/spacing' },
61 | { icon:
, title: 'Breakpoints', href: '/components/breakpoints' }
62 | ].map((item) => (
63 |
73 | ))}
74 |
75 |
76 | {/* Theme Configuration Example */}
77 |
78 | Theme Configuration
79 |
80 |
81 |
82 | {/* Live Theme Preview */}
83 |
84 | Live Theme Preview
85 |
86 |
87 |
Button Variants
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
103 |
104 |
105 |
106 | {/* Theme Structure */}
107 |
108 | Theme Structure
109 |
110 |
111 |
112 |
113 | | Property |
114 | Type |
115 | Description |
116 |
117 |
118 |
119 |
120 | | colors |
121 | ThemeColors |
122 | Color palette configuration |
123 |
124 |
125 | | typography |
126 | ThemeTypography |
127 | Typography settings |
128 |
129 |
130 | | spacing |
131 | ThemeSpacing |
132 | Spacing scale configuration |
133 |
134 |
135 | | breakpoints |
136 | ThemeBreakpoints |
137 | Responsive breakpoints |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 | );
146 | }
--------------------------------------------------------------------------------
/app/components/switch/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useState } from 'react';
4 | import { motion } from 'framer-motion';
5 | import { Switch } from '@/components/ui/switch';
6 | import { CodeBlock } from '@/components/code-block';
7 |
8 | const installCode = `npm install @parrot-ui/react`;
9 |
10 | const usageCode = `import { Switch } from '@parrot-ui/react';
11 |
12 | function App() {
13 | const [enabled, setEnabled] = useState(false);
14 |
15 | return (
16 |
21 | );
22 | }`;
23 |
24 | export default function SwitchPage() {
25 | const [enabled1, setEnabled1] = useState(false);
26 | const [enabled2, setEnabled2] = useState(true);
27 | const [enabled3, setEnabled3] = useState(false);
28 |
29 | return (
30 |
31 |
36 |
37 | Switch
38 |
39 |
40 | A beautiful switch component with smooth animations and multiple sizes.
41 |
42 |
43 | {/* Installation */}
44 |
45 | Installation
46 |
47 |
48 |
49 | {/* Usage */}
50 |
54 |
55 | {/* Examples */}
56 |
57 | Examples
58 |
59 | {/* Sizes */}
60 |
61 |
Sizes
62 |
63 |
69 |
75 |
81 |
82 |
83 |
84 | {/* Disabled */}
85 |
86 |
Disabled State
87 |
88 |
93 |
98 |
99 |
100 |
101 |
102 | {/* Props */}
103 |
104 | Props
105 |
106 |
107 |
108 |
109 | | Prop |
110 | Type |
111 | Default |
112 | Description |
113 |
114 |
115 |
116 |
117 | | checked |
118 | boolean |
119 | false |
120 | Whether the switch is checked |
121 |
122 |
123 | | onChange |
124 | (checked: boolean) ={'>'} void |
125 | undefined |
126 | Called when the switch state changes |
127 |
128 |
129 | | label |
130 | string |
131 | undefined |
132 | Label text for the switch |
133 |
134 |
135 | | size |
136 | sm | md | lg |
137 | md |
138 | Size of the switch |
139 |
140 |
141 | | disabled |
142 | boolean |
143 | false |
144 | Whether the switch is disabled |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 | );
153 | }
--------------------------------------------------------------------------------
/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useState } from 'react';
4 | import { motion, AnimatePresence } from 'framer-motion';
5 | import { useTheme } from '@/hooks/use-theme';
6 | import { cn } from '@/lib/utils';
7 | import { ChevronLeft, ChevronRight } from 'lucide-react';
8 |
9 | export interface CalendarProps {
10 | value?: Date;
11 | onChange?: (date: Date) => void;
12 | minDate?: Date;
13 | maxDate?: Date;
14 | disabled?: boolean;
15 | className?: string;
16 | variant?: 'default' | 'outline' | 'ghost';
17 | size?: 'sm' | 'md' | 'lg';
18 | }
19 |
20 | export function Calendar({
21 | value,
22 | onChange,
23 | minDate,
24 | maxDate,
25 | disabled = false,
26 | className,
27 | variant = 'default',
28 | size = 'md',
29 | }: CalendarProps) {
30 | const { theme } = useTheme();
31 | const [currentDate, setCurrentDate] = useState(value || new Date());
32 | const [viewDate, setViewDate] = useState(value || new Date());
33 |
34 | const daysInMonth = new Date(
35 | viewDate.getFullYear(),
36 | viewDate.getMonth() + 1,
37 | 0
38 | ).getDate();
39 |
40 | const firstDayOfMonth = new Date(
41 | viewDate.getFullYear(),
42 | viewDate.getMonth(),
43 | 1
44 | ).getDay();
45 |
46 | const monthNames = [
47 | 'January', 'February', 'March', 'April', 'May', 'June',
48 | 'July', 'August', 'September', 'October', 'November', 'December'
49 | ];
50 |
51 | const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
52 |
53 | const handleDateClick = (day: number) => {
54 | if (disabled) return;
55 |
56 | const newDate = new Date(viewDate.getFullYear(), viewDate.getMonth(), day);
57 |
58 | if (minDate && newDate < minDate) return;
59 | if (maxDate && newDate > maxDate) return;
60 |
61 | setCurrentDate(newDate);
62 | onChange?.(newDate);
63 | };
64 |
65 | const handlePrevMonth = () => {
66 | setViewDate(new Date(viewDate.getFullYear(), viewDate.getMonth() - 1));
67 | };
68 |
69 | const handleNextMonth = () => {
70 | setViewDate(new Date(viewDate.getFullYear(), viewDate.getMonth() + 1));
71 | };
72 |
73 | const isCurrentDate = (day: number) => {
74 | return (
75 | currentDate.getDate() === day &&
76 | currentDate.getMonth() === viewDate.getMonth() &&
77 | currentDate.getFullYear() === viewDate.getFullYear()
78 | );
79 | };
80 |
81 | const isToday = (day: number) => {
82 | const today = new Date();
83 | return (
84 | today.getDate() === day &&
85 | today.getMonth() === viewDate.getMonth() &&
86 | today.getFullYear() === viewDate.getFullYear()
87 | );
88 | };
89 |
90 | const isDisabled = (day: number) => {
91 | if (disabled) return true;
92 |
93 | const date = new Date(viewDate.getFullYear(), viewDate.getMonth(), day);
94 | if (minDate && date < minDate) return true;
95 | if (maxDate && date > maxDate) return true;
96 |
97 | return false;
98 | };
99 |
100 | const sizeClasses = {
101 | sm: 'text-sm',
102 | md: 'text-base',
103 | lg: 'text-lg',
104 | };
105 |
106 | const variantClasses = {
107 | default: 'bg-white/5 border border-white/10',
108 | outline: 'border border-white/20 bg-transparent',
109 | ghost: 'bg-transparent',
110 | };
111 |
112 | return (
113 |
121 | {/* Header */}
122 |
123 |
130 |
131 | {monthNames[viewDate.getMonth()]} {viewDate.getFullYear()}
132 |
133 |
140 |
141 |
142 | {/* Days of Week */}
143 |
144 | {dayNames.map((day) => (
145 |
149 | {day}
150 |
151 | ))}
152 |
153 |
154 | {/* Calendar Grid */}
155 |
156 | {Array.from({ length: firstDayOfMonth }).map((_, index) => (
157 |
158 | ))}
159 | {Array.from({ length: daysInMonth }).map((_, index) => {
160 | const day = index + 1;
161 | const isSelected = isCurrentDate(day);
162 | const isTodayDate = isToday(day);
163 | const isDisabledDate = isDisabled(day);
164 |
165 | return (
166 |
handleDateClick(day)}
171 | disabled={isDisabledDate}
172 | className={cn(
173 | 'aspect-square rounded-full flex items-center justify-center relative',
174 | isSelected && 'bg-primary text-white',
175 | !isSelected && isTodayDate && 'border-2 border-primary',
176 | !isSelected && !isTodayDate && 'hover:bg-white/10',
177 | isDisabledDate && 'opacity-50 cursor-not-allowed'
178 | )}
179 | style={{
180 | backgroundColor: isSelected ? theme.colors.primary : undefined,
181 | borderColor: isTodayDate ? theme.colors.primary : undefined,
182 | }}
183 | >
184 | {day}
185 |
186 | );
187 | })}
188 |
189 |
190 | );
191 | }
--------------------------------------------------------------------------------
/components/sidebar.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useState } from 'react';
4 | import Link from 'next/link';
5 | import { motion, AnimatePresence } from 'framer-motion';
6 | import { ChevronDown, Feather, Layers, Layout, Navigation2, Database, MessageSquare, FileText, Settings } from 'lucide-react';
7 |
8 | const componentGroups = {
9 | 'Core': ['Button', 'Input', 'Select', 'Checkbox', 'Radio', 'Switch', 'Toggle', 'Textarea', 'Badge'],
10 | 'Layout': ['Container', 'Grid', 'Stack', 'Divider', 'Space', 'AspectRatio'],
11 | 'Navigation': ['Menu', 'Tabs', 'Breadcrumb', 'Pagination', 'Stepper'],
12 | 'Data': ['Table', 'List', 'Tree', 'Timeline', 'Calendar', 'Stats'],
13 | 'Feedback': ['Alert', 'Toast', 'Progress', 'Spinner', 'Skeleton', 'Modal'],
14 | 'Forms': ['Form', 'DatePicker', 'TimePicker', 'Upload', 'ColorPicker'],
15 | 'Customization': ['Theme', 'Colors']
16 | };
17 |
18 | const groupIcons = {
19 | 'Core': Layers,
20 | 'Layout': Layout,
21 | 'Navigation': Navigation2,
22 | 'Data': Database,
23 | 'Feedback': MessageSquare,
24 | 'Forms': FileText,
25 | 'Customization': Settings
26 | };
27 |
28 | const groupGradients = {
29 | 'Core': 'from-[#FF6B6B] to-[#FF8E53]',
30 | 'Layout': 'from-[#4ECDC4] to-[#45B7D1]',
31 | 'Navigation': 'from-[#A8E6CF] to-[#3EECAC]',
32 | 'Data': 'from-[#3B82F6] to-[#2DD4BF]',
33 | 'Feedback': 'from-[#F472B6] to-[#EC4899]',
34 | 'Forms': 'from-[#8B5CF6] to-[#6366F1]',
35 | 'Customization': 'from-[#F59E0B] to-[#EF4444]'
36 | };
37 |
38 | export function Sidebar() {
39 | const [activeGroup, setActiveGroup] = useState(null);
40 |
41 | return (
42 |
48 |
49 | {/* Logo Section */}
50 |
51 |
52 |
57 |
58 |
59 |
60 |
61 | ParrotUI
62 |
63 | Component Library
64 |
65 |
66 |
67 |
68 | {/* Navigation */}
69 |
131 |
132 |
133 | );
134 | }
--------------------------------------------------------------------------------
/components/ui/upload.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useRef, useState } from 'react';
4 | import { useTheme } from '@/hooks/use-theme';
5 | import { cn } from '@/lib/utils';
6 | import { motion, AnimatePresence } from 'framer-motion';
7 | import { Upload as UploadIcon, X, File, Image as ImageIcon } from 'lucide-react';
8 |
9 | export interface UploadProps {
10 | accept?: string;
11 | multiple?: boolean;
12 | maxSize?: number;
13 | onUpload?: (files: File[]) => void;
14 | onError?: (error: string) => void;
15 | disabled?: boolean;
16 | className?: string;
17 | variant?: 'default' | 'compact';
18 | preview?: boolean;
19 | }
20 |
21 | export function Upload({
22 | accept,
23 | multiple = false,
24 | maxSize,
25 | onUpload,
26 | onError,
27 | disabled = false,
28 | className,
29 | variant = 'default',
30 | preview = true,
31 | }: UploadProps) {
32 | const { theme } = useTheme();
33 | const inputRef = useRef(null);
34 | const [dragActive, setDragActive] = useState(false);
35 | const [files, setFiles] = useState([]);
36 | const [previews, setPreviews] = useState([]);
37 |
38 | const handleFiles = (newFiles: FileList | null) => {
39 | if (!newFiles) return;
40 |
41 | const validFiles: File[] = [];
42 | const newPreviews: string[] = [];
43 |
44 | Array.from(newFiles).forEach(file => {
45 | if (maxSize && file.size > maxSize) {
46 | onError?.(`File ${file.name} exceeds maximum size of ${maxSize / 1000000}MB`);
47 | return;
48 | }
49 |
50 | validFiles.push(file);
51 |
52 | if (preview && file.type.startsWith('image/')) {
53 | const reader = new FileReader();
54 | reader.onload = (e) => {
55 | newPreviews.push(e.target?.result as string);
56 | setPreviews(prev => [...prev, e.target?.result as string]);
57 | };
58 | reader.readAsDataURL(file);
59 | }
60 | });
61 |
62 | if (validFiles.length) {
63 | setFiles(prev => multiple ? [...prev, ...validFiles] : [validFiles[0]]);
64 | onUpload?.(validFiles);
65 | }
66 | };
67 |
68 | const handleDrop = (e: React.DragEvent) => {
69 | e.preventDefault();
70 | setDragActive(false);
71 | if (!disabled) {
72 | handleFiles(e.dataTransfer.files);
73 | }
74 | };
75 |
76 | const removeFile = (index: number) => {
77 | setFiles(prev => prev.filter((_, i) => i !== index));
78 | setPreviews(prev => prev.filter((_, i) => i !== index));
79 | };
80 |
81 | return (
82 |
83 |
handleFiles(e.target.files)}
89 | className="hidden"
90 | disabled={disabled}
91 | />
92 |
93 | {/* Drop Zone */}
94 |
{ e.preventDefault(); setDragActive(true); }}
96 | onDragLeave={() => setDragActive(false)}
97 | onDrop={handleDrop}
98 | onClick={() => !disabled && inputRef.current?.click()}
99 | className={cn(
100 | 'cursor-pointer transition-all duration-200 border-2 border-dashed rounded-lg',
101 | dragActive ? 'border-primary bg-primary/5' : 'border-border hover:border-primary/50',
102 | disabled && 'opacity-50 cursor-not-allowed',
103 | variant === 'compact' ? 'p-4' : 'p-8',
104 | 'flex flex-col items-center justify-center gap-2'
105 | )}
106 | style={{ borderColor: dragActive ? theme.colors.primary : undefined }}
107 | >
108 |
115 | {variant === 'default' && (
116 | <>
117 |
118 | Drop files here or click to upload
119 |
120 |
121 | {accept ? `Accepts: ${accept}` : 'All file types supported'}
122 | {maxSize && ` • Max size: ${maxSize / 1000000}MB`}
123 |
124 | >
125 | )}
126 |
127 |
128 | {/* File Previews */}
129 |
130 | {files.length > 0 && (
131 |
137 | {files.map((file, index) => (
138 |
142 | {preview && previews[index] ? (
143 |

148 | ) : (
149 |
150 | {file.type.startsWith('image/') ? (
151 |
152 | ) : (
153 |
154 | )}
155 |
156 | )}
157 |
158 |
{file.name}
159 |
160 | {(file.size / 1000).toFixed(1)}KB
161 |
162 |
163 |
169 |
170 | ))}
171 |
172 | )}
173 |
174 |
175 | );
176 | }
--------------------------------------------------------------------------------
/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useState, useEffect } from 'react';
4 | import { motion, AnimatePresence } from 'framer-motion';
5 | import { cn } from '@/lib/utils';
6 | import { useTheme } from '@/hooks/use-theme';
7 |
8 | interface TabsProps {
9 | defaultValue?: string;
10 | value?: string;
11 | onValueChange?: (value: string) => void;
12 | variant?: 'default' | 'pills' | 'underline' | 'gradient';
13 | size?: 'sm' | 'md' | 'lg';
14 | orientation?: 'horizontal' | 'vertical';
15 | children: React.ReactNode;
16 | className?: string;
17 | }
18 |
19 | interface TabListProps {
20 | children: React.ReactNode;
21 | className?: string;
22 | }
23 |
24 | interface TabTriggerProps {
25 | value: string;
26 | disabled?: boolean;
27 | icon?: React.ReactNode;
28 | children: React.ReactNode;
29 | className?: string;
30 | }
31 |
32 | interface TabContentProps {
33 | value: string;
34 | children: React.ReactNode;
35 | className?: string;
36 | }
37 |
38 | const TabsContext = React.createContext<{
39 | selectedTab: string;
40 | setSelectedTab: (value: string) => void;
41 | variant: TabsProps['variant'];
42 | size: TabsProps['size'];
43 | orientation: TabsProps['orientation'];
44 | } | null>(null);
45 |
46 | export function Tabs({
47 | defaultValue,
48 | value,
49 | onValueChange,
50 | variant = 'default',
51 | size = 'md',
52 | orientation = 'horizontal',
53 | children,
54 | className,
55 | }: TabsProps) {
56 | const [selectedTab, setSelectedTab] = useState(value || defaultValue || '');
57 | const { theme } = useTheme();
58 |
59 | useEffect(() => {
60 | if (value !== undefined) {
61 | setSelectedTab(value);
62 | }
63 | }, [value]);
64 |
65 | const handleTabChange = (newValue: string) => {
66 | if (!value) {
67 | setSelectedTab(newValue);
68 | }
69 | onValueChange?.(newValue);
70 | };
71 |
72 | return (
73 |
82 |
94 | {children}
95 |
96 |
97 | );
98 | }
99 |
100 | export function TabList({ children, className }: TabListProps) {
101 | const context = React.useContext(TabsContext);
102 | if (!context) throw new Error('TabList must be used within Tabs');
103 |
104 | const { orientation, variant } = context;
105 |
106 | return (
107 |
117 | {children}
118 |
119 | );
120 | }
121 |
122 | export function TabTrigger({
123 | value,
124 | disabled,
125 | icon,
126 | children,
127 | className,
128 | }: TabTriggerProps) {
129 | const context = React.useContext(TabsContext);
130 | if (!context) throw new Error('TabTrigger must be used within Tabs');
131 |
132 | const { selectedTab, setSelectedTab, variant, size, orientation } = context;
133 | const isSelected = selectedTab === value;
134 |
135 | const sizeClasses = {
136 | sm: 'px-3 py-1.5 text-sm',
137 | md: 'px-4 py-2',
138 | lg: 'px-6 py-3 text-lg',
139 | };
140 |
141 | const variants = {
142 | default: cn(
143 | 'relative transition-colors hover:text-primary',
144 | isSelected ? 'text-primary' : 'text-muted'
145 | ),
146 | pills: cn(
147 | 'relative transition-all rounded-md',
148 | isSelected ? 'bg-primary text-white shadow-lg' : 'hover:bg-white/10'
149 | ),
150 | underline: cn(
151 | 'relative transition-colors',
152 | isSelected && 'after:absolute after:bottom-0 after:left-0 after:w-full after:h-0.5 after:bg-primary',
153 | isSelected ? 'text-primary' : 'text-muted hover:text-primary'
154 | ),
155 | gradient: cn(
156 | 'relative transition-all bg-clip-text',
157 | isSelected
158 | ? 'text-transparent bg-gradient-to-r from-primary to-secondary font-semibold'
159 | : 'hover:text-primary'
160 | ),
161 | };
162 |
163 | return (
164 |
182 | );
183 | }
184 |
185 | export function TabContent({
186 | value,
187 | children,
188 | className,
189 | }: TabContentProps) {
190 | const context = React.useContext(TabsContext);
191 | if (!context) throw new Error('TabContent must be used within Tabs');
192 |
193 | const { selectedTab } = context;
194 | const isSelected = selectedTab === value;
195 |
196 | return (
197 |
198 | {isSelected && (
199 |
209 | {children}
210 |
211 | )}
212 |
213 | );
214 | }
215 |
216 | Tabs.List = TabList;
217 | Tabs.Trigger = TabTrigger;
218 | Tabs.Content = TabContent;
--------------------------------------------------------------------------------
/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useState, useMemo } from 'react';
4 | import { cn } from '@/lib/utils';
5 | import { Checkbox } from './checkbox';
6 | import { ChevronUp, ChevronDown } from 'lucide-react';
7 |
8 | export interface Column {
9 | key: string;
10 | header: string;
11 | sortable?: boolean;
12 | render?: (value: any, row: T) => React.ReactNode;
13 | }
14 |
15 | interface TableProps {
16 | data: T[];
17 | columns: Column[];
18 | sortColumn?: string;
19 | sortDirection?: 'asc' | 'desc';
20 | onSort?: (column: string) => void;
21 | selectable?: boolean;
22 | selectedRows?: string[];
23 | onRowSelect?: (id: string) => void;
24 | onSelectAll?: () => void;
25 | hoverable?: boolean;
26 | striped?: boolean;
27 | compact?: boolean;
28 | bordered?: boolean;
29 | className?: string;
30 | }
31 |
32 | export function Table({
33 | data,
34 | columns,
35 | sortColumn,
36 | sortDirection = 'asc',
37 | onSort,
38 | selectable,
39 | selectedRows = [],
40 | onRowSelect,
41 | onSelectAll,
42 | hoverable = true,
43 | striped = false,
44 | compact = false,
45 | bordered = false,
46 | className,
47 | }: TableProps) {
48 | const [internalSortColumn, setInternalSortColumn] = useState(sortColumn);
49 | const [internalSortDirection, setInternalSortDirection] = useState<'asc' | 'desc'>(sortDirection);
50 |
51 | const handleSort = (column: string) => {
52 | if (!columns.find(col => col.key === column)?.sortable) return;
53 |
54 | if (onSort) {
55 | onSort(column);
56 | } else {
57 | setInternalSortColumn(column);
58 | setInternalSortDirection(prev =>
59 | internalSortColumn === column
60 | ? prev === 'asc' ? 'desc' : 'asc'
61 | : 'asc'
62 | );
63 | }
64 | };
65 |
66 | const sortedData = useMemo(() => {
67 | const sortCol = onSort ? sortColumn : internalSortColumn;
68 | const sortDir = onSort ? sortDirection : internalSortDirection;
69 |
70 | if (!sortCol) return data;
71 |
72 | return [...data].sort((a, b) => {
73 | const aValue = (a as any)[sortCol];
74 | const bValue = (b as any)[sortCol];
75 |
76 | if (aValue === bValue) return 0;
77 | if (aValue === null || aValue === undefined) return 1;
78 | if (bValue === null || bValue === undefined) return -1;
79 |
80 | const comparison = aValue < bValue ? -1 : 1;
81 | return sortDir === 'asc' ? comparison : -comparison;
82 | });
83 | }, [data, sortColumn, sortDirection, internalSortColumn, internalSortDirection, onSort]);
84 |
85 | const allSelected = data.length > 0 && selectedRows.length === data.length;
86 |
87 | return (
88 |
93 |
94 |
98 |
99 | {selectable && (
100 | |
101 |
106 | |
107 | )}
108 | {columns.map((column) => (
109 | column.sortable && handleSort(column.key)}
117 | >
118 |
119 | {column.header}
120 | {column.sortable && (
121 |
122 |
130 |
138 |
139 | )}
140 |
141 | |
142 | ))}
143 |
144 |
145 |
149 | {sortedData.map((row, i) => (
150 |
158 | {selectable && (
159 | |
160 | onRowSelect?.(row.id)}
163 | aria-label={`Select row ${i + 1}`}
164 | />
165 | |
166 | )}
167 | {columns.map((column) => (
168 |
172 | {column.render
173 | ? column.render((row as any)[column.key], row)
174 | : (row as any)[column.key]}
175 | |
176 | ))}
177 |
178 | ))}
179 |
180 |
181 |
182 | );
183 | }
--------------------------------------------------------------------------------
/app/components/input/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { motion } from 'framer-motion';
5 | import { Search, Mail, Lock, User } from 'lucide-react';
6 | import { Input } from '@/components/ui/input';
7 | import { CodeBlock } from '@/components/code-block';
8 |
9 | const installCode = `npm install @parrot-ui/react`;
10 |
11 | const usageCode = `import { Input } from '@parrot-ui/react';
12 |
13 | function App() {
14 | return (
15 |
16 | {/* Basic usage */}
17 |
18 |
19 | {/* With variants */}
20 |
21 |
22 |
23 |
24 | {/* With sizes */}
25 |
26 |
27 |
28 | {/* With icons */}
29 | } placeholder="With left icon" />
30 | } placeholder="With right icon" />
31 |
32 | {/* With states */}
33 |
34 |
35 |
36 |
37 | );
38 | }`;
39 |
40 | export default function InputPage() {
41 | return (
42 |
43 |
48 |
49 | Input
50 |
51 |
52 | A versatile input component with multiple variants, sizes, and states.
53 |
54 |
55 | {/* Installation */}
56 |
57 | Installation
58 |
59 |
60 |
61 | {/* Usage */}
62 |
66 |
67 | {/* Basic Example */}
68 |
76 |
77 | {/* Variants */}
78 |
87 |
88 | {/* States */}
89 |
98 |
99 | {/* Props Table */}
100 |
101 | Props
102 |
103 |
104 |
105 |
106 | | Prop |
107 | Type |
108 | Default |
109 | Description |
110 |
111 |
112 |
113 |
114 | | variant |
115 | default | filled | outline | ghost |
116 | default |
117 | The visual style of the input |
118 |
119 |
120 | | size |
121 | sm | md | lg |
122 | md |
123 | The size of the input |
124 |
125 |
126 | | leftIcon |
127 | ReactNode |
128 | undefined |
129 | Icon to show on the left |
130 |
131 |
132 | | rightIcon |
133 | ReactNode |
134 | undefined |
135 | Icon to show on the right |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 | );
144 | }
--------------------------------------------------------------------------------
/app/components/radio/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useState } from 'react';
4 | import { motion } from 'framer-motion';
5 | import { RadioGroup } from '@/components/ui/radio-group';
6 | import { CodeBlock } from '@/components/code-block';
7 |
8 | const installCode = `npm install @parrot-ui/react`;
9 |
10 | const usageCode = `import { RadioGroup } from '@parrot-ui/react';
11 |
12 | function App() {
13 | const [value, setValue] = useState('apple');
14 |
15 | const options = [
16 | { label: 'Apple', value: 'apple' },
17 | { label: 'Banana', value: 'banana' },
18 | { label: 'Orange', value: 'orange' },
19 | ];
20 |
21 | return (
22 |
28 | );
29 | }`;
30 |
31 | export default function RadioPage() {
32 | const [value1, setValue1] = useState('apple');
33 | const [value2, setValue2] = useState('horizontal');
34 |
35 | const fruitOptions = [
36 | { label: 'Apple', value: 'apple' },
37 | { label: 'Banana', value: 'banana' },
38 | { label: 'Orange', value: 'orange' },
39 | ];
40 |
41 | const orientationOptions = [
42 | { label: 'Horizontal', value: 'horizontal' },
43 | { label: 'Vertical', value: 'vertical' },
44 | ];
45 |
46 | const disabledOptions = [
47 | { label: 'Option 1', value: 'opt1' },
48 | { label: 'Option 2', value: 'opt2', disabled: true },
49 | { label: 'Option 3', value: 'opt3' },
50 | ];
51 |
52 | return (
53 |
54 |
59 |
60 | Radio
61 |
62 |
63 | A customizable radio group component with multiple orientations.
64 |
65 |
66 | {/* Installation */}
67 |
68 | Installation
69 |
70 |
71 |
72 | {/* Usage */}
73 |
77 |
78 | {/* Examples */}
79 |
80 | Examples
81 |
82 | {/* Vertical */}
83 |
84 |
Vertical Layout
85 |
92 |
93 |
94 | {/* Horizontal */}
95 |
96 |
Horizontal Layout
97 |
104 |
105 |
106 | {/* Disabled */}
107 |
108 |
Disabled Options
109 |
115 |
116 |
117 |
118 | {/* Props */}
119 |
120 | Props
121 |
122 |
123 |
124 |
125 | | Prop |
126 | Type |
127 | Default |
128 | Description |
129 |
130 |
131 |
132 |
133 | | options |
134 | RadioOption[] |
135 | required |
136 | Array of radio options |
137 |
138 |
139 | | value |
140 | string |
141 | undefined |
142 | Currently selected value |
143 |
144 |
145 | | onChange |
146 | (value: string) ={'>'} void |
147 | undefined |
148 | Called when selection changes |
149 |
150 |
151 | | name |
152 | string |
153 | required |
154 | Name attribute for the radio group |
155 |
156 |
157 | | orientation |
158 | horizontal | vertical |
159 | vertical |
160 | Layout orientation of the radio group |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 | );
169 | }
--------------------------------------------------------------------------------
/app/components/toggle/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useState } from 'react';
4 | import { motion } from 'framer-motion';
5 | import { Toggle } from '@/components/ui/toggle';
6 | import { CodeBlock } from '@/components/code-block';
7 | import { Bell, BellOff, Bold, Italic, Underline } from 'lucide-react';
8 |
9 | const installCode = `npm install @parrot-ui/react`;
10 |
11 | const usageCode = `import { Toggle } from '@parrot-ui/react';
12 | import { Bell } from 'lucide-react';
13 |
14 | function App() {
15 | const [pressed, setPressed] = useState(false);
16 |
17 | return (
18 |
23 |
24 |
25 | );
26 | }`;
27 |
28 | export default function TogglePage() {
29 | const [pressed1, setPressed1] = useState(false);
30 | const [pressed2, setPressed2] = useState(false);
31 | const [pressed3, setPressed3] = useState(false);
32 |
33 | return (
34 |
35 |
40 |
41 | Toggle
42 |
43 |
44 | A two-state button that can be either on or off.
45 |
46 |
47 | {/* Installation */}
48 |
49 | Installation
50 |
51 |
52 |
53 | {/* Usage */}
54 |
58 |
59 | {/* Examples */}
60 |
61 | Examples
62 |
63 | {/* Basic */}
64 |
65 |
Basic Toggle
66 |
67 |
72 | {pressed1 ? : }
73 |
74 |
75 |
76 |
77 | {/* Text and Icon */}
78 |
79 |
With Text and Icon
80 |
81 |
86 |
87 | Notifications
88 |
89 |
90 |
91 |
92 |
93 | {/* Disabled */}
94 |
95 |
Disabled State
96 |
97 |
102 | Disabled
103 |
104 |
109 | Disabled Pressed
110 |
111 |
112 |
113 |
114 |
115 | {/* Props */}
116 |
117 | Props
118 |
119 |
120 |
121 |
122 | | Prop |
123 | Type |
124 | Default |
125 | Description |
126 |
127 |
128 |
129 |
130 | | pressed |
131 | boolean |
132 | false |
133 | The controlled pressed state of the toggle |
134 |
135 |
136 | | onPressedChange |
137 | (pressed: boolean) ={'>'} void |
138 | undefined |
139 | Event handler called when the pressed state changes |
140 |
141 |
142 | | disabled |
143 | boolean |
144 | false |
145 | Whether the toggle is disabled |
146 |
147 |
148 | | size |
149 | sm | md | lg |
150 | md |
151 | The size of the toggle |
152 |
153 |
154 | | children |
155 | ReactNode |
156 | undefined |
157 | The content to display inside the toggle |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 | );
166 | }
--------------------------------------------------------------------------------
/components/ui/date-picker.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useState } from 'react';
4 | import { useTheme } from '@/hooks/use-theme';
5 | import { cn } from '@/lib/utils';
6 | import { Calendar, ChevronLeft, ChevronRight } from 'lucide-react';
7 | import { motion, AnimatePresence } from 'framer-motion';
8 |
9 | export interface DatePickerProps {
10 | value?: Date;
11 | onChange?: (date: Date) => void;
12 | minDate?: Date;
13 | maxDate?: Date;
14 | disabled?: boolean;
15 | placeholder?: string;
16 | format?: string;
17 | className?: string;
18 | }
19 |
20 | export function DatePicker({
21 | value,
22 | onChange,
23 | minDate,
24 | maxDate,
25 | disabled = false,
26 | placeholder = 'Select date',
27 | format = 'MM/dd/yyyy',
28 | className,
29 | }: DatePickerProps) {
30 | const { theme } = useTheme();
31 | const [isOpen, setIsOpen] = useState(false);
32 | const [currentMonth, setCurrentMonth] = useState(value || new Date());
33 |
34 | const daysInMonth = new Date(
35 | currentMonth.getFullYear(),
36 | currentMonth.getMonth() + 1,
37 | 0
38 | ).getDate();
39 |
40 | const firstDayOfMonth = new Date(
41 | currentMonth.getFullYear(),
42 | currentMonth.getMonth(),
43 | 1
44 | ).getDay();
45 |
46 | const formatDate = (date: Date) => {
47 | const month = (date.getMonth() + 1).toString().padStart(2, '0');
48 | const day = date.getDate().toString().padStart(2, '0');
49 | const year = date.getFullYear();
50 | return `${month}/${day}/${year}`;
51 | };
52 |
53 | const handleDateSelect = (day: number) => {
54 | const selectedDate = new Date(
55 | currentMonth.getFullYear(),
56 | currentMonth.getMonth(),
57 | day
58 | );
59 | onChange?.(selectedDate);
60 | setIsOpen(false);
61 | };
62 |
63 | const nextMonth = () => {
64 | setCurrentMonth(new Date(currentMonth.setMonth(currentMonth.getMonth() + 1)));
65 | };
66 |
67 | const prevMonth = () => {
68 | setCurrentMonth(new Date(currentMonth.setMonth(currentMonth.getMonth() - 1)));
69 | };
70 |
71 | const isDateDisabled = (date: Date) => {
72 | if (minDate && date < minDate) return true;
73 | if (maxDate && date > maxDate) return true;
74 | return false;
75 | };
76 |
77 | const monthNames = [
78 | 'January', 'February', 'March', 'April', 'May', 'June',
79 | 'July', 'August', 'September', 'October', 'November', 'December'
80 | ];
81 |
82 | const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
83 |
84 | return (
85 |
86 |
!disabled && setIsOpen(!isOpen)}
97 | >
98 |
99 |
108 |
109 |
110 |
111 | {isOpen && (
112 |
122 | {/* Header */}
123 |
124 |
130 |
131 | {monthNames[currentMonth.getMonth()]} {currentMonth.getFullYear()}
132 |
133 |
139 |
140 |
141 | {/* Week days */}
142 |
143 | {weekDays.map((day) => (
144 |
149 | {day}
150 |
151 | ))}
152 |
153 |
154 | {/* Calendar grid */}
155 |
156 | {Array.from({ length: firstDayOfMonth }).map((_, index) => (
157 |
158 | ))}
159 | {Array.from({ length: daysInMonth }).map((_, index) => {
160 | const day = index + 1;
161 | const date = new Date(
162 | currentMonth.getFullYear(),
163 | currentMonth.getMonth(),
164 | day
165 | );
166 | const isSelected = value && value.getTime() === date.getTime();
167 | const isDisabled = isDateDisabled(date);
168 |
169 | return (
170 |
186 | );
187 | })}
188 |
189 |
190 | )}
191 |
192 |
193 | );
194 | }
--------------------------------------------------------------------------------
/app/components/colorpicker/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useState } from 'react';
4 | import { motion } from 'framer-motion';
5 | import { ColorPicker } from '@/components/ui/color-picker';
6 | import { CodeBlock } from '@/components/code-block';
7 |
8 | const installCode = `npm install @parrot-ui/react`;
9 |
10 | const usageCode = `import { ColorPicker } from '@parrot-ui/react';
11 |
12 | function App() {
13 | const [color, setColor] = useState('#FF6B6B');
14 |
15 | return (
16 |
22 | );
23 | }`;
24 |
25 | const customPresetsCode = `// Custom preset colors
26 | const presetColors = [
27 | '#FF0000', '#00FF00', '#0000FF',
28 | '#FFFF00', '#FF00FF', '#00FFFF'
29 | ];
30 |
31 | `;
36 |
37 | export default function ColorPickerPage() {
38 | const [color1, setColor1] = useState('#FF6B6B');
39 | const [color2, setColor2] = useState('#4ECDC4');
40 | const [color3, setColor3] = useState('#45B7D1');
41 |
42 | const customPresets = [
43 | '#FF0000', '#00FF00', '#0000FF',
44 | '#FFFF00', '#FF00FF', '#00FFFF',
45 | '#800080', '#008080', '#808000',
46 | '#FFA500'
47 | ];
48 |
49 | return (
50 |
51 |
56 |
57 | ColorPicker
58 |
59 |
60 | A customizable color picker component with preset colors and hex input.
61 |
62 |
63 | {/* Installation */}
64 |
65 | Installation
66 |
67 |
68 |
69 | {/* Usage */}
70 |
74 |
75 | {/* Examples */}
76 |
77 | Examples
78 |
79 | {/* Basic */}
80 |
81 |
Basic ColorPicker
82 |
86 |
87 |
88 | {/* Custom Presets */}
89 |
90 |
Custom Presets
91 |
92 |
97 |
98 |
99 | {/* Without Input */}
100 |
101 |
Without Input
102 |
107 |
108 |
109 | {/* Disabled */}
110 |
111 |
Disabled State
112 |
116 |
117 |
118 |
119 | {/* Props */}
120 |
121 | Props
122 |
123 |
124 |
125 |
126 | | Prop |
127 | Type |
128 | Default |
129 | Description |
130 |
131 |
132 |
133 |
134 | | value |
135 | string |
136 | #000000 |
137 | Selected color in hex format |
138 |
139 |
140 | | onChange |
141 | (color: string) ={'>'} void |
142 | undefined |
143 | Called when color changes |
144 |
145 |
146 | | presetColors |
147 | string[] |
148 | [...] |
149 | Array of preset colors |
150 |
151 |
152 | | disabled |
153 | boolean |
154 | false |
155 | Whether the color picker is disabled |
156 |
157 |
158 | | showInput |
159 | boolean |
160 | true |
161 | Show hex input field |
162 |
163 |
164 | | showPresets |
165 | boolean |
166 | true |
167 | Show preset color buttons |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 | );
176 | }
--------------------------------------------------------------------------------
/app/components/datepicker/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useState } from 'react';
4 | import { motion } from 'framer-motion';
5 | import { DatePicker } from '@/components/ui/date-picker';
6 | import { CodeBlock } from '@/components/code-block';
7 |
8 | const installCode = `npm install @parrot-ui/react`;
9 |
10 | const usageCode = `import { DatePicker } from '@parrot-ui/react';
11 |
12 | function App() {
13 | const [date, setDate] = useState();
14 |
15 | return (
16 |
21 | );
22 | }`;
23 |
24 | const rangeCode = `// With min and max dates
25 | `;
31 |
32 | export default function DatePickerPage() {
33 | const [date1, setDate1] = useState();
34 | const [date2, setDate2] = useState();
35 | const [date3, setDate3] = useState();
36 |
37 | return (
38 |
39 |
44 |
45 | DatePicker
46 |
47 |
48 | A customizable date picker component with calendar dropdown.
49 |
50 |
51 | {/* Installation */}
52 |
53 | Installation
54 |
55 |
56 |
57 | {/* Usage */}
58 |
62 |
63 | {/* Examples */}
64 |
65 | Examples
66 |
67 | {/* Basic */}
68 |
69 |
Basic DatePicker
70 |
71 |
76 |
77 |
78 |
79 | {/* With Range */}
80 |
81 |
With Date Range
82 |
83 |
84 |
91 |
92 |
93 |
94 | {/* Disabled */}
95 |
96 |
Disabled State
97 |
98 |
104 |
105 |
106 |
107 |
108 | {/* Props */}
109 |
110 | Props
111 |
112 |
113 |
114 |
115 | | Prop |
116 | Type |
117 | Default |
118 | Description |
119 |
120 |
121 |
122 |
123 | | value |
124 | Date |
125 | undefined |
126 | Selected date |
127 |
128 |
129 | | onChange |
130 | (date: Date) ={'>'} void |
131 | undefined |
132 | Called when date changes |
133 |
134 |
135 | | minDate |
136 | Date |
137 | undefined |
138 | Minimum selectable date |
139 |
140 |
141 | | maxDate |
142 | Date |
143 | undefined |
144 | Maximum selectable date |
145 |
146 |
147 | | disabled |
148 | boolean |
149 | false |
150 | Whether the date picker is disabled |
151 |
152 |
153 | | placeholder |
154 | string |
155 | "Select date" |
156 | Placeholder text |
157 |
158 |
159 | | format |
160 | string |
161 | "MM/dd/yyyy" |
162 | Date format string |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 | );
171 | }
--------------------------------------------------------------------------------