.github/skills/frontend-development/SKILL.md
Create beautiful, polished, and highly usable Next.js/React/TypeScript interfaces for Clarin CRM. Use when creating or modifying dashboard pages, components, API calls, WebSocket listeners, or UI styling. Enforces visual excellence, micro-interactions, accessibility, and the emerald/slate design system.
npx skillsauth add ricardoalejandro/clarin frontend-developmentInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Every UI must feel premium, polished, and alive. Think Notion, Linear, Vercel Dashboard — dark themes done right. Never ship something that looks "functional but ugly." Every pixel matters.
docker compose build frontendfrontend/src/
app/
layout.tsx → Root layout
page.tsx → Login page
globals.css → Global styles + custom scrollbar classes
dashboard/
layout.tsx → Dashboard layout with sidebar navigation
page.tsx → Dashboard home
chats/page.tsx → Chat management (WhatsApp)
devices/page.tsx → WhatsApp device management
leads/page.tsx → Leads kanban board
settings/page.tsx → Account settings
components/
CreateCampaignModal.tsx → Campaign creation modal
ImportCSVModal.tsx → CSV import modal
NotificationProvider.tsx → Toast notifications
TagInput.tsx → Tag input component
WhatsAppTextInput.tsx → WhatsApp text formatting input
chat/ → Chat-related components
lib/
api.ts → API client + WebSocket factory
utils.ts → Shared utilities
NEVER use green, gray, or any other color palette. Only emerald and slate.
| Element | Classes |
|---------|---------|
| Primary buttons | bg-emerald-600 hover:bg-emerald-700 text-white font-medium shadow-lg shadow-emerald-600/20 |
| Secondary buttons | bg-slate-700 hover:bg-slate-600 text-slate-200 border border-slate-600 |
| Danger buttons | bg-red-600/90 hover:bg-red-600 text-white |
| Page background | bg-slate-900 |
| Cards/panels | bg-slate-800/80 backdrop-blur-sm border border-slate-700/50 rounded-xl shadow-xl |
| Elevated cards | bg-slate-800 border border-slate-700/50 rounded-xl shadow-2xl shadow-black/20 |
| Text primary | text-white or text-slate-100 |
| Text secondary | text-slate-400 |
| Text muted | text-slate-500 |
| Inputs | bg-slate-800/50 border border-slate-600/50 text-white placeholder-slate-500 rounded-lg |
| Input focus | focus:ring-2 focus:ring-emerald-500/40 focus:border-emerald-500 outline-none |
| Hover rows | hover:bg-slate-700/40 transition-colors |
| Dividers | border-slate-700/50 |
| Badges/pills | bg-emerald-500/10 text-emerald-400 text-xs font-medium px-2.5 py-0.5 rounded-full |
| Status green | bg-emerald-500/10 text-emerald-400 |
| Status yellow | bg-amber-500/10 text-amber-400 |
| Status red | bg-red-500/10 text-red-400 |
Layer 0 (page): bg-slate-900
Layer 1 (sidebar): bg-slate-800/95 backdrop-blur-md
Layer 2 (cards): bg-slate-800/80 border-slate-700/50
Layer 3 (modals): bg-slate-800 border-slate-700/50 shadow-2xl
Layer 4 (dropdowns): bg-slate-750 border-slate-600/50 shadow-xl
Overlay: bg-black/60 backdrop-blur-sm
// EVERY interactive element needs transitions
<button className="... transition-all duration-200 hover:scale-[1.02] active:scale-[0.98]">
// Smooth hover on cards
<div className="... transition-all duration-200 hover:border-slate-600 hover:shadow-lg">
// Fade-in loading states
<div className={`transition-opacity duration-300 ${loading ? 'opacity-50' : 'opacity-100'}`}>
// Skeleton loading — not spinners for content areas
{loading ? (
<div className="space-y-3">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-16 bg-slate-700/50 rounded-lg animate-pulse" />
))}
</div>
) : (
<ActualContent />
)}
// Use spinners only for actions (saving, submitting)
{saving && <Loader2 className="w-4 h-4 animate-spin" />}
// Never show just "No data" — make it inviting
<div className="flex flex-col items-center justify-center py-16 text-center">
<div className="w-16 h-16 bg-slate-800 rounded-2xl flex items-center justify-center mb-4">
<Users className="w-8 h-8 text-slate-500" />
</div>
<h3 className="text-lg font-semibold text-slate-300 mb-1">No hay leads todavía</h3>
<p className="text-sm text-slate-500 mb-6 max-w-sm">
Los leads aparecerán aquí cuando se sincronicen desde Kommo o se creen manualmente.
</p>
<button className="bg-emerald-600 hover:bg-emerald-700 text-white px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 shadow-lg shadow-emerald-600/20">
<Plus className="w-4 h-4 mr-2 inline" />
Crear lead
</button>
</div>
Page title: text-2xl font-bold text-white tracking-tight
Section title: text-lg font-semibold text-slate-100
Card title: text-base font-medium text-white
Body text: text-sm text-slate-300
Caption/label: text-xs font-medium text-slate-400 uppercase tracking-wider
Numbers/stats: text-3xl font-bold tracking-tight tabular-nums
Page padding: p-6 (desktop), p-4 (mobile)
Card padding: p-5 or p-6
Section gaps: space-y-6
Element gaps: space-y-3 or gap-3
import { Search, Plus, X, ChevronDown, MoreHorizontal, Settings, Loader2 } from "lucide-react"
// Icons in buttons: w-4 h-4
<button><Plus className="w-4 h-4 mr-2" /> Crear</button>
// Icons standalone: w-5 h-5
<Search className="w-5 h-5 text-slate-400" />
// Icons in empty states: w-8 h-8
<Users className="w-8 h-8 text-slate-500" />
// Tables and lists: ALWAYS handle overflow text
<span className="truncate max-w-[200px]">{name}</span>
// Long content: scroll containers
<div className="overflow-y-auto max-h-[calc(100vh-200px)] scrollbar-thin scrollbar-thumb-slate-700">
// Grid layouts: responsive
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
// Overlay
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4">
// Modal
<div className="bg-slate-800 border border-slate-700/50 rounded-2xl shadow-2xl w-full max-w-lg max-h-[90vh] overflow-y-auto">
// Header
<div className="flex items-center justify-between p-6 border-b border-slate-700/50">
<h2 className="text-lg font-semibold text-white">Título</h2>
<button className="text-slate-400 hover:text-white transition-colors p-1 rounded-lg hover:bg-slate-700">
<X className="w-5 h-5" />
</button>
</div>
// Body
<div className="p-6">...</div>
// Footer
<div className="flex justify-end gap-3 p-6 border-t border-slate-700/50">
<button className="px-4 py-2 text-sm text-slate-300 hover:text-white transition-colors">Cancelar</button>
<button className="px-4 py-2 text-sm bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg font-medium transition-all shadow-lg shadow-emerald-600/20">Guardar</button>
</div>
</div>
</div>
<table className="w-full">
<thead>
<tr className="border-b border-slate-700/50">
<th className="text-left text-xs font-medium text-slate-400 uppercase tracking-wider py-3 px-4">Nombre</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-700/30">
<tr className="hover:bg-slate-700/30 transition-colors cursor-pointer">
<td className="py-3 px-4 text-sm text-slate-200">...</td>
</tr>
</tbody>
</table>
<label className="block text-xs font-medium text-slate-400 uppercase tracking-wider mb-1.5">
Nombre
</label>
<input
type="text"
className="w-full bg-slate-800/50 border border-slate-600/50 text-white rounded-lg px-3 py-2.5 text-sm placeholder-slate-500 focus:ring-2 focus:ring-emerald-500/40 focus:border-emerald-500 outline-none transition-all"
placeholder="Ingresa el nombre..."
/>
// Error state
<p className="text-xs text-red-400 mt-1">Este campo es requerido</p>
// Success: emerald accent
<div className="bg-emerald-500/10 border border-emerald-500/20 text-emerald-300 px-4 py-3 rounded-lg text-sm">
<CheckCircle className="w-4 h-4 inline mr-2" /> Guardado correctamente
</div>
// Error: red accent
<div className="bg-red-500/10 border border-red-500/20 text-red-300 px-4 py-3 rounded-lg text-sm">
<AlertCircle className="w-4 h-4 inline mr-2" /> Error al guardar
</div>
tabIndex, onKeyDown for custom widgets.... and shows full text on hover (title attribute or tooltip)."use client"
import { useState, useEffect, useCallback } from "react"
import { Search, Plus, Loader2 } from "lucide-react"
import { apiGet, createWebSocket } from "@/lib/api"
interface Lead {
id: number
name: string
phone: string
}
export default function LeadsPage() {
const [leads, setLeads] = useState<Lead[]>([])
const [loading, setLoading] = useState(true)
const [search, setSearch] = useState("")
const fetchLeads = useCallback(async () => {
try {
const data = await apiGet("/api/leads")
setLeads(data)
} catch (err) {
console.error("Error fetching leads:", err)
} finally {
setLoading(false)
}
}, [])
useEffect(() => { fetchLeads() }, [fetchLeads])
// WebSocket for real-time
useEffect(() => {
const ws = createWebSocket((msg) => {
const data = JSON.parse(msg.data)
if (data.type === "lead_update") fetchLeads()
})
return () => ws?.close()
}, [fetchLeads])
const filtered = leads.filter(l =>
l.name.toLowerCase().includes(search.toLowerCase())
)
return (
<div className="p-6 bg-slate-900 min-h-screen">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-white tracking-tight">Leads</h1>
<p className="text-sm text-slate-400 mt-1">{leads.length} leads en total</p>
</div>
<button className="bg-emerald-600 hover:bg-emerald-700 text-white px-4 py-2.5 rounded-lg text-sm font-medium transition-all duration-200 shadow-lg shadow-emerald-600/20 flex items-center gap-2">
<Plus className="w-4 h-4" /> Nuevo lead
</button>
</div>
{/* Search */}
<div className="relative mb-4">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
<input
value={search}
onChange={e => setSearch(e.target.value)}
className="w-full bg-slate-800/50 border border-slate-700/50 text-white rounded-lg pl-10 pr-4 py-2.5 text-sm placeholder-slate-500 focus:ring-2 focus:ring-emerald-500/40 focus:border-emerald-500 outline-none transition-all"
placeholder="Buscar leads..."
/>
</div>
{/* Content with loading/empty states */}
{loading ? (
<div className="space-y-3">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-16 bg-slate-800/50 rounded-lg animate-pulse" />
))}
</div>
) : filtered.length === 0 ? (
<EmptyState />
) : (
<LeadsList leads={filtered} />
)}
</div>
)
}
import { apiGet, apiPost, apiPut, apiDelete, createWebSocket } from "@/lib/api"
@/ alias for all imports from src/./api/:path* → backend:8080.useCallback for functions passed as deps or to children.src/app/dashboard/new-page/page.tsxsrc/app/dashboard/layout.tsx sidebar"use client", emerald/slate palettedocker compose build frontend && docker compose up -ddevelopment
Enforce code quality, self-testing, and verification standards for Clarin CRM. Use this skill to ensure every change is compiled, deployed, and validated before presenting to the user. Acts as a senior engineer code review checklist.
tools
Work with Kommo CRM integration in Clarin. Use when modifying the sync worker, API client, lead/contact/tag synchronization, or phone normalization. Covers the one-way Kommo to Clarin sync flow.
development
Make database schema changes for Clarin CRM. Use when adding tables, columns, indexes, or modifying the PostgreSQL schema. Migrations live in database.go InitDB() function.
development
Build, deploy, and verify changes for Clarin CRM. Use this skill after making any code change to compile, deploy, and validate that everything works correctly. Covers Docker-based builds for Go backend and Next.js frontend.