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,kbaredevtoolsficam, e não têm peer de MUI. - Adotar o
Toastdo design system como componente. A substituição do provedor de notificação preserva o comportamento atual; construir o Toast conforme oToast.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/material9.x,@mui/x-data-grid9.x,@mui/x-date-pickers9.x e@mui/utils9.x. Material e MUI X no mesmo major — subir um sem o outro é o erro que otheme/README.mddo pacote nomeia. - RN2.
@garantora/design-systemvai 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 deuseCustomNotificationProvider(que hoje intercepta erro de autenticação e troca a mensagem crua da API por "Acesso não autorizado");HamburgerMenu/RefineThemedLayoutHeaderPropsemcomponents/header/→ o arquivo é apagado, não é importado por ninguém.- RN4. Saem do
package.json:@mui/labe@mui/icons-material(zero usos), e@refinedev/mui(RN3). - RN5. São apagados:
src/pages/blog-posts/,src/pages/categories/esrc/components/header/. Nenhum está emresourcesnem em rota — migrá-los ao MUI 9 seria trabalho em código que ninguém executa. - RN6.
Grid2→Grid. O MUI 7 renomeou (Grid2virouGrid; o antigo virouGridLegacy), e o app importa@mui/material/Grid2em 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. Otscé 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
manualChunkserrado derruba o app com otsclimpo.
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/systemprecisa entrar como dependência direta — o@mui/x-data-grid9 o declara como peer.App.tsx: trocaErrorComponenteRefineSnackbarProviderpelos substitutos.providers/notification.ts: reescrito sem@refinedev/mui, preservando a interceptação de erro de autenticação.- 11 arquivos com
Grid2: import e uso paraGrid. - 5 arquivos com
x-data-gride 3 comx-date-pickers: adaptar ao que a major nova mudar. Atenção aodataGridSxdo 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 odeepmergee 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/categoriesnão existem nocustom-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-pickerse@mui/utilsem 9.x, no mesmo major; design system em#v2.10.0. - [x] CA2.
pnpm installsem violação de peer relacionada a MUI. (Os avisos denotistack, que já existem hoje, saem junto com o@refinedev/mui.) - [x] CA3.
@refinedev/mui,@mui/labe@mui/icons-materialnão estão mais nopackage.json, egrep -r "@refinedev/mui\|@mui/lab\|@mui/icons-material" src/não devolve nada. - [x] CA4.
blog-posts/,categories/ecomponents/header/foram apagados. - [x] CA5.
tsclimpo epnpm buildpassando — 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
tsclimpo; 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
dataGridSxainda 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 otscdeixa de acusar arquivo que vai morrer - [x]
apps/web— substituirErrorComponente 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→Gridnos 11 arquivos (RN6) - [x]
apps/web— seguir otscaté zero, comentando cada API que mudou (RN8) - [x]
apps/web— revalidarcolor-mode(slots das sobrescritas) edataGridSx(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) emake lintsem 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:
colorparasxfoi decisão errada. Move automático levou 43 ocorrências decolor="error"parasx={{ color: "error" }}— e nosx"error" não resolve paraerror.main, vira cor CSS inexistente. Silencioso. Devolvidas à posição de prop.- Props de sistema em elemento que não é do MUI.
width/heightforam parasxem<img>e<svg>(que têm esses atributos nativos), e ocolordo glifo Phosphor noIcontambém — este último apagaria a cor de todo ícone do app. Otscpegou esses, porque<img>não aceitasx. - Props de elemento aninhado. O
TextFielddentro derenderInput={(params) => …}está no span da tag doAutocomplete; o codemod levou as props dele para fora, gerandoslotPropsque referenciavaparamsfora 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.