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
.jsxde referência,ui_kits/escraps/. 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, edocs/impeccable.mdpassa 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). Copiardist/seria internalizar código gerado. - RN3. Os
.prompt.mdvêm porque oapps/web/CLAUDE.mdmanda lê-los antes de mudar qualquer componente compartilhado ("cada um segue o.prompt.mdhomônimo do pacote"). Sem eles, a instrução vira ponteiro quebrado.
O que sai¶
- RN4.
@garantora/design-systemsai doapps/web/package.jsone dopnpm-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, ossh-keyscan, omkdir /root/.sshe o--mount=type=ssh; sai dodocker-compose.ymlossh: [default]do serviçoweb. Oapi.Dockerfilenunca precisou. - RN6. O skill
/devremote-rebuilddeixa 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
Selectpassa para o tema (design-system/theme/components.ts) e sai doappOverridesdocontexts/color-mode/. É onde a regra sempre pertenceu: o tema já dimensionaMuiOutlinedInput.inputem 36px, e o MUI aplicaheight: auto; minHeight: 1.4375emna própria classe.MuiSelect-select— que é o que produzia os 23px. - RN8. O token
surfacePanelHeaderpassa a valer teal a 5% (alpha(secondary.main, 0.05)), que é o que o app pinta hoje, e oFormPanelpassa 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, semanynovo e sem@ts-ignorepara 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 oapps/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 dodataGrid.docs/impeccable.md— hoje é um ponteiro para o pacote. Passa a apontar paraapps/web/src/design-system/e para o repositório arquivado, onde seguemguidelines/e os.jsx.docs/design-tokens.md— deixa de concorrer com o README do pacote: vira a fonte canônica sem rival, com ostokens/*.cssao 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=*.jsonnão retorna nada, epnpm-lock.yamlnão mencionagarantora_design_system. - [x] CA2.
apps/web/src/design-system/contémtheme/(5.ts),tokens/(9.css) eprompts/(27.prompt.md), e otheme/é TypeScript fonte, nãodist/compilado. - [x] CA3. Build limpo, sem
anynovo e sem@ts-ignore—npx tsc -benpx vite buildpassam. Ressalva: o wrapperpnpm buildfalha por um defeito pré-existente do host, não desta spec — ver Registro. - [x] CA4. Depois de
rm -rf node_modules && pnpm install --frozen-lockfilesem 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 webconclui semSSH_AUTH_SOCKdefinido. - [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
Selectvem do tema —grep -n "MuiSelect" src/contexts/color-mode/index.tsxmostra só oIconComponent, e ocomponents.tsdo tema tem a regra. - [x] CA9. O
FormPanelusatheme.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
skipconhecidos, 0 falhas, em 4,1 min - [x] CA11.
make testsem 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— criarsrc/design-system/comtheme/,tokens/,prompts/eREADME.md, a partir do repositório de origem (não dodist/) - [x]
apps/web— trocar os 4 imports por caminho relativo - [x]
apps/web— mover a regra de altura doSelectpara o tema (RN7) - [x]
apps/web—surfacePanelHeaderem teal eFormPanelusando o token (RN8) - [x]
apps/web— remover a dependência dopackage.jsone regenerar o lockfile - [x]
infra/dev-remote— tirar SSH doweb.Dockerfilee 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, epnpm install --frozen-lockfilesem 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:
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 -benpx vite buildpassam. Oesbuildvem do pacote de plataforma e não depende de postinstall. - O container não é afetado: o
web.Dockerfileinstalapnpm@9, que não tem esse comportamento. O rebuild passou. - O que quebra é o atalho
pnpm build, cujo pré-check rodapnpm installe 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.