Skip to content

Commit 09eb585

Browse files
ronifabioclaude
andcommitted
Adiciona sticky footer e vazamento, e offcanvas na navbar
Sticky footer: dois sliders criam a condicao em que o bug aparece — tela alta e pouco conteudo — e as pecas da solucao ligam e desligam uma a uma. min-vh-100 sozinho nao resolve nada, e ver isso na mao e o ponto do simulador. Vazamento horizontal: cinco causas classicas ligaveis e um detector que faz a mesma conta do DevTools, comparando a borda direita de cada elemento com a do container. Ele aponta o culpado pelo nome, o que ensina a tecnica e nao so o fato. Navbar: ganhou a escolha entre collapse e offcanvas, com a cascata responsiva da gaveta replicada. Aqui a direcao se inverte em relacao ao resto do laboratorio: o Bootstrap escreve essas regras em @media (max-width), entao no nivel xs tudo vira gaveta e cada nivel alcancado devolve aquele breakpoint ao fluxo normal. O motor passou a aplicar scale() sempre, mesmo valendo 1: um elemento transformado vira bloco de contencao dos filhos com position: fixed, e e isso que mantem a gaveta preta ao palco em vez de cobrir a pagina inteira do laboratorio. Seis problemas que so apareceram testando no navegador: - flex-grow-1 e mt-auto davam exatamente o mesmo resultado visual, porque o <main> nao tinha fundo proprio. A diferenca entre os dois E quem fica com a sobra: no primeiro o main engorda, no segundo a sobra vira margem. Agora o main tem fundo e borda, e cada opcao de empurrao explica o que faz. - A tela do sticky footer era larga demais: cada paragrafo cabia em uma linha e o conteudo nunca transbordava, entao o terceiro caso, o de rolagem, era inalcancavel. - Os dois diagnosticos mediam dentro de requestAnimationFrame, ou seja, no meio de uma transicao de 0.3s a 0.4s. O veredito saia uma jogada atrasado. Virou uma espera ate o layout se acalmar, que de quebra freia o slider. - O detector de vazamento acusava a tabela mesmo depois de corrigida. Faltava parar de descer em elementos com rolagem propria — que e exatamente o que .table-responsive faz. - A gaveta abria no lugar errado porque o HTML levava a classe .offcanvas junto com .offcanvas-lg. Com as duas, a regra .navbar-expand-lg .offcanvas do Bootstrap forca visibility e transform com !important sempre que o navegador de verdade passa de 992px. A variante responsiva leva so a classe com breakpoint. - Sem a .offcanvas avulsa, o botao de fechar parava de funcionar: o Bootstrap procura a gaveta com closest('.offcanvas'). A propria documentacao resolve com data-bs-target explicito no botao. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent a1128a6 commit 09eb585

12 files changed

Lines changed: 1685 additions & 62 deletions

File tree

‎index.html‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,47 @@ <h3 class="h5 fw-bold">Formulário Responsivo</h3>
212212
</div>
213213
</div>
214214
</div>
215+
216+
<!-- ============================================ -->
217+
<!-- ARMADILHAS COMUNS -->
218+
<!-- ============================================ -->
219+
<h2 class="h4 fw-bold mb-1 mt-5">Armadilhas comuns</h2>
220+
<p class="texto-fraco mb-3">
221+
Os dois bugs que quase toda entrega tem — e como achar o culpado.
222+
</p>
223+
224+
<div class="row g-4">
225+
<div class="col-12 col-md-6 col-lg-4">
226+
<div class="card-simulador painel h-100 p-4 d-flex flex-column">
227+
<span class="badge text-bg-danger align-self-start mb-3">12</span>
228+
<h3 class="h5 fw-bold">Sticky Footer</h3>
229+
<p class="texto-fraco small flex-grow-1">
230+
O rodapé que boia no meio da tela quando a página tem pouco
231+
conteúdo. Ligue e desligue <code>min-vh-100</code>,
232+
<code>flex-grow-1</code> e <code>mt-auto</code> para ver qual
233+
peça faz o quê.
234+
</p>
235+
<a href="./src/simuladores/sticky-footer/index.html" class="btn btn-primary w-100">
236+
Abrir simulador
237+
</a>
238+
</div>
239+
</div>
240+
241+
<div class="col-12 col-md-6 col-lg-4">
242+
<div class="card-simulador painel h-100 p-4 d-flex flex-column">
243+
<span class="badge text-bg-danger align-self-start mb-3">13</span>
244+
<h3 class="h5 fw-bold">Vazamento Horizontal</h3>
245+
<p class="texto-fraco small flex-grow-1">
246+
A barra de rolagem lateral que só aparece no celular. Ligue cada
247+
causa clássica e veja o simulador apontar o elemento culpado,
248+
como você faria no DevTools.
249+
</p>
250+
<a href="./src/simuladores/vazamento/index.html" class="btn btn-primary w-100">
251+
Abrir simulador
252+
</a>
253+
</div>
254+
</div>
255+
</div>
215256
</main>
216257

217258
<footer class="container py-4 border-top border-secondary-subtle">

‎src/js/motor.js‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,25 @@ export function animarMudanca(elementos, mudar) {
6464
});
6565
}
6666

67+
/**
68+
* Adia uma medição até o layout parar de se mexer.
69+
*
70+
* As transições do laboratório duram de 0.3s a 0.4s, e durante esse
71+
* tempo getBoundingClientRect devolve a posição do meio da animação.
72+
* Medir cedo demais produz diagnóstico errado — normalmente com a
73+
* cara de "o aviso está uma jogada atrasado".
74+
*
75+
* Também serve de freio para o slider, que dispara dezenas de
76+
* eventos por segundo enquanto o aluno arrasta.
77+
*/
78+
export function aposAcalmar(fn, espera = 450) {
79+
let id;
80+
return (...args) => {
81+
clearTimeout(id);
82+
id = setTimeout(() => fn(...args), espera);
83+
};
84+
}
85+
6786
/**
6887
* Liga o slider de largura ao palco.
6988
*
@@ -96,7 +115,13 @@ export function criarPalco({
96115
/** Encaixa o palco na tela real quando ele é maior. */
97116
function ajustarZoom(largura) {
98117
const escala = Math.min(1, janela.clientWidth / largura);
99-
palco.style.transform = escala < 1 ? `scale(${escala})` : '';
118+
119+
// O scale fica aplicado mesmo valendo 1. Além de ser inofensivo
120+
// (scale(1) não muda nada visualmente), um elemento transformado
121+
// vira o bloco de contenção dos filhos com position: fixed — é o
122+
// que mantém um offcanvas preso ao palco em vez de cobrir a
123+
// página inteira do laboratório.
124+
palco.style.transform = `scale(${escala})`;
100125
janela.style.height = `${palco.offsetHeight * escala}px`;
101126
return escala;
102127
}

‎src/simuladores/navbar/index.html‎

Lines changed: 56 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,18 @@ <h3 class="h6 fw-bold mb-2 mt-4">O que muda quando ela expande</h3>
134134
</button>
135135
</div>
136136

137+
<p class="text-uppercase small fw-semibold texto-fraco mb-2 mt-4">
138+
Como o menu se esconde
139+
</p>
140+
<div class="d-flex flex-wrap gap-2" data-tipo>
141+
<button type="button" class="btn btn-sm btn-outline-light active" data-valor="collapse">
142+
collapse · empurra para baixo
143+
</button>
144+
<button type="button" class="btn btn-sm btn-outline-light" data-valor="offcanvas">
145+
offcanvas · gaveta lateral
146+
</button>
147+
</div>
148+
137149
<p class="mt-3 mb-0" data-veredito></p>
138150
</section>
139151

@@ -144,40 +156,54 @@ <h3 class="h6 fw-bold mb-2 mt-4">O que muda quando ela expande</h3>
144156
</p>
145157

146158
<div class="palco-janela" data-janela>
147-
<div class="palco" data-palco data-bp="xl">
148-
<nav class="navbar navbar-expand-lg bg-dark rounded" data-navbar>
149-
<div class="container-fluid">
150-
<a class="navbar-brand" href="#">MinhaPágina</a>
151-
152-
<button
153-
class="navbar-toggler"
154-
type="button"
155-
data-bs-toggle="collapse"
156-
data-bs-target="#menu"
157-
aria-controls="menu"
158-
aria-expanded="false"
159-
aria-label="Abrir menu"
160-
>
161-
<span class="navbar-toggler-icon"></span>
162-
</button>
163-
164-
<div class="collapse navbar-collapse" id="menu">
165-
<ul class="navbar-nav me-auto">
166-
<li class="nav-item">
167-
<a class="nav-link active" href="#">Início</a>
168-
</li>
169-
<li class="nav-item"><a class="nav-link" href="#">Cursos</a></li>
170-
<li class="nav-item"><a class="nav-link" href="#">Sobre</a></li>
171-
<li class="nav-item"><a class="nav-link" href="#">Contato</a></li>
172-
</ul>
173-
<a class="btn btn-primary btn-sm" href="#">Entrar</a>
174-
</div>
175-
</div>
176-
</nav>
159+
<div class="palco palco--pagina" data-palco data-bp="xl">
160+
<!-- A navbar é remontada pelo JS conforme o tipo de menu -->
161+
<div data-navbar-wrapper></div>
162+
163+
<div class="miolo">
164+
<h3 class="h6 mb-2">Conteúdo da página</h3>
165+
<p class="small texto-fraco mb-0">
166+
Está aqui só para dar altura à tela — é sobre ele que a gaveta
167+
do offcanvas passa por cima.
168+
</p>
169+
</div>
177170
</div>
178171
</div>
179172
</section>
180173

174+
<!-- Comparação -->
175+
<section class="painel p-4 mb-4">
176+
<h2 class="h6 fw-bold mb-3">Collapse ou offcanvas?</h2>
177+
<div class="table-responsive">
178+
<table class="table table-sm align-middle mb-0">
179+
<thead>
180+
<tr>
181+
<th></th>
182+
<th><code>collapse</code></th>
183+
<th><code>offcanvas</code></th>
184+
</tr>
185+
</thead>
186+
<tbody>
187+
<tr>
188+
<td class="texto-fraco">Como aparece</td>
189+
<td class="texto-fraco">Desce e empurra o conteúdo para baixo</td>
190+
<td class="texto-fraco">Desliza por cima, como uma gaveta</td>
191+
</tr>
192+
<tr>
193+
<td class="texto-fraco">Classe responsiva</td>
194+
<td class="mono texto-fraco">— (basta o navbar-expand-*)</td>
195+
<td class="mono texto-fraco">offcanvas-lg, casada com o expand</td>
196+
</tr>
197+
<tr>
198+
<td class="texto-fraco">Boa para</td>
199+
<td class="texto-fraco">Menus curtos, 3 ou 4 links</td>
200+
<td class="texto-fraco">Menus longos, com seções e busca</td>
201+
</tr>
202+
</tbody>
203+
</table>
204+
</div>
205+
</section>
206+
181207
<!-- Código gerado -->
182208
<section class="painel p-4">
183209
<h2 class="h6 fw-bold mb-3">A classe que está valendo</h2>

‎src/simuladores/navbar/navbar.js‎

Lines changed: 115 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,128 @@
11
/**
22
* Simulador de Navbar Responsiva — Bootstrap 5
33
*
4-
* Dois controles: a largura da tela e o breakpoint de expansão. O aluno
5-
* cruza os dois e descobre a regra — expande a partir de, não colapsa em.
4+
* Três controles: a largura da tela, o breakpoint de expansão e o
5+
* jeito de esconder o menu. O aluno cruza os dois primeiros e
6+
* descobre a regra — expande a partir de, não colapsa em.
67
*/
78
import './navbar.scss';
8-
// o colapso de verdade, para o botão hambúrguer funcionar
9+
// o colapso e a gaveta de verdade, para o botão hambúrguer funcionar
910
import 'bootstrap/js/dist/collapse.js';
11+
import 'bootstrap/js/dist/offcanvas.js';
1012
import { criarPalco, posicaoDe } from '../../js/motor.js';
1113

12-
const navbar = document.querySelector('[data-navbar]');
14+
const wrapper = document.querySelector('[data-navbar-wrapper]');
1315
const veredito = document.querySelector('[data-veredito]');
1416
const codigo = document.querySelector('[data-codigo]');
15-
const grupoExpand = document.querySelector('[data-expand]');
1617

1718
let expand = 'lg';
19+
let tipo = 'collapse';
1820

1921
const saida = {
2022
largura: document.querySelector('[data-largura]'),
2123
sigla: document.querySelector('[data-bp-sigla]'),
2224
regra: document.querySelector('[data-bp-regra]'),
2325
};
2426

25-
/** Troca a classe navbar-expand-* mantendo o resto intacto. */
26-
function aplicarExpand() {
27-
navbar.className = navbar.className
28-
.split(' ')
29-
.filter((c) => !c.startsWith('navbar-expand-'))
30-
.concat(`navbar-expand-${expand}`)
31-
.join(' ');
27+
const LINKS = ['Início', 'Cursos', 'Sobre', 'Contato'];
28+
29+
/**
30+
* Monta a navbar do zero.
31+
*
32+
* Os dois tipos compartilham a estrutura; o que muda é o invólucro
33+
* do menu e o data-bs-toggle do botão. Como o Bootstrap escuta o
34+
* clique por delegação no documento, trocar o HTML inteiro funciona
35+
* sem precisar reinicializar nada na mão.
36+
*/
37+
function montarNavbar() {
38+
const itens = LINKS.map(
39+
(texto, i) => `
40+
<li class="nav-item">
41+
<a class="nav-link ${i === 0 ? 'active' : ''}" href="#">${texto}</a>
42+
</li>`
43+
).join('');
44+
45+
const botao = `
46+
<button class="navbar-toggler" type="button"
47+
data-bs-toggle="${tipo}" data-bs-target="#menu"
48+
aria-controls="menu" aria-label="Abrir menu">
49+
<span class="navbar-toggler-icon"></span>
50+
</button>`;
51+
52+
// Duas coisas importam neste HTML:
53+
//
54+
// 1. A gaveta responsiva leva SÓ a classe offcanvas-lg, sem a
55+
// .offcanvas avulsa. Com as duas, a regra .navbar-expand-lg
56+
// .offcanvas do Bootstrap — que existe para o caso NÃO
57+
// responsivo — força visibility e transform com !important
58+
// sempre que o navegador de verdade passa de 992px, e a gaveta
59+
// aparece aberta no lugar errado.
60+
//
61+
// 2. No offcanvas o backdrop do Bootstrap seria anexado ao body e
62+
// cobriria o laboratório inteiro, não o palco. Desligá-lo é
63+
// opção legítima do componente e mantém a gaveta no palco.
64+
const menu =
65+
tipo === 'offcanvas'
66+
? `
67+
<div class="offcanvas-${expand} offcanvas-end" id="menu"
68+
data-bs-backdrop="false" data-bs-scroll="true"
69+
tabindex="-1" aria-labelledby="menu-titulo">
70+
<div class="offcanvas-header">
71+
<h5 class="offcanvas-title" id="menu-titulo">Menu</h5>
72+
<!-- o data-bs-target aqui não é enfeite: sem a classe
73+
.offcanvas avulsa, o botão de fechar do Bootstrap não
74+
acha sozinho a gaveta a que pertence -->
75+
<button type="button" class="btn-close"
76+
data-bs-dismiss="offcanvas" data-bs-target="#menu"
77+
aria-label="Fechar"></button>
78+
</div>
79+
<div class="offcanvas-body">
80+
<ul class="navbar-nav flex-grow-1">${itens}</ul>
81+
<a class="btn btn-primary btn-sm" href="#">Entrar</a>
82+
</div>
83+
</div>`
84+
: `
85+
<div class="collapse navbar-collapse" id="menu">
86+
<ul class="navbar-nav me-auto">${itens}</ul>
87+
<a class="btn btn-primary btn-sm" href="#">Entrar</a>
88+
</div>`;
89+
90+
wrapper.innerHTML = `
91+
<nav class="navbar navbar-expand-${expand} bg-dark rounded">
92+
<div class="container-fluid">
93+
<a class="navbar-brand" href="#">MinhaPágina</a>
94+
${botao}
95+
${menu}
96+
</div>
97+
</nav>`;
3298
}
3399

34-
grupoExpand.addEventListener('click', (evento) => {
35-
const botao = evento.target.closest('button');
36-
if (!botao) return;
100+
// -------------------------------------------------------------
101+
// Controles
102+
// -------------------------------------------------------------
103+
function ligarGrupo(seletor, aoEscolher) {
104+
const grupo = document.querySelector(seletor);
37105

38-
grupoExpand
39-
.querySelectorAll('button')
40-
.forEach((b) => b.classList.remove('active'));
41-
botao.classList.add('active');
106+
grupo.addEventListener('click', (evento) => {
107+
const botao = evento.target.closest('button');
108+
if (!botao) return;
42109

43-
expand = botao.dataset.valor;
44-
aplicarExpand();
45-
atualizar();
46-
});
110+
grupo.querySelectorAll('button').forEach((b) => b.classList.remove('active'));
111+
botao.classList.add('active');
112+
113+
aoEscolher(botao.dataset.valor);
114+
montarNavbar();
115+
atualizar();
116+
});
117+
}
118+
119+
ligarGrupo('[data-expand]', (v) => (expand = v));
120+
ligarGrupo('[data-tipo]', (v) => (tipo = v));
121+
122+
// -------------------------------------------------------------
123+
// Palco
124+
// -------------------------------------------------------------
125+
montarNavbar();
47126

48127
const { atualizar } = criarPalco({
49128
slider: document.getElementById('largura'),
@@ -58,21 +137,26 @@ const { atualizar } = criarPalco({
58137
bp.min === 0 ? 'padrão, sem media query' : `a partir de ${bp.min}px`;
59138

60139
const expandida = posicaoDe(bp.sigla) >= posicaoDe(expand);
140+
const nome = tipo === 'offcanvas' ? 'a gaveta' : 'o menu';
61141

62142
veredito.innerHTML = expandida
63143
? `<span class="veredito veredito--expandida">
64144
Expandida — a tela alcançou <code>${expand}</code>, os links aparecem em linha
65145
</span>`
66146
: `<span class="veredito veredito--colapsada">
67-
Colapsada — a tela ainda não chegou em <code>${expand}</code>, então vira hambúrguer
147+
Colapsada — a tela ainda não chegou em <code>${expand}</code>, então ${nome} fica atrás do botão
68148
</span>`;
69149

70-
codigo.innerHTML =
71-
`<span class="tag">&lt;nav class=</span>` +
72-
`"<span class="destaque">navbar navbar-expand-${expand}</span>"` +
73-
`<span class="tag">&gt;</span>`;
150+
const classeMenu =
151+
tipo === 'offcanvas'
152+
? `offcanvas-${expand} offcanvas-end`
153+
: 'collapse navbar-collapse';
154+
155+
codigo.innerHTML = [
156+
`<span class="tag">&lt;nav class=</span>"<span class="destaque">navbar navbar-expand-${expand}</span>"<span class="tag">&gt;</span>`,
157+
` <span class="tag">&lt;button class=</span>"<span class="destaque">navbar-toggler</span>" <span class="tag">data-bs-toggle=</span>"<span class="destaque">${tipo}</span>"<span class="tag">&gt;</span>`,
158+
` <span class="tag">&lt;div class=</span>"<span class="destaque">${classeMenu}</span>"<span class="tag">&gt;</span>`,
159+
`<span class="tag">&lt;/nav&gt;</span>`,
160+
].join('\n');
74161
},
75162
});
76-
77-
aplicarExpand();
78-
atualizar();

0 commit comments

Comments
 (0)