← Biblioteca componentes/dados/painel-patrimonio/ficha.md CódigoTela cheia
dados/painel-patrimonio

Painel de patrimônio

🟢 estavel complexidade alta prefixo wealth- painel dashboard interface-simulada mockup grafico svg rosca donut barras sparkline contador patrimonio relatorio-consolidado seletor-de-periodo assessoria portal-do-cliente

Incluir · as linhas que se colam num site novo, depois do core (CLAUDE.md §10)

<link rel="stylesheet" href="componentes/dados/painel-patrimonio/estilo.css">
<script src="componentes/dados/painel-patrimonio/script.js"></script>

markup.html · 310 linhas · 16,2 KB

<!-- ==========================================================================
     PAINEL DE PATRIMÔNIO — o portal do cliente, vivo na página
     Prefixo: wealth-
     Requer: core (tokens, base, motion.css, motion.js) + estilo.css e script.js
     Sem lib. Sem canvas: os dois gráficos são SVG inline.

     A DOBRA. Cabeçalho centrado do core e, embaixo, UM painel grande que
     encena o relatório consolidado que a assessoria entrega. Não é
     ferramenta de entrada de dados: ninguém digita nada aqui. É interface
     simulada, decorativa e viva — em cinco segundos o visitante entende o
     que receberia todo mês.

     AS QUATRO PEÇAS, e o que cada uma prova:
       1. patrimônio consolidado, contando          -> "é uma posição só"
       2. rosca por classe de ativo + leitura       -> "está alocado"
       3. barras por instituição                    -> "atravessa custódias"
       4. linha da janela (sparkline)               -> "e você acompanha"

     O SVG É DECORATIVO, O DADO É TEXTO. As duas peças gráficas levam
     aria-hidden. Quem usa leitor de tela recebe a lista de classes com
     percentual e valor, a lista de instituições com valor e a leitura
     escrita da linha (mínima, máxima, marcações) — nunca a palavra
     "gráfico".

     ESTADO INICIAL NO MARKUP. Todo o período padrão (7d) está escrito aqui:
     o valor, a variação, as cinco fatias com stroke-dasharray calculado, as
     cinco barras com o próprio --wealth-w e o path da linha. Sem
     JavaScript o painel está inteiro e correto — nunca uma casca vazia.
     Os números deste bloco saem da MESMA conta que o script.js faz para os
     outros dois períodos (ver PERIODOS no topo dele); a linha 0 daquela
     tabela tem de repetir o que está aqui.

     AS ABAS SOMEM SEM JAVASCRIPT. O script marca o painel com .is-live, e
     só então as três abas aparecem; no lugar delas, sem script, fica o
     rótulo estático da janela em cena. Três botões que ignoram o clique
     seriam um defeito, e esconder sem dizer qual janela está na tela seria
     outro.

     ENTRADA. data-rv do core no invólucro do painel. O crescimento das
     formas e a contagem do número são do script.js, disparados pelo
     IntersectionObserver dele — nada aqui reimplementa reveal.

     CONTEÚDO ILUSTRATIVO: ver a ressalva no pé do painel e a seção
     Compliance da ficha.
     ========================================================================== -->

<section class="wealth sec-deep sec-pad" data-wealth>
  <div class="wrap">

    <div class="sec-head">
      <p class="eyebrow">Portal do cliente</p>
      <h2 class="sec-title ttl">O seu patrimônio, em <em>uma</em> tela</h2>
      <p class="sec-lead">
        O relatório consolidado chega no dia 5 de cada mês e reúne as suas
        instituições numa posição só. Aqui ele está vivo: escolha a janela e
        o painel inteiro recalcula.
      </p>
    </div>

    <!-- CONTEÚDO ILUSTRATIVO — todos os valores, percentuais e instituições
         deste painel são fictícios e existem só para dar forma à peça. -->
    <div class="wealth-panel glass" data-rv data-wealth-panel>

      <!-- -----------------------------------------------------------------
           BARRA DE TOPO
           Identidade do relatório à esquerda, janela à direita — a mesma
           divisão que a barra de um aplicativo tem. A pílula "ilustrativo"
           é a primitiva .pill do core e mora aqui de propósito: o aviso
           aparece na própria interface, não só na letra do rodapé.
           ---------------------------------------------------------------- -->
      <div class="wealth-bar">

        <p class="wealth-app">
          <span class="wealth-app-mark" aria-hidden="true">
            <svg class="wealth-app-glyph" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 18.5V9.2M9.3 18.5V5.5M14.7 18.5v-6.6M20 18.5V8"/></svg>
          </span>
          <span class="wealth-app-name">Relatório consolidado</span>
          <span class="pill wealth-app-tag">ilustrativo</span>
        </p>

        <div class="wealth-tabs" role="group" aria-label="Janela do relatório" data-wealth-tabs>
          <button class="wealth-tab is-on" type="button" aria-pressed="true"
                  aria-label="Janela de 7 dias" data-wealth-tab>7d</button>
          <button class="wealth-tab" type="button" aria-pressed="false"
                  aria-label="Janela de 30 dias" data-wealth-tab>30d</button>
          <button class="wealth-tab" type="button" aria-pressed="false"
                  aria-label="Janela de 12 meses" data-wealth-tab>12m</button>
        </div>

        <!-- O que ocupa o lugar das abas quando não há script. -->
        <p class="wealth-static">janela de 7 dias</p>

      </div>

      <!-- A troca de janela é silenciosa para quem enxerga a tela inteira e
           invisível para quem não enxerga: esta região anuncia o que mudou,
           e só é preenchida quando alguém clica. -->
      <p class="wealth-live" role="status" aria-live="polite" data-wealth-live></p>

      <div class="wealth-grid">

        <!-- ===============================================================
             1. PATRIMÔNIO CONSOLIDADO
             O número conta ao entrar na tela e reconta a cada troca de
             janela. O valor final está escrito aqui: é ele que fica de pé
             sem JavaScript e é dele que a contagem parte de volta.
             =============================================================== -->
        <div class="wealth-cell wealth-cell-total">
          <h3 class="wealth-cell-title">Patrimônio consolidado</h3>

          <p class="wealth-total">
            <span class="wealth-total-cur">R$</span><span class="wealth-total-num" data-wealth-total>48,12</span><span class="wealth-total-suf">mi</span>
          </p>

          <!-- A variação vem SEMPRE com a janela escrita ao lado: é fato
               passado de um período nomeado, não projeção. -->
          <p class="wealth-delta">
            <span class="wealth-delta-val" data-wealth-delta>+1,0%</span>
            <span class="wealth-delta-lab" data-wealth-delta-lab>na janela de 7 dias</span>
          </p>

          <p class="wealth-close" data-wealth-close>fechamento de 28/08/2026</p>
        </div>

        <!-- ===============================================================
             4. LINHA DA JANELA (sparkline)
             Fica na primeira fila, ao lado do número: a linha é a
             continuação visual dele, não uma quarta caixa perdida no pé.

             O <svg> estica sem manter proporção (preserveAspectRatio="none")
             e o traço se defende com vector-effect. O ponto do fim NÃO é um
             <circle>: sob escala não uniforme ele viraria elipse. É um
             elemento HTML posicionado por --wealth-spark-y, a altura do
             último ponto em porcentagem da caixa — o x é constante por
             construção (o último ponto sempre encosta na margem direita).
             =============================================================== -->
        <div class="wealth-cell wealth-cell-spark">
          <div class="wealth-cell-head">
            <h3 class="wealth-cell-title">Evolução da janela</h3>
            <p class="wealth-spark-end">fecha em <b class="wealth-spark-end-val" data-wealth-spark-end>R$ 48,12 mi</b></p>
          </div>

          <div class="wealth-spark" style="--wealth-spark-y:15.07%" data-wealth-spark>
            <svg class="wealth-spark-svg" viewBox="0 0 600 96" preserveAspectRatio="none"
                 aria-hidden="true" focusable="false">
              <path class="wealth-spark-line" vector-effect="non-scaling-stroke" fill="none"
                    data-wealth-spark-path
                    d="M4 86 L57.82 75.57 L111.64 83.02 L165.45 57.69 L219.27 63.65 L273.09 39.8 L326.91 29.37 L380.73 41.29 L434.55 23.41 L488.36 10 L542.18 24.9 L596 14.47"/>
            </svg>
            <span class="wealth-spark-dot" aria-hidden="true"></span>
          </div>

          <p class="wealth-spark-read">
            mínima <b class="wealth-spark-num" data-wealth-spark-min>R$ 47,64 mi</b> ·
            máxima <b class="wealth-spark-num" data-wealth-spark-max>R$ 48,15 mi</b> ·
            12 marcações na janela
          </p>
        </div>

        <!-- ===============================================================
             2. ROSCA POR CLASSE DE ATIVO
             Cinco fatias. A rosca é aria-hidden; a lista ao lado é a
             informação — classe, percentual e valor, em texto de verdade.
             A cor de cada fatia e da bolinha correspondente sai da MESMA
             fonte (--wealth-s1..s5, escolhidas por :nth-child no CSS), e
             por isso não há como a legenda desencontrar do desenho.
             =============================================================== -->
        <div class="wealth-cell wealth-cell-mix">
          <h3 class="wealth-cell-title">Por classe de ativo</h3>

          <div class="wealth-mix">
            <svg class="wealth-donut" viewBox="0 0 120 120" aria-hidden="true" focusable="false"
                 data-wealth-donut>
              <!-- O anel de trás: sem ele os vãos entre as fatias leem como
                   buracos. Fica FORA do <g> para que os cinco arcos sejam os
                   filhos 1..5 dele — é por essa posição que o CSS escolhe a
                   cor de cada um. -->
              <circle class="wealth-donut-track" cx="60" cy="60" r="46" fill="none"/>
              <!-- rotate(-90) no atributo, e não no CSS: em SVG o
                   transform-origin do CSS precisa de transform-box, e um
                   atributo estático não depende da folha ter carregado. -->
              <g transform="rotate(-90 60 60)" data-wealth-arcs>
                <circle class="wealth-arc" cx="60" cy="60" r="46" fill="none"
                        stroke-dasharray="95.87 193.16" stroke-dashoffset="0"/>
                <circle class="wealth-arc" cx="60" cy="60" r="46" fill="none"
                        stroke-dasharray="49.62 239.4" stroke-dashoffset="-98.27"/>
                <circle class="wealth-arc" cx="60" cy="60" r="46" fill="none"
                        stroke-dasharray="66.97 222.06" stroke-dashoffset="-150.29"/>
                <circle class="wealth-arc" cx="60" cy="60" r="46" fill="none"
                        stroke-dasharray="32.28 256.74" stroke-dashoffset="-219.66"/>
                <circle class="wealth-arc" cx="60" cy="60" r="46" fill="none"
                        stroke-dasharray="32.28 256.74" stroke-dashoffset="-254.34"/>
              </g>
            </svg>

            <ul class="wealth-mix-list" role="list" data-wealth-mix>
              <li class="wealth-mix-item">
                <span class="wealth-mix-dot" aria-hidden="true"></span>
                <span class="wealth-mix-name">Renda fixa</span>
                <span class="wealth-mix-pct" data-wealth-pct>34%</span>
                <span class="wealth-mix-val" data-wealth-val>R$ 16,36 mi</span>
              </li>
              <li class="wealth-mix-item">
                <span class="wealth-mix-dot" aria-hidden="true"></span>
                <span class="wealth-mix-name">Crédito privado</span>
                <span class="wealth-mix-pct" data-wealth-pct>18%</span>
                <span class="wealth-mix-val" data-wealth-val>R$ 8,66 mi</span>
              </li>
              <li class="wealth-mix-item">
                <span class="wealth-mix-dot" aria-hidden="true"></span>
                <span class="wealth-mix-name">Ações e fundos</span>
                <span class="wealth-mix-pct" data-wealth-pct>24%</span>
                <span class="wealth-mix-val" data-wealth-val>R$ 11,55 mi</span>
              </li>
              <li class="wealth-mix-item">
                <span class="wealth-mix-dot" aria-hidden="true"></span>
                <span class="wealth-mix-name">Imobiliário</span>
                <span class="wealth-mix-pct" data-wealth-pct>12%</span>
                <span class="wealth-mix-val" data-wealth-val>R$ 5,77 mi</span>
              </li>
              <li class="wealth-mix-item">
                <span class="wealth-mix-dot" aria-hidden="true"></span>
                <span class="wealth-mix-name">Internacional</span>
                <span class="wealth-mix-pct" data-wealth-pct>12%</span>
                <span class="wealth-mix-val" data-wealth-val>R$ 5,78 mi</span>
              </li>
            </ul>
          </div>
        </div>

        <!-- ===============================================================
             3. POR INSTITUIÇÃO
             Cinco barras horizontais. Nome e valor são texto; a barra é
             enfeite e leva aria-hidden. Cada barra carrega a própria
             proporção em --wealth-w (1 = a maior da janela) e cresce por
             scaleX com origem à esquerda.

             As instituições são MARCAS FICTÍCIAS da casa. Nome de banco ou
             de corretora real é marca registrada e não atravessa.
             =============================================================== -->
        <div class="wealth-cell wealth-cell-inst">
          <h3 class="wealth-cell-title">Por instituição</h3>

          <ul class="wealth-inst-list" role="list" data-wealth-inst>
            <li class="wealth-inst">
              <span class="wealth-inst-name">Meridian</span>
              <span class="wealth-inst-track" aria-hidden="true">
                <span class="wealth-inst-fill" data-wealth-fill style="--wealth-w:1"></span>
              </span>
              <span class="wealth-inst-val" data-wealth-val>R$ 14,92 mi</span>
            </li>
            <li class="wealth-inst">
              <span class="wealth-inst-name">Vantor</span>
              <span class="wealth-inst-track" aria-hidden="true">
                <span class="wealth-inst-fill" data-wealth-fill style="--wealth-w:.743"></span>
              </span>
              <span class="wealth-inst-val" data-wealth-val>R$ 11,08 mi</span>
            </li>
            <li class="wealth-inst">
              <span class="wealth-inst-name">Quadrant</span>
              <span class="wealth-inst-track" aria-hidden="true">
                <span class="wealth-inst-fill" data-wealth-fill style="--wealth-w:.613"></span>
              </span>
              <span class="wealth-inst-val" data-wealth-val>R$ 9,15 mi</span>
            </li>
            <li class="wealth-inst">
              <span class="wealth-inst-name">Altamira</span>
              <span class="wealth-inst-track" aria-hidden="true">
                <span class="wealth-inst-fill" data-wealth-fill style="--wealth-w:.484"></span>
              </span>
              <span class="wealth-inst-val" data-wealth-val>R$ 7,22 mi</span>
            </li>
            <li class="wealth-inst">
              <span class="wealth-inst-name">Kessler</span>
              <span class="wealth-inst-track" aria-hidden="true">
                <span class="wealth-inst-fill" data-wealth-fill style="--wealth-w:.385"></span>
              </span>
              <span class="wealth-inst-val" data-wealth-val>R$ 5,75 mi</span>
            </li>
          </ul>
        </div>

      </div>

      <!-- -----------------------------------------------------------------
           PÉ DO PAINEL
           UM detalhe vivo, e um só: o anel do ponto de sincronia, que
           respira devagar. O sparkline não ganha ponto sozinho, as barras
           não oscilam e nenhum número muda sem alguém pedir — dois detalhes
           vivos ao mesmo tempo viram enfeite.

           A ressalva fica AQUI, visível, ao lado do painel que ela cobre —
           não em letra miúda no rodapé da página.
           ---------------------------------------------------------------- -->
      <div class="wealth-foot">
        <p class="wealth-sync">
          <span class="wealth-sync-dot" aria-hidden="true" data-wealth-ping><span class="wealth-sync-ping"></span></span>
          Custódias sincronizadas
        </p>
        <p class="nota wealth-nota">
          Painel ilustrativo. Os números não representam carteira real nem
          resultado de cliente. Variação de janela é fato passado e não
          garante resultado futuro.
        </p>
      </div>

    </div>

  </div>
</section>

estilo.css · 477 linhas · 19,1 KB

/* ==========================================================================
   PAINEL DE PATRIMÔNIO — prefixo wealth-
   Um painel grande que encena o relatório consolidado: número principal,
   rosca por classe de ativo, barras por instituição e a linha da janela.

   Tokens usados:
     --accent --accent-2   a série dos gráficos e o realce da variação
     --bg                  o fundo do escopo, que dilui a série (ver abaixo)
     --display --sans      números e títulos / interface
     --ease                curva única, em toda transição daqui
     --fg --fg-2 --fg-muted   tinta, herdada do escopo de tom da seção
     --light               os brancos com alfa, sempre via color-mix
     --radius-media        canto do painel e das barras
     --radius-pill         canto das abas e da pílula "ilustrativo"
     --rule --rule-2       as divisórias internas do painel
     --sec-gap             respiro vertical, via .sec-pad do core

   A PALETA DA SÉRIE. Cinco fatias e duas cores de marca: qualquer tentativa
   de esticar sage e tan em cinco passos distintos deixa dois pares quase
   iguais no meio da rosca. A saída é a que todo painel de private banking
   usa — UMA cor, cinco degraus — e o §3 do brief autoriza ("ou por
   opacidade"). Aqui o degrau é color-mix com --bg, e não opacity, por dois
   motivos: opacity criaria camada de composição em cinco elementos que
   nunca mudam, e a bolinha da legenda teria de repetir a mesma conta em
   outro contexto de empilhamento para dar a mesma cor. O último degrau
   troca para --accent-2 justamente onde os passos ficam mais próximos: é o
   par de fatias pequenas (12% e 12%) que precisa da diferença de matiz.

   O PAINEL É .glass COM DESFOQUE ZERO. Da primitiva se quer a borda tênue e
   o brilho radial vindo do topo — que é o desenho de uma superfície de
   aplicativo. O desfoque não: atrás do painel há um fundo opaco de seção,
   não há o que desfocar, e a camada custaria composição de graça. Mesmo
   raciocínio do conteudo/trio-cards-ui-simulada.

   O JS quase não escreve aparência: escreve DADO (dasharray da fatia,
   proporção da barra, offset da linha, número) e uma classe. Duração,
   curva, cor e forma estão todas aqui.
   ========================================================================== */

.wealth{
  /* Canto do painel. Segue o tema: 0 num projeto flat, 16 num arredondado. */
  --wealth-radius:var(--radius-media);

  /* Respiro interno de cada célula da grade. É o mesmo da barra de topo e
     do pé — o painel inteiro respira por esta variável. */
  --wealth-cell-pad:clamp(18px,2vw,28px);

  --wealth-donut:132px;      /* lado da rosca */
  --wealth-spark-h:96px;     /* altura da caixa da linha */
  --wealth-bar-h:8px;        /* espessura da barra de instituição */
  --wealth-bar-radius:var(--radius-media);
  --wealth-inst-name:82px;   /* coluna do nome da instituição */
  --wealth-inst-val:92px;    /* coluna do valor */

  /* Durações das formas crescendo, lidas só pelo CSS. */
  --wealth-grow:.9s;         /* fatia e barra */
  --wealth-draw:1.4s;        /* traçado da linha */

  /* Os dois tempos que o SCRIPT lê, em SEGUNDOS, com parseFloat. Ficam aqui
     porque são ritmo de desenho, não lógica — quem achar a contagem
     apressada mexe numa linha de CSS. Em ms a conta sairia mil vezes maior
     e o painel pareceria travado. */
  --wealth-count-dur:1.1s;   /* contagem do número principal */
  --wealth-ping-hold:2.9s;   /* volta completa do anel de sincronia */

  /* A série: cinco degraus de uma cor só, do maior peso ao menor. */
  --wealth-s1:var(--accent);
  --wealth-s2:color-mix(in srgb,var(--accent) 74%,var(--bg));
  --wealth-s3:color-mix(in srgb,var(--accent) 56%,var(--bg));
  --wealth-s4:color-mix(in srgb,var(--accent) 42%,var(--bg));
  --wealth-s5:color-mix(in srgb,var(--accent-2) 62%,var(--bg));
}


/* ==========================================================================
   O PAINEL
   ========================================================================== */

.wealth-panel{
  --glass-bg:color-mix(in srgb,var(--light) 4%,transparent);
  --glass-line:color-mix(in srgb,var(--light) 9%,transparent);
  --glass-blur:0px;
  margin-top:clamp(40px,4.6vw,64px);
  border-radius:var(--wealth-radius);
  overflow:hidden;
}

/* A região que anuncia a troca de janela para quem não vê a tela. Sai do
   fluxo sem sair da árvore de acessibilidade: display:none e
   visibility:hidden calariam o leitor de tela, que é o único público dela. */
.wealth-live{
  position:absolute;width:1px;height:1px;
  overflow:hidden;white-space:nowrap;
  clip-path:inset(50%);
}


/* ==========================================================================
   BARRA DE TOPO
   Identidade à esquerda, janela à direita — a divisão de uma barra de
   aplicativo. As abas só aparecem com .is-live, a classe que o script
   carimba: sem JavaScript o lugar delas é do rótulo estático.
   ========================================================================== */

.wealth-bar{
  display:flex;flex-wrap:wrap;
  align-items:center;justify-content:space-between;
  gap:12px 16px;
  padding:14px var(--wealth-cell-pad);
  border-bottom:1px solid var(--rule);
}

.wealth-app{display:flex;align-items:center;gap:10px;min-width:0}

.wealth-app-mark{
  flex:0 0 28px;width:28px;height:28px;
  display:flex;align-items:center;justify-content:center;
  border:1px solid var(--rule-2);border-radius:var(--radius-media);
  color:var(--accent);
}
.wealth-app-glyph{width:15px;height:15px}

.wealth-app-name{
  font-family:var(--display);font-weight:700;
  font-size:13px;line-height:1;letter-spacing:.01em;
  color:var(--fg);
}

/* A pílula do core, um ponto menor: aqui ela é rótulo de estado dentro de
   uma barra de 28px de altura, não classificação de card. */
.wealth-app-tag{font-size:9.5px;padding:4px 9px;letter-spacing:.14em}

.wealth-tabs{
  display:none;gap:2px;padding:3px;
  border-radius:var(--radius-pill);
  background:color-mix(in srgb,var(--fg) 7%,transparent);
}
.wealth-panel.is-live .wealth-tabs{display:flex}

.wealth-tab{
  height:26px;padding-inline:13px;
  border:0;border-radius:var(--radius-pill);
  background:transparent;color:var(--fg-muted);
  font-family:var(--sans);font-weight:400;
  font-size:12px;line-height:1;letter-spacing:normal;
  transition:background .4s var(--ease),color .4s var(--ease);
}
.wealth-tab:hover{color:var(--fg-2)}
.wealth-tab.is-on{
  background:color-mix(in srgb,var(--fg) 13%,transparent);
  color:var(--fg);
}

.wealth-static{font-size:12px;line-height:1;letter-spacing:normal;color:var(--fg-muted)}
.wealth-panel.is-live .wealth-static{display:none}


/* ==========================================================================
   A GRADE DE QUATRO PEÇAS
   Doze colunas: número (5) ao lado da linha da janela (7), porque a linha é
   a continuação visual do número; embaixo, as duas leituras de composição,
   metade a metade. As células NÃO têm vão entre si: quem separa é uma
   divisória de 1px, como num painel de verdade. Vão entre células daria
   cartões flutuando dentro de um cartão.
   ========================================================================== */

.wealth-grid{
  display:grid;
  grid-template-columns:repeat(12,minmax(0,1fr));
  grid-template-areas:
    "total total total total total spark spark spark spark spark spark spark"
    "mix   mix   mix   mix   mix   mix   inst  inst  inst  inst  inst  inst";
}

/* min-width:0 em toda célula: sem ele, uma linha longa de valor impede a
   coluna de encolher e a grade estoura para fora do painel. */
.wealth-cell{padding:var(--wealth-cell-pad);min-width:0}
.wealth-cell-total{grid-area:total;display:flex;flex-direction:column}
.wealth-cell-spark{grid-area:spark;border-left:1px solid var(--rule)}
.wealth-cell-mix{grid-area:mix;border-top:1px solid var(--rule)}
.wealth-cell-inst{grid-area:inst;border-top:1px solid var(--rule);border-left:1px solid var(--rule)}

.wealth-cell-title{
  font-family:var(--sans);font-weight:400;
  font-size:11px;line-height:1.2;letter-spacing:.16em;text-transform:uppercase;
  color:var(--fg-muted);
}

.wealth-cell-head{
  display:flex;flex-wrap:wrap;align-items:baseline;
  justify-content:space-between;gap:6px 14px;
}


/* ==========================================================================
   1. PATRIMÔNIO CONSOLIDADO
   ========================================================================== */

/* baseline alinha "R$", o número e "mi" pela linha de base, e não pelo
   centro: é o que faz os três lerem como uma cifra só. */
.wealth-total{
  display:flex;align-items:baseline;gap:8px;
  margin-top:16px;
  font-family:var(--display);font-weight:700;
  line-height:1;letter-spacing:-.03em;
  color:var(--fg);
}
.wealth-total-cur{font-size:clamp(17px,1.5vw,21px);color:var(--fg-2)}

/* tabular-nums é o que impede o número de tremer durante a contagem: sem
   ele cada dígito tem largura própria e a cifra inteira dança. Fonte sem a
   tabela de figuras tabulares ignora e nada quebra. */
.wealth-total-num{
  font-size:clamp(38px,4.4vw,60px);
  font-variant-numeric:tabular-nums;
}
.wealth-total-suf{font-size:clamp(15px,1.3vw,19px);color:var(--fg-2)}

.wealth-delta{
  display:flex;flex-wrap:wrap;align-items:baseline;gap:8px;
  margin-top:16px;
}
.wealth-delta-val{
  font-family:var(--display);font-weight:700;
  font-size:15px;line-height:1;letter-spacing:normal;
  color:var(--accent);
}
.wealth-delta-lab{font-size:13.5px;line-height:1.4;letter-spacing:normal;color:var(--fg-2)}

/* margin-top:auto empurra o fechamento para o pé da célula: o número fica
   com silêncio em volta, que é o que o faz pesar. */
.wealth-close{
  margin-top:auto;padding-top:14px;
  font-size:12.5px;line-height:1.4;letter-spacing:normal;
  color:var(--fg-muted);
}


/* ==========================================================================
   2. ROSCA POR CLASSE DE ATIVO
   O <svg> é decorativo e leva aria-hidden no markup; a lista ao lado é a
   informação. Cor da fatia e cor da bolinha saem da MESMA variável, pela
   mesma posição — não há como a legenda desencontrar do desenho.
   ========================================================================== */

.wealth-mix{
  display:flex;align-items:center;
  gap:clamp(16px,2.2vw,30px);
  margin-top:20px;
}

.wealth-donut{
  flex:0 0 var(--wealth-donut);
  width:var(--wealth-donut);height:var(--wealth-donut);
}

/* O anel de trás. Sem ele, os vãos de 2.4 unidades entre as fatias leem
   como buracos na rosca em vez de separação. */
.wealth-donut-track{
  stroke:color-mix(in srgb,var(--fg) 8%,transparent);
  stroke-width:13;
}

/* A transição é de stroke-dasharray, e não de dashoffset: com dasharray
   fixo o offset só GIRA a fatia — o comprimento do traço é a única coisa
   que a faz crescer do próprio começo. */
.wealth-arc{
  stroke-width:13;stroke-linecap:butt;
  transition:stroke-dasharray var(--wealth-grow) var(--ease);
}
.wealth-arc:nth-child(1){stroke:var(--wealth-s1)}
.wealth-arc:nth-child(2){stroke:var(--wealth-s2);transition-delay:.06s}
.wealth-arc:nth-child(3){stroke:var(--wealth-s3);transition-delay:.12s}
.wealth-arc:nth-child(4){stroke:var(--wealth-s4);transition-delay:.18s}
.wealth-arc:nth-child(5){stroke:var(--wealth-s5);transition-delay:.24s}

.wealth-mix-list{flex:1 1 0;min-width:0;display:grid;gap:10px}

.wealth-mix-item{
  display:grid;
  grid-template-columns:9px minmax(0,1fr) 34px auto;
  align-items:center;gap:10px;
  font-size:13.5px;line-height:1.3;letter-spacing:normal;
}
.wealth-mix-dot{width:9px;height:9px;border-radius:50%}
.wealth-mix-item:nth-child(1) .wealth-mix-dot{background:var(--wealth-s1)}
.wealth-mix-item:nth-child(2) .wealth-mix-dot{background:var(--wealth-s2)}
.wealth-mix-item:nth-child(3) .wealth-mix-dot{background:var(--wealth-s3)}
.wealth-mix-item:nth-child(4) .wealth-mix-dot{background:var(--wealth-s4)}
.wealth-mix-item:nth-child(5) .wealth-mix-dot{background:var(--wealth-s5)}

.wealth-mix-name{color:var(--fg-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.wealth-mix-pct{text-align:right;color:var(--fg);font-variant-numeric:tabular-nums}
.wealth-mix-val{
  text-align:right;color:var(--fg-muted);
  font-variant-numeric:tabular-nums;white-space:nowrap;
}


/* ==========================================================================
   3. POR INSTITUIÇÃO
   Nome e valor são texto; a barra é enfeite e sai da árvore de
   acessibilidade no markup.
   ========================================================================== */

.wealth-inst-list{margin-top:20px;display:grid;gap:14px}

.wealth-inst{
  display:grid;
  grid-template-columns:var(--wealth-inst-name) minmax(0,1fr) var(--wealth-inst-val);
  align-items:center;gap:14px;
  font-size:13.5px;line-height:1.3;letter-spacing:normal;
}
.wealth-inst-name{color:var(--fg-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.wealth-inst-val{
  text-align:right;color:var(--fg);
  font-variant-numeric:tabular-nums;white-space:nowrap;
}

.wealth-inst-track{
  height:var(--wealth-bar-h);
  border-radius:var(--wealth-bar-radius);
  background:color-mix(in srgb,var(--fg) 8%,transparent);
  overflow:hidden;
}

/* scaleX com origem à esquerda, e não width: a barra cresce numa
   propriedade de composição, sem custar layout a cada quadro da transição.
   O preço é que o canto direito do preenchimento é achatado junto — por
   isso --wealth-bar-radius acompanha o tema em vez de ser pílula fixa. */
.wealth-inst-fill{
  display:block;height:100%;
  border-radius:inherit;
  background:color-mix(in srgb,var(--accent) 80%,transparent);
  transform:scaleX(var(--wealth-w,0));transform-origin:left center;
  transition:transform var(--wealth-grow) var(--ease);
}
.wealth-inst:nth-child(2) .wealth-inst-fill{transition-delay:.05s}
.wealth-inst:nth-child(3) .wealth-inst-fill{transition-delay:.1s}
.wealth-inst:nth-child(4) .wealth-inst-fill{transition-delay:.15s}
.wealth-inst:nth-child(5) .wealth-inst-fill{transition-delay:.2s}


/* ==========================================================================
   4. LINHA DA JANELA
   O SVG estica sem manter proporção, para a linha ocupar a célula inteira
   em qualquer largura; vector-effect segura a espessura do traço, que sob
   escala não uniforme engordaria na horizontal.
   ========================================================================== */

.wealth-spark-end{font-size:12.5px;line-height:1.3;letter-spacing:normal;color:var(--fg-muted)}
.wealth-spark-end-val{font-weight:400;color:var(--fg-2);font-variant-numeric:tabular-nums}

.wealth-spark{
  position:relative;
  margin-top:18px;height:var(--wealth-spark-h);
}
/* A base do gráfico. Uma linha só, no tom de divisória do escopo: o
   suficiente para a curva ter chão sem virar grade. */
.wealth-spark::after{
  content:"";position:absolute;left:0;right:0;bottom:0;
  height:1px;background:var(--rule);
}
.wealth-spark-svg{display:block;width:100%;height:100%}

.wealth-spark-line{
  stroke:var(--accent);stroke-width:2;
  stroke-linejoin:round;stroke-linecap:round;
  transition:stroke-dashoffset var(--wealth-draw) var(--ease);
}

/* O ponto do fim é HTML, não <circle>: sob preserveAspectRatio="none" um
   círculo do SVG viraria elipse. Aqui ele é redondo em qualquer largura.
   O x é constante por construção — o último ponto da série sempre encosta
   na margem direita do viewBox —, e só o y viaja em --wealth-spark-y. */
.wealth-spark-dot{
  position:absolute;left:calc(100% - 4px);top:var(--wealth-spark-y,50%);
  width:9px;height:9px;margin:-4.5px 0 0 -4.5px;
  border-radius:50%;background:var(--accent);
  box-shadow:0 0 0 4px color-mix(in srgb,var(--accent) 16%,transparent);
}

.wealth-spark-read{
  margin-top:14px;
  font-size:12.5px;line-height:1.4;letter-spacing:normal;
  color:var(--fg-muted);
}
.wealth-spark-num{font-weight:400;color:var(--fg-2);font-variant-numeric:tabular-nums}


/* ==========================================================================
   PÉ DO PAINEL — sincronia viva e ressalva
   ========================================================================== */

.wealth-foot{
  display:flex;flex-wrap:wrap;
  align-items:center;justify-content:space-between;
  gap:10px 24px;
  padding:14px var(--wealth-cell-pad);
  border-top:1px solid var(--rule);
}

.wealth-sync{
  display:inline-flex;align-items:center;gap:10px;
  font-size:12.5px;line-height:1.3;letter-spacing:normal;
  color:var(--fg-2);white-space:nowrap;
}
.wealth-sync-dot{
  position:relative;flex:0 0 8px;width:8px;height:8px;
  border-radius:50%;background:var(--accent);
}

/* O ÚNICO detalhe vivo do painel. O script escreve um número de 0 a 1 em
   --wealth-ping; a aparência é decidida aqui. Sem script (ou com movimento
   reduzido, quando o laço nem chega a ser criado) o valor fica no 0 do
   fallback e o anel vira um halo parado — estado honesto, não defeito. */
.wealth-sync-ping{
  position:absolute;inset:0;border-radius:50%;
  border:1px solid var(--accent);
  transform:scale(calc(1 + var(--wealth-ping,0) * 2.1));
  opacity:calc((1 - var(--wealth-ping,0)) * .5);
}

/* A ressalva de compliance. Fica no painel que ela cobre, visível, e não em
   letra miúda no rodapé da página. */
/* Tipografia pela primitiva .nota do core. --nota-max:none porque a ressalva
   divide o pé do painel com o indicador de sincronia e é alinhada à DIREITA:
   um teto de medida encolheria a caixa e desencostaria o texto da borda. */
.wealth-nota{
  flex:1 1 340px;
  --nota-max:none;
  text-align:right;
}


/* ---------------------------------------------------------------- movimento reduzido */
/* Duas camadas, e as duas precisam existir. O script não liga o laço e
   pinta os valores finais de uma vez; estas regras garantem que a pintura
   não venha com transição atrás — inclusive na troca de janela, que
   continua funcionando por clique (aba que ignora o clique é defeito, não
   acessibilidade). */
@media(prefers-reduced-motion:reduce){
  .wealth-arc,.wealth-inst-fill,.wealth-spark-line,.wealth-tab{transition:none}
}

/* A mesma suspensão, agora sob comando do script: .is-still marca o quadro
   em que os valores são escritos sem que ninguém veja o caminho — o estado
   zero da entrada e a pintura direta com movimento reduzido. */
.wealth.is-still .wealth-arc,
.wealth.is-still .wealth-inst-fill,
.wealth.is-still .wealth-spark-line{transition:none}


/* ---------------------------------------------------------------- mobile */

/* A grade de doze colunas precisa de ~900 de painel útil para a rosca e a
   lista caberem lado a lado dentro da metade esquerda. Abaixo disso as
   quatro peças viram quatro faixas, na mesma ordem de leitura. */
@media(max-width:960px){
  .wealth-grid{
    grid-template-columns:minmax(0,1fr);
    grid-template-areas:"total" "spark" "mix" "inst";
  }
  .wealth-cell-spark,.wealth-cell-inst{border-left:0}
  .wealth-cell-spark{border-top:1px solid var(--rule)}
  .wealth-close{margin-top:14px;padding-top:0}
}

@media(max-width:640px){
  .wealth{--wealth-donut:120px;--wealth-spark-h:78px;--wealth-inst-val:84px}
  /* Rosca e lista lado a lado em 350px de largura espremeriam o nome da
     classe até a elipse. Empilhadas, a rosca centraliza e a lista fica com
     a linha inteira. */
  .wealth-mix{flex-direction:column;align-items:stretch;gap:20px}
  .wealth-donut{align-self:center}
  .wealth-nota{text-align:left}
}

script.js · 402 linhas · 16,3 KB

/* ==========================================================================
   PAINEL DE PATRIMÔNIO — a janela do relatório e o que ela recalcula.

   UM laço de requestAnimationFrame por painel, e ele faz duas coisas: a
   contagem do número principal e o anel de sincronia respirando. As formas
   (fatia, barra, linha) crescem por TRANSIÇÃO de CSS — o script escreve o
   valor final uma vez e o navegador interpola sozinho, fora da thread
   principal. Animar quatro peças no rAF seria trabalho de graça.

   O laço dorme fora da tela, por IntersectionObserver: sem isso o anel
   continuaria escrevendo estilo sessenta vezes por segundo com a dobra a
   três telas de distância. Medido: zero escritas em 60 quadros com a seção
   fora de vista.

   TEMPO POR dt, COM TETO. A contagem e o anel avançam pelo tempo decorrido,
   não por quadro: em tela de 120Hz o número chegaria em metade do tempo se
   fosse por quadro. O teto de 50ms existe para o retorno de uma aba em
   segundo plano — sem ele o primeiro quadro traria segundos acumulados e o
   anel daria um salto.

   O QUE O SCRIPT NÃO FAZ. Não escreve cor, duração nem curva: escreve DADO
   (o dasharray da fatia, a proporção da barra, o offset da linha, o número)
   e troca classe. Aparência inteira no estilo.css.

   MOVIMENTO REDUZIDO. O laço não é criado. O número aparece no valor final,
   a rosca e as barras nascem prontas e a linha nasce inteira; o anel fica
   parado no halo. As abas continuam trocando a janela por clique — aba que
   ignora o clique é defeito, não acessibilidade.

   SEM DADO PESSOAL. O painel não coleta nada, não lê nada do visitante e
   não grava nada: as três janelas vivem na tabela abaixo e morrem no
   recarregamento. Quem coleta é o conversao/cta-formulario-lead.
   ========================================================================== */

(function () {
  var raizes = document.querySelectorAll('[data-wealth]');
  if (!raizes.length) return;                       // guard

  /* ======================================================================
     A TABELA — o ÚNICO lugar que o consumidor edita.

     Uma linha por janela, na ordem das abas. Tudo é ILUSTRATIVO: nenhum
     número aqui é apurado, nenhuma instituição é real (ver a ficha,
     seção Compliance).

     As três janelas são três edições do mesmo relatório, cada uma com o
     próprio fechamento — é isso que permite o total mudar de uma para
     outra sem mentir: não é o mesmo dia visto de três ângulos, são três
     documentos com três datas.

     Regras que a tabela tem de respeitar, e que ninguém checa por você:
       . mix soma 100 (é percentual de carteira);
       . mixValor soma total (é o mesmo dinheiro, repartido);
       . inst soma total (idem, agora por custódia);
       . serie termina no total (a linha fecha onde o número está);
       . variacao é a razão entre o primeiro e o último ponto da série, e
         o texto de janela vem SEMPRE junto — variação de período é fato
         passado de um período nomeado, nunca projeção.

     A linha 0 tem de repetir o que está escrito no markup.html: é ela que
     fica de pé sem JavaScript, e é para ela que a primeira aba volta.
     ====================================================================== */

  var PERIODOS = [
    {
      rotulo:'7d',
      janela:'na janela de 7 dias',
      anuncio:'Janela de 7 dias',
      fechamento:'fechamento de 28/08/2026',
      total:48.12,
      variacao:'+1,0%',
      mix:[34,18,24,12,12],
      mixValor:[16.36,8.66,11.55,5.77,5.78],
      inst:[14.92,11.08,9.15,7.22,5.75],
      serie:[47.64,47.71,47.66,47.83,47.79,47.95,48.02,47.94,48.06,48.15,48.05,48.12]
    },
    {
      rotulo:'30d',
      janela:'na janela de 30 dias',
      anuncio:'Janela de 30 dias',
      fechamento:'fechamento de 31/08/2026',
      total:48.47,
      variacao:'+2,1%',
      mix:[36,17,23,12,12],
      mixValor:[17.45,8.24,11.15,5.82,5.81],
      inst:[14.71,11.19,9.03,7.57,5.97],
      serie:[47.47,47.62,47.55,47.78,47.91,47.82,48.03,48.16,48.07,48.28,48.41,48.47]
    },
    {
      rotulo:'12m',
      janela:'na janela de 12 meses',
      anuncio:'Janela de 12 meses',
      fechamento:'fechamento de 31/12/2025',
      total:44.08,
      variacao:'+7,4%',
      mix:[39,16,21,13,11],
      mixValor:[17.19,7.05,9.26,5.73,4.85],
      inst:[13.22,10.14,8.37,6.88,5.47],
      serie:[41.05,41.42,41.28,41.96,42.34,42.15,42.78,43.10,42.92,43.44,43.86,44.08]
    }
  ];

  /* ---------------------------------------------------------- geometria */

  /* Os mesmos números do <svg> do markup. Mexer aqui sem mexer lá deixa a
     rosca do estado sem JavaScript com outro raio. */
  var RAIO = 46;
  var CIRC = 2 * Math.PI * RAIO;   /* 289.03 — o dasharray é fatia deste bolo */
  var VAO  = 2.4;                  /* respiro entre fatias, em unidades de arco */

  var SW = 600, SH = 96, PX = 4, PY = 10;   /* viewBox e margens da linha */

  function arred(n) { return Math.round(n * 100) / 100; }
  function numero(v) { return v.toFixed(2).replace('.', ','); }
  function moeda(v) { return 'R$ ' + numero(v) + ' mi'; }

  /* Cada fatia é um traço de comprimento proporcional sobre a mesma
     circunferência, deslocado até o próprio começo. O <g> já está girado
     -90° no markup, então a primeira nasce às 12 horas. */
  function arcos(mix) {
    var fora = [], soma = 0, comp, i;
    for (i = 0; i < mix.length; i++) {
      comp = CIRC * mix[i] / 100 - VAO;
      if (comp < 0) comp = 0;
      fora.push({ dash: arred(comp), off: arred(-CIRC * soma / 100) });
      soma += mix[i];
    }
    return fora;
  }

  /* A série é normalizada pela própria mínima e máxima: a linha usa a
     altura inteira da caixa em qualquer janela. Uma escala fixa deixaria a
     janela de 7 dias — meio ponto percentual de amplitude — reta. */
  function caminho(serie) {
    var mn = serie[0], mx = serie[0], i, x, y, faixa, d = '';
    for (i = 1; i < serie.length; i++) {
      if (serie[i] < mn) mn = serie[i];
      if (serie[i] > mx) mx = serie[i];
    }
    faixa = (mx - mn) || 1;
    for (i = 0; i < serie.length; i++) {
      x = PX + i * (SW - 2 * PX) / (serie.length - 1);
      y = (SH - PY) - ((serie[i] - mn) / faixa) * (SH - 2 * PY);
      d += (i ? ' L' : 'M') + arred(x) + ' ' + arred(y);
    }
    y = (SH - PY) - ((serie[serie.length - 1] - mn) / faixa) * (SH - 2 * PY);
    return { d: d, min: mn, max: mx, fimY: arred(y / SH * 100) };
  }

  /* ------------------------------------------------------------ auxílio */

  /* A flag do core é a fonte. O fallback existe para o componente colado
     sozinho, sem motion.js — e repete a regra do ?motion=on. */
  function reduzido() {
    if (window.LIB && typeof window.LIB.reduce === 'boolean') return window.LIB.reduce;
    return matchMedia('(prefers-reduced-motion: reduce)').matches &&
           location.search.indexOf('motion=on') < 0;
  }

  /* getPropertyValue devolve " 1.1s"; parseFloat resolve o espaço e a
     unidade de uma vez. O CSS documenta que o valor é em segundos. */
  function tempo(raiz, nome, padrao) {
    var v = parseFloat(getComputedStyle(raiz).getPropertyValue(nome));
    return (v > 0 ? v : padrao) * 1000;
  }

  function lista(el, seletor) {
    return el ? [].slice.call(el.querySelectorAll(seletor)) : [];
  }

  /* ---------------------------------------------------------- instância */

  function iniciar(raiz) {
    var painel   = raiz.querySelector('[data-wealth-panel]');
    var arcosEl  = raiz.querySelector('[data-wealth-arcs]');
    var listaMix = raiz.querySelector('[data-wealth-mix]');
    var listaIns = raiz.querySelector('[data-wealth-inst]');
    var caixaSpk = raiz.querySelector('[data-wealth-spark]');
    var linha    = raiz.querySelector('[data-wealth-spark-path]');
    var elTotal  = raiz.querySelector('[data-wealth-total]');
    if (!painel || !arcosEl || !listaMix || !listaIns || !caixaSpk || !linha || !elTotal) return;

    var elDelta    = raiz.querySelector('[data-wealth-delta]');
    var elDeltaLab = raiz.querySelector('[data-wealth-delta-lab]');
    var elClose    = raiz.querySelector('[data-wealth-close]');
    var elFim      = raiz.querySelector('[data-wealth-spark-end]');
    var elMin      = raiz.querySelector('[data-wealth-spark-min]');
    var elMax      = raiz.querySelector('[data-wealth-spark-max]');
    var elVivo     = raiz.querySelector('[data-wealth-live]');
    var elPonto    = raiz.querySelector('[data-wealth-ping]');

    var fatias   = [].slice.call(arcosEl.children);
    var pcts     = lista(listaMix, '[data-wealth-pct]');
    var valsMix  = lista(listaMix, '[data-wealth-val]');
    var valsIns  = lista(listaIns, '[data-wealth-val]');
    var barras   = lista(listaIns, '[data-wealth-fill]');
    var abas     = lista(raiz, '[data-wealth-tab]');
    if (fatias.length < 2 || !abas.length) return;

    var reduz    = reduzido();
    var durConta = tempo(raiz, '--wealth-count-dur', 1.1);
    var durPing  = tempo(raiz, '--wealth-ping-hold', 2.9);

    var atual = 0;
    var mostrado = 0;      /* o número que está escrito na tela, em milhões */
    var conta = null;      /* {de, para, t} enquanto a contagem corre */
    var ping = 0;          /* 0..1 — a volta do anel de sincronia */
    var laco = 0, dentro = false, marca = 0;

    /* As abas só existem para quem tem script: é esta classe que as revela
       e some com o rótulo estático que ocupava o lugar delas. */
    painel.classList.add('is-live');

    /* ------------------------------------------------------------ formas */

    /* Escreve com as transições suspensas, e devolve o painel ao normal no
       mesmo quadro. O reflow no meio é o que sela o estado antes de as
       transições voltarem — sem ele o navegador junta as duas escritas e
       anima o caminho que ninguém deveria ver. */
    function semTransicao(fn) {
      raiz.classList.add('is-still');
      fn();
      void raiz.offsetHeight;
      raiz.classList.remove('is-still');
    }

    function zerarFormas() {
      var i;
      for (i = 0; i < fatias.length; i++) {
        fatias[i].setAttribute('stroke-dasharray', '0 ' + arred(CIRC));
      }
      for (i = 0; i < barras.length; i++) barras[i].style.setProperty('--wealth-w', '0');
      /* O traçado parte do fim: dasharray do tamanho do caminho inteiro e
         offset igual — a linha existe, mas nenhum pedaço dela está visível. */
      var L = linha.getTotalLength();
      linha.style.strokeDasharray = L;
      linha.style.strokeDashoffset = L;
    }

    function aplicarFormas(p) {
      var arc = arcos(p.mix), i;
      for (i = 0; i < fatias.length && i < arc.length; i++) {
        fatias[i].setAttribute('stroke-dasharray', arc[i].dash + ' ' + arred(CIRC - arc[i].dash));
        fatias[i].setAttribute('stroke-dashoffset', arc[i].off);
      }
      var maior = 0;
      for (i = 0; i < p.inst.length; i++) if (p.inst[i] > maior) maior = p.inst[i];
      for (i = 0; i < barras.length && i < p.inst.length; i++) {
        barras[i].style.setProperty('--wealth-w', maior ? arred(p.inst[i] / maior * 1000) / 1000 : 0);
      }
      linha.style.strokeDashoffset = 0;
    }

    /* ------------------------------------------------------------- texto */

    function escreverTexto(p) {
      var cam = caminho(p.serie), i;

      if (elDelta)    elDelta.textContent = p.variacao;
      if (elDeltaLab) elDeltaLab.textContent = p.janela;
      if (elClose)    elClose.textContent = p.fechamento;
      if (elFim)      elFim.textContent = moeda(p.serie[p.serie.length - 1]);
      if (elMin)      elMin.textContent = moeda(cam.min);
      if (elMax)      elMax.textContent = moeda(cam.max);

      for (i = 0; i < pcts.length && i < p.mix.length; i++) pcts[i].textContent = p.mix[i] + '%';
      for (i = 0; i < valsMix.length && i < p.mixValor.length; i++) valsMix[i].textContent = moeda(p.mixValor[i]);
      for (i = 0; i < valsIns.length && i < p.inst.length; i++) valsIns[i].textContent = moeda(p.inst[i]);

      /* O atributo d muda ANTES de qualquer conta de comprimento: getTotalLength
         mede o caminho que está lá, não o anterior. */
      linha.setAttribute('d', cam.d);
      caixaSpk.style.setProperty('--wealth-spark-y', cam.fimY + '%');
    }

    /* ---------------------------------------------------------- contagem */

    function escreverTotal(v) {
      mostrado = v;
      elTotal.textContent = numero(v);
    }

    function contarAte(v, animar) {
      if (!animar) { conta = null; escreverTotal(v); return; }
      conta = { de: mostrado, para: v, t: 0 };
    }

    /* -------------------------------------------------------------- pintura */

    function pintar(i, animar) {
      var p = PERIODOS[i];
      if (!p) return;
      atual = i;

      for (var b = 0; b < abas.length; b++) {
        if (b === i) abas[b].classList.add('is-on');
        else abas[b].classList.remove('is-on');
        abas[b].setAttribute('aria-pressed', b === i ? 'true' : 'false');
      }

      escreverTexto(p);

      if (animar) {
        semTransicao(function () { zerarFormas(); });
        aplicarFormas(p);
      } else {
        semTransicao(function () { aplicarFormas(p); });
      }

      contarAte(p.total, animar);
    }

    /* ------------------------------------------------------------- laço */

    function quadro(agora) {
      if (!marca) marca = agora;
      var dt = agora - marca;
      if (dt > 50) dt = 50;                 /* teto: aba em segundo plano */
      marca = agora;

      ping += dt / durPing;
      while (ping >= 1) ping -= 1;
      if (elPonto) elPonto.style.setProperty('--wealth-ping', ping.toFixed(3));

      if (conta) {
        conta.t += dt;
        var q = conta.t / durConta;
        if (q > 1) q = 1;
        var e = 1 - Math.pow(1 - q, 3);     /* saída suave; a curva do CSS não
                                               se lê de dentro do script */
        escreverTotal(conta.de + (conta.para - conta.de) * e);
        if (q === 1) conta = null;
      }

      laco = dentro ? requestAnimationFrame(quadro) : 0;
    }

    function ligar() {
      if (laco || !dentro) return;
      marca = 0;                            /* zera o relógio: voltar à dobra
                                               depois de um tempo fora não pode
                                               despejar o acumulado num quadro */
      laco = requestAnimationFrame(quadro);
    }

    function desligar() {
      if (!laco) return;
      cancelAnimationFrame(laco);
      laco = 0;
      /* Sair de cena no meio de uma contagem deixaria um número errado
         congelado na tela até a próxima visita. Fecha na hora. */
      if (conta) { escreverTotal(conta.para); conta = null; }
      ping = 0;
      if (elPonto) elPonto.style.setProperty('--wealth-ping', '0');
    }

    /* -------------------------------------------------------------- abas */

    for (var k = 0; k < abas.length; k++) {
      (function (i) {
        abas[i].addEventListener('click', function () {
          if (i === atual) return;
          pintar(i, !reduz);
          /* A troca é visível para quem enxerga o painel e silenciosa para
             quem não enxerga. A região só é preenchida a partir daqui — no
             carregamento ela ficaria anunciando um estado que ninguém pediu. */
          if (elVivo) elVivo.textContent = PERIODOS[i].anuncio + '. Patrimônio consolidado ' +
                                           moeda(PERIODOS[i].total) + '.';
        });
      })(k);
    }

    /* ------------------------------------------------------------ partida */

    /* Com movimento reduzido a dobra nasce inteira e parada: valores finais,
       formas prontas, nenhum laço, nenhum observador para desligar depois. */
    if (reduz) { pintar(0, false); return; }

    /* Sem movimento reduzido, o painel começa VAZIO de formas e com o número
       em zero — atrás do [data-rv] do core, que ainda não revelou nada. */
    escreverTexto(PERIODOS[0]);
    escreverTotal(0);
    semTransicao(function () { zerarFormas(); });

    var primeira = true;
    var io = new IntersectionObserver(function (entradas) {
      for (var i = 0; i < entradas.length; i++) {
        dentro = entradas[i].isIntersecting;
        if (dentro) {
          if (primeira) { primeira = false; pintar(0, true); }
          ligar();
        } else {
          desligar();
        }
      }
    }, { threshold: .15 });

    io.observe(raiz);
  }

  for (var r = 0; r < raizes.length; r++) iniciar(raizes[r]);
})();

Uma dobra inteira que encena o portal do cliente. Um painel de vidro com barra de aplicativo — nome do relatório de um lado, abas de janela do outro — e dentro dele quatro peças que respiram juntas: o patrimônio consolidado num número grande que conta ao entrar na tela, uma rosca de cinco classes de ativo com a leitura em texto ao lado, cinco barras horizontais por instituição e a linha da janela desenhando da esquerda para a direita. Trocar de janela (7d · 30d · 12m) recalcula as quatro: o número reconta, a rosca reanima, as barras recrescem e a linha redesenha.

Resolve a pergunta que decide a conversa numa página de assessoria — o que exatamente eu recebo todo mês? — sem prometer nada e sem pedir nada. Uma lista de entregas afirma; um painel funcionando mostra. É o irmão maior do conteudo/trio-cards-ui-simulada: aquele prova três recursos em três cards pequenos, este dedica a dobra inteira a um só, com a informação respirando. Não é ferramenta de entrada de dados como as irmãs desta leva — ninguém digita nada aqui, e por isso ele funciona igualmente bem no meio da página, entre duas dobras de texto.


Quando usar

Quando não usar


Padrão editorial

Slot Regra
Olho Duas palavras nomeando o lugar, não o benefício: "Portal do cliente", "Sua área".
Título da dobra Uma frase curta com um <em> no meio. É o único itálico de marca da dobra.
Apoio Duas ou três linhas. Diz quando o relatório chega e o que ele reúne — as duas informações que o painel não consegue mostrar sozinho.
Nome do relatório (barra) Duas ou três palavras. É rótulo de aplicativo, não título: "Relatório consolidado".
Pílula da barra Fica em ilustrativo até alguém da área de compliance decidir outra coisa. Ver ## Compliance.
Abas Três, da janela mais curta à mais longa, em forma abreviada (7d, 30d, 12m). O aria-label de cada uma traz a forma por extenso.
Rótulo do número Uma linha em caixa alta. É o que o número é, não o que ele significa.
Variação Percentual sempre com a janela escrita ao lado, na mesma linha. Nunca sozinho.
Fechamento Data por extenso, no pé da célula. É o que impede a leitura "isto é hoje".
Classes de ativo Cinco, sempre cinco: menos que isso a rosca vira pizza, mais que isso os degraus da série deixam de se distinguir. Rótulo de uma linha ("Renda fixa", "Ações e fundos").
Instituições Quatro ou cinco, ordenadas do maior para o menor saldo. Marca fictícia, sempre.
Leitura da linha Mínima, máxima e o número de marcações. É a versão em texto do gráfico — quem usa leitor de tela recebe isto, não a palavra "gráfico".

Os três textos de janela (janela, fechamento, anuncio) são um trio: mudou um, mudaram os três. É o único lugar onde a tabela do script guarda prosa.

Especificação dos assets

Nenhum. Não há foto, não há ícone de terceiro, não há fonte de terceiro. O único desenho é o glifo de quatro barras da marca do aplicativo, feito em SVG de traço aqui mesmo — trocá-lo pelo logotipo do cliente é a única substituição de asset prevista, e ela cabe no mesmo quadrado de 28px.


Compliance

Tudo neste painel é ilustrativo. O bloco vem marcado com <!-- CONTEÚDO ILUSTRATIVO --> no markup, a pílula ilustrativo fica visível na barra de topo e a ressalva mora no pé do próprio painel, em --fg-muted, ao lado do que ela cobre — não em letra miúda no rodapé da página.

Antes de publicar, o consumidor tem de:

  1. Trocar a tabela PERIODOS no topo do script.js e o período padrão escrito no markup.html pelos números que a casa quiser mostrar — e manter as cinco invariantes que o comentário da tabela lista (mix soma 100, mixValor e inst somam total, serie termina em total, variacao é a razão entre o primeiro e o último ponto da série).
  2. Decidir a redação da ressalva com a área de compliance do cliente. A que vem no arquivo é um piso, não uma aprovação: "Painel ilustrativo. Os números não representam carteira real nem resultado de cliente. Variação de janela é fato passado e não garante resultado futuro."
  3. Conferir as instituições. As marcas do arquivo (Meridian, Vantor, Quadrant, Altamira, Kessler; sobram Northbay e Solvane) são fictícias e desenhadas para a biblioteca. Nome de banco ou de corretora real é marca registrada e não atravessa.
  4. Manter a janela ao lado de todo percentual de variação. Variação de período é fato passado de um período nomeado; sozinha, ela lê como rentabilidade prometida.

O que a peça não faz, e não deve passar a fazer: não projeta, não recomenda, não coleta nome, e-mail nem telefone, e não grava nada em localStorage. Se a dobra precisar terminar em captura, ela aponta para conversao/cta-formulario-lead — o painel não vira formulário.


Como usar

<link rel="stylesheet" href="componentes/dados/painel-patrimonio/estilo.css">
<script src="componentes/dados/painel-patrimonio/script.js"></script>

A seção declara sec-deep; o painel é a primitiva .glass do core com o desfoque zerado. O período padrão inteiro está escrito no markup.html — valor, variação, stroke-dasharray das cinco fatias, --wealth-w das cinco barras e o d da linha. Ele é a linha 0 da tabela do script, e as duas cópias precisam bater.

Ganchos

Gancho Papel
[data-wealth] Raiz. O script parte daqui e nunca do documento — a dobra pode repetir na página.
[data-wealth-panel] O painel. Recebe .is-live quando o script sobe: é essa classe que revela as abas.
[data-wealth-tab] · [data-wealth-tabs] As três abas e o grupo delas.
[data-wealth-live] Região aria-live que anuncia a janela nova. Só é preenchida no clique.
[data-wealth-total] · [data-wealth-delta] · [data-wealth-delta-lab] · [data-wealth-close] Número, variação, texto da janela e data de fechamento.
[data-wealth-arcs] O <g> girado −90° da rosca. Os cinco filhos dele são as fatias, e a posição de cada uma escolhe a cor.
[data-wealth-mix] · [data-wealth-pct] · [data-wealth-val] A lista da rosca e os campos que mudam nela.
[data-wealth-inst] · [data-wealth-fill] A lista de instituições e a barra de cada linha.
[data-wealth-spark] · [data-wealth-spark-path] · [data-wealth-spark-end] · [data-wealth-spark-min] · [data-wealth-spark-max] A caixa da linha, o caminho e os três valores escritos.
[data-wealth-ping] O ponto de sincronia. É o único elemento que o laço escreve a cada quadro.
--wealth-radius · --wealth-cell-pad Canto do painel (segue --radius-media) e respiro de cada célula (clamp(18px,2vw,28px)).
--wealth-donut · --wealth-spark-h Lado da rosca (132px) e altura da caixa da linha (96px).
--wealth-bar-h · --wealth-bar-radius · --wealth-inst-name · --wealth-inst-val Espessura (8px), canto, coluna do nome (82px) e do valor (92px) das barras.
--wealth-grow · --wealth-draw Duração da fatia e da barra (.9s) e do traçado da linha (1.4s). Só o CSS lê.
--wealth-count-dur · --wealth-ping-hold Contagem (1.1s) e volta do anel (2.9s), em segundos: o script lê com parseFloat.
--wealth-s1 … --wealth-s5 Os cinco degraus da série. Fatia e bolinha da legenda leem o mesmo.
--wealth-w · --wealth-spark-y · --wealth-ping Escritos pelo script: proporção da barra, altura do ponto final da linha, volta do anel (0 a 1).
.is-live · .is-still · .is-on Script presente · transições suspensas por um quadro · aba ativa.

Variações


Gotchas


Histórico

Projeto Onde Data Diferenças
autoral construído do zero na biblioteca 2026-09-03 —

Origem. Não há referência única. O padrão — painel de portal do cliente, com seletor de janela e um número consolidado grande — é recorrente em três famílias de site de instituição financeira: os portais de private banking e multi-family office, os agregadores de carteira e os painéis de fintech de investimento. Da forma comum a elas vieram quatro decisões: a barra de aplicativo com identidade à esquerda e seletor à direita; o número grande com variação e data de fechamento embaixo; a rosca de alocação sempre acompanhada da leitura em lista; e a divisória de 1px entre as células, sem vão — vão entre células daria cartões flutuando dentro de um cartão.

Decisões de tradução para o contrato.

Divergências conhecidas.