Frontend
Tailwind CSS
Framework CSS utility-first para desenvolvimento rápido e customizável
O que é Tailwind CSS?
Tailwind CSS é um framework CSS utility-first que oferece classes utilitárias de baixo nível para construir designs customizados diretamente no HTML. Ao invés de componentes pré-construídos, oferece building blocks para criar interfaces únicas.
Por que utilizamos Tailwind CSS na IngenioLab?
- Utility-first: Classes pequenas e reutilizáveis
- Customização total: Controle completo sobre o design
- Performance: CSS otimizado com purging automático
- Developer Experience: IntelliSense e hot reload
- Design System: Consistência através de tokens
- Responsivo: Mobile-first por padrão
Instalação
# Via NPMnpm install -D tailwindcss postcss autoprefixer# Inicializar configuraçãonpx tailwindcss init -p# Com Vite (automático)npm create vite@latest meu-projeto -- --template react-tsnpm install -D tailwindcss postcss autoprefixer
Configuração IngenioLab
1. tailwind.config.js:
/** @type {import('tailwindcss').Config} */export default {content: ["./index.html","./src/**/*.{js,ts,jsx,tsx}",],theme: {extend: {// Cores da IngenioLabcolors: {primary: {50: '#eff6ff',100: '#dbeafe',500: '#3b82f6',600: '#2563eb',900: '#1e3a8a',},secondary: {50: '#f8fafc',500: '#64748b',900: '#0f172a',},// Cores customizadasingeniolab: {blue: '#007bff',green: '#28a745',red: '#dc3545',}},// FontesfontFamily: {sans: ['Inter', 'system-ui', 'sans-serif'],mono: ['JetBrains Mono', 'monospace'],},// Espaçamentos customizadosspacing: {'72': '18rem','84': '21rem','96': '24rem',},// Breakpoints customizadosscreens: {'xs': '475px','3xl': '1600px',},// Animaçõesanimation: {'fade-in': 'fadeIn 0.5s ease-in-out','slide-up': 'slideUp 0.3s ease-out',},keyframes: {fadeIn: {'0%': { opacity: '0' },'100%': { opacity: '1' },},slideUp: {'0%': { transform: 'translateY(10px)', opacity: '0' },'100%': { transform: 'translateY(0)', opacity: '1' },}}},},plugins: [require('@tailwindcss/forms'),require('@tailwindcss/typography'),require('@tailwindcss/aspect-ratio'),],}
2. postcss.config.js:
export default {plugins: {tailwindcss: {},autoprefixer: {},},}
3. CSS principal:
/* src/index.css */@tailwind base;@tailwind components;@tailwind utilities;/* Estilos base customizados */@layer base {html {@apply scroll-smooth;}body {@apply bg-gray-50 text-gray-900;}h1, h2, h3, h4, h5, h6 {@apply font-semibold text-gray-900;}}/* Componentes customizados */@layer components {.btn {@apply px-4 py-2 rounded-lg font-medium transition-colors duration-200;}.btn-primary {@apply bg-primary-500 hover:bg-primary-600 text-white;}.btn-secondary {@apply bg-gray-200 hover:bg-gray-300 text-gray-900;}.card {@apply bg-white rounded-xl shadow-lg border border-gray-200 p-6;}.input {@apply w-full px-3 py-2 border border-gray-300 rounded-lgfocus:ring-2 focus:ring-primary-500 focus:border-primary-500;}}/* Utilitários customizados */@layer utilities {.text-balance {text-wrap: balance;}.scrollbar-hide {-ms-overflow-style: none;scrollbar-width: none;}.scrollbar-hide::-webkit-scrollbar {display: none;}}
Componentes React com Tailwind
1. Button Component:
// src/components/ui/Button.tsximport { ButtonHTMLAttributes, forwardRef } from 'react'import { cva, type VariantProps } from 'class-variance-authority'import { cn } from '@/utils/cn'const buttonVariants = cva('inline-flex items-center justify-center rounded-lg text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',{variants: {variant: {default: 'bg-primary-500 text-white hover:bg-primary-600',destructive: 'bg-red-500 text-white hover:bg-red-600',outline: 'border border-gray-300 bg-white hover:bg-gray-50',secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',ghost: 'hover:bg-gray-100',link: 'text-primary-500 underline-offset-4 hover:underline',},size: {default: 'h-10 px-4 py-2',sm: 'h-8 rounded-md px-3 text-xs',lg: 'h-12 rounded-lg px-8',icon: 'h-10 w-10',},},defaultVariants: {variant: 'default',size: 'default',},})export interface ButtonPropsextends ButtonHTMLAttributes<HTMLButtonElement>,VariantProps<typeof buttonVariants> {asChild?: boolean}const Button = forwardRef<HTMLButtonElement, ButtonProps>(({ className, variant, size, asChild = false, ...props }, ref) => {return (<buttonclassName={cn(buttonVariants({ variant, size, className }))}ref={ref}{...props}/>)})export { Button, buttonVariants }// Utilitário para classes condicionais// src/utils/cn.tsimport { type ClassValue, clsx } from 'clsx'import { twMerge } from 'tailwind-merge'export function cn(...inputs: ClassValue[]) {return twMerge(clsx(inputs))}
2. Card Component:
// src/components/ui/Card.tsximport { HTMLAttributes, forwardRef } from 'react'import { cn } from '@/utils/cn'const Card = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (<divref={ref}className={cn('rounded-xl border border-gray-200 bg-white p-6 shadow-lg',className)}{...props}/>))const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (<divref={ref}className={cn('flex flex-col space-y-1.5 pb-4', className)}{...props}/>))const CardTitle = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLHeadingElement>>(({ className, ...props }, ref) => (<h3ref={ref}className={cn('text-lg font-semibold leading-none tracking-tight', className)}{...props}/>))const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(({ className, ...props }, ref) => (<div ref={ref} className={cn('', className)} {...props} />))export { Card, CardHeader, CardTitle, CardContent }
3. Layout Components:
// src/components/Layout.tsximport { ReactNode } from 'react'interface LayoutProps {children: ReactNode}export const Layout = ({ children }: LayoutProps) => {return (<div className="min-h-screen bg-gray-50">{/* Header */}<header className="bg-white shadow-sm border-b border-gray-200"><div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"><div className="flex justify-between items-center h-16"><div className="flex items-center space-x-4"><h1 className="text-xl font-bold text-gray-900">IngenioLab</h1></div><nav className="hidden md:flex space-x-8"><a href="/" className="text-gray-900 hover:text-primary-500 transition-colors">Home</a><a href="/users" className="text-gray-900 hover:text-primary-500 transition-colors">Users</a><a href="/dashboard" className="text-gray-900 hover:text-primary-500 transition-colors">Dashboard</a></nav></div></div></header>{/* Main Content */}<main className="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8">{children}</main></div>)}// src/components/Dashboard.tsxexport const Dashboard = () => {return (<div className="space-y-8">{/* Header */}<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between"><h2 className="text-2xl font-bold text-gray-900">Dashboard</h2><div className="mt-4 sm:mt-0"><button className="btn btn-primary">New Report</button></div></div>{/* Stats Grid */}<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"><div className="card"><div className="text-sm font-medium text-gray-500">Total Users</div><div className="text-3xl font-bold text-gray-900 mt-2">1,234</div><div className="text-sm text-green-600 mt-2">+12% from last month</div></div><div className="card"><div className="text-sm font-medium text-gray-500">Revenue</div><div className="text-3xl font-bold text-gray-900 mt-2">$45,678</div><div className="text-sm text-red-600 mt-2">-2% from last month</div></div><div className="card"><div className="text-sm font-medium text-gray-500">Orders</div><div className="text-3xl font-bold text-gray-900 mt-2">891</div><div className="text-sm text-green-600 mt-2">+8% from last month</div></div><div className="card"><div className="text-sm font-medium text-gray-500">Conversion Rate</div><div className="text-3xl font-bold text-gray-900 mt-2">2.4%</div><div className="text-sm text-green-600 mt-2">+0.3% from last month</div></div></div>{/* Charts Section */}<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"><div className="card"><h3 className="text-lg font-semibold text-gray-900 mb-4">Revenue Chart</h3><div className="h-64 bg-gray-100 rounded-lg flex items-center justify-center"><span className="text-gray-500">Chart Placeholder</span></div></div><div className="card"><h3 className="text-lg font-semibold text-gray-900 mb-4">User Growth</h3><div className="h-64 bg-gray-100 rounded-lg flex items-center justify-center"><span className="text-gray-500">Chart Placeholder</span></div></div></div></div>)}
Formulários com Tailwind
1. Form Components:
// src/components/forms/UserForm.tsximport { useState } from 'react'import { Button } from '@/components/ui/Button'import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'interface UserFormData {name: stringemail: stringrole: stringisActive: boolean}export const UserForm = () => {const [formData, setFormData] = useState<UserFormData>({name: '',email: '',role: 'user',isActive: true})const [errors, setErrors] = useState<Partial<UserFormData>>({})const handleSubmit = (e: React.FormEvent) => {e.preventDefault()// Validação e envioconsole.log('Form data:', formData)}return (<Card className="max-w-2xl mx-auto"><CardHeader><CardTitle>Criar Usuário</CardTitle></CardHeader><CardContent><form onSubmit={handleSubmit} className="space-y-6">{/* Name Field */}<div><label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-2">Nome *</label><inputid="name"type="text"value={formData.name}onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}className={`input ${errors.name ? 'border-red-500 focus:ring-red-500 focus:border-red-500' : ''}`}placeholder="Nome completo"/>{errors.name && (<p className="mt-1 text-sm text-red-600">{errors.name}</p>)}</div>{/* Email Field */}<div><label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-2">Email *</label><inputid="email"type="email"value={formData.email}onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}className={`input ${errors.email ? 'border-red-500 focus:ring-red-500 focus:border-red-500' : ''}`}placeholder="usuario@exemplo.com"/>{errors.email && (<p className="mt-1 text-sm text-red-600">{errors.email}</p>)}</div>{/* Role Select */}<div><label htmlFor="role" className="block text-sm font-medium text-gray-700 mb-2">Role</label><selectid="role"value={formData.role}onChange={(e) => setFormData(prev => ({ ...prev, role: e.target.value }))}className="input"><option value="user">User</option><option value="admin">Admin</option><option value="moderator">Moderator</option></select></div>{/* Active Checkbox */}<div className="flex items-center"><inputid="isActive"type="checkbox"checked={formData.isActive}onChange={(e) => setFormData(prev => ({ ...prev, isActive: e.target.checked }))}className="h-4 w-4 text-primary-500 focus:ring-primary-500 border-gray-300 rounded"/><label htmlFor="isActive" className="ml-2 block text-sm text-gray-900">Usuário ativo</label></div>{/* Actions */}<div className="flex justify-end space-x-4 pt-4"><Button variant="secondary" type="button">Cancelar</Button><Button type="submit">Criar Usuário</Button></div></form></CardContent></Card>)}
Responsividade
1. Grid Responsivo:
// src/components/ResponsiveGrid.tsxexport const ResponsiveGrid = () => {return (<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">{Array.from({ length: 8 }).map((_, index) => (<div key={index} className="card"><h3 className="text-lg font-semibold mb-2">Card {index + 1}</h3><p className="text-gray-600">Conteúdo do card que se adapta automaticamente ao tamanho da tela.</p></div>))}</div>)}
2. Navigation Responsiva:
// src/components/ResponsiveNav.tsximport { useState } from 'react'export const ResponsiveNav = () => {const [isMenuOpen, setIsMenuOpen] = useState(false)return (<nav className="bg-white shadow-lg"><div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"><div className="flex justify-between h-16">{/* Logo */}<div className="flex items-center"><h1 className="text-xl font-bold text-gray-900">IngenioLab</h1></div>{/* Desktop Menu */}<div className="hidden md:flex items-center space-x-8"><a href="/" className="text-gray-700 hover:text-primary-500 transition-colors">Home</a><a href="/users" className="text-gray-700 hover:text-primary-500 transition-colors">Users</a><a href="/dashboard" className="text-gray-700 hover:text-primary-500 transition-colors">Dashboard</a><Button size="sm">Login</Button></div>{/* Mobile menu button */}<div className="md:hidden flex items-center"><buttononClick={() => setIsMenuOpen(!isMenuOpen)}className="text-gray-700 hover:text-primary-500 focus:outline-none focus:text-primary-500"><svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}d="M4 6h16M4 12h16M4 18h16" /></svg></button></div></div>{/* Mobile Menu */}{isMenuOpen && (<div className="md:hidden"><div className="px-2 pt-2 pb-3 space-y-1 sm:px-3 bg-gray-50"><a href="/" className="block px-3 py-2 text-gray-700 hover:text-primary-500">Home</a><a href="/users" className="block px-3 py-2 text-gray-700 hover:text-primary-500">Users</a><a href="/dashboard" className="block px-3 py-2 text-gray-700 hover:text-primary-500">Dashboard</a><div className="px-3 py-2"><Button size="sm" className="w-full">Login</Button></div></div></div>)}</div></nav>)}
Dark Mode
1. Dark Mode Configuration:
// tailwind.config.jsmodule.exports = {darkMode: 'class',// ... resto da configuração}
2. Dark Mode Provider:
// src/contexts/ThemeContext.tsximport { createContext, useContext, useEffect, useState } from 'react'type Theme = 'light' | 'dark'interface ThemeContextType {theme: ThemesetTheme: (theme: Theme) => voidtoggleTheme: () => void}const ThemeContext = createContext<ThemeContextType | undefined>(undefined)export function ThemeProvider({ children }: { children: React.ReactNode }) {const [theme, setTheme] = useState<Theme>(() => {if (typeof window !== 'undefined') {return (localStorage.getItem('theme') as Theme) || 'light'}return 'light'})useEffect(() => {const root = window.document.documentElementroot.classList.remove('light', 'dark')root.classList.add(theme)localStorage.setItem('theme', theme)}, [theme])const toggleTheme = () => {setTheme(prev => prev === 'light' ? 'dark' : 'light')}return (<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>{children}</ThemeContext.Provider>)}export const useTheme = () => {const context = useContext(ThemeContext)if (!context) {throw new Error('useTheme must be used within ThemeProvider')}return context}
3. Dark Mode Classes:
/* src/index.css */@layer components {.card {@apply bg-white dark:bg-gray-800 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 p-6;}.btn-primary {@apply bg-primary-500 hover:bg-primary-600 dark:bg-primary-600 dark:hover:bg-primary-700 text-white;}.input {@apply w-full px-3 py-2 border border-gray-300 dark:border-gray-600bg-white dark:bg-gray-700 text-gray-900 dark:text-whiterounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500;}}
4. Theme Toggle:
// src/components/ThemeToggle.tsximport { useTheme } from '@/contexts/ThemeContext'export const ThemeToggle = () => {const { theme, toggleTheme } = useTheme()return (<buttononClick={toggleTheme}className="p-2 rounded-lg bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">{theme === 'light' ? (<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clipRule="evenodd" /></svg>) : (<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" /></svg>)}</button>)}
Plugins Recomendados
1. @tailwindcss/forms:
npm install -D @tailwindcss/forms
2. @tailwindcss/typography:
npm install -D @tailwindcss/typography
3. Uso dos plugins:
// Componente com typographyexport const Article = () => {return (<article className="prose prose-lg dark:prose-invert max-w-none"><h1>Título do Artigo</h1><p>Conteúdo do artigo com tipografia otimizada...</p></article>)}
Performance
1. Purging automático:
// tailwind.config.js - já configurado automaticamentemodule.exports = {content: ["./src/**/*.{js,jsx,ts,tsx}","./index.html"],// Tailwind remove classes não utilizadas automaticamente}
2. Class variance authority:
npm install class-variance-authority clsx tailwind-merge