Conn2Flow
Navegação das docs

SDD

req-147: Listagem com scroll horizontal em vez de colapso automático em tela larga

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.

  • 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:

  1. Em CSS não existe rolar num eixo só. Quando um eixo é auto, o navegador converte o visible do outro em auto também. O resultado foi um scroll vertical dentro da listagem que engolia o scroll da página — foi o que o operador reportou.

  2. O wrapper guarda a busca, o seletor de quantidade e a paginação. Prendê-los numa caixa com overflow cortaria 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

  1. Em viewport ≥ 1200px nenhuma coluna é colapsada e a tabela rola lateralmente.

  2. A listagem não cria scroll vertical próprio; o scroll da página continua funcionando.

  3. Abaixo do limiar o comportamento responsivo anterior é preservado.

  4. Vale para as duas interfaces (interface e interface-v2), pela BIBLIOTECA e não por módulo.

  5. (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.

  6. (2ª rodada) As ações ficam na PRIMEIRA coluna, ancoradas na borda esquerda, e continuam alcançáveis com a tabela rolada.

  7. (2ª rodada) O número de cabeçalhos tem de bater com o de células em toda listagem: interface.php monta 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.

O Futuro é Híbrido: Humano + IA.

O Conn2Flow é a ferramenta. Se você precisa do Arquiteto para desenhar a estratégia da sua empresa usando essa tecnologia...

Contrate Consultoria Especializada