Componentes
49. Detalhe de registro — painel e modal
seed-componentes.md v1.43 · §49seção 51 de 9849-detalhe-de-registro-painel-e-modal-estavel-f7-3.md · MD5 2bf3e368Título completo no canon: Detalhe de registro — painel e modal — estável (F7.3, 2026-08-16) · PD1–PD19 · gabarito A8 · fecha a pendência PN-P3
O que esta seção resolve, e por que ela demorou. A PN-P3 está aberta desde a Fase 5: todo ERP é lista → detalhe, e o nosso caminho morria na lista. A seção não foi escrita antes por um motivo declarado — não havia LEITURA VISUAL do arquétipo, só extração de valores, e a F7.2 provou caro o que isso produz (números certos, composição errada). A leitura chegou em 2026-08-15, na §13.4 do
estudo-clickup-completo.md, com sete dispositivos observados: C50 modal que navega entre irmãos sem fechar, com posição no conjunto em cabeçalho fixo · C51 propriedades em grade de duas colunas · C52 campos customizados em lista com ícone de tipo e rótulo truncado · C53 ações do campo reveladas no hover · C54 corpo truncado com "Expandir" · C55 rail de seções DENTRO do modal · C56 contadores de metadado como ícone+número.Base (3 rodadas encadeadas + rodada zero, 2026-08-15). Rodada zero — leitura visual: confirmada na §13.4 antes de qualquer spec. Regra da fase. R1 canon: Carbon dá a régua por VOLUME, não por gosto — modal quando um subconjunto pequeno é editável; se há campos suficientes para exigir rolagem, painel lateral ou página inteira; e o side panel existe para manter o usuário em contexto com a página. Fiori dá o object page (seções e subseções com navegação por âncora ou aba) e duas réguas operacionais que entraram inteiras: a paginação entre irmãos só aparece quando a lista de origem tem ao menos dois itens, e ao excluir o item corrente seleciona-se o anterior — se não sobra nada, fecha. Fiori também manda usar o mesmo layout em exibição e edição: o conteúdo não muda de lugar. R2 mercado/stack: Radix Dialog entrega os dois regimes (modal e não-modal) com controle fino de foco, o que barateia a decisão PD1 — é o mesmo primitivo. Nota de fronteira registrada: o Radix foi adquirido pela WorkOS e a cadência caiu em alguns componentes; o Base UI é hoje a camada de primitivos mais ativa, e o shadcn/ui é o consumo dominante. Não muda decisão aqui; muda o que vigiamos. R3 normas / fora do circuito: o cabeçalho fixo do C50 é risco de norma, não detalhe estético — a falha F110 do WCAG 2.2 descreve exatamente o sticky que cobre o elemento focado, e o SC 2.4.11 é AA; o conserto canônico é
scroll-padding. O hover do C53 é a armadilha maior: o Spectrum corrigiu este caso em novembro de 2025 trocandovisibility:hiddenporopacity:0, porquevisibility:hiddenedisplay:noneremovem da árvore de acessibilidade e o botão deixa de ser alcançável por Tab. O "Expandir" do C54 precisa dearia-expanded+aria-controlse de rótulo que nomeie o quê.A LACUNA QUE A PESQUISA REVELOU, e é o achado da rodada: não existe canon para troca de conteúdo DENTRO de um diálogo já aberto. Todo o corpus trata abertura e fechamento. O anúncio do nome do diálogo acontece quando o foco entra nele; trocar o texto do título com o foco parado não produz anúncio nenhum. Sem instrumento próprio, navegar entre irmãos é silencioso para leitor de tela. O PD3 é decisão nossa, declarada como tal.
Gate de 2026-08-15 (Rafael, sobre amostra A/B navegável): "modal A por padrão, e opção do modelo B caso o usuário prefira, então vamos ter as duas opções. no modelo A, deixa opção de expandir a janela depois que ela for aberta." Daí saem o PD1 (dois regimes, modal como padrão), o PD17 (dois degraus da janela) e o PD18 (alternância a partir do próprio detalhe).
49.1 Os contratos#
| # | Contrato | O que ele descarta, e por quê |
|---|---|---|
| PD1 | Três regimes, escolhidos por VOLUME de conteúdo, com o MODAL como padrão. Modal centrado (registro que cabe sem rolagem longa) · painel lateral não-modal (o contexto da lista importa durante a leitura) · página inteira (objeto com muitas seções e edição global). O painel é preferência do usuário, persistida por usuário e não por sessão | Copiar o modal do produto de referência como regime único. Carbon é explícito: campos suficientes para exigir rolagem já pedem painel ou página. E descarta manter dois desenhos do detalhe: o markup é UM só, movido de hospedeiro (ver PD16) |
| PD2 | A paginação entre irmãos só existe com conjunto ≥ 2, exibe a posição (9/10) e nas pontas fica desabilitada |
Seta sempre presente e inerte: com um registro só, um controle que não faz nada é botão morto. Régua literal do Fiori |
| PD3 | Trocar de irmão NÃO devolve o foco ao topo; o foco permanece no controle acionado e a troca é anunciada por live region polite no formato "Registro N de M — título" |
Reabrir o diálogo a cada irmão (perde rolagem e re-anuncia tudo) e confiar no aria-labelledby, que só é lido na abertura. Sem canon — decisão declarada |
| PD4 | Registro excluído → seleciona o anterior; conjunto vazio → fecha | Manter o detalhe aberto sobre um registro que não existe mais |
| PD5 | Cabeçalho do detalhe é FIXO, e o contêiner de rolagem declara scroll-padding-top maior que a altura dele |
Sticky sem compensação: é a falha F110 do SC 2.4.11. Medido no gabarito: cabeçalho 68px, scroll-padding-top 76px — 64px reprovava por 4px, e "quase" não passa numa norma cujo teste é o foco sumir atrás da barra |
| PD6 | Ausência de valor é SEMPRE travessão, na grade e na lista de campos | A palavra "Vazio". O produto de referência usa os dois tratamentos no mesmo registro (C52) — divergência registrada e não copiada; o DT7 já escolheu o travessão |
| PD7 | A grade de propriedades responde ao CONTÊINER (CP27): 1 coluna até 520px, 2 acima, 3 a partir de 900px | Breakpoint de viewport. Consequência que vale a lei: expandir a janela faz caber mais, nunca deixa o dado maior |
| PD8 | Altura de linha do campo é FIXA; quem cede por falta de largura é o RÓTULO, com o texto completo em title e equivalente por foco (X2 do §23) |
Truncar o valor primeiro. O valor é o dado; o rótulo é a etiqueta dele, e etiqueta curta ainda identifica |
| PD9 | Ações do campo reveladas no hover E no :focus-within, com opacity:0 — nunca visibility:hidden nem display:none — e FORA do fluxo (position:absolute) |
Os dois primeiros removem da árvore de acessibilidade (SC 2.1.1). E manter as ações no fluxo reserva largura permanente para o que está invisível: medido, 88px que faziam o valor truncar sempre no painel de 460px |
| PD10 | O corte do corpo é disclosure de verdade: <button> com aria-expanded + aria-controls resolvendo em elemento real, rótulo que nomeia o quê ("Expandir descrição") e muda com o estado; corte por line-clamp |
Link, <details>/<summary> (perde o controle do clamp por número de linhas) e o rótulo "Expandir" solto, que fora do contexto visual não informa nada |
| PD11 | O rail de seções é navegação por ÂNCORA, não abas: documento único rolável, aria-current em exatamente uma seção, nome acessível em cada botão |
Abas: fragmentariam o objeto e quebrariam a rolagem única que o PD10 pressupõe. Fiori admite âncora ou aba; âncora é o caso quando as seções são partes do mesmo objeto |
| PD12 | Contador de metadado leva nome textual ("3 anexos"); o ícone não é rótulo | O número nu: não diz de quê. Herda o §21 |
| PD13 | O detalhe tem URL própria e, ao fechar, o foco volta à linha de origem — se ela saiu do DOM, ao lugar mais próximo dela | Estado só em memória: impede compartilhar o registro e devolve o foco ao topo do documento, fazendo perder o lugar na fila |
| PD14 | Regime modal: showModal() nativo, aria-modal, fundo inerte, Esc fecha. Regime painel: NÃO-MODAL — sem trap de foco, com a lista viva ao lado |
Painel lateral com trap: contradiz a razão de ele existir. Para diálogo não-modal a norma pede atalho global que mova o foco entre o diálogo e a página |
| PD15 | Campos editam por edição direta, herdando PN22–PN41 do §48 sob o SC 2.5.7 | Modo de edição global na v1. Fiori: o conteúdo não muda de lugar entre exibição e edição |
| PD16 | UM só markup de detalhe no documento, movido entre os dois hospedeiros | Clonar o conteúdo por regime. Dois desenhos vivos é como um deles envelhece sozinho — e a troca de regime passaria a perder o estado do registro |
| PD17 | A janela tem DOIS DEGRAUS DECLARADOS (padrão e expandido), operados por botão de dois estados com aria-pressed, rótulo e ícone acompanhando. O degrau é preferência do usuário e sobrevive à troca de registro. No degrau estreito o botão some |
Alça de arraste como caminho único: cai sob o SC 2.5.7 e exigiria alternativa sem arraste de qualquer forma — o mesmo raciocínio que transformou o resizer de coluna do §51 em slider. Fiori admite arraste com tamanhos predefinidos e encaixe; adotamos os tamanhos e dispensamos o arraste. Descarta também a janela que cresce até o conteúdo: com registro curto o botão ficaria inerte |
| PD19 | A lista de origem só ACOMPANHA quando está viva. No regime painel o realce segue a navegação entre irmãos — a lista está visível ao lado e a sincronia é o serviço. No regime modal ela não acompanha; a marcação é aplicada no fechamento, junto com o foco, e aponta onde a leitura parou | Sincronizar sempre. Achado do gate de 2026-08-16, verbatim: "quando navego com a seta pra cima ou para baixo, ele está mexendo as duas janelas… era pra mexer só a que abriu sobreposta". Sob o véu do modal o fundo está inerte: movimento ali não informa nada e compete com o conteúdo que tem o foco. Descarta também devolver o foco à linha que abriu a leitura — quem navegou do 3 ao 7 espera voltar ao 7 |
| PD18 | A alternância de regime vive DENTRO do cabeçalho do detalhe | Um controle de preferência fora do detalhe. Com o diálogo modal aberto o fundo é inerte, e o controle ficaria inalcançável exatamente quando seria usado — medido nesta produção |
49.2 Anatomia#
┌─ cabeçalho FIXO (PD5) ──────────────────────────────────────────────┐
│ [↑][↓] 9/10 «chip de status» Título do registro … [⤢][×] │
├──────────────────────────────────────────────────┬──────────────────┤
│ contadores de metadado (PD12) │ rail de seções │
│ grade de propriedades — 1 · 2 · 3 col (PD7) │ (PD11, âncora) │
│ DESCRIÇÃO + [Expandir descrição] (PD10) │ ▤ │
│ CAMPOS DO DOMÍNIO — lista, ícone de tipo, │ ✉ ② │
│ rótulo truncável, ações no hover/foco (PD8/PD9)│ ∿ │
│ COMENTÁRIOS │ │
└──────────────────────────────────────────────────┴──────────────────┘
Os dois hospedeiros, com a mesma anatomia:
| Modal (padrão) | Painel lateral | |
|---|---|---|
| Elemento | <dialog> com showModal() |
<aside> irmão da região de conteúdo |
| Fundo | inerte | vivo — a lista continua operável |
| Largura | 860px · expandido 1360×940 | largura de peça lateral |
| Grade de propriedades | 2 col · 3 col no expandido | 1 col |
| Botão de tamanho | presente | ausente (não há dois tamanhos a oferecer) |
49.3 Acessibilidade#
- O diálogo é nomeado pelo título do registro (
aria-labelledbyapontando para o<h2>que exibe o nome). O nome muda a cada irmão; o anúncio não vem daí, vem da live region do PD3. - Foco entra no detalhe ao abrir, volta ao gatilho ao fechar (PD13).
- Nenhuma ação depende de hover (PD9): tudo que o mouse revela, o teclado revela por
:focus-within, e o alvo tem 44px no eixo curto também — regra que este projeto já reprovou quatro vezes por medir só um dos eixos. - Cabeçalho fixo × foco:
scroll-padding-topmaior que o cabeçalho (PD5, SC 2.4.11). - Contraste: 28 pares medidos nos dois temas, 56 · 0 (
contraste-f73.py). Nenhum par novo: tudo é consumo de token canônico. Um achado real da medição: a primeira versão usavaborder-defaultna fronteira dos botões, que mede 1,49 sobre branco e 2,32 no escuro — reprova o SC 1.4.11 nos dois temas. Trocado porborder-interactive(3,38 e 4,50), que nasceu na v6.9 exatamente para este papel.
49.4 Estados#
| Estado | Tratamento |
|---|---|
| Valor ausente | travessão, nos dois blocos (PD6) |
| Rótulo longo | trunca com reticências, title com o texto completo, altura fixa (PD8) |
| Corpo longo | line-clamp + disclosure nomeado (PD10) |
| Primeiro/último do conjunto | seta da ponta desabilitada, nunca inerte sem sinal (PD2) |
| Conjunto vazio | o detalhe fecha (PD4); a lista mostra o zero-resultados do §25, com saída |
| Degrau estreito | o detalhe vira página: ocupa a tela, o botão de tamanho some (PD17) |
49.5 Números MEDIDOS na produção do gabarito#
A spec nasce com relações; os números são medidos no artefato — método do §55.
| Medida | Valor |
|---|---|
| Largura do modal, degrau padrão | 860px |
| Largura × altura, degrau expandido | 1360 × 940px |
| Colunas da grade: painel · modal · expandido | 1 · 2 · 3 |
| Tipografia do rótulo e do valor nos dois degraus | 12px e 14px — idênticas |
Altura do cabeçalho fixo · scroll-padding-top |
68px · 76px |
| Altura da linha de campo do domínio | 36px |
| Alvo dos botões do cabeçalho e do rail de seções | 44 × 44px |
| Alvo das ações de campo | 44 × 36px (a linha inteira é o alvo de leitura) |
| Fronteira de controle × superfície (claro · escuro) | 3,38 · 4,50 |
49.6 Validação#
| Camada | Placar |
|---|---|
suite-detalhe.mjs (render) — as guardas do §49 |
54 · 0 (47 na produção + 7 nascidas do gate de 2026-08-16) |
suite-composicao.mjs (render, 2 temas) |
33 · 0 |
suite-container.mjs — blocos CQ e RF, com alvo explícito |
15 · 0 |
contraste-f73.py — 28 pares × 2 temas |
56 · 0 |
| Fidelidade do gerador | reprodução byte-perfeita conferida por MD5 |
Escopo declarado da
suite-container.mjs: ela foi escrita para os gabaritos de dado A2/A3 e cobra árvore comrole=tree(ARV-01…03), que é estrutura do A2 e não do A8 — o escopo do detalhe é lista plana, e dar hierarquia à sidebar só para satisfazer um teste seria inventar estrutura para agradar instrumento. Os blocos que se aplicam (CQ container query, RF reflow) rodam com o A8 passado como alvo explícito. Isto é decisão registrada, não placar vermelho tolerado.
49.7 Pendências desta seção#
| # | Pendência | Por que fica aberta |
|---|---|---|
| PD-P1 | O §56 comentários não é decidido aqui. A fronteira do C85 — comentário como cartão dentro do cartão do objeto — é peça em peça, que o CP2 proíbe no caso base | Há um dispositivo observado, e ele é justamente o caso difícil. A rodada zero exige leitura visual do arquétipo, e uma leitura de um caso não é leitura do arquétipo. Navegar antes |
| PD-P2 | O regime página inteira (3º degrau do PD1) não tem gabarito | Nenhum registro do escopo atual exige. Nasce quando houver caso SEED que o peça, sob a mesma régua de volume |
| PD-P3 | Persistência da preferência de regime e do degrau da janela | É contrato de produto, não de DS: o gabarito demonstra o estado; onde ele mora é decisão do ERP |
| PD-P4 | Deep link do PD13 declarado, não exercitado no gabarito | O gabarito é arquivo único sem rota; provar URL própria exige aplicação |
Também cita o §49: tela-painel.