Introdução
O Design System do UpForService é a fonte única de verdade para a linguagem visual do produto. Ele fornece a designers e engenheiros tokens, padrões e diretrizes compartilhados para construir interfaces consistentes e acessíveis em escala.
Tokens em primeiro lugar
Cada cor, tamanho e sombra é um token nomeado. Nenhum valor mágico nos componentes.
Composável
Primitivos compõem padrões. Padrões compõem páginas.
Acessível
Contraste WCAG AA, navegação por teclado e HTML semântico em todo lugar.
Princípios de Design
Quatro princípios guiam cada decisão no Design System do UpForService.
Confiança pela clareza
Transações financeiras e contratos de serviço exigem comunicação inequívoca. Cada elemento de interface deve ser claro sobre o que faz e o que acontecerá a seguir.
Simples na UI, rico no domínio
A UI expõe o caminho feliz. O modelo de domínio carrega versionamento, histórico de eventos e máquinas de estado desde o início. A complexidade vive na camada certa.
Acessível por padrão
Contraste, navegação por teclado, focus rings e HTML semântico não são recursos opcionais. São requisitos de base para cada componente.
Preparado para internacionalização
Cada string é uma chave de tradução. Layouts levam em conta a expansão do texto. Formatos de moeda e número seguem as convenções do locale.
Cores
Todas as cores são definidas como propriedades CSS customizadas em globals.css e mapeadas para o Tailwind via @theme inline. A paleta suporta temas claro e escuro.
Marca
Primary #0A6FFF: 3.0:1 on white (AA-Large) · use --primary-text for body textPrimária
--primary
#0A6FFF
Hover primária
--primary-hover
#0858D4
Texto primário
--primary-text
#0858D4
Terciária
--tertiary
#7C3AED
Fundo e Superfície
Fundo
--background
#FFFFFF
Azul suave
--surface-pale-blue
#F8FAFF
Azul tintado
--surface-tint-blue
#F0F5FF
Frio
--surface-cool
#F8FAFC
Navy
--surface-navy
#0A1628
Navy médio
--surface-navy-mid
#0D1F3C
Texto
All text tokens: 4.5:1+ AA on white backgroundTítulo
--heading
#111827
Primeiro plano
--foreground
#1F2937
Text secondary
--text-secondary
#4B5563
Silenciado
--muted-foreground
#6B7280
Quieto
--quiet
#9CA3AF
Chrome de UI
Card
--card
#FFFFFF
Surface secondary
--secondary
#F8FAFC
Destaque
--accent
#F0F5FF
Borda
--border
#E5E7EB
Input
--input
#E5E7EB
Superfície suave
--muted
#F8FAFF
Status
All status foreground tokens meet 4.5:1 AA on their respective bg tokensDestrutivo
--destructive
#EF4444
Aviso
--warning
#D97706
Sucesso
--success
#059669
Informação
--info
#2563EB
Fundo aviso
--warning-bg
#FFFBEB
Fundo sucesso
--success-bg
#ECFDF5
Desabilitado
Desabilitado
--disabled
#E5E7EB
Texto desabilitado
--disabled-foreground
#9CA3AF
Modo escuro
Todos os tokens têm overrides para modo escuro na classe .dark em globals.css. O tema escuro usa a mesma paleta navy das seções de marketing, mantendo a consistência visual entre os modos.
Tipografia
Três famílias de fontes são carregadas via otimização de fontes do Next.js. Bricolage Grotesque para títulos de exibição, Inter para corpo e UI, e JetBrains Mono para código e valores cripto.
Famílias de fontes
Display
Aa Bb Cc
Bricolage Grotesque
--font-display
Títulos, marketing
Sans
Aa Bb Cc
Inter
--font-sans
Corpo, labels de UI
Mono
Aa Bb Cc
JetBrains Mono
--font-mono
Código, endereços cripto
Escala tipográfica
Display XL
48–72px · 800 · display
Display LG
36–48px · 800 · display
Display MD
30–36px · 700 · display
H1
36px · 700 · display
H2
30px · 700 · display
H3
24px · 600 · display
H4
20px · 600 · sans
Body large
18px · 400 · sans
Body
16px · 400 · sans
Body small
14px · 400 · sans
Caption
12px · 400 · sans
Label
14px · 500 · sans
Mono
14px · 400 · mono
Espaçamentos
Grid base de 4px. Todos os valores são múltiplos de 4px. Use a escala do Tailwind diretamente — p-4 = 16px, gap-6 = 24px.
Borda e Raio
Os tokens de raio são definidos como propriedades CSS customizadas e mapeados para o Tailwind via @theme inline. Use rounded-pill, rounded-card, rounded-md, rounded-sm, rounded-xs.
Nenhum
--radius-none
0px
xs
--radius-xs
6px
sm
--radius-sm
10px
md
--radius-md
16px
Card / lg
--radius-card
24px
Pílula / full
--radius-pill
9999px
Espessuras de borda
border
1px
border-2
2px
border-4
4px
Sombras
Sombras são nomeadas semanticamente. Use shadow-raised para elementos elevados, shadow-feature-panel para cards de marketing, e shadow-cta-glow para botões de ação primária.
raised
--shadow-raised
feature-panel
--shadow-feature-panel
marketing
--shadow-marketing
cta-glow
--shadow-cta-glow
Anel de foco
Todos os elementos interativos usam --ring para visibilidade de foco. Aplicado via focus-visible:ring-[3px] focus-visible:ring-ring/50.
Escala de Z-index
Definida no JSON de tokens. Use esses valores para manter ordem de empilhamento consistente em toda a aplicação.
Breakpoints & Grid
Design responsivo mobile-first. Escala de breakpoints padrão do Tailwind com container de conteúdo max-w-[1200px] e max-w-[1400px] para navegação em largura total.
Container & gutters
Largura máx. conteúdo
1200px
Páginas, seções
Largura máx. nav
1400px
Nav superior, rodapé
Gutter (mobile)
px-4 (16px)
Todos os viewports
Gutter (md+)
px-6 (24px)
Tablet para cima
Colunas do grid
12
Base CSS Grid
Componentes
Construído sobre primitivos shadcn/ui com Radix UI. Todos os componentes usam class-variance-authority para variantes e aceitam uma prop className para extensão.
Badge
Label inline para status, categorias e metadados. Usa rounded-full e suporta asChild.
Variantes
Exemplos de status
import { Badge } from '@/components/ui/badge'
<Badge>Active</Badge>
<Badge variant="destructive">Failed</Badge>
<Badge variant="outline">Pending</Badge>
<Badge variant="secondary">Draft</Badge>
// Status with icon
<Badge className="bg-success text-success-foreground">
<CheckCircle2 className="size-3" />
Confirmed
</Badge>
// Eyebrow — purely typographic, no visual container
<Badge variant="eyebrow">Service category</Badge>Card
Container para conteúdo agrupado. Usa rounded-card (24px). Sub-componentes: CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter.
Padrão
Revisão detalhada dos ciclos de renderização, tamanho do bundle e Core Web Vitals do seu app React.
Feature (elevado)
Os fundos são liberados somente após você confirmar que o serviço foi entregue conforme acordado.
import { Card, CardHeader, CardTitle, CardDescription,
CardAction, CardContent, CardFooter } from '@/components/ui/card'
// Default
<Card>
<CardHeader>
<CardTitle>Service offer</CardTitle>
<CardDescription>React performance audit</CardDescription>
<CardAction><Badge>Active</Badge></CardAction>
</CardHeader>
<CardContent>Description goes here.</CardContent>
<CardFooter><Button>View details</Button></CardFooter>
</Card>
// Feature (elevated shadow)
<Card variant="feature">...</Card>Avatar
Representação de usuário com imagem, fallback de iniciais e suporte a badge. Construído sobre o primitivo Avatar do Radix UI.
Tamanhos
Grupo de avatars
Cores de fundo determinísticas
8 fundos em OKLCH L=0.45 C=0.10 — todos passam 4.5:1 WCAG AA contra iniciais brancas.
import { Avatar, AvatarImage, AvatarFallback,
AvatarGroup, AvatarGroupCount } from '@/components/ui/avatar'
// With image + fallback
<Avatar size="lg">
<AvatarImage src="/photo.jpg" alt="Lucy Brandão" />
<AvatarFallback
style={{ backgroundColor: 'var(--avatar-bg-3)', color: 'var(--avatar-fg)' }}>
LB
</AvatarFallback>
</Avatar>
// Group
<AvatarGroup>
<Avatar><AvatarFallback>AB</AvatarFallback></Avatar>
<Avatar><AvatarFallback>CD</AvatarFallback></Avatar>
<AvatarGroupCount>+5</AvatarGroupCount>
</AvatarGroup>Campo de texto
Input de texto de uma linha. Compartilha tokens de foco, erro e desabilitado de forma consistente entre todos os controles de formulário.
Padrão
Com texto auxiliar
Visível na sua página de perfil público.
Estado de erro
Digite um endereço de e-mail válido.
Desabilitado
Endereço bloqueado após verificação.
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
// Default
<div className="space-y-2">
<Label htmlFor="title">Service title</Label>
<Input id="title" placeholder="React performance audit" />
</div>
// Error state
<div className="space-y-2">
<Label htmlFor="email" className="text-destructive">Email</Label>
<Input id="email" aria-invalid="true"
className="border-destructive focus-visible:ring-destructive/20" />
<p className="text-xs text-destructive" role="alert">
Enter a valid email address.
</p>
</div>
// Disabled
<Input disabled value="locked@value.com" />Label associado (via htmlFor) ou um aria-label. Mensagens de erro usam aria-invalid="true".Select
Seleção por dropdown construída sobre o Radix UI Select. Navegável por teclado, suporta grupos, labels e itens desabilitados.
Padrão
Com grupos
Estado de erro
Digite um endereço de e-mail válido.
Desabilitado
import { Select, SelectTrigger, SelectValue, SelectContent,
SelectItem, SelectGroup, SelectLabel, SelectSeparator
} from '@/components/ui/select'
<Select>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select category" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Technology</SelectLabel>
<SelectItem value="web">Web development</SelectItem>
<SelectItem value="mobile">Mobile apps</SelectItem>
</SelectGroup>
<SelectSeparator />
<SelectGroup>
<SelectLabel>Creative</SelectLabel>
<SelectItem value="design">Graphic design</SelectItem>
</SelectGroup>
</SelectContent>
</Select>SelectLabel e SelectSeparator para opções agrupadas. Itens desabilitados usam data-[disabled].Textarea
Input de texto de múltiplas linhas. Usa field-sizing-content para crescimento automático de altura. Compartilha os mesmos tokens de foco, erro e desabilitado que o Campo de texto.
Padrão
Máximo de 2 000 caracteres.
Com texto auxiliar
Sua avaliação será pública no perfil do prestador.
Estado de erro
A bio deve ter pelo menos 20 caracteres.
Desabilitado
Notas internas são visíveis apenas para você.
import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label'
<div className="space-y-2">
<Label htmlFor="desc">Service description</Label>
<Textarea id="desc"
placeholder="Describe what's included…"
/>
<p className="text-xs text-muted-foreground">
Maximum 2 000 characters.
</p>
</div>Label. Estado de erro usa aria-invalid="true". O auto-resize via field-sizing-content não precisa de JavaScript.Separator
Divisor visual fino. Horizontal por padrão; use orientation="vertical" para contextos inline.
Horizontal
Section A
Section B
Section C
Vertical
import { Separator } from '@/components/ui/separator'
// Horizontal (default) — between sections
<Separator />
// Vertical — between inline items
<div className="flex items-center gap-4 h-8">
<span>Overview</span>
<Separator orientation="vertical" />
<span>Reviews</span>
</div>
// Semantic (not decorative)
<Separator decorative={false} aria-label="Between navigation regions" />decorative={false} e um aria-label quando o separador transmite estrutura (ex: entre regiões de navegação).Skeleton
Placeholder animado exibido enquanto o conteúdo carrega. Use className para corresponder à forma do conteúdo real.
Linhas de texto
Card
Avatar + texto
import { Skeleton } from '@/components/ui/skeleton'
// Text lines
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
</div>
// Avatar + text
<div className="flex items-center gap-3">
<Skeleton className="size-10 rounded-full" />
<div className="space-y-2">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-20" />
</div>
</div>
// Wrap loading region for screen readers
<div aria-busy="true" aria-label="Loading offers">
<Skeleton className="h-48 w-full rounded-card" />
</div>aria-busy="true" e remova quando o conteúdo real aparecer. O Skeleton não carrega role ARIA.Checkbox
Input binário para seleções booleanas. Construído sobre o Radix UI Checkbox — totalmente acessível por teclado.
States
import { Checkbox } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label'
// Default
<div className="flex items-center gap-2">
<Checkbox id="terms" />
<Label htmlFor="terms">Accept terms of service</Label>
</div>
// Controlled
const [checked, setChecked] = useState(false)
<Checkbox checked={checked} onCheckedChange={setChecked} />
// Indeterminate (e.g. select-all with partial selection)
<Checkbox checked="indeterminate" />Label via htmlFor. O estado indeterminado usa checked="indeterminate" e requer um trigger programático — não é um estado HTML nativo.Switch
Toggle para estados on/off. Prefira Switch ao Checkbox quando a ação tem efeito imediato (sem necessidade de submit).
Padrão (tamanhos sm + default)
Estados
import { Switch } from '@/components/ui/switch'
import { Label } from '@/components/ui/label'
// Uncontrolled
<div className="flex items-center gap-2">
<Switch id="notifications" />
<Label htmlFor="notifications">Email notifications</Label>
</div>
// Controlled
const [on, setOn] = useState(false)
<Switch checked={on} onCheckedChange={setOn} />
// Small size
<Switch size="sm" />Label. O Switch comunica seu estado via aria-checked — nenhum ARIA adicional é necessário.Tabs
Organiza conteúdo relacionado em painéis selecionáveis. Suporta duas variantes visuais: default (pílula) e line.
Padrão (pílula)
Descrição completa do serviço, inclusões, processo e prazo estimado aparecem aqui.
Linha
Descrição completa do serviço, inclusões, processo e prazo estimado aparecem aqui.
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
// Default (pill) variant
<Tabs defaultValue="overview">
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="reviews">Reviews</TabsTrigger>
</TabsList>
<TabsContent value="overview">Content A</TabsContent>
<TabsContent value="reviews">Content B</TabsContent>
</Tabs>
// Line variant
<TabsList variant="line">
<TabsTrigger value="overview">Overview</TabsTrigger>
</TabsList>aria-controls / aria-labelledby pelo Radix.Tooltip
Label contextual não-interativa acionada em hover/foco. Envolva elementos interativos com TooltipProvider na raiz da árvore.
Padrão
import { Tooltip, TooltipTrigger, TooltipContent,
TooltipProvider } from '@/components/ui/tooltip'
// Wrap the tree once (typically in a layout)
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" aria-label="Escrow info">
<ShieldCheck className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
Funds held in escrow until delivery is confirmed
</TooltipContent>
</Tooltip>
</TooltipProvider>TooltipProvider delayDuration={0} para remover o delay de abertura.Dialog
Overlay modal que concentra atenção em uma decisão crítica ou formulário. Construído sobre o Radix UI Dialog — aprisiona foco, fecha com Escape.
Padrão
Ação destrutiva
import { Dialog, DialogTrigger, DialogContent, DialogHeader,
DialogTitle, DialogDescription, DialogFooter
} from '@/components/ui/dialog'
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Open dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Confirm order</DialogTitle>
<DialogDescription>
Payment will be held in escrow until delivery.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline">Cancel</Button>
<Button>Place order</Button>
</DialogFooter>
</DialogContent>
</Dialog>DialogTitle e DialogDescription são obrigatórios — fornecem o nome acessível e a descrição consumidos por aria-labelledby / aria-describedby no container do dialog.Callout
Mensagem contextual inline com ícone. Usada para destacar cues de confiança, notas de status e alertas não-bloqueantes sem interromper o fluxo do usuário.
Info (padrão)
Sucesso
Aviso
Perigo
Neutro
import { Callout } from '@/components/ui/callout'
import { ShieldCheck, AlertTriangle } from 'lucide-react'
// Info (default) — escrow trust cue
<Callout icon={<ShieldCheck />}>
Your payment is protected by Solana escrow.
</Callout>
// Warning
<Callout variant="warning" icon={<AlertTriangle />}>
This offer will be paused after 30 days of inactivity.
</Callout>
// Critical — add role="alert" for screen readers
<Callout variant="danger" icon={<AlertCircle />} role="alert">
Payment failed. Please try again.
</Callout>role="alert" para que leitores de tela a anunciem imediatamente.Pagination
Paginação baseada em offset com truncamento para grandes números de páginas. A página 1 omite o parâmetro page. O componente real usa useSearchParams para estado via URL — importe de @/components/public/pagination.
Padrão (página 2 de 8)
Primeira página (anterior desabilitado)
Última página (próxima desabilitada)
import { Pagination } from '@/components/public/pagination'
// URL-driven — reads/writes ?page= search param
<Suspense>
<Pagination total={180} pageSize={20} page={currentPage} />
</Suspense>Design System v1
Este é um documento vivo. Novos componentes e tokens serão adicionados conforme o produto evolui. Baixe os tokens para usá-los no Figma ou outras ferramentas.