Pular para conteúdo

0017 — Internalizar o design system e aposentar o pacote

Status CONCLUÍDA
Branch spec/0017-internalizar-design-system
Criada em 2026-08-04
Apps afetados web · infra (api e e2e sem impacto de código)

Status: RASCUNHO → APROVADA → EM IMPLEMENTAÇÃO → CONCLUÍDA

1. Problema

O design system tem duas fontes editáveis para a mesma verdade: o repositório Garantora/garantora_design_system e o apps/web. Nada obriga as duas a concordarem, e elas já não concordam. As divergências não são hipóteses — estão registradas nas próprias specs:

Divergência Onde está registrada
MuiSelect sem altura: o tema dimensiona MuiOutlinedInput.input e esquece o Select, então o campo nasce com 23px em vez de 36. Remendado no app spec 0016, RN18
Cabeçalho de painel: o pacote define surfacePanelHeader como navy 5%; o app pinta teal 5% e nunca usa o token spec 0016, seção 3
Três divergências conscientes entre as telas e o ui_kits do pacote spec 0015, Registro
O README.md do pacote é "reconstrução paralela a partir dos SVGs do Figma", enquanto docs/design-tokens.md é canônico CLAUDE.md da raiz

O padrão é sempre o mesmo: alguém acha um defeito no tema, corrigir no lugar certo custa caro, e a correção fica no app. O FormPanel da spec 0016 é o caso exemplar — a peça foi criada em components/shared/ justamente porque promovê-la ao pacote foi avaliado e descartado por custo.

E o custo é real. Mudar 540 linhas de tema exige: commit no outro repositório, tag, bump da dependência git no package.json, pnpm install, lockfile, rebuild. Para uma constante de cor.

Há ainda um custo que ninguém escolheu pagar: por ser dependência git de repositório privado, o pacote obriga o build do container a ter um agente SSH — apk add git openssh-client, ssh-keyscan, --mount=type=ssh no web.Dockerfile e ssh: [default] no compose. Sem agente, docker compose build web falha antes de começar. Foi o que travou dois builds nesta sessão e o que motivou o skill /devremote-rebuild a existir.

Por fim: o app consome só o tema. Das ~700 KB do pacote, o apps/web importa exatamente cinco arquivos TypeScript (540 linhas). Os .jsx, tokens/*.css, assets/ e styles.css nunca são importados — são referência. Paga-se o preço de um pacote versionado para distribuir cinco arquivos a um consumidor.

2. Objetivo

O design system passa a ter uma fonte só, dentro do monorepo, onde corrigir um defeito do tema é uma linha no mesmo commit da tela que o revelou.

3. Fora de escopo

  • Redesenho visual. A entrega é pixel-idêntica (RN8). Toda mudança de aparência é defeito, não feature.
  • Os componentes .jsx de referência, ui_kits/ e scraps/. Nunca foram importados; continuam legíveis no repositório arquivado. Trazê-los seria adotar ~700 KB de código sem consumidor.
  • guidelines/*.card.html — as 20 fichas visuais (cores, contraste, tipografia, espaçamento). Mesma natureza dos .jsx: referência renderizável. Ficam no repositório arquivado, e docs/impeccable.md passa a dizer isso.
  • Passar o app a importar os tokens/*.css. Eles vêm para o monorepo como fonte da verdade legível e sincronizável com o Figma, mas o runtime continua no tema MUI — é assim hoje. Trocar o mecanismo é outra spec.
  • Arquivar o repositório de origem. É passo do usuário (exige admin no GitHub); a spec entrega o aviso a colocar lá e registra a pendência.
  • Publicar o design system para terceiros. Se um dia houver segundo consumidor, extrair de volta para um pacote é trabalho conhecido — e barato, porque a fonte estará organizada.

4. Regras de negócio

Regras de estrutura e de tema, verificáveis por leitura e por medição.

O que vem, e para onde

  • RN1. O design system passa a viver em apps/web/src/design-system/, com três diretórios:
Diretório Conteúdo Origem
theme/ index.ts, palette.ts, components.ts, typography.ts, dataGrid.ts (540 linhas) theme/*.ts do repo de origem — as fontes TS, não o dist/ compilado
tokens/ 9 arquivos .css (base, colors, dark, elevation, fonts, motion, semantic, spacing, typography) tokens/
prompts/ 27 arquivos .prompt.md dos componentes components/**/*.prompt.md, achatados
  • RN2. As fontes vêm do repositório, não do pacote instalado: o pacote publica só dist/ (JS compilado + .d.ts + source maps). Copiar dist/ seria internalizar código gerado.
  • RN3. Os .prompt.md vêm porque o apps/web/CLAUDE.md manda lê-los antes de mudar qualquer componente compartilhado ("cada um segue o .prompt.md homônimo do pacote"). Sem eles, a instrução vira ponteiro quebrado.

O que sai

  • RN4. @garantora/design-system sai do apps/web/package.json e do pnpm-lock.yaml (5 entradas). Os 4 imports do app viram relativos:
@garantora/design-system/theme            → ../../design-system/theme
@garantora/design-system/theme/dataGrid   → ../../design-system/theme/dataGrid
  • RN5. O build do container deixa de precisar de SSH. Saem do infra/dev-remote/web.Dockerfile: apk add git openssh-client, o ssh-keyscan, o mkdir /root/.ssh e o --mount=type=ssh; sai do docker-compose.yml o ssh: [default] do serviço web. O api.Dockerfile nunca precisou.
  • RN6. O skill /devremote-rebuild deixa de exigir agente SSH. A verificação de chave e a checagem contra o GitHub são removidas do script — passam a ser custo sem causa. O restante (decidir o serviço, construir, recriar, verificar) fica.

As divergências, resolvidas

  • RN7. A altura do Select passa para o tema (design-system/theme/components.ts) e sai do appOverrides do contexts/color-mode/. É onde a regra sempre pertenceu: o tema já dimensiona MuiOutlinedInput.input em 36px, e o MUI aplica height: auto; minHeight: 1.4375em na própria classe .MuiSelect-select — que é o que produzia os 23px.
  • RN8. O token surfacePanelHeader passa a valer teal a 5% (alpha(secondary.main, 0.05)), que é o que o app pinta hoje, e o FormPanel passa a usar o token em vez da fórmula literal. O pacote dizia navy a 5%; adotar o navy mudaria o matiz de todo painel do app. Entre alinhar o token ao que está no ar e repintar a aplicação inteira, esta spec alinha o token — e o token deixa de ser decorativo.

Invariantes

  • RN9. Zero mudança visual. Fora a altura do Select (que já está corrigida no ar pela 0016 e aqui só muda de lugar), nenhuma tela pode mudar um pixel. Verificável: as medidas da spec 0016 repetidas antes e depois, nos dois viewports.
  • RN10. pnpm build (tsc) limpo, sem any novo e sem @ts-ignore para fazer as fontes TS compilarem dentro do app.
  • RN11. A suíte e2e atual (98 testes) passa sem edição. É a rede de segurança desta spec: se o tema quebrar, ela acusa.
  • RN12. Permissões, papéis e contrato de API: sem impacto.

5. Impacto por app

apps/api

Sem impacto. Nenhum modelo, endpoint, migration, schema ou teste.

apps/web

Arquivo Mudança
src/design-system/theme/*.ts novo — 5 fontes TS vindas do repo de origem (RN1, RN2)
src/design-system/tokens/*.css novo — 9 arquivos de token
src/design-system/prompts/*.prompt.md novo — 27 prompts de componente (RN3)
src/design-system/README.md novo — o que é cada diretório e como mexer
src/contexts/color-mode/index.tsx import relativo; sai a sobrescrita do MuiSelect (RN7)
src/pages/{renters,properties,credit-analyses}/index.tsx import relativo do dataGrid
src/components/shared/FormPanel.tsx passa a usar surfacePanelHeader (RN8)
package.json · pnpm-lock.yaml sai a dependência git (RN4)

data-testid: nenhum novo — não há elemento novo na interface.

Sider: sem impacto.

Responsividade & PWA: sem mudança de comportamento. O bundle muda de composição (o tema deixa de vir de node_modules), então o hash dos assets muda e o service worker serve versão nova — comportamento normal de qualquer deploy.

infra/

Arquivo Mudança
infra/dev-remote/web.Dockerfile saem git, openssh-client, ssh-keyscan e --mount=type=ssh (RN5)
infra/dev-remote/docker-compose.yml sai ssh: [default] do serviço web (RN5)
.claude/skills/devremote-rebuild/ sai o tratamento de agente SSH do script e da documentação (RN6)

apps/e2e

Sem spec novo. Não há fluxo novo para cobrir — a mudança é estrutural. A verificação é a suíte existente passar sem edição (RN11), o que a torna mais valiosa aqui do que qualquer teste novo seria.

Documentação

  • CLAUDE.md (raiz) — a seção Design descreve o pacote como implementação e diz que o apps/web/design-system/ local "foi removido em 2026-08-01". Esta spec reverte aquela decisão; o texto tem de contar isso, com o motivo, não apagar a história.
  • apps/web/CLAUDE.md — imports, caminho dos .prompt.md, o aviso do dataGrid.
  • docs/impeccable.md — hoje é um ponteiro para o pacote. Passa a apontar para apps/web/src/design-system/ e para o repositório arquivado, onde seguem guidelines/ e os .jsx.
  • docs/design-tokens.md — deixa de concorrer com o README do pacote: vira a fonte canônica sem rival, com os tokens/*.css ao lado.

6. Contrato de API

Sem contrato novo. Nenhuma rota, campo ou código de erro. A mudança não atravessa a fronteira do apps/web.

7. Critérios de aceite

  • [x] CA1. grep -rn "@garantora/design-system" apps/ --include=*.ts --include=*.tsx --include=*.json não retorna nada, e pnpm-lock.yaml não menciona garantora_design_system.
  • [x] CA2. apps/web/src/design-system/ contém theme/ (5 .ts), tokens/ (9 .css) e prompts/ (27 .prompt.md), e o theme/ é TypeScript fonte, não dist/ compilado.
  • [x] CA3. Build limpo, sem any novo e sem @ts-ignore — npx tsc -b e npx vite build passam. Ressalva: o wrapper pnpm build falha por um defeito pré-existente do host, não desta spec — ver Registro.
  • [x] CA4. Depois de rm -rf node_modules && pnpm install --frozen-lockfile sem agente SSH, a instalação conclui e o app constrói. É a prova de que a dependência privada sumiu de verdade. (O comando ainda sai com código 1 pelo mesmo defeito pré-existente do host — ver Registro.)
  • [x] CA5. docker compose build web conclui sem SSH_AUTH_SOCK definido.
  • [x] CA6. As medidas da spec 0016 se repetem contra o container reconstruído: top bar 64px, ícone 40×40 (20×20 no desktop), margem 16px, 12px entre cartões, campos e selects em 36px com 16px entre si, zero scroll horizontal em 375px e 1440px.
  • [x] CA7. Comparação visual antes/depois em Inquilinos, Imóveis, Análise de Crédito, Parâmetros e um formulário, nos dois viewports: sem diferença.
  • [x] CA8. A altura do Select vem do tema — grep -n "MuiSelect" src/contexts/color-mode/index.tsx mostra só o IconComponent, e o components.ts do tema tem a regra.
  • [x] CA9. O FormPanel usa theme.garantora.surfacePanelHeader, e o token vale teal a 5% nos dois temas.
  • [x] CA10. Suíte e2e completa passa sem nenhuma edição de spec (RN11). — 98 passando, 2 skip conhecidos, 0 falhas, em 4,1 min
  • [x] CA11. make test sem regressão (a API não foi tocada).
  • [x] CA12. Os quatro documentos de design refletem a nova estrutura, e nenhum aponta para node_modules/@garantora/design-system.

8. Plano de implementação

Ordem do monorepo: api → web → e2e. A api não é tocada.

  • [x] apps/api — sem impacto
  • [x] apps/web — criar src/design-system/ com theme/, tokens/, prompts/ e README.md, a partir do repositório de origem (não do dist/)
  • [x] apps/web — trocar os 4 imports por caminho relativo
  • [x] apps/web — mover a regra de altura do Select para o tema (RN7)
  • [x] apps/web — surfacePanelHeader em teal e FormPanel usando o token (RN8)
  • [x] apps/web — remover a dependência do package.json e regenerar o lockfile
  • [x] infra/dev-remote — tirar SSH do web.Dockerfile e do compose (RN5)
  • [x] .claude/skills/devremote-rebuild — remover o tratamento de agente (RN6)
  • [x] Documentação — CLAUDE.md, apps/web/CLAUDE.md, docs/impeccable.md, docs/design-tokens.md
  • [x] apps/e2e — sem spec novo; a suíte atual é a verificação
  • [x] make test, pnpm build, e pnpm install --frozen-lockfile sem agente SSH
  • [x] /devremote-rebuild + comparação visual antes/depois nos dois viewports
  • [x] Rodar a suíte e2e completa (é a rede de segurança desta spec — aqui ela se paga)

9. Decisões em aberto

Nenhuma que trave a implementação. As quatro foram fechadas na entrevista:

Pergunta Decisão
O que vem do pacote Tema (fontes TS) + tokens/*.css + os 27 .prompt.md
Onde vive apps/web/src/design-system/
Repositório de origem Arquivar; o monorepo vira a fonte
Divergências conhecidas Resolver junto (Select no tema; surfacePanelHeader em teal)

Pendência de handoff, não bloqueante: arquivar Garantora/garantora_design_system exige admin no GitHub e é passo do usuário. A spec entrega o texto do aviso a colocar no README de lá. Enquanto o repositório seguir editável, o problema que originou esta spec continua tecnicamente vivo — só deixa de ser automático.

10. Registro

Zero mudança visual, medido — não afirmado

A linha de base foi capturada antes de tocar em qualquer arquivo, contra o container que servia a main: 10 combinações de tela × viewport (Inquilinos, formulário de inquilino, Imóveis, Análise de Crédito e Parâmetros, em 375px e 1440px), medindo overflow horizontal, tamanho e cor do contêiner do ícone, alturas de select e input, gaps entre campos, cor do cabeçalho de painel, cor de fundo e família tipográfica.

Depois do rebuild, a mesma medição: nenhuma diferença em nenhum campo. Isso cobre justamente os dois pontos de risco — o painelHeaderBg passou a vir do token surfacePanelHeader em vez da fórmula literal, e as alturas passaram a vir do tema em vez do appOverrides, e os valores continuam idênticos (rgba(0, 139, 139, 0.05) e 36px).

O SSH sumiu do build — e isso é verificável

O container foi reconstruído com env -u SSH_AUTH_SOCK, isto é, sem agente SSH algum: EXIT=0. Antes desta spec o mesmo comando falhava antes de compilar qualquer coisa, com invalid empty ssh agent socket. É a prova mais direta de que a dependência git privada saiu de verdade.

O skill /devremote-rebuild, criado horas antes justamente para contornar esse problema, encolheu: saíram a criação do agente, o ssh-add, a verificação contra o GitHub e a variável SSH_KEY. Sobrou o que sempre valeu a pena — construir e recriar num passo e mostrar o estado.

Um defeito pré-existente que apareceu no caminho

Ao fazer a instalação limpa que o CA4 exige, o pnpm install passou a terminar com código 1:

[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: core-js-pure@3.49.0, esbuild@0.25.12

Não é desta spec. O diff do lockfile tem 14 inserções e 28 remoções e não toca esbuild nem core-js-pure — o que mudou foi só a dependência removida. É o pnpm 11 do host exigindo aprovação explícita de build scripts, coisa que ninguém tinha disparado porque ninguém tinha apagado o node_modules recentemente.

Tentei as três formas documentadas de configurar isso — pnpm.onlyBuiltDependencies no package.json, only-built-dependencies[] no .npmrc e onlyBuiltDependencies num pnpm-workspace.yaml — e nenhuma silenciou o aviso nesta versão. Revertí as três em vez de deixar configuração morta no repositório.

O impacto real é pequeno e vale registrar com precisão:

  • O app constrói normalmente: npx tsc -b e npx vite build passam. O esbuild vem do pacote de plataforma e não depende de postinstall.
  • O container não é afetado: o web.Dockerfile instala pnpm@9, que não tem esse comportamento. O rebuild passou.
  • O que quebra é o atalho pnpm build, cujo pré-check roda pnpm install e herda o código 1.

Fica como pendência do ambiente, fora do escopo desta spec.

A rede de segurança segurou

Suíte e2e completa: 98 passando, 2 skip, 0 falhas, em 4,1 min, contra o container reconstruído — e sem nenhuma edição de spec (RN11). Numa mudança em que o tema inteiro trocou de origem, é o que a comparação de medidas não alcança: navegação, formulário que submete, permissão por papel, filtro que consulta o servidor.

Referências que sobreviveram em comentário

O CA1 pede zero menções a @garantora/design-system e três resistiram à troca dos imports, todas em texto: o exemplo no docstring do próprio theme/index.ts, um comentário no color-mode e um cabeçalho de spec do e2e. Corrigidas — um ponteiro errado em comentário envelhece igual a um import errado, e engana mais.

Arquivamento: aviso publicado, o botão é seu

O aviso foi escrito no topo do README de Garantora/garantora_design_system e publicado (commit 7afd6cc) — explica para onde o design system foi, por quê, o que veio para o monorepo, o que permanece lá como referência, e pede que não se abram PRs. Essa ordem é obrigatória: repositório arquivado é somente leitura, então o aviso tinha de entrar antes do arquivamento.

Falta virar a chave, e isso não dá para fazer daqui: a máquina não tem o gh nem token de API do GitHub — a chave SSH autentica operações git, não a API REST. São dois cliques em Settings → General → Danger Zone → Archive this repository.

Enquanto ele seguir editável, o problema que originou esta spec continua tecnicamente vivo — só deixa de ser automático.