TOC — Table of Contents lateral

Menu flutuante no canto superior direito com links âncora pra cada seção (<h2>) da página. Tem botão hamburger ↔ X pra colapsar, animações em cascata na entrada e gera automaticamente os itens Topo e Rodapé.

Vê o componente em ação em páginas com muitos <h2> — o menu fica no canto superior direito quando a tela é grande.

Como usar — pra quem vai aplicar numa página
0:00 / 0:00

Quando usar

Usar em:

  • Páginas longas com scroll grande
  • 3+ seções marcadas com <h2>
  • Documentação técnica, tutoriais extensos, páginas de referência

Não usar em:

  • Páginas curtas (1-2 telas)
  • Hub de cards (home-grid) — não faz sentido
  • Páginas com info-accordion dominante — já existe o #sidenav do layout pra esse caso (é o que esta página usa, aliás)

Inserção mínima

Em qualquer página da documentação, adicione uma única linha no topo do <body>:

<nav class="page-toc"></nav>

Pronto. O layout.js faz o resto: escaneia os <h2>, gera os links, insere "Topo" e "Rodapé", injeta o botão toggle, aplica scroll-margin-top.

Atributos opcionais nos títulos

Pra customizar o texto no TOC sem alterar o <h2> visível:

<h2 id="plain" data-toc-title="Plain" data-sub="Texto SQL puro">
    Plain — texto SQL puro
</h2>
Atributo Efeito
data-toc-title Texto que aparece no TOC (se diferente do <h2>)
data-sub Subtítulo menor em cinza claro embaixo do título
id Âncora do link. Se omitido, o JS gera a partir do texto

Override manual

Se a página quiser controle total (listar seções que não são <h2>, por exemplo), coloca o conteúdo dentro do <nav>:

<nav class="page-toc">
    <div class="page-toc-title">Minhas seções</div>
    <ul>
        <li><a href="#secao-1">Seção customizada</a></li>
    </ul>
</nav>

O JS detecta que já tem filhos e preserva.

Breakpoints

Largura Comportamento
> 1440px Menu aberto por default. Clicar num link mantém aberto (convive com o conteúdo lado a lado).
641–1440px Menu fechado por default. Hamburger visível; abrir sobrepõe conteúdo. Clicar num link fecha o menu sozinho.
≤ 640px Hamburger e menu somem totalmente (smartphone).
Como funciona por dentro — pra quem quer entender a mecânica
0:00 / 0:00

Onde mora o código

O componente ficou dividido em duas partes reutilizáveis:

Parte Arquivo
CSS — estilos, transições, animações, breakpoints apoio/documentacao/css/docs.css → bloco "PAGE TOC"
JS — detecção, auto-gen a partir dos h2, listeners apoio/documentacao/js/layout.js → função initPageToc(inner)

Nenhum HTML inline da página precisa ter <style> ou <script> próprio pro TOC. Tudo mora central.

Fluxo do initPageToc(inner)

Quando o layout.js termina de renderizar a página, ele chama a função junto dos outros inits. O fluxo passo a passo:

  1. document.querySelector('.page-toc') — se não tem o <nav>, aborta
  2. Se o <nav> já tem filhos, aborta (preserva override manual)
  3. inner.querySelectorAll('h2') — se tem menos de 2, aborta
  4. Injeta "Topo" como primeiro <li> (<a href="#" data-toc-top="1">)
  5. Pra cada h2: gera id (se não tiver) e aplica scroll-margin-top: 80px
  6. data-toc-title (fallback = texto do h2) e data-sub (opcional)
  7. Injeta "Rodapé" como último <li> (<a href="#" data-toc-bottom="1">)
  8. Calcula animationDelay de cada <li> (0.25s base + 0.05s por item)
  9. Cria o <button class="page-toc-toggle active"> com 2 SVGs (menu + close)
  10. Listeners nos links:
    • data-toc-topscrollTo({ top: 0 })
    • data-toc-bottomscrollTo({ top: scrollHeight })
    • Em tela ≤ 1440px → fecha o menu automaticamente
  11. Se window.innerWidth ≤ 1440px, inicia com .collapsed (menu escondido por default em tela menor)
  12. Insere o botão antes do nav no DOM

As 4 animações coreografadas

O que dá personalidade ao componente são 4 animações que tocam em sequência:

1. Entrada da barra
translateX(calc(100% + 40px)) → 0 + opacity: 0 → 1, 0.3s. A barra desliza de fora da tela pra dentro.

2. Links em cascata
Cada <li> entra com translateX(16px → 0) + fade, 0.3s. Os delays são aplicados via JS: 0.25s no primeiro (pra a barra terminar primeiro) e 0.05s de intervalo entre cada item seguinte.

3. Cross-fade X ↔ hamburger
Os dois SVGs ficam sobrepostos (position: absolute). Quando o estado muda:

  • Ícone saindo: opacity 1 → 0 + rotate 0 → 90° (gira pra fora)
  • Ícone entrando: opacity 0 → 1 + rotate -90° → 0 (gira pra dentro)

4. Linhas do hamburger crescendo
As 3 <line> do SVG do hamburger têm transform-origin: left center. Quando aparecem, crescem com scaleX(0 → 1) em cascata — delays 0s, 0.1s, 0.2s. Visualmente: linhas sendo "desenhadas" da esquerda pra direita, uma por vez.

Decisões de design

Por que position: fixed em vez de sticky?
Sticky depende de um container pai. Como o layout injeta o conteúdo dentro de #content-inner com max-width, sticky criaria alinhamento inconsistente em páginas largas. Fixed garante posicionamento igual em qualquer página.

Por que largura fixa de 220px?
Texto em 2 linhas (título + subtítulo) precisa de ~200px pra respirar sem quebra estranha. Mais que isso rouba espaço do conteúdo principal em telas 1280–1440px.

Por que breakpoint 1440px?
Em tela menor, a janela não sobra largura suficiente pro menu de 220px conviver lado a lado com o conteúdo sem sobrepor. Acima de 1440px cabe permanentemente — menu fica aberto. Abaixo, menu começa fechado mas hamburger continua visível pra abrir sob demanda.

Por que menu fecha ao clicar num link em tela ≤1440?
UX de mobile menu. Se o menu sobrepõe conteúdo, depois de escolher a seção ele precisa sumir pra revelar o que o usuário foi ver.

Por que 0.25s de delay no primeiro link?
A barra tem transition: transform 0.3s ease. Se os links começassem em 0s, apareceriam no meio da entrada da barra — visualmente bagunçado. 0.25s dá tempo da barra chegar perto do destino antes.

Por que os links entram da direita pra esquerda?
A barra entra do lado direito. Os links seguindo a mesma direção reforçam o fluxo visual do canto da tela. Se entrassem de cima pra baixo, pareceria que vieram de lugar diferente.

Por que "Topo" e "Rodapé" com href="#" + preventDefault?
Se fossem href="#topo" / href="#rodape", sujariam a URL com âncoras falsas. Atributos data-toc-top / data-toc-bottom marcam os elementos pro handler reconhecer e chamar scrollTo() sem alterar o endereço.

Por que z-index 60 e 61?
O audio-player é sticky com z-index: 50. Pro TOC não ficar atrás dele quando abre sobreposto, precisa ser maior. Botão (61) sempre acima do próprio menu (60) pra permanecer clicável.

Por que data-sub em vez de parsing do "—" no texto?
Parsing heurístico é frágil — qualquer <h2> com travessão por outro motivo viraria subtítulo por engano. data-sub é explícito e deixa o autor decidir.

Histórico de iterações

Construído em 2026-04-23 em um único dia. Começou como CSS inline em uma página e evoluiu por iteração até virar componente centralizado:

  1. v0 — CSS inline, 9 links hardcoded na página
  2. v0.1 — animação de fade + translateY dos links
  3. v0.2 — hamburger toggle com display none/block
  4. v0.3 — animação sequencial das 3 linhas do hamburger
  5. v0.4 — delay de 0.25s no primeiro link (pra barra terminar antes)
  6. v0.5 — translateY trocado por translateX (direita → esquerda)
  7. v0.6 — linha separadora entre items + cross-fade X ↔ hamburger
  8. v1.0 — migração pra docs.css + layout.js com auto-gen dos h2
  9. v1.1 — breakpoints reformulados (1440 / 640)
  10. v1.2 — z-index 60/61 pra ficar acima do audio-player sticky
  11. v1.3 — font-weight 700 no título + auto-fechar ao clicar em tela pequena
  12. v1.4 — itens Topo e Rodapé adicionados automaticamente (presente)