├── .eslintrc.json ├── public ├── ui.png ├── logo.png ├── favicon.ico ├── favicon.png ├── vercel.svg └── next.svg ├── src ├── app │ ├── favicon.ico │ ├── api │ │ └── random │ │ │ └── route.ts │ ├── globals.scss │ ├── layout.tsx │ └── page.tsx ├── lib │ └── utils.ts └── components │ ├── ui │ ├── label.tsx │ ├── input.tsx │ ├── switch.tsx │ ├── button.tsx │ └── dropdown-menu.tsx │ ├── Documentation.tsx │ ├── UserItem.tsx │ ├── UserItem.source.ts │ ├── UserItemInline.source.js │ ├── Form.tsx │ └── UserItemInline.source.ts ├── Makefile ├── next.config.mjs ├── postcss.config.mjs ├── components.json ├── .gitignore ├── scripts └── useritem-watch.ts ├── tsconfig.json ├── README.md ├── package.json └── tailwind.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guillaumeduhan/useritem/HEAD/public/ui.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guillaumeduhan/useritem/HEAD/public/logo.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guillaumeduhan/useritem/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guillaumeduhan/useritem/HEAD/public/favicon.png -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guillaumeduhan/useritem/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | gt: 2 | git add . 3 | git commit -m 'commit' 4 | git push 5 | 6 | dev: 7 | npm i && npm run dev -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | }; 6 | export default config; -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.scss", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | package-lock.json 9 | 10 | # testing 11 | /coverage 12 | 13 | # next.js 14 | /.next/ 15 | /out/ 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /src/app/api/random/route.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { faker } from '@faker-js/faker'; 3 | import { NextResponse } from 'next/server'; 4 | 5 | export async function GET() { 6 | const generateRandomData = () => { 7 | return { 8 | avatarUrl: faker.image.avatar(), 9 | description: faker.internet.email(), 10 | icon: faker.datatype.boolean(), 11 | online: faker.datatype.boolean(), 12 | status: faker.datatype.boolean(), 13 | name: faker.person.fullName(), 14 | verified: faker.datatype.boolean(), 15 | }; 16 | }; 17 | 18 | return NextResponse.json(generateRandomData()); 19 | } -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/useritem-watch.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import chokidar from 'chokidar'; 4 | 5 | const SOURCE_FILE = path.resolve('src/components/UserItem.tsx'); 6 | const TARGET_FILE = path.resolve('src/components/UserItem.source.ts'); 7 | 8 | const generate = () => { 9 | const content = fs.readFileSync(SOURCE_FILE, 'utf8'); 10 | 11 | const output = `// AUTO-GENERATED — DO NOT EDIT 12 | export const USERITEM_SOURCE = \`${content 13 | .replace(/`/g, '\\`') 14 | .replace(/\$\{/g, '\\${')}\`; 15 | `; 16 | 17 | fs.writeFileSync(TARGET_FILE, output, 'utf8'); 18 | console.log('[UserItem] source.ts updated'); 19 | }; 20 | 21 | generate(); 22 | chokidar.watch(SOURCE_FILE).on('change', generate); 23 | -------------------------------------------------------------------------------- /src/app/globals.scss: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | html { 4 | @apply bg-gradient-to-t from-neutral-950 to-neutral-900 text-neutral-300 text-lg; 5 | } 6 | 7 | input { 8 | @apply border-neutral-200 dark:border-neutral-700 bg-neutral-800; 9 | } 10 | 11 | span[data-state="checked"] { 12 | @apply bg-black dark:bg-neutral-200; 13 | } 14 | 15 | span[data-state="unchecked"] { 16 | @apply bg-neutral-500 dark:bg-neutral-700; 17 | } 18 | 19 | button[role="switch"] { 20 | @apply border-neutral-200 dark:border-neutral-700 bg-neutral-800; 21 | } 22 | 23 | @keyframes fadeIn { 24 | from { 25 | opacity: 0; 26 | } 27 | 28 | to { 29 | opacity: 1; 30 | } 31 | } 32 | 33 | .fadeIn { 34 | animation: fadeIn 1.5s ease-in-out forwards; 35 | } -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as LabelPrimitive from "@radix-ui/react-label" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | import * as React from "react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const labelVariants = cva( 8 | "font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-[16px]" 9 | ) 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )) 22 | Label.displayName = LabelPrimitive.Root.displayName 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "noEmit": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "bundler", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ], 24 | "paths": { 25 | "@/*": [ 26 | "./src/*" 27 | ], 28 | "public/*": [ 29 | "./public/*" 30 | ] 31 | }, 32 | "target": "ES2017" 33 | }, 34 | "include": [ 35 | "next-env.d.ts", 36 | "**/*.ts", 37 | "**/*.tsx", 38 | ".next/types/**/*.ts" 39 | , "src/components/UserItemInline.source.js" ], 40 | "exclude": [ 41 | "node_modules" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SwitchPrimitives from "@radix-ui/react-switch" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const Switch = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | 23 | 24 | )) 25 | Switch.displayName = SwitchPrimitives.Root.displayName 26 | 27 | export { Switch } 28 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "landing-useritem", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "concurrently \"next dev\" \"npm run watch:useritem\"", 7 | "build": "next build", 8 | "start": "next start", 9 | "predev": "rm -fr .next", 10 | "lint": "next lint", 11 | "test": "jest", 12 | "watch:useritem": "ts-node scripts/useritem-watch.ts" 13 | }, 14 | "dependencies": { 15 | "@radix-ui/react-dropdown-menu": "^2.1.16", 16 | "@radix-ui/react-label": "^2.0.2", 17 | "@radix-ui/react-slot": "^1.0.2", 18 | "@radix-ui/react-switch": "^1.0.3", 19 | "@tailwindcss/postcss": "^4.1.18", 20 | "animate.css": "^4.1.1", 21 | "class-variance-authority": "^0.7.0", 22 | "clsx": "^2.1.0", 23 | "lucide-react": "^0.321.0", 24 | "next": "15.0.7", 25 | "react": "^18", 26 | "react-dom": "^18", 27 | "sass": "^1.70.0", 28 | "sonner": "^2.0.7", 29 | "tailwind-merge": "^3.4.0", 30 | "tailwindcss": "^4.1.18", 31 | "toast": "^0.5.4" 32 | }, 33 | "devDependencies": { 34 | "@babel/preset-env": "^7.23.9", 35 | "@babel/preset-typescript": "^7.23.3", 36 | "@faker-js/faker": "^8.4.1", 37 | "@swc/core": "^1.15.5", 38 | "@types/jest": "^29.5.12", 39 | "@types/node": "^20", 40 | "@types/react": "^18", 41 | "@types/react-dom": "^18", 42 | "autoprefixer": "^10.0.1", 43 | "chokidar": "^5.0.0", 44 | "concurrently": "^9.2.1", 45 | "esbuild": "^0.27.1", 46 | "eslint": "^8", 47 | "eslint-config-next": "14.1.0", 48 | "jest": "^29.7.0", 49 | "postcss": "^8", 50 | "ts-node": "^10.9.2", 51 | "typescript": "^5.9.3" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: [ 5 | './pages/**/*.{ts,tsx}', 6 | './components/**/*.{ts,tsx}', 7 | './app/**/*.{ts,tsx}', 8 | './src/**/*.{ts,tsx}', 9 | ], 10 | prefix: "", 11 | theme: { 12 | container: { 13 | center: true, 14 | padding: "2rem", 15 | screens: { 16 | "2xl": "1400px", 17 | }, 18 | }, 19 | extend: { 20 | colors: { 21 | border: "hsl(var(--border))", 22 | input: "hsl(var(--input))", 23 | ring: "hsl(var(--ring))", 24 | background: "hsl(var(--background))", 25 | foreground: "hsl(var(--foreground))", 26 | primary: { 27 | DEFAULT: "hsl(var(--primary))", 28 | foreground: "hsl(var(--primary-foreground))", 29 | }, 30 | secondary: { 31 | DEFAULT: "hsl(var(--secondary))", 32 | foreground: "hsl(var(--secondary-foreground))", 33 | }, 34 | destructive: { 35 | DEFAULT: "hsl(var(--destructive))", 36 | foreground: "hsl(var(--destructive-foreground))", 37 | }, 38 | muted: { 39 | DEFAULT: "hsl(var(--muted))", 40 | foreground: "hsl(var(--muted-foreground))", 41 | }, 42 | accent: { 43 | DEFAULT: "hsl(var(--accent))", 44 | foreground: "hsl(var(--accent-foreground))", 45 | }, 46 | popover: { 47 | DEFAULT: "hsl(var(--popover))", 48 | foreground: "hsl(var(--popover-foreground))", 49 | }, 50 | card: { 51 | DEFAULT: "hsl(var(--card))", 52 | foreground: "hsl(var(--card-foreground))", 53 | }, 54 | }, 55 | borderRadius: { 56 | lg: "var(--radius)", 57 | md: "calc(var(--radius) - 2px)", 58 | sm: "calc(var(--radius) - 4px)", 59 | }, 60 | keyframes: { 61 | "accordion-down": { 62 | from: { height: "0" }, 63 | to: { height: "var(--radix-accordion-content-height)" }, 64 | }, 65 | "accordion-up": { 66 | from: { height: "var(--radix-accordion-content-height)" }, 67 | to: { height: "0" }, 68 | }, 69 | }, 70 | animation: { 71 | "accordion-down": "accordion-down 0.2s ease-out", 72 | "accordion-up": "accordion-up 0.2s ease-out", 73 | }, 74 | }, 75 | }, 76 | plugins: [require("tailwindcss-animate")], 77 | } -------------------------------------------------------------------------------- /src/components/Documentation.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState } from "react"; 4 | 5 | export default function Documentation() { 6 | const [propsList, setPropsList] = useState([ 7 | { prop: 'avatar', type: 'boolean', description: 'Determines whether the avatar should be displayed or not.', default: 'true' }, 8 | { prop: 'avatarUrl', type: 'string', description: 'URL of the user\'s avatar image. Default value is a default avatar image URL.', default: '' }, 9 | { prop: 'border', type: 'boolean', description: 'Determines whether a border should be displayed around the component.', default: 'true' }, 10 | { prop: 'color', type: 'string', description: 'Background color of the avatar.', default: '#ccc' }, 11 | { prop: 'description', type: 'string', description: 'User description or email.', default: 'johndoe@mailcom' }, 12 | { prop: 'disabled', type: 'boolean', description: 'Determines whether the user item should be disabled.', default: 'false' }, 13 | { prop: 'onClick', type: '(event: MouseEvent) => void', description: 'Function to handle click events on the user item.', default: '' }, 14 | { prop: 'loading', type: 'boolean', description: 'Determines whether a loading state should be displayed.', default: 'false' }, 15 | { prop: 'width', type: 'number', description: 'Width of the component.', default: '275' }, 16 | { prop: 'online', type: 'boolean', description: 'Determines whether the user is online.', default: 'false' }, 17 | { prop: 'reverse', type: 'boolean', description: 'Determines whether the layout should be reversed. Default value is `false`.', default: 'false' }, 18 | { prop: 'small', type: 'boolean', description: '', default: 'false' }, 19 | { prop: 'squared', type: 'boolean', description: 'Determines whether the corners should be squared. Default value is `false`.', default: 'false' }, 20 | { prop: 'status', type: 'boolean', description: 'Determines whether the user is a status user.', default: 'false' }, 21 | { prop: 'shadow', type: 'boolean', description: 'Determines whether a shadow effect should be added.', default: 'false' }, 22 | { prop: 'style', type: 'React.CSSProperties or undefined', description: 'Custom CSS styles for the component.', default: '' }, 23 | { prop: 'title', type: 'string', description: 'User\'s name or title. Default value is "John Doe".', default: 'John Doe' }, 24 | ]); 25 | 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {propsList.map((prop: any) => ( 38 | 39 | 40 | 41 | 42 | 43 | 44 | ))} 45 | 46 |
PropTypeDefaultDescription
{prop.prop}{prop.type}{prop.default}{prop.description}
47 | ); 48 | } -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "animate.css"; 2 | import { Suspense } from "react"; 3 | import "./globals.scss"; 4 | import { Toaster, toast } from 'sonner' 5 | 6 | export default function RootLayout({ 7 | children, 8 | }: { 9 | children: React.ReactNode; 10 | }) { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 18 | UserItem — Customizable User Component for JavaScript & React 19 | 20 | 21 | 25 | 26 | 64 | 65 | 66 | 67 | {/* Favicon */} 68 | 69 | 70 | 71 | {/* Open Graph */} 72 | 76 | 80 | 81 | 82 | 83 | 84 | 85 | {/* Twitter */} 86 | 87 | 91 | 95 | 96 | 97 | 98 | 99 | {children} 100 | 101 | 102 | 103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /src/components/UserItem.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | export type UserItemProps = { 3 | avatar: boolean; 4 | avatarBackgroundColor?: string; 5 | avatarUrl?: string; 6 | border?: boolean; 7 | description?: string; 8 | disabled?: boolean; 9 | name: string; 10 | onClick?: (() => void) | null; 11 | onlyAvatar?: boolean; 12 | reverse?: boolean; 13 | showStatus?: boolean; 14 | status?: 'online' | 'offline' | 'busy'; 15 | squared?: boolean; 16 | style?: React.CSSProperties; 17 | verified?: boolean; 18 | }; 19 | 20 | const UserItem = ({ data, setData }: { data: UserItemProps, setData: (u: UserItemProps) => void }) => { 21 | 22 | if (!data) return

No data.

23 | 24 | const { 25 | avatar, 26 | avatarBackgroundColor = '#ddd', 27 | avatarUrl = '', 28 | border, 29 | description, 30 | disabled, 31 | name, 32 | onClick, 33 | showStatus, 34 | status, 35 | onlyAvatar, 36 | reverse, 37 | squared, 38 | style, 39 | verified 40 | } = data; 41 | 42 | const getStatusColor = () => { 43 | if (!status) return; 44 | switch (status) { 45 | case 'offline': 46 | return 'bg-red-500 border-red-400' 47 | case 'busy': 48 | return 'bg-amber-500 border-amber-400' 49 | case 'online': 50 | return 'bg-green-500 border-green-400' 51 | default: 52 | break; 53 | } 54 | } 55 | 56 | return
{ 58 | if (onClick) onClick() 59 | }} 60 | style={{ 61 | maxWidth: 240 62 | }} 63 | className={` 64 | useritem group px-2 w-full py-2 flex items-center justify-center gap-2 bg-transparent hover:bg-white/5 cursor-pointer transition duration-300 max-w-full truncate 65 | ${border && 'border border-neutral-200 dark:border-neutral-700 hover:border-neutral-100 hover:dark:border-neutral-600'} 66 | ${!squared && 'rounded'} 67 | ${disabled && "pointer-events-none opacity-50"} 68 | ${style} 69 | ${reverse && 'flex-row-reverse text-right'} 70 | `} 71 | > 72 | {avatar &&
0 ? `url(${avatarUrl})` : '', 79 | backgroundSize: 'cover' 80 | }}> 81 | {!avatarUrl && {name[0] || 'A'}} 82 | {showStatus && } 83 |
} 84 | {!onlyAvatar &&
85 |
86 |

{name}

89 | {verified && } 92 |
93 | {description && {description}} 96 |
} 97 |
98 | } 99 | export default UserItem; -------------------------------------------------------------------------------- /src/components/UserItem.source.ts: -------------------------------------------------------------------------------- 1 | // AUTO-GENERATED — DO NOT EDIT 2 | export const USERITEM_SOURCE = `'use client'; 3 | export type UserItemProps = { 4 | avatar: boolean; 5 | avatarBackgroundColor?: string; 6 | avatarUrl?: string; 7 | border?: boolean; 8 | description?: string; 9 | disabled?: boolean; 10 | name: string; 11 | onClick?: (() => void) | null; 12 | onlyAvatar?: boolean; 13 | reverse?: boolean; 14 | showStatus?: boolean; 15 | status?: 'online' | 'offline' | 'busy'; 16 | squared?: boolean; 17 | style?: React.CSSProperties; 18 | verified?: boolean; 19 | }; 20 | 21 | const UserItem = ({ data, setData }: { data: UserItemProps, setData: (u: UserItemProps) => void }) => { 22 | 23 | if (!data) return

No data.

24 | 25 | const { 26 | avatar, 27 | avatarBackgroundColor = '#ddd', 28 | avatarUrl = '', 29 | border, 30 | description, 31 | disabled, 32 | name, 33 | onClick, 34 | showStatus, 35 | status, 36 | onlyAvatar, 37 | reverse, 38 | squared, 39 | style, 40 | verified 41 | } = data; 42 | 43 | const getStatusColor = () => { 44 | if (!status) return; 45 | switch (status) { 46 | case 'offline': 47 | return 'bg-red-500 border-red-400' 48 | case 'busy': 49 | return 'bg-amber-500 border-amber-400' 50 | case 'online': 51 | return 'bg-green-500 border-green-400' 52 | default: 53 | break; 54 | } 55 | } 56 | 57 | return
{ 59 | if (onClick) onClick() 60 | }} 61 | style={{ 62 | maxWidth: 240 63 | }} 64 | className={\` 65 | useritem group px-2 w-full py-2 flex items-center justify-center gap-2 bg-transparent hover:bg-white/5 cursor-pointer transition duration-300 max-w-full truncate 66 | \${border && 'border border-neutral-200 dark:border-neutral-700 hover:border-neutral-100 hover:dark:border-neutral-600'} 67 | \${!squared && 'rounded'} 68 | \${disabled && "pointer-events-none opacity-50"} 69 | \${style} 70 | \${reverse && 'flex-row-reverse text-right'} 71 | \`} 72 | > 73 | {avatar &&
0 ? \`url(\${avatarUrl})\` : '', 80 | backgroundSize: 'cover' 81 | }}> 82 | {!avatarUrl && {name[0] || 'A'}} 83 | {showStatus && } 84 |
} 85 | {!onlyAvatar &&
86 |
87 |

{name}

90 | {verified && } 93 |
94 | {description && {description}} 97 |
} 98 |
99 | } 100 | export default UserItem;`; 101 | -------------------------------------------------------------------------------- /src/components/UserItemInline.source.js: -------------------------------------------------------------------------------- 1 | // AUTO-GENERATED — DO NOT EDIT 2 | export const USERITEM_JSX_INLINE_SOURCE = ` 3 | 'use client'; 4 | import React from 'react'; 5 | 6 | const UserItem = ({ data, setData }) => { 7 | if (!data) return

No data.

; 8 | 9 | const { 10 | avatar, 11 | avatarBackgroundColor = '#ddd', 12 | avatarUrl, 13 | border, 14 | description, 15 | disabled, 16 | name, 17 | onClick, 18 | showStatus, 19 | status, 20 | onlyAvatar, 21 | reverse, 22 | squared, 23 | style, 24 | verified, 25 | } = data; 26 | 27 | const statusColor = { 28 | online: '#22c55e', 29 | offline: '#ef4444', 30 | busy: '#f59e0b', 31 | }; 32 | 33 | return ( 34 |
onClick && onClick()} 36 | style={{ 37 | display: 'flex', 38 | alignItems: 'center', 39 | gap: 8, 40 | padding: 8, 41 | maxWidth: 240, 42 | cursor: disabled ? 'default' : 'pointer', 43 | opacity: disabled ? 0.5 : 1, 44 | pointerEvents: disabled ? 'none' : 'auto', 45 | border: border ? '1px solid #e5e7eb' : undefined, 46 | borderRadius: squared ? 0 : 8, 47 | flexDirection: reverse ? 'row-reverse' : 'row', 48 | textAlign: reverse ? 'right' : 'left', 49 | backgroundColor: 'transparent', 50 | transition: 'background-color 0.2s ease', 51 | ...style, 52 | }} 53 | onMouseEnter={(e) => { 54 | e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.04)'; 55 | }} 56 | onMouseLeave={(e) => { 57 | e.currentTarget.style.backgroundColor = 'transparent'; 58 | }} 59 | > 60 | {avatar && ( 61 |
78 | {!avatarUrl && {name?.[0] || 'A'}} 79 | 80 | {showStatus && status && ( 81 | 93 | )} 94 |
95 | )} 96 | 97 | {!onlyAvatar && ( 98 |
99 |
107 |

117 | {name} 118 |

119 | 120 | {verified && ( 121 | 128 | 132 | 133 | )} 134 |
135 | 136 | {description && ( 137 | 146 | {description} 147 | 148 | )} 149 |
150 | )} 151 |
152 | ); 153 | }; 154 | 155 | export default UserItem; 156 | 157 | `; -------------------------------------------------------------------------------- /src/components/Form.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { 4 | DropdownMenu, 5 | DropdownMenuContent, 6 | DropdownMenuTrigger, 7 | } from '@/components/ui/dropdown-menu'; 8 | import { Input } from '@/components/ui/input'; 9 | import { Switch } from '@/components/ui/switch'; 10 | import { Button } from '@/components/ui/button'; 11 | import { Settings } from 'lucide-react'; 12 | 13 | const UserOptionsDropdown = ({ formData, setFormData }: any) => { 14 | const handleChange = (e: any) => { 15 | const { name, value, type, checked } = e.target; 16 | setFormData({ 17 | ...formData, 18 | [name]: type === 'checkbox' ? checked : value, 19 | }); 20 | }; 21 | 22 | return ( 23 | 24 | 25 | 29 | 30 | 31 | 32 |
33 | 34 | 35 | 36 | 37 | 42 | 43 | 44 | 47 | handleChange({ target: { name: 'avatar', type: 'checkbox', checked } }) 48 | } 49 | /> 50 | 51 | 52 | 57 | 58 | 59 | 65 | 66 | 67 | 70 | handleChange({ target: { name: 'border', type: 'checkbox', checked } }) 71 | } 72 | /> 73 | 74 | 75 | 78 | handleChange({ target: { name: 'disabled', type: 'checkbox', checked } }) 79 | } 80 | /> 81 | 82 | 83 | 86 | handleChange({ target: { name: 'onlyAvatar', type: 'checkbox', checked } }) 87 | } 88 | /> 89 | 90 | 91 | 94 | handleChange({ target: { name: 'reverse', type: 'checkbox', checked } }) 95 | } 96 | /> 97 | 98 | 99 | 102 | handleChange({ target: { name: 'squared', type: 'checkbox', checked } }) 103 | } 104 | /> 105 | 106 | 107 | 110 | handleChange({ target: { name: 'verified', type: 'checkbox', checked } }) 111 | } 112 | /> 113 | 114 | 115 | 118 | handleChange({ target: { name: 'showStatus', type: 'checkbox', checked } }) 119 | } 120 | /> 121 | 122 | 123 | 133 | 134 | {/* 135 | */} 141 | 142 |
143 |
144 | ); 145 | }; 146 | 147 | export default UserOptionsDropdown; 148 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | 'use client'; 3 | import UserItem, { UserItemProps } from '@/components/UserItem'; 4 | import { useState } from 'react' 5 | import UserOptionsDropdown from '@/components/Form'; 6 | import { Minus, Plus } from 'lucide-react'; 7 | import { Button } from '@/components/ui/button'; 8 | import { USERITEM_SOURCE } from '@/components/UserItem.source'; 9 | import { toast } from 'sonner'; 10 | import { USERITEM_INLINE_SOURCE, USERITEM_JSX_INLINE_SOURCE } from '@/components/UserItemInline.source'; 11 | 12 | const Homepage = () => { 13 | const [formData, setFormData] = useState({ 14 | avatar: true, 15 | avatarBackgroundColor: "#03b66e", 16 | avatarUrl: "https://avatars.githubusercontent.com/u/31253241?v=4&size=64", 17 | border: true, 18 | description: "", 19 | disabled: false, 20 | name: "Guillaume Duhan", 21 | onClick: () => alert("hello world"), 22 | onlyAvatar: false, 23 | reverse: false, 24 | showStatus: false, 25 | status: 'online', 26 | squared: false, 27 | style: {}, 28 | verified: true 29 | }); 30 | 31 | const SCALES = ['scale-100', 'scale-110', 'scale-120', 'scale-140', 'scale-160', 'scale-180', 'scale-200'] as const; 32 | 33 | const [scale, setScale] = useState<(typeof SCALES)[number]>('scale-120'); 34 | 35 | const updateScale = (direction: 'up' | 'down') => { 36 | setScale((prev) => { 37 | const index = SCALES.indexOf(prev); 38 | if (index === -1) return SCALES[0]; 39 | 40 | return direction === 'up' 41 | ? SCALES[Math.min(index + 1, SCALES.length - 1)] 42 | : SCALES[Math.max(index - 1, 0)]; 43 | }); 44 | }; 45 | 46 | const copyTsx = async () => { 47 | try { 48 | await navigator.clipboard.writeText(USERITEM_SOURCE); 49 | toast.success("Copied to clipboard!") 50 | } catch (error) { } 51 | } 52 | 53 | const copyTsxInline = async () => { 54 | try { 55 | await navigator.clipboard.writeText(USERITEM_INLINE_SOURCE); 56 | toast.success("Copied to clipboard!") 57 | } catch (error) { } 58 | } 59 | 60 | const copyJsxInline = async () => { 61 | try { 62 | await navigator.clipboard.writeText(USERITEM_JSX_INLINE_SOURCE); 63 | toast.success("Copied to clipboard!") 64 | } catch (error) { } 65 | } 66 | 67 | return
68 |
69 |
70 | 71 |
72 |
73 |
74 |
75 | 76 |
updateScale('up')}> 77 | 78 |
79 |
updateScale('down')}> 80 | 81 |
82 |

{scale.replace('scale-', '')}%

83 |
84 |
85 |
86 |

Copy/paste

87 |
88 |
89 | 95 | 101 | 107 |
108 |
109 |
110 |
111 | } 112 | export default Homepage; -------------------------------------------------------------------------------- /src/components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 3 | import { Check, ChevronRight, Circle } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const DropdownMenu = DropdownMenuPrimitive.Root 8 | 9 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger 10 | 11 | const DropdownMenuGroup = DropdownMenuPrimitive.Group 12 | 13 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal 14 | 15 | const DropdownMenuSub = DropdownMenuPrimitive.Sub 16 | 17 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup 18 | 19 | const DropdownMenuSubTrigger = React.forwardRef< 20 | React.ElementRef, 21 | React.ComponentPropsWithoutRef & { 22 | inset?: boolean 23 | } 24 | >(({ className, inset, children, ...props }, ref) => ( 25 | 34 | {children} 35 | 36 | 37 | )) 38 | DropdownMenuSubTrigger.displayName = 39 | DropdownMenuPrimitive.SubTrigger.displayName 40 | 41 | const DropdownMenuSubContent = React.forwardRef< 42 | React.ElementRef, 43 | React.ComponentPropsWithoutRef 44 | >(({ className, ...props }, ref) => ( 45 | 53 | )) 54 | DropdownMenuSubContent.displayName = 55 | DropdownMenuPrimitive.SubContent.displayName 56 | 57 | const DropdownMenuContent = React.forwardRef< 58 | React.ElementRef, 59 | React.ComponentPropsWithoutRef 60 | >(({ className, sideOffset = 4, ...props }, ref) => ( 61 | 62 | 71 | 72 | )) 73 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName 74 | 75 | const DropdownMenuItem = React.forwardRef< 76 | React.ElementRef, 77 | React.ComponentPropsWithoutRef & { 78 | inset?: boolean 79 | } 80 | >(({ className, inset, ...props }, ref) => ( 81 | 90 | )) 91 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName 92 | 93 | const DropdownMenuCheckboxItem = React.forwardRef< 94 | React.ElementRef, 95 | React.ComponentPropsWithoutRef 96 | >(({ className, children, checked, ...props }, ref) => ( 97 | 106 | 107 | 108 | 109 | 110 | 111 | {children} 112 | 113 | )) 114 | DropdownMenuCheckboxItem.displayName = 115 | DropdownMenuPrimitive.CheckboxItem.displayName 116 | 117 | const DropdownMenuRadioItem = React.forwardRef< 118 | React.ElementRef, 119 | React.ComponentPropsWithoutRef 120 | >(({ className, children, ...props }, ref) => ( 121 | 129 | 130 | 131 | 132 | 133 | 134 | {children} 135 | 136 | )) 137 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName 138 | 139 | const DropdownMenuLabel = React.forwardRef< 140 | React.ElementRef, 141 | React.ComponentPropsWithoutRef & { 142 | inset?: boolean 143 | } 144 | >(({ className, inset, ...props }, ref) => ( 145 | 154 | )) 155 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName 156 | 157 | const DropdownMenuSeparator = React.forwardRef< 158 | React.ElementRef, 159 | React.ComponentPropsWithoutRef 160 | >(({ className, ...props }, ref) => ( 161 | 166 | )) 167 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName 168 | 169 | const DropdownMenuShortcut = ({ 170 | className, 171 | ...props 172 | }: React.HTMLAttributes) => { 173 | return ( 174 | 178 | ) 179 | } 180 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut" 181 | 182 | export { 183 | DropdownMenu, 184 | DropdownMenuTrigger, 185 | DropdownMenuContent, 186 | DropdownMenuItem, 187 | DropdownMenuCheckboxItem, 188 | DropdownMenuRadioItem, 189 | DropdownMenuLabel, 190 | DropdownMenuSeparator, 191 | DropdownMenuShortcut, 192 | DropdownMenuGroup, 193 | DropdownMenuPortal, 194 | DropdownMenuSub, 195 | DropdownMenuSubContent, 196 | DropdownMenuSubTrigger, 197 | DropdownMenuRadioGroup, 198 | } 199 | -------------------------------------------------------------------------------- /src/components/UserItemInline.source.ts: -------------------------------------------------------------------------------- 1 | // AUTO-GENERATED — DO NOT EDIT 2 | export const USERITEM_INLINE_SOURCE = ` 3 | 'use client'; 4 | import React from 'react'; 5 | 6 | export type UserItemProps = { 7 | avatar: boolean; 8 | avatarBackgroundColor?: string; 9 | avatarUrl?: string; 10 | border?: boolean; 11 | description?: string; 12 | disabled?: boolean; 13 | name: string; 14 | onClick?: (() => void) | null; 15 | onlyAvatar?: boolean; 16 | reverse?: boolean; 17 | showStatus?: boolean; 18 | status?: 'online' | 'offline' | 'busy'; 19 | squared?: boolean; 20 | style?: React.CSSProperties; 21 | verified?: boolean; 22 | }; 23 | 24 | const UserItem = ({ 25 | data, 26 | setData, 27 | }: { 28 | data: UserItemProps; 29 | setData: (u: UserItemProps) => void; 30 | }) => { 31 | if (!data) return

No data.

; 32 | 33 | const { 34 | avatar, 35 | avatarBackgroundColor = '#ddd', 36 | avatarUrl, 37 | border, 38 | description, 39 | disabled, 40 | name, 41 | onClick, 42 | showStatus, 43 | status, 44 | onlyAvatar, 45 | reverse, 46 | squared, 47 | style, 48 | verified, 49 | } = data; 50 | 51 | const statusColor: Record = { 52 | online: '#22c55e', 53 | offline: '#ef4444', 54 | busy: '#f59e0b', 55 | }; 56 | 57 | return ( 58 |
onClick?.()} 60 | style={{ 61 | display: 'flex', 62 | alignItems: 'center', 63 | gap: 8, 64 | padding: '8px', 65 | cursor: disabled ? 'default' : 'pointer', 66 | opacity: disabled ? 0.5 : 1, 67 | pointerEvents: disabled ? 'none' : 'auto', 68 | border: border ? '1px solid #e5e7eb' : undefined, 69 | borderRadius: squared ? 0 : 8, 70 | maxWidth: 240, 71 | flexDirection: reverse ? 'row-reverse' : 'row', 72 | textAlign: reverse ? 'right' : 'left', 73 | transition: 'background-color 0.2s ease', 74 | ...style, 75 | }} 76 | onMouseEnter={(e) => { 77 | e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.04)'; 78 | }} 79 | onMouseLeave={(e) => { 80 | e.currentTarget.style.backgroundColor = 'transparent'; 81 | }} 82 | > 83 | {avatar && ( 84 |
102 | {!avatarUrl && {name[0] ?? 'A'}} 103 | 104 | {showStatus && status && ( 105 | 117 | )} 118 |
119 | )} 120 | 121 | {!onlyAvatar && ( 122 |
128 |
136 |

146 | {name} 147 |

148 | 149 | {verified && ( 150 | 157 | 161 | 162 | )} 163 |
164 | 165 | {description && ( 166 | 175 | {description} 176 | 177 | )} 178 |
179 | )} 180 |
181 | ); 182 | }; 183 | 184 | export default UserItem; 185 | `; 186 | 187 | // AUTO-GENERATED — DO NOT EDIT 188 | export const USERITEM_JSX_INLINE_SOURCE = ` 189 | 'use client'; 190 | import React from 'react'; 191 | 192 | const UserItem = ({ data, setData }) => { 193 | if (!data) return

No data.

; 194 | 195 | const { 196 | avatar, 197 | avatarBackgroundColor = '#ddd', 198 | avatarUrl, 199 | border, 200 | description, 201 | disabled, 202 | name, 203 | onClick, 204 | showStatus, 205 | status, 206 | onlyAvatar, 207 | reverse, 208 | squared, 209 | style, 210 | verified, 211 | } = data; 212 | 213 | const statusColor = { 214 | online: '#22c55e', 215 | offline: '#ef4444', 216 | busy: '#f59e0b', 217 | }; 218 | 219 | return ( 220 |
onClick && onClick()} 222 | style={{ 223 | display: 'flex', 224 | alignItems: 'center', 225 | gap: 8, 226 | padding: 8, 227 | maxWidth: 240, 228 | cursor: disabled ? 'default' : 'pointer', 229 | opacity: disabled ? 0.5 : 1, 230 | pointerEvents: disabled ? 'none' : 'auto', 231 | border: border ? '1px solid #e5e7eb' : undefined, 232 | borderRadius: squared ? 0 : 8, 233 | flexDirection: reverse ? 'row-reverse' : 'row', 234 | textAlign: reverse ? 'right' : 'left', 235 | backgroundColor: 'transparent', 236 | transition: 'background-color 0.2s ease', 237 | ...style, 238 | }} 239 | onMouseEnter={(e) => { 240 | e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.04)'; 241 | }} 242 | onMouseLeave={(e) => { 243 | e.currentTarget.style.backgroundColor = 'transparent'; 244 | }} 245 | > 246 | {avatar && ( 247 |
264 | {!avatarUrl && {name?.[0] || 'A'}} 265 | 266 | {showStatus && status && ( 267 | 279 | )} 280 |
281 | )} 282 | 283 | {!onlyAvatar && ( 284 |
285 |
293 |

303 | {name} 304 |

305 | 306 | {verified && ( 307 | 314 | 318 | 319 | )} 320 |
321 | 322 | {description && ( 323 | 332 | {description} 333 | 334 | )} 335 |
336 | )} 337 |
338 | ); 339 | }; 340 | 341 | export default UserItem; 342 | 343 | `; --------------------------------------------------------------------------------