-
Status do intake: implemented-pending-homologation
-
Data: 2026-08-29
-
Origem: pedido do Engenheiro Chefe durante o BATCH-149 ("tem como incluir no DataTables pra ele não colapsar automaticamente quando a tela for grande… eu quero que crie scroll lateral"), com complemento: "se tiver muita coisa, campos muito grandes, eu quero continuar vendo e podendo ir com o scroll horizontal"
-
Classificação: usabilidade do painel administrativo
-
Batch: BATCH-149
1. O comportamento que incomodava
O DataTables carrega a extensão responsive, que esconde colunas assim que a tabela não cabe e
devolve o conteúdo atrás de um botão +, um registro por vez, num modal.
Numa tela de 1200px ou mais isso tira da vista dados que caberiam com um arrasto lateral — e obriga a abrir um modal por linha só para comparar dois registros. Com campos longos o efeito piora: a tabela colapsa cedo, justamente quando há mais o que ver.
Medido em /admin-paginas/ antes da mudança, com viewport de 1400px: 25 botões +, cinco
colunas visíveis e o resto escondido.
2. O que foi feito
Acima de um limiar de largura (padrão 1200px), o responsive é desligado e a tabela passa a rolar na
horizontal. Abaixo dele o colapso continua valendo: em celular, rolar uma tabela de dez colunas é
pior do que abrir o detalhe.
A escolha é feita na criação e não acompanha o redimensionamento da janela — trocar de modo exige destruir e recriar a tabela, o que perderia página, busca e ordenação.
O limiar é configurável por gestor.listagemLarguraSemColapso.
A caixa envolve apenas a TABELA
Primeira tentativa aplicou o overflow-x ao wrapper do DataTables. Errado, por dois motivos que só
apareceram no teste:
-
Em CSS não existe rolar num eixo só. Quando um eixo é
auto, o navegador converte ovisibledo outro emautotambém. O resultado foi um scroll vertical dentro da listagem que engolia o scroll da página — foi o que o operador reportou. -
O wrapper guarda a busca, o seletor de quantidade e a paginação. Prendê-los numa caixa com
overflowcortaria os dropdowns deles.
Envolvendo só a tabela (<div class="listagem-scroll-horizontal"> criado no evento init.dt), o
eixo vertical pode ser hidden sem prejuízo — a tabela não transborda verticalmente — e os controles
ficam de fora, livres.
Por que nowrap é parte da solução, não enfeite
width: auto derruba o width: 100% que o DataTables escreve inline; sem isso a tabela se espreme
para caber e o scroll nunca aparece.
E white-space: nowrap é o que dá largura real às colunas. Sem ele o texto quebra em várias
linhas, a tabela volta a caber e o scroll fica inútil — exatamente o caso dos campos longos que
motivaram o pedido.
3. Validação (navegador real, Playwright)
| Medida | 1400px | 900px |
|---|---|---|
| Caixa de rolagem criada | sim | não (correto) |
| Rola na horizontal | sim | — |
| Rola na vertical | não | — |
overflow-y computado |
hidden |
— |
| Largura da tabela / da caixa | 2819px / 1092px | — |
Botões + (colunas colapsadas) |
0 | 25 |
| Scroll vertical da PÁGINA | preservado | preservado |
| Filhos da caixa | ["TABLE"] |
— |
| Busca / paginação / info / qtd dentro da caixa | não | — |
4. Critérios de aceite
-
Em viewport ≥ 1200px nenhuma coluna é colapsada e a tabela rola lateralmente.
-
A listagem não cria scroll vertical próprio; o scroll da página continua funcionando.
-
Abaixo do limiar o comportamento responsivo anterior é preservado.
-
Vale para as duas interfaces (
interfaceeinterface-v2), pela BIBLIOTECA e não por módulo. -
(2ª rodada) Quem rola é a JANELA, não uma caixa em volta da tabela — a barra precisa nascer onde o usuário já procura, sem depender de descer a página para descobri-la.
-
(2ª rodada) As ações ficam na PRIMEIRA coluna, ancoradas na borda esquerda, e continuam alcançáveis com a tabela rolada.
-
(2ª rodada) O número de cabeçalhos tem de bater com o de células em toda listagem:
interface.phpmonta a tabela em dois lugares, e reordenar só um desloca a tabela inteira.
5. 2ª rodada — a rolagem foi para a JANELA e as ações mudaram de coluna
O retorno do operador: "para leigo ele vai aparecer que tem que descer o scroll vertical da página para começar a aparecer o scroll horizontal".
Procede. A barra de uma caixa nasce no rodapé da caixa. Numa listagem de 25 linhas, quem não conhece o sistema precisava descer a página inteira para descobrir que havia rolagem lateral. A barra da janela fica sempre no rodapé da janela, onde todo mundo já procura.
Então a tabela passou a estourar a largura da página, e quem rola é o documento. A classe vai no
<body>, e overflow-x: visible nos ancestrais é o que permite o estouro — um hidden em qualquer
um deles cortaria a tabela em vez de deixar rolar, produzindo uma listagem truncada sem aviso.
As ações foram para a primeira coluna
Com a rolagem, "última coluna" virou "fora da tela": alcançar editar e excluir exigia rolar até o fim
de uma tabela larga, em toda linha. A coluna de opções passou para a primeira posição e ganhou
position: sticky, ficando ancorada na borda esquerda mesmo com a tabela rolada.
Dois defeitos que isso causou, e o que eles ensinaram
1. A tabela inteira saiu deslocada em uma posição. interface.php monta a tabela em DOIS lugares
— o cabeçalho/columns e o <tbody> que o servidor entrega antes de o JavaScript rodar
(deferLoading). Eu reordenei só o primeiro. O resultado: o status aparecendo sob "Data Modificação"
e a coluna de ações mostrando o id como texto.
Lição: quando duas rotinas produzem partes da MESMA estrutura, mudar a ordem em uma delas é meia correção — e o sintoma aparece longe da causa.
2. O botão de bloquear registro sumiu. Consequência do mesmo deslocamento: o renderizador dos
botões estava preso a targets: -1, e a coluna já não era a última.
Uma decisão revertida
Tentei mover o controle + do modo responsivo para a coluna do nome com
responsive.details.type = 'column', porque no mobile ele caiu na mesma célula dos botões de ação.
A expansão parou de funcionar e a alteração foi revertida.
Um A/B contra o commit anterior mostrou que o comportamento de expansão no mobile é o mesmo de antes da mudança — a sobreposição do controle com os botões existe, mas não é regressão deste lote. Fica registrado como observação, não como pendência inventada.
Destaque visual da coluna
O fundo da coluna ancorada precisa ser opaco: com a tabela rolada, o conteúdo das outras colunas
passa por baixo dela. O #fff inicial cumpria a necessidade técnica sem intenção visual, e deixava a
coluna branca no meio de um cabeçalho cinza. Agora ela tem tom próprio no cabeçalho e nas células, e
o hover da linha alcança a coluna também — senão ela pareceria "morta" enquanto o resto reage.
6. Validação da 2ª rodada
| Medida (1400px) | Resultado |
|---|---|
| Rolagem | da JANELA (documento 1920px, viewport 1400px) |
| Caixa em volta da tabela | não existe mais |
| Primeira coluna | Opções, com os botões |
position da coluna |
sticky |
| Cabeçalho: opções / demais | rgb(234,238,243) / rgb(249,250,251) |
| Célula: opções / demais | rgb(244,246,249) / transparente |
Alinhamento verificado em 7 telas (admin-paginas, usuarios, publisher, modulos,
admin-componentes, admin-layouts, menus): todas com "Opções" na primeira posição e o número de
cabeçalhos igual ao de células — nenhum deslocamento. A correção está na BIBLIOTECA
(interface.php + os dois runtimes), não em módulo nenhum.
PHPUnit 942/942; Vitest 378/378.
7. 3ª rodada — destaque na paleta da marca
Pedido do operador: usar a tonalidade do logo, manter o fundo dos ícones branco e acrescentar hover.
As duas cores foram medidas no próprio assets/images/logo-principal.png, por contagem de
pixels (ignorando os cinzas do fundo):
| Cor | Hex | Pixels | Onde aparece no logo |
|---|---|---|---|
| Ciano | #18a8c0 |
14.283 | o símbolo e o "2" |
| Azul-marinho | #182840 |
4.529 | o quadrado e o texto |
Ficam como variáveis CSS (--c2f-marca-ciano, --c2f-marca-navy e dois tons claros derivados) para
que uma futura troca de identidade seja uma edição só, e não uma caçada por hex espalhado pela folha.
O logo é asset do core, não do projeto — então usar essa paleta na interface administrativa é coerente: é a marca do próprio gestor, não a de um cliente.
Os botões seguem brancos
A coluna inteira já é a área destacada. Pintar os botões também competiria com os ícones, que são o que o operador procura na tela. O destaque deles vem no hover: anel na cor da marca, sombra e um deslocamento de 1px, para o alvo ficar óbvio antes do clique.
Mantive o elemento <button>/<a> simples, como o operador sugeriu.
Medido
| Elemento | Valor |
|---|---|
| Cabeçalho da coluna | rgb(230,244,248) (demais: rgb(249,250,251)) |
| Célula da coluna | rgb(242,250,252) |
| Botão em repouso | rgb(255,255,255) |
| Botão no hover | fundo branco preservado; anel rgb(24,168,192) = #18a8c0 |
| Botão no hover | translateY(-1px) |
O fundo da coluna é opaco por necessidade: com a tabela rolada, o conteúdo das outras colunas
passa por baixo dela, e um rgba deixaria o texto atravessar.
PHPUnit 948/948; Vitest 378/378.
8. 4ª rodada — a colisão de chave que a reordenação expôs
Relato: as listagens de assinatura do Photon (subscriptions-plans, subscriptions-status,
subscriptions-service-types) "zoadas", com o que pareciam opções extras na coluna de ações.
Não eram opções extras. Era HTML quebrado vazando como texto:
cloud-nano | " data-content="Clique para Editar" data-id="editar">✏A causa
A coluna de ações declarava 'data' => $banco['id'] — o mesmo campo que uma coluna de dado pode
exibir. E exibe: subscriptions-service-types declara
Array('id' => 'id', 'nome' => ..., 'formatar' => Array(
'id' => 'encapsular', 'capsula' => '<div class="ui basic label">#id#</div>', ...))Quando duas colunas declaram o mesmo data, a última vence ao montar o objeto da linha.
Enquanto a coluna de ações era a última, ela vencia e recebia o id limpo — o acidente ficava
escondido. Ao movê-la para a frente, quem passou a vencer foi a coluna "Slug", já formatada.
O botão então virava:
<a href="editar/?id=<div class="ui basic label">cloud-nano</div>" data-content="...">A tag fecha no meio do atributo, o <div> renderiza como elemento (o "chip" que parecia um botão
extra) e o resto do HTML vira texto na tela.
A correção
A coluna de ações ganhou chave própria (INTERFACE_COLUNA_ACOES = '_gestor_acoes_id'), alimentada
com o id antes do laço de formatação — porque interface_formatar_dado() muta $dado[...].
Isso elimina a colisão independentemente da ordem das colunas, que é melhor do que reordenar de volta: a ordem deixou de ser o que segurava a corretude.
O que isso ensinou
Mover a coluna não criou o defeito — ele já estava armado. A ordem anterior o mantinha latente, e qualquer outra mudança de ordem o teria disparado igual. Uma correção que depende de posição não é uma correção; é um equilíbrio.
9. Validação da 4ª rodada
| Tela do Photon | Antes | Depois |
|---|---|---|
subscriptions-plans |
HTML vazado, colunas sumidas | 6 colunas alinhadas, 2 botões, href=editar/?id=cloud-nano |
subscriptions-status |
idem | 5 colunas alinhadas, 2 botões, href=editar/?id=awaiting-service |
subscriptions-service-types |
idem | 4 colunas alinhadas, 2 botões, href=editar/?id=hosting |
Nenhum texto vazado na coluna de ações (medido: textContent da célula é vazio, só ícones).
PHPUnit 951/951; Vitest 378/378.