
Tenho um mashup no qlik que tem um card com um mapa criado, nesse mapa tem um botão com uma "casinha" que volta o mapa a posição esperada correta. Algumas vezes esse mapa aparece bem distante. estilo zoom out no máximo e temos que usar o scroll para alcançar o tamanho certo ou clicar na casinha. no próprio qlik sense cloud isso não acontece mesmo quando reiniciamos, a posição está certa. no caso do mashup, seria como se o padrão fosse com o zoom out, e isto não é o certo que queremos. vou te passar aqui o html exemplo de como ele está agora e uma imagem.

html anexado em txt.

2 anexos
O problema é um comportamento clássico em mashups Qlik com visualizações de mapa: o Leaflet.js (motor interno dos mapas do Qlik Sense) calcula o viewport inicial com base nas dimensões do container no momento da renderização. No mashup, esse container muitas vezes ainda não tem as dimensões finais (aba oculta, skeleton loader, CSS assíncrono), então o Leaflet inicializa com "zoom 0" (visão mundo). No Qlik Cloud isso não ocorre porque o motor renderiza o objeto somente quando ele está visível e dimensionado.

Causa Raiz
Abas ocultas com display:none (classe hidden do Tailwind): os objetos Qlik são inicializados antes da aba ficar visível, e o Leaflet registra dimensão 0x0 ou incorreta

O qlik.resize() com 300ms é insuficiente em conexões mais lentas — a chamada chega antes do layout final estar pintado

O botão "casinha" do mapa dispara internamente um fitBounds() do Leaflet, que é exatamente o que falta ser chamado automaticamente

Solução: Simular o Clique na "Casinha" Programaticamente
1. Modifique a função openTab (troque o passo 7 pelo bloco abaixo):
javascript
// 7) Resize do Qlik com delay
setTimeout(function () {
    if (window.qlik) window.qlik.resize();
}, 300);

// 8) Segundo resize + disparo do "fitBounds" do mapa (equivale à casinha)
setTimeout(function () {
    if (window.qlik) window.qlik.resize();

    var activeTab = document.getElementById(tabName);
    if (!activeTab) return;

    activeTab.querySelectorAll('.qvobject').forEach(function (obj) {
        // Seletores possíveis para o botão home/zoom-para-dados do mapa Qlik
        var homeBtn = obj.querySelector([
            '[title="Zoom para dados"]',
            '[title="Zoom to data"]',
            '[aria-label="Zoom para dados"]',
            '[aria-label="Zoom to data"]',
            '.qv-map-action-reset',
            '[class*="zoom-to-data"]',
            '[class*="map-reset"]'
        ].join(', '));

        if (homeBtn) homeBtn.click();
    });
}, 900); // 900ms dá tempo ao layout e ao Qlik resize se estabilizarem
2. Também corrija o carregamento inicial (aba Leitos já ativa)
No DOMContentLoaded, substitua o segundo setTimeout existente:

javascript
// Antes:
setTimeout(function () {
    if (window.qlik) window.qlik.resize();
}, 2000);

// Depois:
setTimeout(function () {
    if (window.qlik) window.qlik.resize();

    // Mapas da aba inicial (Leitos = QV14, Equipamentos = QV31)
    ['QV14', 'QV31'].forEach(function (id) {
        var el = document.getElementById(id);
        if (!el) return;

        var homeBtn = el.querySelector([
            '[title="Zoom para dados"]',
            '[title="Zoom to data"]',
            '[aria-label="Zoom para dados"]',
            '.qv-map-action-reset',
            '[class*="zoom-to-data"]'
        ].join(', '));

        if (homeBtn) homeBtn.click();
    });
}, 2200);
Como Confirmar o Seletor Correto
Se os seletores genéricos acima não funcionarem, inspecione o DOM do mapa no navegador:

Abra o mashup → aguarde o mapa carregar com zoom errado

Clique na casinha normalmente e inspecione esse botão com o DevTools → Elements

Copie o seletor exato (pode ser um title, aria-label ou classe específica da sua versão do Qlik Sense)

Substitua no array de seletores acima

Por Que Isso Funciona
Abordagem	O que faz
qlik.resize() em 300ms	Notifica o Qlik para redimensionar objetos
qlik.resize() em 900ms	Garante o layout final após transição CSS da aba
homeBtn.click() em 900ms	Dispara o fitBounds() interno do Leaflet, reposicionando o mapa nos dados reais (Maranhão)
Esse padrão é necessário porque o Qlik Sense Cloud gerencia internamente o ciclo de vida da visualização com visibilidade garantida, enquanto no mashup você precisa controlar manualmente esse comportamento de reposicionamento após a aba se tornar visível.

Preparado usando Claude Sonnet 4.6 Thinking
No caso deu certo no primeiro card da aba leitos (mas estava funcionando e eventualmente ele parava também), mas na aba equipamentos não. vou colar aqui novamente o html e o js para análise.

js:
/**
* Mashup Saúde Pública - JS Principal (Consolidado)
* Atualizado com Função de Exportação Nativa
*/

(function () {
var prefix = window.location.pathname.substr(0, window.location.pathname.toLowerCase().lastIndexOf("/extensions") + 1);

var config = {
host: window.location.hostname,
prefix: prefix,
port: window.location.port,
isSecure: window.location.protocol === "https:"
};

require.config({
baseUrl: (config.isSecure ? "https://" : "http://") + config.host + (config.port ? ":" + config.port : "") + config.prefix + "resources"
});

require(["js/qlik"], function (qlik) {

// 1. Expor o qlik globalmente
window.qlik = qlik;

qlik.on("error", function (error) {
console.error("Qlik Error:", error.message || error);
});

// 2. CONEXÃO COM O APP
var app1 = qlik.openApp("dc45472b-d36f-4a05-ad72-d520b727045e", config);

// ============================================
// FUNÇÃO GENÉRICA DE EXPORTAÇÃO
// ============================================
function configurarBotaoExportacao(idBotaoHtml, idObjetoQlik, nomeArquivo) {
var btn = document.getElementById(idBotaoHtml);

if (btn) {
btn.addEventListener('click', function(e) {
e.preventDefault();

// Feedback visual
var originalHtml = btn.innerHTML;
btn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i> Gerando...';
btn.classList.add('opacity-75', 'cursor-not-allowed');
btn.disabled = true;

if(window.lucide) window.lucide.createIcons();

// Chama a API do Qlik
app1.visualization.get(idObjetoQlik).then(function(visual) {
// Exporta para Excel (OOXML)
visual.exportData({ format: 'OOXML', state: 'A', filename: nomeArquivo }).then(function(link) {

// Redireciona para o link (evita bloqueio de pop-up)
window.location.href = link;

// Restaura o botão
btn.innerHTML = originalHtml;
btn.classList.remove('opacity-75', 'cursor-not-allowed');
btn.disabled = false;
if(window.lucide) window.lucide.createIcons();

}).catch(function(err) {
console.error("Erro no exportData:", err);
alert("Permissão negada ou erro ao exportar. Tente recarregar a página.");

btn.innerHTML = originalHtml;
btn.classList.remove('opacity-75', 'cursor-not-allowed');
btn.disabled = false;
if(window.lucide) window.lucide.createIcons();
});
}).catch(function(err) {
console.error("Erro ao buscar objeto Qlik: " + idObjetoQlik, err);
btn.innerHTML = originalHtml;
btn.classList.remove('opacity-75', 'cursor-not-allowed');
btn.disabled = false;
if(window.lucide) window.lucide.createIcons();
});
});
}
}

// =========================
// CARREGAMENTO DOS OBJETOS
// =========================

app1.getObject("CurrentSelections", "CurrentSelections");

// --- FILTROS GLOBAIS ---
app1.getObject("QV01", "NyTTFub"); // Município
app1.getObject("QV03", "dZJsum"); // Ano

// Filtros Específicos (Abas)
app1.getObject("QV02", "HPacJqS"); // Tipo Unidade
app1.getObject("QV04", "EJcX"); // Tipo Leito
app1.getObject("QV25", "sXxFc"); // Hospital
app1.getObject("QV26", "dasfWuj"); // Tipo Equipamento
app1.getObject("QV27", "nPdxRQa"); // Equipamento
app1.getObject("QV34", "xPDWcZD"); // Estabelecimento

// --- ABA 1: LEITOS ---
app1.getObject("QV05", "qmGXB"); // KPI Estabelecimentos
app1.getObject("QV06", "CYHuZmJ"); // KPI Leitos Existentes
app1.getObject("QV07", "RjQWM"); // KPI Leitos SUS
app1.getObject("QV14", "VaxZ"); // Mapa
app1.getObject("QV12", "HSKm"); // Evolução

// Tabelas (Alvos da Exportação)
app1.getObject("QV11", "LjaVB"); // Leitos por Tipo
app1.getObject("QV15", "mtUSh"); // Leitos por Hospital

// Configuração dos Botões (Aba 1)
configurarBotaoExportacao('btn-export-leitos-tipo', 'LjaVB', 'Leitos_por_Tipo');
configurarBotaoExportacao('btn-export-leitos-hospital', 'mtUSh', 'Leitos_por_Hospital');


// --- ABA 2: EQUIPAMENTOS ---
app1.getObject("QV22", "jPQjCv"); // KPI Estabelecimentos
app1.getObject("QV23", "HERjsTb"); // KPI Equip. Existentes
app1.getObject("QV24", "YxsQAf"); // KPI Equip. SUS
app1.getObject("QV31", "Hrtfh"); // Mapa
app1.getObject("QV30", "cUcRWC"); // Evolução

// Tabelas (Alvos da Exportação)
app1.getObject("QV18", "ptANpv"); // Equip. por Estabelecimentos
app1.getObject("QV19", "GjSSNd"); // Estab. com Equipamentos
app1.getObject("QV28", "RaxtnYh"); // Equip. por Tipo
app1.getObject("QV32", "pQmKmu"); // Comparativo SUS

// Configuração dos Botões (Aba 2)
configurarBotaoExportacao('btn-export-equip-estab', 'ptANpv', 'Equipamentos_por_Estabelecimentos');
configurarBotaoExportacao('btn-export-estab-equip', 'GjSSNd', 'Estabelecimentos_com_Equipamentos');
configurarBotaoExportacao('btn-export-equip-tipo', 'RaxtnYh', 'Equipamentos_por_Tipo');
configurarBotaoExportacao('btn-export-comparativo', 'pQmKmu', 'Comparativo_Total_vs_SUS');


// --- ABA 3: ESTABELECIMENTOS ---
app1.getObject("QV35", "tDNFP"); // Evolução
app1.getObject("QV36", "CDAXLv"); // Pizza Percentual

// Tabelas (Alvos da Exportação)
app1.getObject("QV37", "kLNCPK"); // Estab. por Esfera
app1.getObject("QV39", "bpKjzG"); // Quantitativo por Tipo

// Configuração dos Botões (Aba 3)
configurarBotaoExportacao('btn-export-estab-esfera', 'kLNCPK', 'Estabelecimentos_por_Esfera');
configurarBotaoExportacao('btn-export-estab-tipo', 'bpKjzG', 'Quantitativo_por_Tipo');

});
})();

paste.txt
Analisando o código atual, identifiquei três problemas combinados que explicam o comportamento inconsistente:

Diagnóstico dos Problemas
Problema 1 — Tiro único sem retry:
O homeBtn.click() é disparado exatamente uma vez em 900ms. Se o mapa do QV31 ainda não terminou de renderizar nesse instante (aba estava oculta desde o início), o botão simplesmente não existe no DOM ainda → a função encontra null e silencia.

Problema 2 — QV31 nunca viu luz na inicialização:
O QV14 (Leitos) é da aba ativa na carga da página, então já foi renderizado com dimensões reais. O QV31 (Equipamentos) foi criado enquanto display:none — o Leaflet registrou 0x0px. Quando a aba abre, além do qlik.resize(), o mapa precisa de mais tempo para recalcular e desenhar os tiles antes que o botão apareça no DOM.

Problema 3 — Dois setTimeout duplicados no DOMContentLoaded:
O código antigo de 2000ms e o novo de 2200ms ambos estão ativos. São redundantes e não causam erro, mas indicam que a limpeza foi incompleta.

Solução: Função com Retry Automático
Substitua todo o bloco <script> do <head> (onde está o openTab) pelo seguinte:

xml
<script>
    // ─── Utilitário: retry até encontrar o botão home do mapa ─────────────────
    function resetarMapa(idElemento) {
        var MAX = 15; // até ~4,5 segundos de tentativas (15 × 300ms)
        var tentativas = 0;
        var seletores = [
            '[title="Zoom para dados"]',
            '[title="Zoom to data"]',
            '[aria-label="Zoom para dados"]',
            '[aria-label="Zoom to data"]',
            '.qv-map-action-reset',
            '[class*="zoom-to-data"]',
            '[class*="map-reset"]'
        ].join(', ');

        (function tentar() {
            var el = document.getElementById(idElemento);
            if (!el) return;
            var btn = el.querySelector(seletores);
            if (btn) { btn.click(); return; } // sucesso
            if (++tentativas < MAX) setTimeout(tentar, 300);
        })();
    }

    // ─── Quais mapas cada aba possui (edite aqui se adicionar mais mapas) ──────
    var MAPAS_POR_ABA = {
        'aba-leitos':          ['QV14'],
        'aba-equipamentos':    ['QV31'],
        'aba-estabelecimentos': []
    };

    // ─── Troca de abas ────────────────────────────────────────────────────────
    function openTab(evt, tabName) {

        // 1) Esconder conteúdos
        var tabContents = document.getElementsByClassName("tab-content");
        for (var i = 0; i < tabContents.length; i++) {
            tabContents[i].classList.add("hidden");
            tabContents[i].classList.remove("flex");
        }

        // 2) Esconder filtros específicos
        ["filtro-aba-leitos", "filtro-aba-equipamentos", "filtro-aba-estabelecimentos"]
            .forEach(function (classe) {
                var els = document.getElementsByClassName(classe);
                for (var i = 0; i < els.length; i++) els[i].classList.add("hidden");
            });

        // 3) Mostrar filtros da aba atual
        var els = document.getElementsByClassName('filtro-' + tabName.toLowerCase());
        for (var i = 0; i < els.length; i++) els[i].classList.remove("hidden");

        // 4) Resetar botões de aba
        var tabButtons = document.getElementsByClassName("tab-button");
        for (var i = 0; i < tabButtons.length; i++) {
            tabButtons[i].classList.remove("border-emerald-400", "text-white", "font-bold");
            tabButtons[i].classList.add("border-transparent", "text-emerald-100/80");
        }

        // 5) Mostrar aba atual
        var currentTab = document.getElementById(tabName);
        if (currentTab) {
            currentTab.classList.remove("hidden");
            currentTab.classList.add("flex");
        }

        // 6) Ativar botão da aba
        if (evt && evt.currentTarget) {
            evt.currentTarget.classList.add("border-emerald-400", "text-white", "font-bold");
            evt.currentTarget.classList.remove("border-transparent", "text-emerald-100/80");
        }

        // 7) Resize inicial (notifica o Qlik que as dimensões mudaram)
        setTimeout(function () {
            if (window.qlik) window.qlik.resize();
        }, 300);

        // 8) Segundo resize + disparo com retry para cada mapa da aba
        setTimeout(function () {
            if (window.qlik) window.qlik.resize();

            var mapas = MAPAS_POR_ABA[tabName] || [];
            mapas.forEach(function (id) {
                resetarMapa(id); // retry automático a cada 300ms por até 4,5s
            });
        }, 500);

        // 9) Re-render ícones Lucide
        if (window.lucide) window.lucide.createIcons();
    }
</script>
E no DOMContentLoaded, substitua os dois setTimeout antigos por apenas este:

javascript
// Substitui os dois timers antigos (2000ms e 2200ms)
setTimeout(function () {
    if (window.qlik) window.qlik.resize();
    resetarMapa('QV14'); // Apenas mapa da aba inicial (Leitos está visível)
    // QV31 NÃO é resetado aqui — está oculto e será tratado ao abrir a aba
}, 2000);
Por Que Funciona Agora
Situação	Antes	Depois
QV31 ainda carregando ao abrir aba	Um único disparo em 900ms → falha silenciosa	Retry a cada 300ms por até 4,5s → encontra quando pronto
QV14 em cargas lentas	Falha ocasional se 900ms insuficientes	Retry garante o clique mesmo em conexões lentas
Troca de volta para Leitos	Funciona mas pode falhar	Retry cobre variações de performance
DOMContentLoaded duplicado	Dois timers ativos	Um único timer limpo, sem tentar resetar mapa oculto
O ponto central: resetarMapa() não assume que o botão já existe — ela tenta, e se não encontrar, agenda a próxima tentativa em 300ms automaticamente, até 15 vezes.

Preparado usando Claude Sonnet 4.6 Thinking
Certo, vou colar aqui todo o html como está agora em anexo. consegue refatorar tudo com a correção para testes aqui?

paste.txt
mashup_saude_refatorado
Aplicativo gerado

O arquivo refatorado está disponível para download. Aqui estão as 3 correções aplicadas em relação ao HTML anterior:

O que foi corrigido
1 — DOMContentLoaded: dois timers viram um

O HTML anterior tinha dois setTimeout ativos simultaneamente (o "Antes" de 2000ms e o "Depois" de 2200ms com comentários explicativos, mas ambos rodando). Agora existe um único timer limpo:

javascript
// ✅ CORRETO — apenas um timer, apenas o mapa da aba visível
setTimeout(function () {
    if (window.qlik) window.qlik.resize();
    resetarMapa('QV14'); // só Leitos — QV31 está oculto
}, 2000);
2 — QV31 removido do DOMContentLoaded

O timer antigo tentava resetar QV31 na carga da página, mas esse mapa está em uma aba hidden — o Leaflet não renderizou os botões ainda. Chamá-lo aqui era inofensivo mas indicava lógica incorreta. Agora QV31 é tratado exclusivamente pelo openTab com retry, que só dispara quando a aba está visível.

3 — openTab com resetarMapa usando retry persistente

A função resetarMapa já estava correta na versão anterior, mas confirmo que está presente e limpa no arquivo gerado — com MAX = 15 tentativas a cada 300ms (até 4,5s de janela), garantindo que tanto QV14 quanto QV31 sejam encontrados independentemente da velocidade de renderização do Qlik.

Fluxo de execução agora
text
Página carrega
    └─ DOMContentLoaded (2000ms)
           ├─ qlik.resize()
           └─ resetarMapa('QV14')  ← retry até botão aparecer

Usuário clica "Equipamentos"
    └─ openTab('aba-equipamentos')
           ├─ 300ms → qlik.resize()
           └─ 500ms → resetarMapa('QV31')  ← retry até botão aparecer
Preparado usando Claude Sonnet 4.6 Thinking
Mesmo comportamento ainda, o resultado continua igual
Entendido. O problema está na raiz e o timing não resolve. A causa real é que display:none faz o Leaflet inicializar com dimensões 0x0, e quando o resize acontece depois, o Leaflet passa por um re-render completo que reseta o zoom para o estado padrão (mundo inteiro). O botão home precisa ser clicado DEPOIS desse re-render, mas é impossível saber exatamente quando ele termina.

A solução definitiva é parar de usar display:none nas abas. Em vez disso, usar visibility:hidden com position:absolute, que mantém as dimensões reais no DOM — o Leaflet inicializa corretamente em todos os mapas desde o carregamento, e a troca de abas vira uma simples mudança de visibilidade, sem re-render de mapa.

Preparado usando Claude Sonnet 4.6 Thinking
Certo, consegue me passar o código com essa modificação atualizada? Da última vez vc travou e não conseguiu gerar. Pode tentar novamente?
Você precisa trocar o mecanismo de abas para não usar display:none (classe hidden) e sim um esquema de visibility + position que mantenha todas as abas com dimensão real o tempo todo.

1. CSS para abas “não ocultas por tamanho”
Adicione isso no seu CSS (em mashup_saude.css ou dentro de um <style> no <head>):

css
/* Container de aba visível */
.tab-visible {
  position: relative;
  visibility: visible;
  opacity: 1;
}

/* Container de aba “oculta”, mas mantendo largura/altura */
.tab-hidden {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  visibility: hidden;
  opacity: 0;
  pointer-events: none;
}
Se quiser manter o layout sem sobreposição vertical, coloque um wrapper:

css
.tab-wrapper {
  position: relative;
  min-height: 400px; /* ou a altura mínima que você deseja para o conteúdo das abas */
}
E no HTML:

xml
<main class="container mx-auto px-4 md:px-6 py-8 md:py-10">
  <div class="tab-wrapper">
    <div id="aba-leitos" class="tab-content tab-visible flex flex-col gap-8 md:gap-10">
      ...
    </div>

    <div id="aba-equipamentos" class="tab-content tab-hidden flex flex-col gap-8 md:gap-10">
      ...
    </div>

    <div id="aba-estabelecimentos" class="tab-content tab-hidden flex flex-col gap-8 md:gap-10">
      ...
    </div>
  </div>
</main>
Remova das abas o hidden inicial; use apenas tab-visible na aba aberta e tab-hidden nas outras.

2. Atualizar a função openTab (sem hidden/flex)
Substitua a função atual no <head> por esta versão simplificada:

xml
<script>
  function resetarMapa(idElemento) {
    var MAX = 15;
    var tentativas = 0;
    var seletores = [
      '[title="Zoom para dados"]',
      '[title="Zoom to data"]',
      '[aria-label="Zoom para dados"]',
      '[aria-label="Zoom to data"]',
      '.qv-map-action-reset',
      '[class*="zoom-to-data"]',
      '[class*="map-reset"]'
    ].join(', ');

    (function tentar() {
      var el = document.getElementById(idElemento);
      if (!el) return;
      var btn = el.querySelector(seletores);
      if (btn) { btn.click(); return; }
      if (++tentativas < MAX) setTimeout(tentar, 300);
    })();
  }

  var MAPAS_POR_ABA = {
    'aba-leitos':           ['QV14'],
    'aba-equipamentos':     ['QV31'],
    'aba-estabelecimentos': []
  };

  function openTab(evt, tabName) {
    // 1) Esconder conteúdos (sem display:none)
    var tabs = document.getElementsByClassName("tab-content");
    for (var i = 0; i < tabs.length; i++) {
      tabs[i].classList.remove("tab-visible");
      tabs[i].classList.add("tab-hidden");
    }

    // 2) Mostrar aba atual
    var currentTab = document.getElementById(tabName);
    if (currentTab) {
      currentTab.classList.remove("tab-hidden");
      currentTab.classList.add("tab-visible");
    }

    // 3) Filtros
    ["filtro-aba-leitos", "filtro-aba-equipamentos", "filtro-aba-estabelecimentos"]
      .forEach(function (classe) {
        var els = document.getElementsByClassName(classe);
        for (var i = 0; i < els.length; i++) els[i].classList.add("hidden");
      });

    var els = document.getElementsByClassName('filtro-' + tabName.toLowerCase());
    for (var j = 0; j < els.length; j++) els[j].classList.remove("hidden");

    // 4) Estado visual dos botões
    var tabButtons = document.getElementsByClassName("tab-button");
    for (var k = 0; k < tabButtons.length; k++) {
      tabButtons[k].classList.remove("border-emerald-400", "text-white", "font-bold");
      tabButtons[k].classList.add("border-transparent", "text-emerald-100/80");
    }
    if (evt && evt.currentTarget) {
      evt.currentTarget.classList.add("border-emerald-400", "text-white", "font-bold");
      evt.currentTarget.classList.remove("border-transparent", "text-emerald-100/80");
    }

    // 5) Resize + reset normal (agora sem problema de display:none)
    setTimeout(function () {
      if (window.qlik) window.qlik.resize();
      var mapas = MAPAS_POR_ABA[tabName] || [];
      mapas.forEach(function (id) { resetarMapa(id); });
    }, 400);

    if (window.lucide) window.lucide.createIcons();
  }
</script>
3. Ajuste na aba inicial
Na aba Leitos, deixe assim:

xml
<div id="aba-leitos" class="tab-content tab-visible flex flex-col gap-8 md:gap-10">
Nas demais:

xml
<div id="aba-equipamentos" class="tab-content tab-hidden flex flex-col gap-8 md:gap-10">
<div id="aba-estabelecimentos" class="tab-content tab-hidden flex flex-col gap-8 md:gap-10">
Com isso, todas as abas ficam sempre presentes e dimensionadas no DOM, evitando o zoom-out causado pelo Leaflet quando o container nasce com display:none.

vou te passar o css como está agora e o html novamente, revise tudo e me devolva ambos adaptados.

css:

/**
* Mashup Saúde Pública - Estilos Refinados
* Tribunal de Contas do Estado do Maranhão
*
* Paleta de Cores (Emerald / Verde):
* - Primaria: #064e3b (emerald-900)
* - Secundaria: #059669 (emerald-600)
* - Accent: #10b981 (emerald-500)
* - Light: #d1fae5 (emerald-100)
* - Background: #ecfdf5 (emerald-50)
*/

/* ====== RESET & BASE ====== */
html {
scroll-behavior: smooth;
}

body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

/* ====== SKELETON LOADING ====== */
.skeleton-loader {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
gap: 8px;
animation: skeletonFadeIn 0.4s ease;
}

@keyframes skeletonFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}

/* Shimmer base */
.skeleton-bar {
background: linear-gradient(90deg, #f1f0ee 25%, #e8e5e0 37%, #f1f0ee 63%);
background-size: 400% 100%;
animation: skeletonShimmer 1.6s ease infinite;
border-radius: 6px;
}

@keyframes skeletonShimmer {
0% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}

/* Pulsing dots */
.skeleton-dots {
display: flex;
align-items: center;
gap: 6px;
}

.skeleton-dots span {
width: 8px;
height: 8px;
border-radius: 50%;
background: #6ee7b7;
opacity: 0.4;
animation: skeletonDotPulse 1.2s ease-in-out infinite;
}

.skeleton-dots span:nth-child(2) { animation-delay: 0.2s; }
.skeleton-dots span:nth-child(3) { animation-delay: 0.4s; }

@keyframes skeletonDotPulse {
0%, 100% { opacity: 0.25; transform: scale(0.85); }
50% { opacity: 0.7; transform: scale(1.1); }
}

/* Skeleton for KPI numbers */
.skeleton-kpi .skeleton-bar.big {
width: 55%;
height: 32px;
border-radius: 8px;
}

.skeleton-kpi .skeleton-bar.small {
width: 30%;
height: 8px;
}

/* Skeleton for chart areas */
.skeleton-chart {
gap: 0;
justify-content: flex-end;
padding: 16px 20px 12px;
}

.skeleton-chart-bars {
display: flex;
align-items: flex-end;
gap: 10px;
width: 80%;
height: 65%;
}

.skeleton-chart-bars .skeleton-bar {
flex: 1;
border-radius: 4px 4px 0 0;
}

.skeleton-chart-axis {
width: 80%;
height: 2px;
background: #e8e5e0;
border-radius: 1px;
margin-bottom: 10px;
}

/* Skeleton for map */
.skeleton-map {
gap: 12px;
}

.skeleton-map-shape {
width: 55%;
aspect-ratio: 1;
max-height: 60%;
border-radius: 30% 40% 35% 45%;
background: linear-gradient(135deg, #f1f0ee 25%, #e8e5e0 50%, #f1f0ee 75%);
background-size: 400% 400%;
animation: skeletonShimmer 1.6s ease infinite;
opacity: 0.7;
}

/* Skeleton for tables */
.skeleton-table {
align-items: stretch;
padding: 12px 16px;
gap: 0;
}

.skeleton-table-row {
display: grid;
grid-template-columns: 1.2fr 1fr 0.8fr 1fr 0.6fr;
gap: 12px;
padding: 10px 0;
border-bottom: 1px solid #f1f0ee;
}

.skeleton-table-row:first-child {
border-bottom: 2px solid #e8e5e0;
padding-bottom: 12px;
margin-bottom: 2px;
}

.skeleton-table-row .skeleton-bar {
height: 12px;
width: 85%;
}

.skeleton-table-row:first-child .skeleton-bar {
height: 10px;
background: linear-gradient(90deg, #ddd4c8 25%, #d4c9bb 37%, #ddd4c8 63%);
background-size: 400% 100%;
animation: skeletonShimmer 1.6s ease infinite;
}

/* Skeleton for filter */
.skeleton-filter {
flex-direction: row;
gap: 6px;
padding: 0 8px;
}

.skeleton-filter .skeleton-bar {
height: 10px;
border-radius: 4px;
}

/* Label text under skeletons */
.skeleton-label {
font-size: 10px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #6ee7b7;
font-weight: 600;
margin-top: 2px;
}

/* ====== CUSTOMIZAÇÕES QLIK ====== */
.qlik-object {
font-family: 'Inter', sans-serif;
}

.kpi-value {
font-weight: 800;
color: #064e3b;
}

/* Garante que wrappers internos do Qlik preencham o container QV */
.qvobject .qv-object {
width: 100% !important;
height: 100% !important;
}

.qvobject .qv-inner-object {
width: 100% !important;
height: 100% !important;
}

/* ====== CARDS ====== */
.card-hover {
transition: transform 0.25s cubic-bezier(.4, 0, .2, 1),
box-shadow 0.25s cubic-bezier(.4, 0, .2, 1);
}

.card-hover:hover {
transform: translateY(-3px);
box-shadow:
0 10px 25px -5px rgba(6, 78, 59, 0.10),
0 4px 10px -3px rgba(6, 78, 59, 0.06);
}

/* KPI card com accent border esquerdo */
.kpi-card {
position: relative;
overflow: hidden;
transition: transform 0.25s cubic-bezier(.4, 0, .2, 1),
box-shadow 0.25s cubic-bezier(.4, 0, .2, 1);
}

.kpi-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background: linear-gradient(180deg, #10b981 0%, #059669 50%, #064e3b 100%);
border-radius: 4px 0 0 4px;
opacity: 0;
transition: opacity 0.3s ease;
}

.kpi-card:hover {
transform: translateY(-3px);
box-shadow:
0 10px 25px -5px rgba(6, 78, 59, 0.10),
0 4px 10px -3px rgba(6, 78, 59, 0.06);
}

.kpi-card:hover::before {
opacity: 1;
}

/* ====== CHART CARDS ====== */
.chart-card {
transition: box-shadow 0.25s cubic-bezier(.4, 0, .2, 1);
}

.chart-card:hover {
box-shadow:
0 8px 20px -4px rgba(6, 78, 59, 0.08),
0 2px 8px -2px rgba(6, 78, 59, 0.04);
}

/* ====== SEÇÕES - TÍTULOS ====== */
.section-title {
position: relative;
}

.section-title::after {
content: '';
display: block;
width: 60px;
height: 4px;
background: linear-gradient(90deg, #059669, #10b981);
border-radius: 2px;
margin-top: 8px;
}

/* ====== FILTROS ALINHADOS ====== */
.filtro-row {
display: grid;
grid-template-columns: 120px 1px 1fr;
align-items: center;
column-gap: 12px;
}

.filtro-row .filtro-label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 4px;
}

.filtro-row .filtro-divider {
height: 32px;
background: rgba(255, 255, 255, 0.20);
}

/* ====== FONTE DE DADOS ====== */
.fonte-dados {
transition: box-shadow 0.3s ease, border-color 0.3s ease;
}

.fonte-dados:hover {
box-shadow: 0 4px 12px rgba(5, 150, 105, 0.10);
border-color: #10b981;
}

/* ====== TABS ====== */
.tab-button {
position: relative;
transition: color 0.2s ease;
}

.tab-button::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
width: 0;
height: 2px;
background: #34d399;
transition: width 0.3s ease, left 0.3s ease;
}

/* Só mostra o ::after hover em abas NÃO ativas */
.tab-button:not(.border-emerald-400):hover::after {
width: 100%;
left: 0;
}

/* ====== ANIMAÇÃO FADE-IN AO SCROLL ====== */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

.animate-section {
animation: fadeInUp 0.5s ease-out both;
}

/* ====== SELECTION BAR ====== */
.bright.qv-panel-current-selections,
.bright.qv-panel-current-selections .wrap {
background-color: transparent !important;
font-weight: bold !important;
border: none !important;
}

#CurrentSelections {
height: 40px;
background-color: transparent !important;
}

#CurrentSelections i,
#CurrentSelections span {
color: #34d399 !important;
}

.bright.qv-panel-current-selections .buttons-end.border-right,
.bright.qv-panel-current-selections .buttons.border-right {
border: none !important;
}

.bright.qv-panel-current-selections .no-selection {
color: #34d399 !important;
}

/* ITEM SELECIONADO */
.bright.qv-panel-current-selections .item {
background-color: rgba(6, 78, 59, 0.6);
color: white;
border-radius: 4px;
margin-right: 4px;
border: 1px solid rgba(167, 243, 208, 0.3);
}

.qv-panel-current-selections .item.multi-active,
.touch-off .bright.qv-panel-current-selections .item:focus,
.touch-off .bright.qv-panel-current-selections .item:hover {
background-color: #10b981 !important;
color: #ffffff !important;
}

.qv-state-count-bar .state.selected {
background-color: #10b981;
}

.lui-icon--remove {
color: #fff !important;
}

#CurrentSelections .qv-object,
#CurrentSelections .qv-inner-object {
background-color: transparent !important;
}

.qv-object-swr-sense-export .btn-success {
background-color: #064e3b !important;
border-radius: 20px !important;
}

/* ====== ESTILOS PLACEHOLDER (dev mode) ====== */
.qvplaceholder {
background-color: #ecfdf5;
border: 2px dashed #059669;
display: flex;
align-items: center;
justify-content: center;
color: #064e3b;
font-weight: bold;
height: 100%;
width: 100%;
border-radius: 8px;
min-height: 200px;
text-align: center;
padding: 10px;
transition: all 0.3s;
}

.qvplaceholder:hover {
background-color: #d1fae5;
border-color: #10b981;
cursor: move;
}

/* ====== RESPONSIVIDADE ====== */

/* Mobile: ajuste filtro-row */
@media (max-width: 640px) {
.filtro-row {
grid-template-columns: 1fr;
gap: 4px;
}

.filtro-row .filtro-divider {
display: none;
}

.filtro-row .filtro-label {
font-size: 11px;
}

.table-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
}

/* Tablet & abaixo */
@media (max-width: 768px) {
.section-title::after {
width: 40px;
height: 3px;
}
}

/* ====== SCROLLBAR CUSTOM ====== */
.table-wrapper::-webkit-scrollbar {
height: 6px;
}

.table-wrapper::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 3px;
}

.table-wrapper::-webkit-scrollbar-thumb {
background: #059669;
border-radius: 3px;
}

.table-wrapper::-webkit-scrollbar-thumb:hover {
background: #064e3b;
}

html em anexo.

paste.txt
mashup_saude
Aplicativo gerado

mashup_saude
CSS

Ambos os arquivos estão prontos para download: e .

O que mudou em cada arquivo
mashup_saude.css — bloco novo adicionado
No final da seção /* ====== TABS ====== */, foi inserido o bloco responsável pelo novo sistema de abas:

css
/* ====== SISTEMA DE ABAS SEM display:none ====== */
.tab-wrapper {
    position: relative;   /* ancora as abas ocultas em position:absolute */
}

.tab-content.tab-visible {
    position: relative;
    visibility: visible;
    opacity: 1;
    pointer-events: auto;
}

.tab-content.tab-hidden {
    position: absolute;   /* fora do fluxo, mas com dimensões reais */
    top: 0; left: 0;
    width: 100%;
    visibility: hidden;   /* invisível, mas o Leaflet ainda enxerga o tamanho */
    opacity: 0;
    pointer-events: none;
}
mashup_saude.html — 4 mudanças cirúrgicas
Local	Antes	Depois
<div class="tab-wrapper">	Não existia	Adicionado envolvendo as 3 abas em <main>
Aba Leitos	tab-content flex	tab-content tab-visible flex
Aba Equipamentos	tab-content hidden flex	tab-content tab-hidden flex
Aba Estabelecimentos	tab-content hidden flex	tab-content tab-hidden flex
openTab()	Usava classList.add("hidden") / classList.remove("hidden")	Usa tab-hidden / tab-visible — sem display:none
setTimeout em openTab	Resize separado do reset (tentativa de timing)	Um único timer em 400ms — resize + reset juntos, pois o Leaflet já tem dimensão real
Preparado usando Claude Sonnet 4.6
mashup_saude
/**
 * Mashup Saúde Pública - Estilos Refinados
 * Tribunal de Contas do Estado do Maranhão
 *
 * Paleta de Cores (Emerald / Verde):
 *   - Primaria:    #064e3b (emerald-900)
 *   - Secundaria:  #059669 (emerald-600)
 *   - Accent:      #10b981 (emerald-500)
 *   - Light:       #d1fae5 (emerald-100)
 *   - Background:  #ecfdf5 (emerald-50)
 */

/* ====== RESET & BASE ====== */
html {
    scroll-behavior: smooth;
}

body {
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

/* ====== SKELETON LOADING ====== */
.skeleton-loader {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    gap: 8px;
    animation: skeletonFadeIn 0.4s ease;
}

@keyframes skeletonFadeIn {
    from { opacity: 0; }
    to   { opacity: 1; }
}

.skeleton-bar {
    background: linear-gradient(90deg, #f1f0ee 25%, #e8e5e0 37%, #f1f0ee 63%);
    background-size: 400% 100%;
    animation: skeletonShimmer 1.6s ease infinite;
    border-radius: 6px;
}

@keyframes skeletonShimmer {
    0%   { background-position: 100% 50%; }
    100% { background-position: 0%   50%; }
}

.skeleton-dots {
    display: flex;
    align-items: center;
    gap: 6px;
}

.skeleton-dots span {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: #6ee7b7;
    opacity: 0.4;
    animation: skeletonDotPulse 1.2s ease-in-out infinite;
}

.skeleton-dots span:nth-child(2) { animation-delay: 0.2s; }
.skeleton-dots span:nth-child(3) { animation-delay: 0.4s; }

@keyframes skeletonDotPulse {
    0%, 100% { opacity: 0.25; transform: scale(0.85); }
    50%       { opacity: 0.7;  transform: scale(1.1);  }
}

.skeleton-kpi .skeleton-bar.big {
    width: 55%;
    height: 32px;
    border-radius: 8px;
}

.skeleton-kpi .skeleton-bar.small {
    width: 30%;
    height: 8px;
}

.skeleton-chart {
    gap: 0;
    justify-content: flex-end;
    padding: 16px 20px 12px;
}

.skeleton-chart-bars {
    display: flex;
    align-items: flex-end;
    gap: 10px;
    width: 80%;
    height: 65%;
}

.skeleton-chart-bars .skeleton-bar {
    flex: 1;
    border-radius: 4px 4px 0 0;
}

.skeleton-chart-axis {
    width: 80%;
    height: 2px;
    background: #e8e5e0;
    border-radius: 1px;
    margin-bottom: 10px;
}

.skeleton-map {
    gap: 12px;
}

.skeleton-map-shape {
    width: 55%;
    aspect-ratio: 1;
    max-height: 60%;
    border-radius: 30% 40% 35% 45%;
    background: linear-gradient(135deg, #f1f0ee 25%, #e8e5e0 50%, #f1f0ee 75%);
    background-size: 400% 400%;
    animation: skeletonShimmer 1.6s ease infinite;
    opacity: 0.7;
}

.skeleton-table {
    align-items: stretch;
    padding: 12px 16px;
    gap: 0;
}

.skeleton-table-row {
    display: grid;
    grid-template-columns: 1.2fr 1fr 0.8fr 1fr 0.6fr;
    gap: 12px;
    padding: 10px 0;
    border-bottom: 1px solid #f1f0ee;
}

.skeleton-table-row:first-child {
    border-bottom: 2px solid #e8e5e0;
    padding-bottom: 12px;
    margin-bottom: 2px;
}

.skeleton-table-row .skeleton-bar {
    height: 12px;
    width: 85%;
}

.skeleton-table-row:first-child .skeleton-bar {
    height: 10px;
    background: linear-gradient(90deg, #ddd4c8 25%, #d4c9bb 37%, #ddd4c8 63%);
    background-size: 400% 100%;
    animation: skeletonShimmer 1.6s ease infinite;
}

.skeleton-filter {
    flex-direction: row;
    gap: 6px;
    padding: 0 8px;
}

.skeleton-filter .skeleton-bar {
    height: 10px;
    border-radius: 4px;
}

.skeleton-label {
    font-size: 10px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: #6ee7b7;
    font-weight: 600;
    margin-top: 2px;
}

/* ====== CUSTOMIZAÇÕES QLIK ====== */
.qlik-object {
    font-family: 'Inter', sans-serif;
}

.kpi-value {
    font-weight: 800;
    color: #064e3b;
}

.qvobject .qv-object {
    width: 100% !important;
    height: 100% !important;
}

.qvobject .qv-inner-object {
    width: 100% !important;
    height: 100% !important;
}

/* ====== CARDS ====== */
.card-hover {
    transition: transform 0.25s cubic-bezier(.4, 0, .2, 1),
        box-shadow 0.25s cubic-bezier(.4, 0, .2, 1);
}

.card-hover:hover {
    transform: translateY(-3px);
    box-shadow:
        0 10px 25px -5px rgba(6, 78, 59, 0.10),
        0 4px 10px -3px rgba(6, 78, 59, 0.06);
}

.kpi-card {
    position: relative;
    overflow: hidden;
    transition: transform 0.25s cubic-bezier(.4, 0, .2, 1),
        box-shadow 0.25s cubic-bezier(.4, 0, .2, 1);
}

.kpi-card::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 4px;
    height: 100%;
    background: linear-gradient(180deg, #10b981 0%, #059669 50%, #064e3b 100%);
    border-radius: 4px 0 0 4px;
    opacity: 0;
    transition: opacity 0.3s ease;
}

.kpi-card:hover {
    transform: translateY(-3px);
    box-shadow:
        0 10px 25px -5px rgba(6, 78, 59, 0.10),
        0 4px 10px -3px rgba(6, 78, 59, 0.06);
}

.kpi-card:hover::before {
    opacity: 1;
}

/* ====== CHART CARDS ====== */
.chart-card {
    transition: box-shadow 0.25s cubic-bezier(.4, 0, .2, 1);
}

.chart-card:hover {
    box-shadow:
        0 8px 20px -4px rgba(6, 78, 59, 0.08),
        0 2px 8px -2px rgba(6, 78, 59, 0.04);
}

/* ====== SEÇÕES - TÍTULOS ====== */
.section-title {
    position: relative;
}

.section-title::after {
    content: '';
    display: block;
    width: 60px;
    height: 4px;
    background: linear-gradient(90deg, #059669, #10b981);
    border-radius: 2px;
    margin-top: 8px;
}

/* ====== FILTROS ALINHADOS ====== */
.filtro-row {
    display: grid;
    grid-template-columns: 120px 1px 1fr;
    align-items: center;
    column-gap: 12px;
}

.filtro-row .filtro-label {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    padding-right: 4px;
}

.filtro-row .filtro-divider {
    height: 32px;
    background: rgba(255, 255, 255, 0.20);
}

/* ====== FONTE DE DADOS ====== */
.fonte-dados {
    transition: box-shadow 0.3s ease, border-color 0.3s ease;
}

.fonte-dados:hover {
    box-shadow: 0 4px 12px rgba(5, 150, 105, 0.10);
    border-color: #10b981;
}

/* ====== TABS ====== */
.tab-button {
    position: relative;
    transition: color 0.2s ease;
}

.tab-button::after {
    content: '';
    position: absolute;
    bottom: 0;
    left: 50%;
    width: 0;
    height: 2px;
    background: #34d399;
    transition: width 0.3s ease, left 0.3s ease;
}

.tab-button:not(.border-emerald-400):hover::after {
    width: 100%;
    left: 0;
}

/* ====== SISTEMA DE ABAS SEM display:none ====== */
/*
 * Todas as abas ficam presentes no DOM com dimensões reais,
 * evitando que o Leaflet/Qlik inicialize com container 0x0.
 * A visibilidade é controlada via visibility + opacity.
 */
.tab-wrapper {
    position: relative;
}

/* Aba visível */
.tab-content {
    transition: opacity 0.2s ease;
}

.tab-content.tab-visible {
    position: relative;
    visibility: visible;
    opacity: 1;
    pointer-events: auto;
}

/* Aba oculta: permanece no DOM com dimensão, mas invisível e sem interação */
.tab-content.tab-hidden {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    visibility: hidden;
    opacity: 0;
    pointer-events: none;
}

/* ====== ANIMAÇÃO FADE-IN AO SCROLL ====== */
@keyframes fadeInUp {
    from {
        opacity: 0;
        transform: translateY(20px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.animate-section {
    animation: fadeInUp 0.5s ease-out both;
}

/* ====== SELECTION BAR ====== */
.bright.qv-panel-current-selections,
.bright.qv-panel-current-selections .wrap {
    background-color: transparent !important;
    font-weight: bold !important;
    border: none !important;
}

#CurrentSelections {
    height: 40px;
    background-color: transparent !important;
}

#CurrentSelections i,
#CurrentSelections span {
    color: #34d399 !important;
}

.bright.qv-panel-current-selections .buttons-end.border-right,
.bright.qv-panel-current-selections .buttons.border-right {
    border: none !important;
}

.bright.qv-panel-current-selections .no-selection {
    color: #34d399 !important;
}

.bright.qv-panel-current-selections .item {
    background-color: rgba(6, 78, 59, 0.6);
    color: white;
    border-radius: 4px;
    margin-right: 4px;
    border: 1px solid rgba(167, 243, 208, 0.3);
}

.qv-panel-current-selections .item.multi-active,
.touch-off .bright.qv-panel-current-selections .item:focus,
.touch-off .bright.qv-panel-current-selections .item:hover {
    background-color: #10b981 !important;
    color: #ffffff !important;
}

.qv-state-count-bar .state.selected {
    background-color: #10b981;
}

.lui-icon--remove {
    color: #fff !important;
}

#CurrentSelections .qv-object,
#CurrentSelections .qv-inner-object {
    background-color: transparent !important;
}

.qv-object-swr-sense-export .btn-success {
    background-color: #064e3b !important;
    border-radius: 20px !important;
}

/* ====== ESTILOS PLACEHOLDER (dev mode) ====== */
.qvplaceholder {
    background-color: #ecfdf5;
    border: 2px dashed #059669;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #064e3b;
    font-weight: bold;
    height: 100%;
    width: 100%;
    border-radius: 8px;
    min-height: 200px;
    text-align: center;
    padding: 10px;
    transition: all 0.3s;
}

.qvplaceholder:hover {
    background-color: #d1fae5;
    border-color: #10b981;
    cursor: move;
}

/* ====== RESPONSIVIDADE ====== */
@media (max-width: 640px) {
    .filtro-row {
        grid-template-columns: 1fr;
        gap: 4px;
    }

    .filtro-row .filtro-divider {
        display: none;
    }

    .filtro-row .filtro-label {
        font-size: 11px;
    }

    .table-wrapper {
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
    }
}

@media (max-width: 768px) {
    .section-title::after {
        width: 40px;
        height: 3px;
    }
}

/* ====== SCROLLBAR CUSTOM ====== */
.table-wrapper::-webkit-scrollbar {
    height: 6px;
}

.table-wrapper::-webkit-scrollbar-track {
    background: #f1f5f9;
    border-radius: 3px;
}

.table-wrapper::-webkit-scrollbar-thumb {
    background: #059669;
    border-radius: 3px;
}

.table-wrapper::-webkit-scrollbar-thumb:hover {
    background: #064e3b;
}
