Frontend Showcase
Estrutura React de referência para produtos SaaS, pronta para servir de ponto de partida: login simulado, casca de sistema corporativo, painel com gráficos, uma tabela única com exportação para Excel e PDF, e uma paleta de comandos aberta pelo teclado.
O Frontend Showcase é o par do Backend Showcase, agora do lado do navegador: uma estrutura React pronta para servir de ponto de partida de um produto novo. Ela junta, num app que roda de verdade, as peças que todo SaaS (Software as a Service, software vendido por assinatura e usado pelo navegador) acaba precisando. São elas: a tela de login, a casca de um sistema corporativo, um painel com gráficos, listagens com busca e exportação, e uma paleta de comandos aberta pelo teclado. Cada peça dessas tem um tutorial próprio em algum lugar. O que falta é vê-las juntas, e é isso que o projeto mostra.
A escolha de não ter servidor é proposital. O login é simulado no navegador, a sessão fica no sessionStorage (armazenamento que dura enquanto a aba fica aberta) e as listagens usam dados de exemplo gravados no código. O foco é a forma do frontend: como as telas, o estado, o tema e a navegação se encaixam. O contrato REST (Representational State Transfer, o jeito mais comum de um navegador pedir dados a um servidor) equivalente, com autenticação, paginação e documentação, está no projeto irmão.
Conceitos fundamentais
| Conceito | O que é |
|---|---|
| SaaS (Software as a Service) | Produto que roda no navegador e é vendido por assinatura, sem instalação na máquina do usuário. |
| ERP (Enterprise Resource Planning, sistema corporativo de gestão) | Sistema administrativo de empresa. Aqui interessa a casca dele: menu lateral que recolhe, barra fixa no topo e a área de conteúdo no meio. |
| SPA (Single Page Application, aplicação de página única) | Site que troca de tela sem recarregar o navegador. A primeira visita baixa o app inteiro; daí em diante a navegação é instantânea. |
| Design tokens (variáveis de design) | Valores de cor, arredondamento, espaçamento e fonte declarados uma vez, num lugar só. Cada componente lê o token em vez de fixar o valor dentro de si. |
| oklch (espaço de cor) | Forma de escrever cor em que clarear 5% clareia igual em qualquer matiz. Nenhuma cor chama mais atenção que a outra. |
| Vertical Slice (fatia vertical) | Organização do código por funcionalidade (auth, shell, records), não por camada técnica. Cada pasta contém tudo que a funcionalidade precisa. |
| Etiquetas | Marcadores coloridos que classificam um valor (um status, um segmento, uma despesa). A mesma cor significa a mesma coisa na tabela, no formulário e no relatório exportado. |
| Paleta de comandos | Caixa de busca que abre por atalho de teclado e leva a qualquer tela ou ação, sem passar pelo menu. |
| Busca aproximada (fuzzy-match) | Pesquisa que encontra o resultado mesmo com o termo digitado pela metade ou com letra trocada. Digitar “clien” acha “Clientes”. |
| Carregamento sob demanda (lazy import) | Biblioteca pesada que só é baixada quando o usuário aciona a função que precisa dela. Quem nunca exporta uma planilha nunca baixa o código que gera planilhas. |
| Pacote inicial (bundle) | O arquivo de JavaScript que o navegador baixa para abrir o app. Quanto menor, mais rápido a primeira tela aparece. |
| store (repositório de estado) | Lugar único onde fica um dado que várias telas leem, como quem está logado. Cada tela lê da store em vez de receber o dado de mão em mão. |
| Zustand | Biblioteca pequena que cria e gerencia essas stores no React, sem a cerimônia de configuração das alternativas mais antigas. |
| build (construção) | Etapa que monta os arquivos finais do app a partir do código-fonte, antes de publicar. |
| deploy (publicação) | Envio do resultado do build para o servidor que atende o público. |
Stack
| Tecnologia | Papel |
|---|---|
| Node.js 24 LTS | Runtime local, o ambiente que executa as ferramentas de build |
| pnpm 11 | Gerenciador de pacotes |
| TypeScript 6 | Linguagem |
| Vitest 3 + Testing Library | Testes unitários das peças mais frágeis |
| Playwright 1 | Um teste ponta a ponta que percorre o caminho feliz do app |
| Biome 2.4 | Lint e formatação numa ferramenta só |
| Tecnologia | Papel |
|---|---|
| React 19.2 (com React Compiler) | Interface, com o compilador cuidando das otimizações |
| Vite 8 (motor Rolldown) | Build de produção em menos de dois segundos |
| Tailwind CSS 4.2 (motor Oxide) | Estilo escrito em tokens |
| TanStack Router 1 | Rotas derivadas da árvore de arquivos, cada tela num pedaço separado |
| TanStack Table 8 | Motor da tabela: ordenação, filtro e paginação |
| shadcn/ui | Componentes base (Button, Card, Sheet, Command) |
| Recharts 3 | Gráficos do painel, pintados com as cores do tema |
| react-hook-form 7 + Zod 4 | Formulários validados por schema |
| Zustand 5 | Estado global, biblioteca pequena |
| cmdk 1.1 | Paleta de comandos |
| sonner 2 | Avisos flutuantes, acessíveis e empilháveis |
| date-fns 4 + react-day-picker 9 | Calendário em pt-BR sem carregar a biblioteca de datas inteira |
| ExcelJS 4 + jsPDF 4 | Exportação para Excel e PDF, carregada só no clique |
A base é Vite com TanStack Router, por dois motivos. O build de produção sai em menos de dois segundos, graças ao motor Rolldown. E o que uma rota faz está escrito no arquivo dela, sem convenção escondida em outro lugar.
Arquitetura: fatia vertical
O código segue Vertical Slice: cada funcionalidade tem os próprios componentes, hooks, schemas, estado e testes na mesma pasta. Quem vai mexer no login abre features/auth/ e encontra ali tudo que o login precisa, sem caçar o formulário numa pasta de componentes e a validação em outra.
A segunda escolha grande responde onde o usuário age. Um sistema corporativo acumula botões, e aqui cada tipo de ação tem um lugar fixo. O que é do usuário fica na barra do topo. O que é da tela fica no cabeçalho da página. O que é do formulário fica numa barra que surge no rodapé quando alguém edita algo. O salto para outra tela fica na paleta de comandos. Quem aprende a regra uma vez sabe onde procurar em qualquer tela.
| Padrão | Descrição | Detalhes |
|---|---|---|
| Fatia por funcionalidade | auth, shell, action, records, operations e theme, cada uma completa na própria pasta | Nenhuma importa código de dentro da outra |
| Quatro lugares para agir | Barra do topo, cabeçalho da página, rodapé do formulário e paleta de comandos | Cada lugar cobre uma intenção: usuário, tela, formulário, salto rápido |
| Barra de salvar que aparece ao editar | Salvar e Cancelar deslizam à vista no primeiro campo alterado | Com o formulário intocado ela fica escondida do leitor de tela e desativada |
| Uma tabela para tudo | Toda listagem do app passa pelo mesmo componente <DataTable<T>> | Ordenação, busca, paginação, carregamento, lista vazia e exportação num contrato só |
| Tokens declarados uma vez | Cor, arredondamento, espaçamento e fonte ficam declarados no bloco @theme do Tailwind | Tema claro, escuro e o do sistema, guardados no navegador |
| Paleta com os ícones do menu | O ícone que abre a tela pelo mouse é o mesmo que aparece na busca por teclado | A memória visual serve nos dois caminhos |
1. Login simulado e guarda de rota
A primeira tela prova que dá para montar um fluxo de login inteiro sem servidor, preservando o que o usuário sente: o botão que gira, o erro que aparece, o sucesso que leva ao painel e a sessão que continua depois de um F5. O estado fica numa store Zustand que grava no sessionStorage. Uma identidade de demonstração é aceita, e o resto é recusado depois de 600 ms de espera simulada.
O arquivo _authed.tsx guarda as rotas internas: quem não fez login volta para /login, e o logout limpa a sessão. É o padrão clássico de uma SPA, sem rede.
Tela de login com credenciais demo pré-preenchidas

Três decisões valem leitura:
- As credenciais ficam num arquivo só (
src/features/auth/demo-credentials.ts). Quem faz uma cópia do projeto acha em segundos o lugar de trocar a senha de demonstração, em vez de caçá-la no meio do componente da tela. sessionStorageno lugar delocalStorage: a sessão acaba quando a aba fecha. Combina com a promessa de um login simulado, e evita abrir o projeto seis meses depois já logado, como se ele guardasse alguma coisa de verdade.- 600 ms de espera simulada: sem esse atraso, a resposta chega no mesmo instante do clique, e o botão nunca chega a mostrar que está carregando. Com ele, a tela passa pelos mesmos estados de um sistema com rede no meio.
2. Uma tabela para todas as listagens
Listagem é a tela que mais se repete num sistema corporativo. Quando cada uma monta o próprio HTML (HyperText Markup Language, a marcação que estrutura a página), a busca de uma funciona diferente da outra e a terceira nem tem paginação. Aqui toda lista passa por um componente só. Ele é simples o bastante para servir o cadastro de clientes e o módulo financeiro sem mudar de forma.
Listagem financeira com etiquetas coloridas e botões na barra

Oito decisões sustentam esse componente:
| Padrão | Descrição | Detalhes |
|---|---|---|
| Ordenação em três estados | Clicar no cabeçalho cicla entre ordem original, crescente e decrescente | O terceiro clique volta ao início; nenhum menu extra para o usuário aprender |
| Busca única | Um campo filtra a tabela inteira, com botão de limpar dentro dele | O texto de exemplo do campo muda conforme a tela |
| Paginação no navegador | Seletor de 5, 10 ou 20 linhas por página | Sem ida ao servidor, porque não há servidor: tudo é processado na máquina do usuário |
| Linhas alternadas | Uma branca, uma cinza-claro, para o olho não pular de linha | O realce do mouse e o da seleção passam por cima sem apagar o padrão |
| Carregando e vazio fazem parte do contrato | O componente já desenha os dois estados por padrão | A tela vazia aceita um conteúdo próprio, em vez de uma frase genérica |
| Espaço livre na barra de ferramentas | Os botões de criar entram à esquerda; busca e exportação ficam à direita | A tabela recebe os botões como conteúdo, sem uma propriedade nova para cada caso |
| Exportação carregada no clique | As bibliotecas de Excel e de PDF só são baixadas quando alguém exporta | Elas ficam fora do pacote inicial e só são baixadas no clique |
| O nome do arquivo identifica a tela de origem | Mesmas cores, título e rodapé da tela, com um código curto no nome | O padrão é showcase-<arquivo>-<código>.<extensão> |
PDF exportado mantendo as etiquetas coloridas

Princípio de design. A cor de uma etiqueta significa a mesma coisa em todo lugar. Segmento é azul, verde ou âmbar. Status é verde ou cinza. No financeiro, entrada é verde e saída é rosa. A regra vale na célula da tabela, na caixa de seleção do formulário e dentro da planilha exportada. Uma função chamada
ExportRichCellredesenha as etiquetas no Excel e no PDF, para o relatório parecer uma continuação da tela.
3. Tema azul-petróleo e tokens semânticos
Tema escuro virou item comum, e a escolha que resta é qual cor entra no lugar do preto absoluto, que produz o contraste mais alto possível contra o texto claro. A inspiração veio do lucide.dev, cujo tema escuro usa um azul-petróleo. A partir dele, subi a luminosidade em 1% e ajustei o matiz, o que dá à paleta uma assinatura própria.
O que sustenta essa paleta são os tokens semânticos. Cor, arredondamento e tipografia ficam declarados uma única vez, no bloco @theme do Tailwind. Cada componente lê essas variáveis em vez de guardar o valor por conta própria. Assim, trocar de tema é trocar a tabela de tokens, e nenhum componente precisa ser tocado.
No tema escuro, a luminosidade organiza a tela. O menu lateral fica um degrau mais escuro que o fundo, e a atenção vai para o conteúdo. O card fica um degrau mais claro que o fundo, com espaçamento em volta. A borda usa um tom intermediário, que separa sem chamar mais atenção que o texto. Os elementos escuros dos dois temas têm a mesma matiz, com croma muito baixo (entre 0.003 e 0.008). Por isso trocar de tema parece uma transição contínua, e não um corte de cor.
Os tokens valem além da tela. Os gráficos do painel leem a mesma paleta, e ela é aplicada também nas planilhas exportadas. Mudar um token muda a cor do card, da barra do gráfico e da célula do Excel de uma vez só.
A preferência de tema fica no localStorage, e um script no <head> a lê antes da primeira pintura da tela. Sem esse script, quem escolheu o tema escuro veria um flash branco a cada carregamento, no intervalo entre o HTML chegar e o React montar.
A rota /design-system, disponível com o app rodando, mostra os tokens ao vivo: superfícies, ações, cores dos gráficos, etiquetas, variantes de botão, espaçamento, arredondamento e a razão de cada escolha. Ela traz também uma seção Segurança em produção, que detalha o que muda ao trocar o login simulado por um servidor real.
4. Paleta de comandos
O atalho ⌘K no Mac, ou ⌃K no Linux e no Windows, abre uma caixa de busca que percorre as páginas e as ações rápidas do app. A busca é aproximada: digitar “clien” já encontra “Clientes”, e uma letra trocada não atrapalha.
Os ícones dessa caixa são os mesmos do menu lateral. Quem usa o mouse e quem usa o teclado enxergam o mesmo símbolo para a mesma tela. A lista sai da mesma configuração de navegação que desenha o menu, então uma tela nova aparece nos dois lugares sem trabalho extra.
A inspiração vem de GitHub, Linear e Vercel, produtos em que a paleta virou o caminho principal de navegação.
5. Segurança: o que o navegador não esconde
Um projeto sem servidor deixa à vista um limite que produtos reais costumam esconder: tudo que chega ao navegador é público. Não existe chave de acesso neste projeto, e não haveria onde guardá-la. Qualquer variável de ambiente escrita como import.meta.env.VITE_* sai legível no JavaScript entregue ao usuário. Um segredo de verdade fica no servidor.
A guarda de rota tem o mesmo limite. O _authed.tsx melhora a navegação de quem não fez login, e não faz mais que isso. Quem editar o sessionStorage pelas ferramentas do navegador alcança as telas do mesmo jeito. Neste projeto isso é aceitável, porque não existe dado real atrás delas. Num sistema de verdade, quem decide o que o usuário pode ver é o servidor, a cada requisição. A tela só mostra a decisão que já foi tomada lá.
Deixar as duas coisas escritas evita que alguém copie a estrutura e leve a conveniência da simulação para dentro de um produto com dado de cliente.
Decisões pequenas com efeito grande
| Padrão | Descrição | Detalhes |
|---|---|---|
| Pacote inicial enxuto | As bibliotecas de exportação entram só no clique | Quem nunca exporta não baixa nenhuma delas |
| A mesma cor em todas as camadas | A mesma paleta na tabela, no formulário, no gráfico e no relatório | ExportRichCell leva o token para dentro do Excel e do PDF |
| Arquivo nomeado por domínio e operação | customer.schema.ts, login.form.tsx | Achar o arquivo de uma funcionalidade vira uma busca óbvia |
| Rota desconhecida devolve o app | O Worker responde com o index.html em qualquer caminho que não reconhece | Sem isso, um F5 em /dashboard daria erro 404 |
| Testes nas peças de maior risco | Vitest nas peças frágeis, Playwright no caminho feliz | A cobertura segue o risco de cada peça |
O deploy sai em Cloudflare Workers. O build estático da pasta dist/ é servido pelo Workers Static Assets, publicado em fs.thiagocaja.dev, e cada push na main dispara build e deploy.
A linha not_found_handling: "single-page-application" no wrangler.jsonc faz o Worker devolver o index.html sempre que o caminho pedido não existe como arquivo. Uma SPA depende disso: /dashboard não é uma pasta no servidor, é uma rota que existe só dentro do React. Sem essa linha, um F5 nessa tela daria erro 404.
Convenções
- Código em inglês: identificadores, nomes de arquivo e caminhos de rota. Quem chega no projeto não traduz termos no meio da leitura.
- Interface em pt-BR: rótulos, textos de acessibilidade, campos, botões e títulos de página, na língua do usuário final.
- Arquivos nomeados por domínio e operação (
customer.schema.ts,login.form.tsx). - Variáveis de sim ou não começam por um prefixo que faz a pergunta:
isDirty,isSaving,hasError,shouldRemember.
Status
| Aspecto | Estado |
|---|---|
| Maturidade | Pronta para servir de ponto de partida, em evolução |
| Cobertura de testes | Unit (Vitest) + um teste ponta a ponta (Playwright) |
| Disponibilidade | Live em Cloudflare Workers (fs.thiagocaja.dev) |
| Repositório | Privado, código sob solicitação |
| Monetização | Nenhuma, projeto de portfólio |
Backend e frontend formam um par: um serve o contrato REST com autenticação, paginação e documentação viva; o outro consome esse contrato numa interface React. Juntos mostram a mesma aplicação de ponta a ponta, cada lado como um artefato que se sustenta sozinho.