Pular para conteúdo

0014 — MUI 9 e design system 2.10.0

Status CONCLUÍDA
Branch spec/0014-atualizacao-mui-e-design-system
Criada em 2026-08-03
Apps afetados api (sem impacto) · web · e2e

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

1. Problema

O apps/web está três majors atrás do ecossistema que o design system passou a usar como referência, e isso deixou de ser questão de estar atualizado: virou incompatibilidade declarada.

Hoje no app Publicado
@mui/material 6.1.7 9.2.0
@mui/x-data-grid 7.23.5 9.10.1
@mui/x-date-pickers 7.29.4 9.10.1
@garantora/design-system 2.9.2 2.10.0

A 2.10.0 do design system mudou as peerDependencies para @mui/material: ^7.3.0 || ^9.0.0 — o 6 saiu da faixa. É contra o 9.2.0 que o tsc do pacote roda. Enquanto o app fica no 6, cada versão nova do pacote é instalada em violação de peer, e o aviso que hoje é ruído se torna, em algum ponto, tela quebrada.

E há um bloqueio no meio do caminho. O @refinedev/mui — mesmo na última versão, 8.0.2 — declara @mui/material: ^6.1.7, @mui/lab: ^6.0.0-beta.14 e @mui/x-data-grid: ^7.23.5. Não existe versão do MUI que satisfaça o design system e o @refinedev/mui ao mesmo tempo: um dos dois tem de sair da equação.

Junto disso, o package.json carrega peso morto que a própria migração cobraria: @mui/lab e @mui/icons-material não têm um único uso no src/ (o segundo ficou órfão quando a spec 0012 migrou tudo para Phosphor), e 8 dos 11 arquivos que importam @refinedev/mui são resto do template — components/header/ não é importado em lugar nenhum e blog-posts/categories têm zero referências no App.tsx, como o CLAUDE.md da raiz já registrava.

2. Objetivo

O apps/web roda no MUI 9 — Material e MUI X no mesmo major, como o ecossistema passou a exigir — com o design system em 2.10.0 e sem nenhuma dependência presa a uma major antiga. O que era peso morto sai em vez de ser migrado.

3. Fora de escopo

  • Redesenhar qualquer tela. Subir major é trabalho de compatibilidade; onde o MUI 9 mudar o visual por conta própria, o alvo é manter o que existe, não aproveitar para melhorar.
  • Trocar o Refine por outra coisa. Sai apenas o @refinedev/mui; @refinedev/core, @refinedev/react-router, @refinedev/rest, @refinedev/react-hook-form, kbar e devtools ficam, e não têm peer de MUI.
  • Adotar o Toast do design system como componente. A substituição do provedor de notificação preserva o comportamento atual; construir o Toast conforme o Toast.prompt.md é outra entrega.
  • @mui/material-pigment-css. Aparece nos peers do material 9 como opcional; não entra.
  • API. Nenhuma mudança de contrato, model, migration ou endpoint.

4. Regras de negócio

  • RN1. O app passa a usar @mui/material 9.x, @mui/x-data-grid 9.x, @mui/x-date-pickers 9.x e @mui/utils 9.x. Material e MUI X no mesmo major — subir um sem o outro é o erro que o theme/README.md do pacote nomeia.
  • RN2. @garantora/design-system vai para #v2.10.0.
  • RN3. @refinedev/mui é removido. O uso vivo são três coisas, e cada uma tem substituto:
  • ErrorComponent (rota *) → página 404 própria, no padrão de tela do app;
  • RefineSnackbarProvider + useNotificationProvider → provedor de notificação do app, preservando o comportamento de useCustomNotificationProvider (que hoje intercepta erro de autenticação e troca a mensagem crua da API por "Acesso não autorizado");
  • HamburgerMenu/RefineThemedLayoutHeaderProps em components/header/ → o arquivo é apagado, não é importado por ninguém.
  • RN4. Saem do package.json: @mui/lab e @mui/icons-material (zero usos), e @refinedev/mui (RN3).
  • RN5. São apagados: src/pages/blog-posts/, src/pages/categories/ e src/components/header/. Nenhum está em resources nem em rota — migrá-los ao MUI 9 seria trabalho em código que ninguém executa.
  • RN6. Grid2 → Grid. O MUI 7 renomeou (Grid2 virou Grid; o antigo virou GridLegacy), e o app importa @mui/material/Grid2 em 11 arquivos.
  • RN7. O comportamento visível não muda. Nenhuma tela ganha ou perde elemento, nenhum data-testid é removido ou renomeado. É o que permite a suíte e2e existente servir de prova da migração.
  • RN8. As quebras de v7, v8 e v9 são descobertas empiricamente — tsc, build e browser —, não por memória. As duas últimas majors são recentes; qualquer lista de mudanças escrita aqui de cabeça seria invenção. O tsc é o guia, e cada adaptação ganha comentário dizendo qual API mudou.
  • RN9. O bundle é medido antes e depois. A spec 0012 mostrou que o empacotamento é onde uma atualização de biblioteca cobra caro sem avisar — e que o manualChunks errado derruba o app com o tsc limpo.

5. Impacto por app

apps/api

Sem impacto. Nenhum modelo, endpoint, migration ou regra de service muda.

apps/web

  • package.json / pnpm-lock.yaml: MUI 9 (material, x-data-grid, x-date-pickers, utils), design system 2.10.0; saem @refinedev/mui, @mui/lab, @mui/icons-material. Conferir se @mui/system precisa entrar como dependência direta — o @mui/x-data-grid 9 o declara como peer.
  • App.tsx: troca ErrorComponent e RefineSnackbarProvider pelos substitutos.
  • providers/notification.ts: reescrito sem @refinedev/mui, preservando a interceptação de erro de autenticação.
  • 11 arquivos com Grid2: import e uso para Grid.
  • 5 arquivos com x-data-grid e 3 com x-date-pickers: adaptar ao que a major nova mudar. Atenção ao dataGridSx do pacote, que estiliza por classe (.MuiDataGrid-columnHeaders, .MuiDataGrid-cell) — se as classes mudaram, o visual da lista se desfaz sem erro de compilação.
  • contexts/color-mode: revalidar o deepmerge e as sobrescritas locais (MuiSelect, MuiChip, MuiTablePagination, MuiDatePicker, MuiInputBase) — os caminhos de slot que o fix 0002 e a spec 0012 acertaram podem ter mudado de nome.
  • data-testid: nenhum novo, nenhum removido (RN7).
  • Sider: os itens de blog-posts/categories não existem no custom-sider (nunca estiveram), então apagar as páginas não o toca.
  • Responsividade & PWA: validar desktop e mobile. O fix 0002 deixou os campos em 16px pelo tema do pacote — confirmar que a regra sobreviveu à major nova, porque é exatamente o tipo de coisa que uma atualização de tema desfaz calada. PWA: o bundle muda inteiro; o service worker recacheia no próximo load.

apps/e2e

  • Specs: nenhuma nova. A suíte existente é o teste de regressão — se a migração preservou comportamento (RN7), ela passa sem uma linha alterada. Spec nova aqui seria sinal de que algo mudou que não devia.
  • Seeds: sem dados novos.

6. Contrato de API

Sem mudança de contrato. Nenhum endpoint criado, alterado ou removido.

7. Critérios de aceite

  • [x] CA1. @mui/material, @mui/x-data-grid, @mui/x-date-pickers e @mui/utils em 9.x, no mesmo major; design system em #v2.10.0.
  • [x] CA2. pnpm install sem violação de peer relacionada a MUI. (Os avisos de notistack, que já existem hoje, saem junto com o @refinedev/mui.)
  • [x] CA3. @refinedev/mui, @mui/lab e @mui/icons-material não estão mais no package.json, e grep -r "@refinedev/mui\|@mui/lab\|@mui/icons-material" src/ não devolve nada.
  • [x] CA4. blog-posts/, categories/ e components/header/ foram apagados.
  • [x] CA5. tsc limpo e pnpm build passando — inclusive o passo do Workbox, que a spec 0012 mostrou ser onde o tamanho do pacote reprova.
  • [x] CA6. As 12 rotas principais montam no build de produção sem erro de runtime. (A spec 0012 subiu o app em branco com tsc limpo; build é o que prova.)
  • [x] CA7. A lista de Análise de Crédito mantém o visual do design system — cabeçalho em overline, zebra, hover tingido e paginação fixa —, o que prova que o dataGridSx ainda casa com as classes da major nova.
  • [x] CA8. Nenhum campo focável abaixo de 16px em viewport mobile (a garantia do fix 0002 sobrevive à atualização).
  • [x] CA9. A notificação de erro de autenticação continua mostrando "Acesso não autorizado" em vez do JSON cru da API, e a rota * continua levando a uma tela de não encontrado.
  • [x] CA10. A suíte e2e existente passa sem alteração de spec — é a prova da RN7.
  • [x] CA11. Tamanho do bundle registrado antes e depois, com o crescimento (ou a redução, já que três pacotes saem) justificado.

8. Plano de implementação

  • [x] apps/api — sem impacto
  • [x] apps/web — medir o bundle antes (RN9)
  • [x] apps/web — apagar o código morto (RN5) e as dependências órfãs (RN4), antes de subir versão: menos código para migrar, e o tsc deixa de acusar arquivo que vai morrer
  • [x] apps/web — substituir ErrorComponent e o provedor de notificação (RN3)
  • [x] apps/web — subir MUI para 9.x e o design system para 2.10.0 (RN1, RN2)
  • [x] apps/web — Grid2 → Grid nos 11 arquivos (RN6)
  • [x] apps/web — seguir o tsc até zero, comentando cada API que mudou (RN8)
  • [x] apps/web — revalidar color-mode (slots das sobrescritas) e dataGridSx (classes)
  • [x] apps/web — medir o bundle depois; validar as 12 rotas no build de produção
  • [x] apps/web — validar no browser em desktop e mobile, nos dois temas
  • [x] apps/e2e — rodar a suíte existente como regressão (sem escrever spec nova)
  • [x] make test (nada de API) e make lint sem apontamento novo — o total caiu de 53 para 37

9. Decisões em aberto

Nenhuma. As três da abertura foram decididas:

  • Alvo: MUI 9, o mais recente — é contra ele que o design system roda o tsc.
  • @refinedev/mui: removido, porque prende o app na major 6 e o uso vivo são três coisas substituíveis.
  • Código morto: apagado, não migrado.

Risco assumido, e é o maior desta spec: as quebras do v8 e do v9 serão descobertas durante a implementação. Não há como dimensionar o trabalho antes de tentar compilar — a estimativa honesta é "de uma hora a um dia", e o marco que separa os dois cenários é o dataGridSx continuar casando com as classes do DataGrid 9. Se não casar, a correção é no design system, não aqui, e a spec bloqueia como a 0012 bloqueou nos tokens.

10. Registro

2026-08-03 — MUI 6 → 9 concluído. 310 erros de tsc até zero.

O salto de três majors cobrou o que a RN8 previa: as quebras foram descobertas compilando, e o tsc apontou 310 erros no primeiro build contra o 9. Todas de uma mesma natureza — o v9 removeu o que estava depreciado desde o v5/v6:

Quebra Ocorrências Virou
inputProps 125 slotProps.htmlInput
InputLabelProps 101 slotProps.inputLabel
Props de sistema (fontWeight, display, mb, …) ~530 dentro de sx
InputProps 15 slotProps.input
PaperProps 4 slotProps.paper
SelectProps 3 slotProps.select
primaryTypographyProps 2 slotProps.primary
Grid2 11 arquivos Grid
params.inputProps do Autocomplete 7 params.slotProps.htmlInput
renderTags do Autocomplete 1 renderValue, com getItemProps
DatePickerProps<Dayjs> 1 DatePickerProps (deixou de ser genérico)

O erro que mais custou foi meu, não do MUI. A primeira tentativa converteu prop a prop e depois tentou unir os slotProps duplicados por bloco de linhas. A detecção de bloco não reconhecia elemento fechado por > (com filhos), então entradas de um elemento vazaram para o seguinte: um campo ficou com as props de outro, e uma linha ganhou inputLabel duas vezes. Pior, isso compila — o tsc só reclamou de 9 atributos duplicados, e a contagem de inputLabel caiu de 101 para 96 sem nada acusar. Cinco campos perderam props em silêncio.

Revertido o mecânico e refeito com varredura que acha o span de cada tag contando aspas e chaves, reescrevendo um slotProps por elemento, sem fusão posterior. A prova de que funcionou é aritmética: 247 props convertidas de 247 esperadas, zero props legadas restantes, zero chaves de slot repetidas.

Duas armadilhas do mesmo tipo apareceram depois:

  • color para sx foi decisão errada. Move automático levou 43 ocorrências de color="error" para sx={{ color: "error" }} — e no sx "error" não resolve para error.main, vira cor CSS inexistente. Silencioso. Devolvidas à posição de prop.
  • Props de sistema em elemento que não é do MUI. width/height foram para sx em <img> e <svg> (que têm esses atributos nativos), e o color do glifo Phosphor no Icon também — este último apagaria a cor de todo ícone do app. O tsc pegou esses, porque <img> não aceita sx.
  • Props de elemento aninhado. O TextField dentro de renderInput={(params) => …} está no span da tag do Autocomplete; o codemod levou as props dele para fora, gerando slotProps que referenciava params fora de escopo. Sete casos, devolvidos.

O maior risco da spec não se materializou. O dataGridSx do pacote estiliza por classe, e a spec registrava que um renome no DataGrid 9 desfaria o visual das listas sem erro de compilação. Medido no build de produção, nos dois temas: cabeçalho tingido (rgba(0,85,119,0.05) no claro, rgba(77,166,204,0.08) no escuro), título overline 11px/600/uppercase, linha de 52px, zebra presente com surfaceRowAlt, rodapé sticky, padding de célula 14px. Nada se desfez.

O @refinedev/mui saiu, e com ele o notistack. Substitutos: contexts/toast — Snackbar + Alert do MUI, com auto-dismiss por item (o autoHideDuration do Snackbar fecharia a pilha inteira, e um toast novo reiniciaria o relógio dos antigos) — e pages/not-found, espelhando a tela de "sem permissão". O useCustomNotificationProvider foi reescrito preservando o que ele fazia de único: trocar o JSON cru de erro de autenticação por "Acesso não autorizado".

O bundle diminuiu: vendor 1805 → 1748 KB, total 2204 → 2151 KB (−53 KB). Três pacotes saíram e nenhum entrou além de @mui/system (peer do DataGrid 9) e @mui/utils, que já existia.

Lint caiu de 53 para 37 apontamentos — os arquivos mortos levaram os deles. Os 14 erros restantes são os mesmos de antes (Chip sem uso, propertiesApi, AnalysisStatus).

Uma nota de versão: o pnpm add @mui/utils sem versão trouxe a 9.x quando o app estava no material 6 — desalinhamento que passei a evitar declarando a versão. Agora tudo no 9.x, o que é o ponto da spec.

Validação: tsc limpo; build de produção passando, inclusive o passo do Workbox; as 12 rotas montam no build sem erro de runtime; mobile em cartões sem rolagem horizontal; 404 nova respondendo; campos em 16px (o piso do fix 0002 sobreviveu).

A suíte e2e pegou o que nada mais pegou — e é o melhor argumento a favor do CA10.

Primeira rodada: falhas em série no data-testid da paginação das três listas. Causa: com o MUI X 9 o GridPagination ignora as próprias props e espalha apenas rootProps.slotProps.basePagination na raiz. O caminho slotProps.pagination, que funcionava no v7, deixou de entregar o atributo ao DOM — e nada acusava: tipo válido, tsc limpo, build limpo, as 12 rotas montando, dataGridSx intacto, campos em 16px, tela visualmente correta. Só um teste que procura o atributo revelou.

Numa migração de três majors, o que quebra não é o que o compilador vê: é o contrato invisível entre a biblioteca e os testes. Corrigido nas três listas e verificado no build de produção antes de gastar outro rebuild de imagem.

Segunda rodada: 256 passaram, 6 skipped, 0 falhas (11,9 min, chromium + firefox + webkit), sem uma linha alterada em spec de teste — o que fecha a RN7 e o CA10.