name: modals description: Use this skill whenever building, editing, or reviewing modals/dialogs in the PWA. Defines the canonical .app-modal pattern (header verde + body branco + actions padronizadas) usado em todos os modais centrais nao-bottom-sheet. Substitui qualquer documentacao previa de modais.
Modais — Padrao .app-modal
Toda construcao ou edicao de modal central (nao bottom sheet) segue o padrao consolidado .app-modal.is-themed. Esta skill e a fonte canonica. Ao encontrar um modal que nao segue (modais de users, cdm-modal, etc), refatorar pra cá quando tocar.
Bottom sheets (mobile, slide de baixo) seguem outro padrao — ver
design-system§8 "Bottom Sheet". Esta skill cobre apenas modais centrais.
Padrao de modal de ACAO (consolidado 2026-06): o canonico de modais de ACAO (forms, listas, menus que o usuario opera) e o BottomSheet (header branco, titulo verde a esquerda, X quadrado claro, backdrop escuro sem blur, slide-up de baixo) — ver
design-system§8. Os centrais.app-modal.is-themed(esta skill) ficam pra AVISO/notice/confirm (.app-confirm-modal: icone + mensagem + confirmar/cancelar) e casos especiais. Excecao: os 2 centrais de ACAO do dashboard — busca de lote (.app-modal-lookup-result) e senha (.app-modal-password-decision) — continuam centrais mas adotam o VISUAL de acao via overrides ESCOPADOS (header claro + titulo verde a esquerda + X claro + backdrop escuro), sem tocar no chrome.is-themedcompartilhado pelos demais ~28 modais.
1. Quando usar
Use o padrao .app-modal.is-themed para:
- Formularios de criacao (ex: nova filial). Obs.: forms de ACAO preferem BottomSheet (ver §12) — o "novo cliente" (
ClientQuickCreateModal) migrou pra bottom-sheet em 2026-06 - Formularios de edicao (ex: editar cliente, editar filial)
- Confirmacoes destrutivas (ex: inativar cliente em cascata)
- View+edit hibrido (ex: detalhe de filial com modo edit inline)
- Status changes com motivo (ex: inativar/reativar cliente)
NAO use para:
- Bottom sheets mobile (slide de baixo, drag handle) — outro padrao
- Overlays full-screen (zoom de foto, camera) —
PhotoZoomViewerstyle - Toasts (feedback transiente nao-bloqueante) — ver
design-system§13 - Alertas inline (estado persistente em area da pagina) — ver
design-system§12 - Tela de login / esqueci senha (estilo proprio
login-modal-*) — fora da app autenticada
2. Estrutura JSX canonica
Padrao copy-paste para um modal novo. Cada elemento tem responsabilidade documentada em §5–§9 abaixo.
'use client';
import { type FormEvent } from 'react';
import { createPortal } from 'react-dom';
import { useFocusTrap } from '../../lib/use-focus-trap';
type Props = {
open: boolean;
saving: boolean;
errorMessage: string | null;
onClose: () => void;
onSubmit: (...) => Promise<void>;
};
export function MeuModal({ open, saving, errorMessage, onClose, onSubmit }: Props) {
const focusTrapRef = useFocusTrap(open);
if (!open) return null;
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (saving) return;
await onSubmit(/* ... */);
}
return createPortal(
<div className="app-modal-backdrop">
<section
ref={focusTrapRef}
className="app-modal is-themed"
role="dialog"
aria-modal="true"
aria-labelledby="meu-modal-title"
onClick={(event) => event.stopPropagation()}
>
<header className="app-modal-header">
<div className="app-modal-title-wrap">
<h3 id="meu-modal-title" className="app-modal-title">
Titulo do modal
</h3>
<p className="app-modal-description">Subtitulo opcional</p>
</div>
<button
type="button"
className="app-modal-close"
onClick={onClose}
disabled={saving}
aria-label="Fechar"
>
<span aria-hidden="true">×</span>
</button>
</header>
{errorMessage ? <p className="sdv-modal-error">{errorMessage}</p> : null}
<form className="app-modal-content" onSubmit={handleSubmit}>
<label className="app-modal-field">
<span className="app-modal-label">Nome (obrigatório)</span>
<input
className="app-modal-input"
value={value}
disabled={saving}
onChange={(event) => setValue(event.target.value.toUpperCase())}
/>
</label>
<div className="app-modal-actions">
<button type="submit" className="app-modal-submit" disabled={saving}>
{saving ? 'Salvando...' : 'Salvar'}
</button>
<button
type="button"
className="app-modal-secondary"
onClick={onClose}
disabled={saving}
>
Cancelar
</button>
</div>
</form>
</section>
</div>,
document.body
);
}
3. Variantes
| Modificador | Quando usar | Largura |
|---|---|---|
.is-themed |
Sempre — define header verde brand + body branco + actions à direita | 38rem |
.is-wide |
Formularios grandes (>5 campos, ou linhas com 2+ colunas) | 46rem |
.is-action |
Modal central que e FORM de acao (nao aviso/confirm): troca o header verde pelo visual de acao (header claro + titulo verde a esquerda + X claro + backdrop escuro sem blur). Additivo, opt-in. | — |
.app-modal-submit.is-danger |
Botao primario de acao destrutiva (inativacao, exclusao em cascata) | — |
.app-modal-submit.is-warning |
Botao primario de "prosseguir com aviso" laranja (continuar apesar de divergencia de lote; confirmar reclassificacao) | — |
Combinar livremente: .app-modal is-themed is-wide.
Visual de ACAO compartilhado (
.is-action, 2026-06-18): modificador additivo/opt-in sobre.is-themedque troca o header verde pelo visual de acao — header branco, titulo verde a esquerda, X quadrado claro (#eef1ee) e backdrop escuro sem blur (rgba(0,0,0,.55)via lista:has(.is-action)); tambem recolore.app-modal-descriptione o back-arrow.type-modal-backpra escuro (senao sumiriam no header claro). Especificidade.app-modal.is-themed.is-action(0,4,0) vence o chrome base (0,3,0) e nao toca no verde dos modais de AVISO/confirm. Use em modais centrais que sao FORM/confirm que o usuario opera. Ja aplicado em todos os modais do detalhe da amostra (/samples/[sampleId]): cabecalho + container "Informacoes" — editar informacoes (.sample-detail-reg-edit-modal), enviar amostra (.sample-detail-lookup-modal), imprimir etiqueta (.sample-detail-print-modal), invalidar (.sample-detail-invalidate-modal), reverter liga (.blend-revert-modal); container "Classificacao" — detalhe da classificacao (.cld-modal), confirmar salvar, confirmar motivo da edicao, gerar laudo (.sample-detail-lookup-modal), safra do laudo (.report-harvest-select-modal) e reclassificar (.sample-detail-reclassify-modal); container "Resumo comercial" — venda/perda (.sample-detail-movement-modal), atribuir dono a liga (.sample-detail-movement-owner-modal, sub-modal stacked), cancelar movimentacao (.sample-detail-compact-modal) e cancelar envio (refatorado decdm-modallegacy pra.sample-detail-compact-modal); avisos reativos — propagacao de safra/proprietario (.blend-harvest-propagation-modal) e invalidacao bloqueada por liga (.sample-invalidate-blocked-modal). Tambem aplicado no fluxo de classificacao por foto (/camera): os 11 modaisClassification*(erros/avisos*Mismatch/*Failed/NotFound/StatusInvalid/ExtractionError/ManualConfirm, os de acaoType/Classifiercom back-arrow, e oSuccess); oSampleLookupResultModal(.app-modal-lookup-result) ja era visual de acao (precursor, sem.is-action). Tambem em.is-action: o modal de confirmacao "Etiqueta enviada" (ApprovalLabelModal, sucesso pos-impressao da etiqueta de aprovacao, aberto pelo leque do "+" em/samples) — mesmo visual de sucesso (check verde + auto-close) doClassificationSuccessModal. Com isso o detalhe da amostra E o fluxo/cameranao usam mais header verde em NENHUM modal. O verde.is-themedsegue como padrao de aviso/confirm nas telas ainda nao migradas (ex.:/userse o resto de/clients). 2026-06-19: o detalhe do cliente (/clients/[clientId]) migrou pro visual de acao — modais inline status do cliente (inativar/reativar) e editar informacoes (.client-detail-edit-modal, compacto ~30rem espelhando o.sample-detail-reg-edit-modal: inputs/labels menores, gridsminmax(0,1fr)+min-width:0eoverflow-x:hiddenno form pra nao vazar/cortar), e os modais de filialClientUnitModal(Nova filial) +ClientUnitDetailModal(ver+editar,.cudm-modal) — todos.is-action, ~30rem, acoes 50/50 invertidas e sem placeholders de formato; campos pendentes com borda laranja.is-pending. Os containers brancos (.sdv-info-compact) de Informacoes, Filiais e Endereco fiscal (PJ) acompanham (sem header verde —.sdv-card-themedaposentado). O restante de/clients(cascade, status de filial) segue verde. Excecoes no detalhe (NAO migram, nao sao dialog): os overlays de EFEITO — carimbo de venda/perda (.sdv-stamp-overlay) e X de invalidacao (.sdv-x-effect) — e oPhotoZoomViewer(foto full-screen). Os precursores escopados que ja faziam isso a mao —.app-modal-lookup-result(busca de lote) e.samples-filter-modal(filtros) — podem adotar.is-actionquando forem tocados. CSS emglobals.css, secao "Modais CENTRAIS de ACAO".
Confirm SEM header (enxuto): um
.app-confirm-modalpode dispensar o<header className="app-modal-header">— o titulo vai no CORPO via.app-confirm-modal-title(entre o icone e a mensagem). O card.is-themed(overflow: hidden+ branco + radius) fica redondo sem o header. Usado no "Descartar amostra?" doNewSampleModale no excluir visita/relatorio do/informe(este com backdrop escuro opt-in.app-modal-backdrop.is-scrim-dark—rgba(0,0,0,.55)SEM blur, igual ao scrim dos 2 centrais de acao). Continua viacreatePortal(document.body)(§ Portal) — sem portar, um confirm dentro de um sheet/modal ja portalado fica ATRAS dele no empilhamento.
Sheet SOBRE sheet (
<BottomSheet stacked>): desde 2026-06, um bottom-sheet pode abrir sobre outro — a propstackedeleva backdrop+sheet pro tier--z-modal-stacked(600/610) e o scroll-lock/ESC/back viram ref-contados/gated-ao-topo no componente (verdesign-system§8 "stacked"). Ex.: "novo cliente" (ClientQuickCreateModal) sobre "Nova amostra" (NewSampleModal). Pegadinha: o confirm de "Descartar?" do sheet DE CIMA NAO deve ser um.app-modal.is-stackedportalado (colidiria no mesmo tier 600 do sheet de cima) — use um overlay INTERNO ao sheet (position:absolute; inset:0, como o overlay de sucesso).
Existe um
.app-modal"compacto" sem.is-themed(legacy: 430px max, fundo glass). NAO usar pra modais novos. Existe apenas pracdm-modal, modais de users, cam-* e similares — todos candidatos a refatoracao quando tocar.
4. Tokens visuais (referencia rapida)
Definicoes em app/globals.css linhas 1015–1405. NAO duplicar; usar as classes.
Backdrop
- Fundo translucido
rgba(245, 245, 241, 0.18)+backdrop-filter: blur(20px)(efeito glass) - z-index:
var(--z-modal-backdrop) - Animacao de entrada:
app-modal-backdrop-in 0.3s ease
Modal .app-modal.is-themed
- Largura:
min(38rem, calc(100vw - 1.5rem))(default) oumin(46rem, ...)(.is-wide) border-radius: clamp(24px, 7vw, 32px)background: #ffffff(puro, nao gradiente —is-themedzera o glass do.app-modalbase)overflow: hidden+display: flex; flex-direction: column- Animacao de entrada:
app-modal-card-in 0.35s cubic-bezier(0.22, 1, 0.36, 1)
Header (.app-modal-header sob .is-themed)
background: linear-gradient(135deg, var(--brand-green), var(--brand-green-soft))color: #ffffffpadding: clamp(0.95rem, 3vw, 1.15rem) clamp(1.1rem, 3.5vw, 1.4rem)align-items: center(vertical)- Top corners arredondados explicitamente (clamp 24-32px) por causa do stacking context do backdrop-filter
Titulo (.app-modal-title sob .is-themed)
font-size: clamp(1.15rem, 3vw, 1.45rem)font-weight: 700color: #ffffffletter-spacing: -0.01em,line-height: 1.2
Subtitulo (.app-modal-description)
- Opcional (omitir o
<p>se nao tiver) color: rgba(255, 255, 255, 0.85),font-size: 0.85rem
Close (.app-modal-close sob .is-themed)
- Quadrado
2.2rem × 2.2rem,border-radius: 9px background: rgba(255, 255, 255, 0.16)- Hover:
rgba(255, 255, 255, 0.28) :active=transform: scale(0.94)- Conteudo:
<span aria-hidden="true">×</span>
Body (.app-modal-content sob .is-themed)
padding: clamp(1rem, 3vw, 1.4rem)display: flex; flex-direction: column; gap: clamp(0.7rem, 2vw, 0.95rem)overflow-y: auto,flex: 1- Sob
.is-themedo body costuma ser um<form>com submit handler
Actions (.app-modal-actions sob .is-themed)
display: flex; gap: 0.6rem; justify-content: flex-end- Submit a esquerda do Cancelar (visualmente, devido ao
flex-end+ ordem do JSX) - Border-top
1px solid rgba(0, 0, 0, 0.06)separa do body
5. Campos (.app-modal-field + .app-modal-input)
Estrutura
<label className="app-modal-field">
<span className="app-modal-label">Nome (obrigatório)</span>
<input className="app-modal-input" value={...} onChange={...} />
</label>
.app-modal-input sob .is-themed
border: 2px solid rgba(0, 0, 0, 0.16)— espessura dobrada vs default; bordas precisam ser visiveis sem focobackground: #ffffffborder-radius: 12pxpadding: 0.82rem 1.1remfont-size: 1rem- Focado:
border-color: rgba(22, 91, 42, 0.5)+box-shadow: 0 0 0 3px rgba(22, 91, 42, 0.1)(glow verde) - Inputs
type="date": no WebKit/iOS o input nativo ignorawidth/max-width(usa a largura intrinseca do valor — o pseudo::-webkit-date-and-time-valuetemmin-widthproprio) e estoura o modal (campo gigante + scroll lateral).min-width:0/max-width:100%em.app-modal-field/.app-modal-inputnao bastam. O fix de verdade (regra.app-modal.is-themed .app-modal-input[type='date']):-webkit-appearance: none(vira input de texto, respeitawidth:100%) +::-webkit-date-and-time-value { min-width: 0; text-align: left }. O icone de calendario volta viabackground-image(a.is-themedpinta o fundo branco, entao precisa de especificidade 0,3,1). Mesmo padrao de.samples-filter-field-input[type='date']. Nao remover.
.app-modal-label sob .is-themed
color: rgba(0, 0, 0, 0.6)font-size: 0.78remfont-weight: 600letter-spacing: 0.02em
Textarea
Usar a mesma classe app-modal-input no <textarea>. Adicionar rows={2-3} e maxLength conforme caso.
<label className="app-modal-field">
<span className="app-modal-label">Motivo (obrigatório)</span>
<textarea
className="app-modal-input"
value={...}
rows={2}
maxLength={300}
onChange={...}
/>
</label>
Layout multi-coluna
Para 2 campos lado a lado (ex: CPF | telefone), envolver com <div className="sdv-edit-row">:
<div className="sdv-edit-row">
<label className="app-modal-field">...</label>
<label className="app-modal-field">...</label>
</div>
.sdv-edit-row = display: grid; grid-template-columns: 1fr 1fr; gap: clamp(8px, 2.2vw, 10px).
Para proporcoes diferentes, usar style={{ gridTemplateColumns: '1fr 2fr' }} inline.
Para campo full-width dentro de grid de 2 colunas, usar modificador .is-full no .app-modal-field (depende do CSS scoping local — ver cudm-info-grid > .app-modal-field.is-full em globals.css como exemplo).
ClientLookupField num modal (dropdown que escapa)
Modal central que hospeda um <ClientLookupField> (ex.: envio fisico e gerar laudo no detalhe da amostra): o dropdown de resultados e position: absolute dentro do .app-modal-content (que tem overflow: auto), entao fica recortado pelas bordas. Pra ele "escapar" sem aumentar o modal, adicionar a classe .sample-detail-lookup-modal no <section> (junto de .app-modal is-themed ...):
- libera
overflow: visibleno modal e no.app-modal-content(o card continua arredondado peloborder-radiuse o header tem radius proprio — so o dropdown, que e filho, passa pra fora); - o dropdown ganha
max-heightpra ~4 itens + scroll vertical (overflow-x: hidden); - capar resultados com a prop
maxResults={10}noClientLookupField(alem de 10, o usuario refina a digitacao); - pra abrir com a busca ja preenchida (ex.: nome anotado no informe, modal de vinculo do viewer Relatórios), usar a prop
initialSearch— so inicializa o estado no mount; o primeiro foco no campo ja dispara as sugestoes.
Multi-select de clientes: chips dentro do box .samples-filter-multi .samples-filter-multi--lookup + ClientLookupField com clearOnSelect (o pai mantem o array e renderiza os chips). Rotulo do chip capado em ~10 chars + … (nome completo no title), placeholder sai quando ha selecao. Ver design-system §"Campos de filtro multi-select".
6. Erros e validacao
Erro generico do modal (topo)
Antes do <form>, mostrar mensagem geral em .sdv-modal-error:
{
errorMessage ? <p className="sdv-modal-error">{errorMessage}</p> : null;
}
.sdv-modal-error = fundo rgba(192, 57, 43, 0.08) + texto #8a2727 + borda vermelha clara.
Erro por campo
Aplicar .has-error no .app-modal-input:
<input
className={`app-modal-input${hasError ? ' has-error' : ''}`}
...
/>
{hasError ? <span className="cudm-edit-error">{errorMessage}</span> : null}
.app-modal-input.has-error = border-color: rgba(196, 92, 92, 0.5) (vermelho suave).
.cudm-edit-error = texto #c45c5c font-size 0.78rem abaixo do input.
Erros de validacao em fluxo de submit
Padrao do ClientQuickCreateModal:
- State
submitted = falseinicialmente - Ao clicar Salvar:
setSubmitted(true)antes de validar - Erros so aparecem se
submitted && !canSubmit— evita marcar campo como vermelho antes do usuario interagir - Erro do campo entra como placeholder vermelho dentro do input (
placeholder={hasError ? hint : ''}) + classe.cqc-input-error::placeholder { color: #c45c5c }
Convencao de feedback do projeto: erro de validacao dentro do input (placeholder vermelho + borda vermelha suave), nunca abaixo nem com tooltip. Ver memoria
feedback_error_inside_field.
7. Sucesso
Padrao 1: success overlay com check verde sobre o modal (modal continua visivel mas conteudo coberto pelo SVG por ~900ms antes de fechar).
{
showSuccess ? (
<div className="client-detail-success-check">
<svg viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="12" r="10" />
<path d="m9 12 2 2 4-4" />
</svg>
</div>
) : null;
}
CSS em app/globals.css (.client-detail-success-check, .client-create-success-overlay).
Padrao 2: toast global + fechamento imediato — usar para fluxos rapidos onde nao da pra parar pra contemplar a animacao. Ver lib/toast/ToastProvider.tsx.
Modais novos: prefira toast (mais rapido). Use overlay so quando o usuario precisa visualmente confirmar a operacao antes de seguir.
8. Botoes (actions)
Ordem
<div className="app-modal-actions"> JSX sempre na ordem [Submit, Secondary]:
<div className="app-modal-actions">
<button type="submit" className="app-modal-submit">
Salvar
</button>
<button type="button" className="app-modal-secondary" onClick={onClose}>
Cancelar
</button>
</div>
Sob .is-themed, flex; justify-content: flex-end faz Submit aparecer a esquerda do Cancelar visualmente. Mantem a ordem JSX [Submit, Secondary].
Largura total 50/50 (variante muito usada)
Vários modais do detalhe da amostra (e o de filtros em /samples) usam ações 50/50: os dois botões dividem a largura do modal igualmente, em vez do flex-end canônico. Ordem nessa variante: secundário à ESQUERDA, primário à DIREITA (ex: Cancelar / Limpar / Voltar à esquerda; Registrar / Aplicar / Invalidar / Confirmar à direita) — ou seja, JSX [Secondary, Submit], o inverso da ordem canônica.
Implementação: regra compartilhada em globals.css — NÃO criar uma nova por modal. Só adicione a classe da fileira de ações às duas listas de seletores existentes:
.app-modal.is-themed .app-modal-actions.sample-detail-reclassify-actions,
.app-modal.is-themed .app-modal-actions.sample-detail-reg-edit-actions,
.app-modal.is-themed .app-modal-actions.sample-detail-movement-actions,
.app-modal.is-themed .app-modal-actions.sample-detail-invalidate-actions,
.app-modal.is-themed .app-modal-actions.blend-revert-actions,
.app-modal.is-themed .app-modal-actions.samples-filter-modal-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.6rem;
}
/* + a mesma classe na lista `.<classe> > button { width: 100%; min-width: 0 }` */
No JSX, a fileira leva className="app-modal-actions <classe-da-modal>" e os botões na ordem [Secondary, Submit]. A .sample-detail-compact-modal já é 50/50 por conta própria (flex > button { flex: 1 }) — não precisa entrar na regra compartilhada.
Submit
.app-modal-submit:
- Pill (
border-radius: 999px) - Gradient verde brand
min-height: 3.2remfont-weight: 700- Sombra verde difusa
.app-modal-submit.is-danger:
- Gradient vermelho
#c0392b → #b03224 - Sombra vermelha
- Usar somente em acoes terminais (inativar com cascade, deletar)
.app-modal-submit.is-warning:
- Gradient laranja
#f59e0b → #e67e22 - Usar em acoes de "prosseguir com aviso" — nao destrutivas, mas exigem atencao (continuar apesar de divergencia de lote; confirmar reclassificacao de amostra ja classificada)
Secondary
.app-modal-secondary:
- Pill
border-radius: 999px - Fundo translucido branco
min-height: 3rem- Border
1px solid rgba(214, 214, 214, 0.5)
Estados disabled / saving
disabled={saving}em todos os inputs e botoes durante submitdisabled={saving || !canSubmit}em Submit pra bloquear submit invalido- Submit shows
'Salvando...'durante saving (ou texto contextual:'Inativando...','Processando...') :disabled=opacity: 0.84(continua legivel) +cursor: not-allowed
9. UX comportamental obrigatoria
Focus trap
Sempre usar useFocusTrap(open) (em lib/use-focus-trap.ts):
const focusTrapRef = useFocusTrap(open);
// ...
<section ref={focusTrapRef} className="app-modal is-themed" ...>
Captura Tab/Shift+Tab dentro do modal. Sem isso, foco escapa pro fundo.
Backdrop click
Por default, backdrop fecha o modal — adicionar onClick={onClose} no .app-modal-backdrop. Excecoes: modais de fluxo critico (cascade de inativacao, classificacao em andamento) nao devem fechar por backdrop. Usar app-modal-backdrop-no-dismiss quando aplicavel.
Sempre adicionar onClick={(e) => e.stopPropagation()} no <section> interno pra cliques dentro do modal nao fecharem.
ESC
useFocusTrap so captura Tab. Para ESC fechar, adicionar effect no componente:
useEffect(() => {
if (!open) return;
function handleEsc(e: KeyboardEvent) {
if (e.key === 'Escape') onClose();
}
document.addEventListener('keydown', handleEsc);
return () => document.removeEventListener('keydown', handleEsc);
}, [open, onClose]);
Nem todos os modais existentes implementam isso. Padronizar nos modais novos.
Portal (OBRIGATORIO)
Modal central sempre renderiza via createPortal(..., document.body). Sem excecao. O JSX root vira:
return createPortal(
<div className="app-modal-backdrop" onClick={onClose}>
<section className="app-modal is-themed" ...>
...
</section>
</div>,
document.body,
);
Por que e obrigatorio: todas as rotas da app sao envolvidas por <PageTransition> (components/PageTransition.tsx), que aplica transform: scale(...) + will-change: transform, opacity no wrapper .page-transition-content durante navegacoes. Qualquer transform != none em ancestral cria stacking context que captura o position: fixed do .app-modal-backdrop — o modal acaba abaixo da topbar, do pseudo mobile-edge-shell-auth::after (z-index 9999) e de qualquer outro elemento com z-index alto em irmaos do wrapper. Sintoma classico: "modal abre atras da pagina".
Portal pra document.body escapa qualquer stacking context ancestral, agora e no futuro — robusto contra qualquer novo transform/filter/backdrop-filter em ancestral.
SSR-safe: o padrao do projeto e nao usar guard mounted. O if (!open) return null (ou render condicional do pai) garante que createPortal so e chamado client-side, quando document.body existe. Ver ClientUnitModal, BlendRevertModal, SampleMovementModal, SampleInvalidateBlockedModal como referencia.
Excecao legitima: status modals inline em app/clients/[clientId]/page.tsx que ja vivem direto sob <AppShell> em rotas SEM contexto de stacking problematico podem ficar inline — mas isso e legacy, novos modais sempre via portal.
Aria
Sempre:
role="dialog"aria-modal="true"aria-labelledby="<id-do-titulo>"(id no<h3>do header)aria-describedbyopcional, no<p className="app-modal-description">
Botao close:
aria-label="Fechar"(ou"Fechar novo cliente"se mais especifico ajudar)<span aria-hidden="true">×</span>no conteudo
Animacao de entrada
Coberta pelos keyframes globais (app-modal-backdrop-in, app-modal-card-in). NAO inventar transicoes proprias. Modal aparece com fade + scale subtil em ~0.35s.
Reset de form ao abrir
useEffect(() => {
if (!open) return;
setForm(EMPTY_FORM);
setError(null);
setSubmitted(false);
// ...
}, [open]);
Garantir que reabrir o modal sempre comece limpo.
10. Mensagens em pt-BR
Todos os textos do modal em pt-BR (titulo, label, placeholder, botao, mensagens de erro). Sem ingles em UI. Ver memoria feedback_messages_portuguese.
UPPERCASE em campos de nome/dados cadastrais (event.target.value.toUpperCase()) — convencao do projeto pra dados de cliente, fazenda, endereco.
11. Modais existentes — status
✅ Seguem o padrao .is-themed
Cliente
| Modal | Arquivo | Variantes |
|---|---|---|
| ClientUnitModal (Nova filial) | components/clients/ClientUnitModal.tsx |
is-themed is-action (compacto ~30rem, 50/50 client-unit-modal-actions; form em grid c/ acoes fixas + body rolavel; sucesso via efeito de check client-detail-success-check) |
| ClientUnitDetailModal (view + edit) | components/clients/ClientUnitDetailModal.tsx |
is-themed is-action (compacto ~30rem, 50/50 cudm-edit-actions) |
| ClientInactivateWithCascadeModal | components/clients/ClientInactivateWithCascadeModal.tsx |
is-themed + .is-danger |
| Edit Client (inline na detail page) | app/clients/[clientId]/page.tsx ~L1490 |
is-themed is-action (compacto ~30rem, espelha o reg-edit do lote) |
| Status modal cliente (inline) | app/clients/[clientId]/page.tsx ~L1991 |
is-themed is-action (ações 50/50 client-detail-status-actions) |
| Status modal unit (inline) | app/clients/[clientId]/page.tsx ~L2092 |
is-themed is-action (ações 50/50 client-detail-status-actions) |
Dashboard
O OperationModal ("Amostras pendentes") deixou de ser modal central — migrou pra BottomSheet (
components/dashboard/OperationModal.tsxrenderiza<BottomSheet className="is-operations">). Sai de baixo no mobile e vira modal central no desktop (CSS responsivo >901px do.bottom-sheet). Verdesign-system§8 "Bottom Sheet" (variante.is-operations). A classe.app-modal-dashboarde seus overrides foram removidos; os cards internos seguem usando.app-modal-card*.
Formularios de informe (prospector + comercial)
| Modal | Arquivo | Variantes |
|---|---|---|
| Excluir item (confirm, viewer Relatórios) | components/informe/RelatoriosViewer.tsx (inline, titulo por tipo) |
is-themed app-confirm-modal SEM header (titulo no corpo) + .is-danger + backdrop .is-scrim-dark |
| Excluir informe (dashboard do prospector) | components/dashboard/prospector/ProspectorDashboard.tsx |
is-themed app-confirm-modal SEM header (titulo no corpo) + .is-danger + backdrop .is-scrim-dark (portal) |
| Descartar informe (sheet do prospector) | components/visits/VisitReportFormSheet.tsx |
is-themed app-confirm-modal is-stacked SEM header (titulo no corpo) + .is-danger + backdrop .is-scrim-dark (portal) |
| Descartar visita (sheet do comercial) | components/informe/CommercialVisitFormSheet.tsx |
is-themed app-confirm-modal is-stacked + .is-danger (portal) |
| Descartar relatorio (sheet do comercial) | components/informe/WeeklyReportFormSheet.tsx |
is-themed app-confirm-modal is-stacked + .is-danger (portal) |
| Relatorio ja enviado (aviso 409, bloqueante) | components/informe/WeeklyReportForm.tsx |
is-themed app-confirm-modal is-stacked (botao unico "Entendi", portal) |
| Excluir visita/relatorio (pagina comercial) | components/informe/InformeCommercialPage.tsx |
app-confirm-modal SEM header (titulo no corpo) + .is-danger + backdrop .is-scrim-dark (portal, titulo por tipo) |
| Vincular cliente (curadoria ADM/Cadastro) | components/informe/RelatoriosViewer.tsx (inline JSX) |
is-themed sample-detail-lookup-modal rsm-link-modal (portal; lookup com initialSearch + contexto .rsm-link-context; estado vazio → ClientQuickCreateModal prefilled que vincula no onCreated) |
| Remover vinculo (curadoria ADM/Cadastro) | components/informe/RelatoriosViewer.tsx (inline JSX) |
is-themed app-confirm-modal + .is-warning (portal; re-vinculavel, nao e destrutivo) |
Os formularios abrem em BottomSheets (
.bottom-sheet.is-informe, ver skilldesign-system§8). Confirms de descarte empilham sobre o sheet (.is-stacked,dragDisabledenquanto aberto) — mesmo padrao do "Descartar amostra?" do NewSampleModal. TODOS os confirms acima renderizam viacreatePortal(document.body)(regra do §2) exceto o "Excluir item" do viewer Relatórios (RelatoriosViewer), inline historico (os modais de vinculo do mesmo viewer ja portam).
Extracao da classificacao (/camera — Q.cls.2)
Todos seguem .app-modal.is-themed. Ordem do fluxo: idle → preview → handleSendPhoto → detecting → detected → extracting → (3a/3b se falha; senão) confirming (Review) → selecting-type (Type) → selecting-classifier (Classifier) → submitting → success. Mismatch/reclassify aparecem no caminho do save.
| Modal | Arquivo | Sub-caminho | Variantes |
|---|---|---|---|
| ClassificationReviewModal | components/samples/ClassificationReviewModal.tsx |
Q.cls.2.3 (revisão pós-extração) | is-themed is-wide |
| ClassificationTypeModal | components/samples/ClassificationTypeModal.tsx |
Q.cls.2.8 (seleção de tipo) | is-themed |
| ClassificationClassifierModal | components/samples/ClassificationClassifierModal.tsx |
Q.cls.2.9 (seleção classificadores) | is-themed |
| ClassificationExtractionErrorModal | components/samples/ClassificationExtractionErrorModal.tsx |
Sub-caminhos 3a + 3b | is-themed |
| ClassificationDetectFailedModal | components/samples/ClassificationDetectFailedModal.tsx |
Ficha não detectada (detect-failed) | is-themed |
| ClassificationManualConfirmModal | components/samples/ClassificationManualConfirmModal.tsx |
2º modal de 3b | is-themed |
| ClassificationLotMismatchModal | components/samples/ClassificationLotMismatchModal.tsx |
Sub-caminho 2 (lote diverge) | is-themed |
| ClassificationDataMismatchModal | components/samples/ClassificationDataMismatchModal.tsx |
Sub-caminho 4 (sacas/safra) | is-themed is-wide |
| ClassificationReclassifyModal | components/samples/ClassificationReclassifyModal.tsx |
Sub-caminho 5 (reclassificação) | is-themed + .is-warning |
| ClassificationNotFoundModal | components/samples/ClassificationNotFoundModal.tsx |
Flow A legacy fallback | is-themed |
| ClassificationStatusInvalidModal | components/samples/ClassificationStatusInvalidModal.tsx |
Status inválido (no Avançar) | is-themed |
| ClassificationSuccessModal | components/samples/ClassificationSuccessModal.tsx |
Tela de sucesso pós-classificação | is-themed |
Padrao da extracao: avisos de erro/mismatch usam
role="alertdialog"; modal de tipo+classifier+revisao usamrole="dialog". Modais com seta de Voltar no header (Type, ManualConfirm, Classifier) reutilizam a classe.type-modal-backque aplica fundo branco translucido + ESC = onBack. 2026-06-18: todo o fluxo/cameraadotou.is-action(visual de acao) — header claro + titulo verde + backdrop escuro sem blur; sob.is-actiono.type-modal-backe a.app-modal-descriptionviram escuros (regras emglobals.css). OClassificationReviewModalcentral foi REMOVIDO (codigo morto, 2026-06-18) — a revisao e um BottomSheet (ClassificationReviewSheetBodyno.camera-preview-sheet). A classe.review-modal(container) saiu doglobals.css; as demais.review-*(form/section/field/grid/fundos/warning) seguem, usadas pelo SheetBody.
⚠ Visual igual mas implementacao com classes proprias (refatorar quando tocar)
| Modal | Arquivo | Pendencia |
|---|---|---|
| ClientQuickCreateModal | components/clients/ClientQuickCreateModal.tsx |
MIGRADO pra BottomSheet (2026-06: slide de baixo + stacked) — NAO e mais modal central. Ainda usa classes client-quick-create-* pros campos (coabitam com o chrome do sheet); "Descartar cadastro?" e overlay INTERNO (.client-quick-create-discard-overlay, scrim escuro position:absolute) que envolve o card canonico .app-modal.is-themed.app-confirm-modal (mesmo visual do "Descartar amostra?" do NewSampleModal, sem portar — ver pegadinha "Sheet sobre sheet" no §3). |
⚠ Compactos sem .is-themed (legados, refatorar quando tocar)
Estes usam .app-modal simples (430px max, fundo glass) ou variante cdm-modal em vez de .is-themed:
cdm-modal(Client Detail Modal em/clients,/users,/samples)InactivateUserModal,CancelInactivationDialog,InactivateConfirmDialogem/usersSampleLookupResultModal— usa.app-modal-lookup-resultlegacy mas ja renderiza via portal pra body (fix pra bug de stacking sob<PageTransition>no dashboard).
Refatorar pra
.is-themedsomente quando tiver outro motivo pra mexer no modal — nao e prioridade visual hoje.
samples-filter-modal(/samplese, desde 2026-06-17,/clients— que reusa o mesmo CSS, keyed por classe, com<select>simples no lugar dos campos retráteis) usa.app-modal.is-themedmas adota o VISUAL DE AÇÃO (form que o usuário opera): overrides escopados a.samples-filter-modaldão header CLARO (#fff) + título verde à esquerda + X claro (#eef1ee) + backdrop ESCURO sem blur — igual ao.app-modal-lookup-resulte aos bottom sheets; o header verde do.is-themedfica reservado pra AVISO/confirm. Estrutura custom: body rolavel (.samples-filter-modal-content=overflow-y: auto; overflow-x: hidden) + actions fixas fora do scroll (.samples-filter-modal-form { flex: 1; grid-template-rows: minmax(0,1fr) auto }). Largura 26rem via.app-modal.is-themed.samples-filter-modal(3 classes). Os 3 campos de cliente (Proprietário/Comprador/Enviado para) são RETRÁTEIS (disclosure — verdesign-system§"Campos de filtro multi-select"). Ainda inline (semcreatePortal) — pendencia legacy.
Modal de venda/perda (
SampleMovementModal,.sample-detail-movement-modal) migrou pra.is-themed(header + content +app-modal-field/-input/-actions). Largura 30rem escopada;position: relativepra ancorar o overlay do carimbo. Campos empilhados (cada um em linha própria full-width: Comprador/Motivo → Sacas → Data → Observações); o botão "Todas" fica inline com o input de sacas via.sdv-mov-qty-inline(mesma altura). Ações 50/50 (.sample-detail-movement-actions— regra compartilhada com reclassify/reg-edit) na ordem Cancelar (esq) / Registrar (dir). O sub-modal "Atribuir dono" (ligas) também é.is-themede renderiza viacreatePortalpra body (o pai temoverflow: hidden). Sucesso = carimbo: overlay.sdv-stamp-overlay.is-sale/.is-losscom.sdv-stampna diagonal ("Vendido" dourado / "Perdido" vermelho, anel duplo +mix-blend-mode: multiply), animação de slam (sdv-stamp-slam) e tremor do modal (.is-stamping→sdv-stamp-shake), com guardaprefers-reduced-motion. O carimbo aparece antes de fechar (oSampleMovementsPanelsegura o modal ~1.5s viastampType). O modal de cancelar movimentação (noSampleMovementsPanel) também migrou pra.is-themed(.sample-detail-compact-modal, 28rem, ações 50/50 com.app-modal-submit.is-danger+ secundário "Voltar"). Os cards de venda/perda não têm mais editar — só excluir (cancelar) — e a lista.sdv-com-movementstem scroll interno com teto de ~4 cards no mobile (max-height; no desktop o pane preenche,max-height: none). 2026-06-18: venda/perda, atribuir dono e cancelar movimentação carregam.is-action(header claro + backdrop escuro sem blur — ver §3); os overlays de carimbo seguem como efeito, sem chrome de modal. O cancelar envio (confirm no timeline de Movimentações, antescdm-modallegacy inline empage.tsx) foi refatorado pra.app-modal is-themed is-action sample-detail-compact-modal(header/content/actions canônicos, X com×).
Modais de invalidar amostra (
.sample-detail-invalidate-modal, inline em/samples/[sampleId]) e reverter liga (BlendRevertModal) migraram pra.is-themed, ações 50/50 (.sample-detail-invalidate-actions/.blend-revert-actions) com.app-modal-submit.is-danger. Avisos em vermelho via.sdv-warn-box(ícone de triângulo) dentro do.app-modal-content. Sucesso = efeito de X vermelho (.sdv-x-effect, overlay full-screen viacreatePortal): "Movimentações canceladas" (fica na página) ou "Amostra invalidada" / "Liga revertida" (depois volta pra/samplesviarouter.push) — substitui as mensagens verdes (generalNotice) que apareciam no container. EstadoxEffect: string | null+ helpershowXEffect(label, redirectToList). 2026-06-18: ambos passaram a carregar.is-action(header claro + backdrop escuro sem blur — ver §3); o vermelho de destrutivo segue vindo do botao.is-dangere do.sdv-warn-box, nao do header.
Modal de edicao de registro (
/samples/[sampleId],.sample-detail-reg-edit-modal) migrou pra.is-themedcom a mesma estrutura custom de body rolavel + actions fixas (grid.sample-detail-reg-edit-form { flex: 1; grid-template-rows: minmax(0,1fr) auto }, body em.sample-detail-reg-edit-body). Largura 30rem escopada (3 classes), inputs mais compactos enotice-slotcolapsado quando vazio. Botoes 50/50 (largura toda, grid1fr 1frvia.sample-detail-reg-edit-actions— regra compartilhada com o modal de reclassificacao.sample-detail-reclassify-actions) na ordem Cancelar (esq) / Salvar (dir) — divergencia intencional do canonico[Submit, Secondary]em flex-end. Erros de validacao por campo (placeholder vermelho +.app-modal-input.has-error, limpa ao focar; o submit NAO bloqueia por campo invalido — deixa a validacao rodar e marcar o campo) e sucesso via efeito de check (.client-create-success-overlay, sem mensagem). Ainda inline (sob AppShell). 2026-06-18: carrega.is-action(header claro + backdrop escuro sem blur — ver §3), junto de "enviar amostra" e "imprimir etiqueta" do mesmo container.
Modal de confirmacao de propagacao (safra/proprietario) (
BlendHarvestPropagationModal,.blend-harvest-propagation-modal) segue.is-themed+createPortal, acoes 50/50 (.blend-harvest-propagation-actions— regra compartilhada) na ordem Cancelar (esq) /Aplicar e propagar(dir,.app-modal-submit.is-warning). Disparado pelo 409BLEND_HARVEST_PROPAGATION_REQUIREDao salvar a edicao de SAFRA ou PROPRIETARIO de um lote que e origem de ligas ativas (avisar-e-confirmar): lista as ligas afetadas com a transicao de safra e/ou proprietario (.bhp-*, com rotulo.bhp-change-labelpor linha) e destaca as comercializadas/classificadas (.sdv-warn-box+ chips.bhp-chip). Ao confirmar, re-submete o update comconfirmHarvestPropagation: true. 2026-06-18: carrega.is-action(header claro + backdrop escuro sem blur — ver §3), junto doSampleInvalidateBlockedModal(.sample-invalidate-blocked-modal, aviso reativo de invalidacao bloqueada por liga) — esses 2 avisos reativos fecham o detalhe da amostra 100% no visual de acao.
Modal de selecao de safra do laudo (
ReportHarvestSelectModal,.report-harvest-select-modal) segue.is-themed+createPortal, acoes 50/50 (.report-harvest-select-actions) na ordem Voltar (esq) / Confirmar (dir) — Confirmar so habilita apos escolher uma safra. Aberto ao confirmar o destinatario do laudo quando a amostra tem mais de uma safra (liga): lista as safras como radio cards (.rhs-option+.rhs-option-mark,role="radiogroup", selecao unica) pra o operador escolher qual sai no laudo — o PDF nunca imprime a safra concatenada (anti-vazamento de liga). "Voltar" reabre o modal de destinatario; "x" cancela.
Container de Classificacao do detalhe (
/samples/[sampleId]) — visual de ACAO (2026-06-18): todos os modais do container de classificacao adotaram.is-action(ver §3). O detalhe da classificacao (.cld-modal) ja era.app-modal is-themed is-wide(mantem oscld-*— cld-header/section/field/grid — pro layout interno, incl. a secao "Tipo" do audit Q.cls.2) e ganhouis-action. O confirmar motivo da edicao era.app-modallegacy (sem.is-themed) e foi modernizado pra.app-modal is-themed is-action(corpo usa.app-modal-field/-input/-actionscanonicos). Tambem em.is-action: confirmar salvar (.sample-detail-compact-modal), gerar laudo (.sample-detail-lookup-modal, export), safra do laudo (.report-harvest-select-modal, componente via portal) e reclassificar (.sample-detail-reclassify-modal). OPhotoZoomViewer(foto ampliada) NAO migra — overlay full-screen, excecao legitima. Sem CSS novo: tudo vem do modificador compartilhado.is-action.
Botao "Enviar" unificado via CHOOSER (2026-06-18): o botao "Enviar" do detalhe (
/samples/[sampleId], card Informacoes) abre PRIMEIRO um modal chooserSendMethodChooserModal(components/samples/SendMethodChooserModal.tsx,.app-modal.is-themed.is-action.type-modalespelhando oClassificationTypeModal) com 2 escolhas: "Descricao" (→ gerar laudo/export;.is-disabled+.type-modal-choice-hint"Disponivel apos classificar" quando a amostra NAO e CLASSIFIED) e "Fisico" (→ enviar amostra). Roteia pros fluxos que ja existiam (handlers reaproveitados; backend inalterado). O antigo botao "Laudo" saiu do card Classificacao (→ "Classificar" ocupa a largura toda,flex:1). Os modais de destino — "Gerar laudo" (.sample-detail-lookup-modal) e "Enviar amostra" — ganharam seta de voltar (.type-modal-back) que reabre o chooser; no "Enviar amostra" a seta so aparece em envio NOVO (na EDICAO via timeline nao ha chooser/seta). O header agrupa seta+titulo com a classe nova.sdv-send-head-left(o resto reusatype-modal*). EstadosendChooserModalOpennopage.tsx(+ cleanup por sampleId).
🚫 Excecoes legitimas (NAO refatorar)
| Modal | Por que e excecao |
|---|---|
| ForgotPasswordModal | Estilo da tela de login (login-modal-*), fora da app autenticada |
| PhotoZoomViewer | Overlay full-screen pra zoom de foto — nao e modal de form |
Mapa visual do fluxo da extracao
scanner (idle)
│
▼ tira foto
preview ──── Tirar outra ──── (resetClassificationFlow)
│ "Enviar"
▼
detecting ─── Foto sem ficha visivel ──► detect-failed (modal) ─── "Continuar" ───────────┐
│ ficha detectada │
▼ │
detected (success-icon, 800ms) │
│ │
▼ │
extracting ◄────────────────────────────────────────────────────────────────────────────────┘
│
├── lote=null (hasContext) ──► extraction-error-illegible ─── "Tirar outra" ─── reset
│ ─── "Cancelar" ─── router.back()
│
├── catch (timeout/offline) ──► extraction-error-technical ─ "Tirar outra" ─ reset
│ "Continuar manual"─► manual-confirm ─ "Confirmar" ─► startManualMode → confirming (Review com lote/sacas/safra editaveis)
│ "Cancelar" ─ router.back()
│
▼ extracao OK
confirming (ReviewModal) ─── "Cancelar" ─── reset
│
▼ "Avançar" — sheet valida lote obrigatorio (inline) + ≥1 campo + numerico → handleReviewAdvance valida status/existencia ANTES do tipo:
│ ├── Flow A: resolve lote → nao encontrado ─► not-found (NotFoundModal) ─ "Voltar"/"Cancelar"
│ ├── status ∉ {RC, CLASSIFIED} ───────────► status-invalid (StatusInvalidModal) ─ "Cancelar"/"Ver detalhes"
│ └── ok ─► selecting-type
▼
selecting-type (TypeModal) ─── ← Voltar (seta) ─── confirming
│ click num tipo
▼
selecting-classifier (ClassifierModal) ─── ← Voltar (seta) ─── selecting-type
│ "Confirmar"
▼
handleConfirmClassification (status/resolve ja feitos no Avancar)
├── lote(editavel) ≠ contextSampleLot ──► lot-mismatch (LotMismatchModal) ─ "Tirar outra" ─ reset
│ "Cancelar" ─ router.back()
├── divergencias sacas/safra ───────────► data-mismatch (DataMismatchModal) ─ ESCOLHA campo a campo ─► "Aplicar" ─► save (ou overwrite-confirm se CLASSIFIED)
├── sample CLASSIFIED ──────────────────► overwrite-confirm (ReclassifyModal com reason):
│ ├ "Confirmar recl." (laranja/is-warning, direita) ─► save
│ ├ "Voltar" (esquerda) ─ confirming (review / dados extraidos, pra reconferir o lote)
│ └ "x" ─ cancela processo → camera (hasContext ? router.back : reset)
└── tudo OK ─────────────────────────────► saveClassification → submitting → success
12. Checklist de revisao
Ao construir ou revisar um modal:
-
<div className="app-modal-backdrop">+<section className="app-modal is-themed">(comis-widese >5 campos ou linhas multi-coluna) -
<header className="app-modal-header">com.app-modal-title-wrap(titulo + descricao opcional) e.app-modal-close -
<form className="app-modal-content">com submit handler - Campos como
<label className="app-modal-field"><span className="app-modal-label">...</span><input className="app-modal-input">...</label> - Multi-coluna usando
.sdv-edit-row(1fr 1fr) oustyle={{ gridTemplateColumns }}inline - Erro generico do topo em
<p className="sdv-modal-error"> - Erro por campo via
.app-modal-input.has-error+<span className="cudm-edit-error"> -
<div className="app-modal-actions">na ordem[Submit, Secondary] - Submit:
.app-modal-submit(verde),.app-modal-submit.is-danger(vermelho, terminal) ou.app-modal-submit.is-warning(laranja, prosseguir-com-aviso) - Secondary:
.app-modal-secondary -
disabled={saving}em todos os inputs e botoes - Submit muda texto durante saving (
'Salvando...','Inativando...', etc) -
useFocusTrap(open)no<section> -
role="dialog",aria-modal="true",aria-labelledbyno<section> - Close button com
aria-label="Fechar"e<span aria-hidden>×</span> - Reset de form em
useEffect(() => { if (open) setForm(EMPTY); }, [open]) - Backdrop fecha por click (default) —
onClick={onClose}no backdrop,onClick={stopPropagation}no section -
createPortal(..., document.body)no return — obrigatorio pra todo modal central (escapa stacking context do<PageTransition>que envolve todas as rotas) - Textos em pt-BR (titulo, labels, botoes, mensagens)
- Sem cores inventadas — apenas tokens da paleta (
design-system§2) - Sem botao verde no
:activetransitorio (apenas.app-modal-submitque ja e verde por design) — ver skillbutton-press-effectpra receita completa de tap feedback
13. Como editar um modal divergente
Quando encontrar um modal listado em "⚠ Compactos sem .is-themed" que precisa de mudanca:
- Migrar pra
.is-themed: trocar wrappers proprios por.app-modal-header/-content/-field/-input/-actions - Manter classe scoped pra customizacoes especificas (ex:
cudm-info-grid,client-quick-create-flags) — coabitam bem com.app-modal-* - Largura: testar se
38rem(default) basta; senao adicionar.is-wide(46rem) - Remover CSS duplicado (header verde, close button, botoes) que era replicado localmente
- Smoke test visual: abrir o modal antes/depois e comparar — efeito final deve ser identico
Exemplo de referencia do canonico central: ClientUnitModal. (O ClientQuickCreateModal, antes citado aqui como modal central divergente, virou BottomSheet em 2026-06 — ver §1 e a secao de Bottom Sheet no design-system §8.)
14. Quando criar classes scoped
Use classes proprias (<algo>-modal, <algo>-header, etc) somente para:
- Layouts especificos do conteudo do modal (ex:
cudm-info-grid2 colunas pra dados de filial,sdv-cascade-listlista de amostras vinculadas) - Customizacoes pontuais que nao cabem nos tokens globais (ex: eyebrow do header
cudm-header-eyebrow) - Estados especificos do dominio (ex:
cudm-header-inactivebadge "Inativa")
NAO criar classe propria pra:
- Backdrop (sempre
.app-modal-backdrop) - Modal container, header, body, actions (sempre
.app-modal*) - Botoes (sempre
.app-modal-submit/-secondary) - Campos (sempre
.app-modal-field/-input/-label) - Header verde (sempre
.is-themed)
Se precisar customizar um desses, adicionar classe extra ao lado da canonica em vez de substituir:
<section className="app-modal is-themed is-wide cudm-modal">
E definir overrides no CSS pelo seletor combinado:
.cudm-modal .app-modal-header {
/* override pontual */
}