Skill: site vivo ocupando o slide inteiro
Insere num slide um site que ocupa o slide inteiro (edge to edge, sem moldura por padrão), via <iframe> full-bleed. Casos típicos: demonstrar o produto ao vivo, abrir a landing page durante o pitch, mostrar um dashboard, um CodePen, um mapa, uma doc.
O ponto difícil de embutir um site num deck não é o iframe, é a convivência com o deck: o iframe rouba o foco do teclado (quebra a navegação por seta) e muitos sites recusam ser incorporados. Esta skill resolve os dois.
Os dois problemas (e as soluções)
Foco / navegação. Assim que o iframe pega foco,
ArrowRight/ArrowDown/PageDownvão para o site, não para o deck, e o apresentador fica preso no slide. Solução: guarda de interação. Por padrão o site fica visível mas não clicável (uma camada transparente por cima). O apresentador clica para interagir; para voltar a navegar o deck, aperta Esc (ou clica fora). Simples e à prova de falha em qualquer navegador.Sites que bloqueiam incorporação. Google, YouTube (home), bancos, Instagram e muitos outros mandam
X-Frame-Options: DENYouContent-Security-Policy: frame-ancestorse aparecem em branco / "recusou conectar" dentro do iframe.Duas verdades duras, apuradas na prática (ver
dev/BUGS.md, B2):- O bloqueio é indetectável em runtime. Um iframe bloqueado e um iframe cross-origin
perfeitamente saudável lançam o mesmo
SecurityErrorao se tocar nocontentDocument. Não existe teste confiável dentro do navegador. Não tente. - Fallback ATRÁS do iframe não funciona. O iframe bloqueado não fica transparente: o Chrome desenha a própria página de erro, cinza e opaca, por cima. Quem está atrás nunca aparece. O usuário vê "recusou conectar" e mais nada.
Solução: decidir em tempo de AUTORIA, não em runtime. Quem monta o slide confere o domínio antes e declara o resultado no HTML:
curl -sI https://exemplo.com | grep -iE 'x-frame-options|content-security-policy'Se bloquear, marque o container com
data-blockede o fallback é promovido ACIMA do iframe (z-indexmaior), não atrás:.webview[data-blocked] iframe { display: none; } .webview[data-blocked] .webview-fallback { display: flex; } /* fica por cima, não atrás */Para apresentação crítica, o melhor continua sendo baixar o site para
assets/webview/<slug>/e apontar o iframe para o arquivo local: nunca bloqueia, funciona offline porfile://, e é o que combina com a ética offline-first do Mira.- O bloqueio é indetectável em runtime. Um iframe bloqueado e um iframe cross-origin
perfeitamente saudável lançam o mesmo
Antes de gerar, avise o usuário se o domínio for um dos que costumam bloquear e ofereça a opção local/offline.
Duas fontes possíveis para o site
- URL ao vivo (padrão).
<iframe src="https://...">. Depende de rede na hora da apresentação e de o site permitir incorporação. Ótimo para demos com internet e sites próprios. - Cópia local (recomendada para apresentação crítica / offline). Baixe a página para
assets/webview/<slug>/index.html(com seus assets) e apontesrc="assets/webview/<slug>/index.html". Nunca é bloqueado por X-Frame, roda porfile://, alinhado com a ética offline-first do Mira (vermira-offline). Guarde os arquivos só emassets/(diretiva de pastas do deck: a raiz do deck tem sóindex.htmle launchers).
REGRA ZERO (herdada, com ressalva do site)
Todo slide do Mira tem loop interno, mas o site fica firme: nada animado passa por cima do conteúdo do site. O loop interno vive na guarda de interação, e só enquanto o apresentador ainda não clicou:
- Pill "clique para interagir" pulsando (sobe e desce, com glow laranja).
- Borda respirando laranja por dentro da moldura do slide.
Ao clicar, a guarda some e o próprio site (vivo) passa a ser o movimento. Descreva o loop em uma frase ("enquanto não se clica, a borda respira laranja e a pill pulsa; ao clicar, o site assume").
Composição (o site toma o slide inteiro)
- Full-bleed: a
<section>do slide vai compadding: 0,height: 100vh,overflow: hidden. O iframe preencheinset: 0(100% x 100%),border: 0. Sem título nem chrome por padrão: o site é o slide. - Sem moldura visível por padrão. Se o usuário pedir contexto, ofereça uma barra de endereço fake opcional no topo (mostrando a URL), mas isso reduz a área do site; o padrão é edge to edge.
- Laranja (#FF904D) aparece só na guarda (pill + borda), nunca sobreposto ao site.
CSS + HTML canônico (gerar assim)
Substitua SLUG por um id curto único (ex.: demo-produto), URL pela fonte (link ou assets/webview/.../index.html) e TITULO_DESCRITIVO por um title acessível. As strings visíveis já vêm acentuadas: mantenha a acentuação (segue agents/_shared/idioma.md).
<style>
/* Slide de webview: o site ocupa o slide inteiro, sem moldura por padrao. */
.webview-slide {
position: relative;
width: 100%;
height: 100vh;
overflow: hidden;
background: #000;
padding: 0; /* full-bleed: o site vai edge to edge */
}
.webview-frame {
position: absolute; inset: 0;
width: 100%; height: 100%;
border: 0; display: block;
background: #fff;
pointer-events: none; /* bloqueado ate o apresentador clicar */
z-index: 1;
}
.webview-slide.interacting .webview-frame { pointer-events: auto; }
/* Fallback: fica ATRAS do iframe. So aparece se o site recusar
incorporacao (X-Frame-Options). NAO fica atras do iframe (a pagina de erro do Chrome e
opaca e cobriria o fallback): e promovido ACIMA dele via [data-blocked], declarado em
tempo de autoria depois de conferir o site no curl. */
.webview-fallback {
position: absolute; inset: 0; z-index: 0;
display: flex; flex-direction: column; align-items: center; justify-content: center;
gap: 14px; text-align: center; padding: 32px;
color: #fff; background: #0a0a0a;
font: 500 18px/1.4 system-ui, sans-serif;
}
.webview-fallback a { color: #FF904D; text-decoration: underline; font-weight: 700; }
/* Guarda de interacao: intercepta o clique para nao roubar a navegacao do
deck. Loop interno (REGRA ZERO) vive aqui, so enquanto nao se interage. */
.webview-guard {
position: absolute; inset: 0; z-index: 2;
display: flex; align-items: flex-end; justify-content: center;
padding-bottom: 28px; cursor: pointer; background: transparent;
transition: opacity .35s ease;
}
.webview-guard::after { /* borda que respira laranja por dentro */
content: ""; position: absolute; inset: 0; pointer-events: none;
animation: wv-border 3s ease-in-out infinite;
}
@keyframes wv-border {
0%,100% { box-shadow: inset 0 0 0 3px rgba(255,144,77,0.25), inset 0 0 40px rgba(255,144,77,0.10); }
50% { box-shadow: inset 0 0 0 3px rgba(255,144,77,0.60), inset 0 0 80px rgba(255,144,77,0.22); }
}
.webview-hint {
position: relative; z-index: 1;
font: 600 15px/1 system-ui, sans-serif; color: #fff;
background: rgba(10,10,10,0.72); backdrop-filter: blur(6px);
border: 1px solid rgba(255,144,77,0.55); border-radius: 999px;
padding: 10px 18px; display: inline-flex; align-items: center; gap: 8px;
animation: wv-pulse 2.4s ease-in-out infinite;
}
@keyframes wv-pulse {
0%,100% { transform: translateY(0); box-shadow: 0 0 0 rgba(255,144,77,0); }
50% { transform: translateY(-4px); box-shadow: 0 8px 30px rgba(255,144,77,0.35); }
}
.webview-slide.interacting .webview-guard { opacity: 0; pointer-events: none; }
</style>
<section class="webview-slide" id="webview-SLUG">
<!-- Fallback: aparece so se o site recusar incorporacao (X-Frame-Options) -->
<div class="webview-fallback">
<p>Este site não permite ser incorporado.</p>
<p><a href="URL" target="_blank" rel="noopener">Abrir em nova aba</a></p>
</div>
<iframe
class="webview-frame"
src="URL"
title="TITULO_DESCRITIVO"
loading="eager"
referrerpolicy="no-referrer-when-downgrade"
allow="fullscreen; autoplay; encrypted-media; clipboard-read; clipboard-write; geolocation *">
</iframe>
<!-- Guarda de interacao + loop interno (REGRA ZERO) -->
<div class="webview-guard" role="button" tabindex="0" aria-label="Clique para navegar no site">
<span class="webview-hint">Clique para navegar no site · Esc volta ao deck</span>
</div>
</section>
<script>
(function () {
var slide = document.getElementById('webview-SLUG');
if (!slide) return;
var guard = slide.querySelector('.webview-guard');
var frame = slide.querySelector('.webview-frame');
function enter() { slide.classList.add('interacting'); try { frame.focus(); } catch (e) {} }
function leave() { slide.classList.remove('interacting'); try { frame.blur(); } catch (e) {} try { window.focus(); } catch (e) {} }
guard.addEventListener('click', enter);
guard.addEventListener('keydown', function (e) {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); enter(); }
});
document.addEventListener('keydown', function (e) { if (e.key === 'Escape') leave(); });
/* Ponte de teclado (OBRIGATÓRIA): com o foco dentro do iframe, o deck
não ouve tecla nenhuma — Esc não sai e os modos E/P morrem. Em iframe
same-origin (cópia local), escuta lá dentro: Esc sai; E/P saem e
reenviam a tecla para o deck. Cross-origin não permite a ponte
(mais um motivo para preferir a cópia local). */
function typingInFrame(doc) {
var t = doc.activeElement;
return t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName));
}
function bindFrameKeys() {
try {
var doc = frame.contentDocument;
if (!doc || doc.__mwvKeys) return;
doc.__mwvKeys = true;
doc.addEventListener('keydown', function (e) {
if (e.key === 'Escape') { e.preventDefault(); leave(); return; }
if (/^[ep]$/i.test(e.key) && !typingInFrame(doc)) {
e.preventDefault(); e.stopPropagation();
leave();
document.dispatchEvent(new KeyboardEvent('keydown', { key: e.key, bubbles: true }));
}
}, true);
} catch (err) { /* cross-origin: sem ponte */ }
}
frame.addEventListener('load', bindFrameKeys);
bindFrameKeys();
/* Fora da interação o iframe nunca fica com o foco (sites roubam foco
no load); devolve o teclado ao deck. */
window.addEventListener('blur', function () {
setTimeout(function () {
if (!slide.classList.contains('interacting') && document.activeElement === frame) {
try { frame.blur(); } catch (e) {}
try { window.focus(); } catch (e) {}
}
}, 0);
});
})();
</script>
Por que funciona: a guarda (z-index 2, fundo transparente) deixa o site visível mas captura o clique, então o teclado do deck nunca é roubado sem intenção; ao clicar entra em interacting (iframe ganha pointer-events), e Esc sai. A camada de fallback (z-index 0) só aparece por baixo se o iframe vier em branco por bloqueio. A ponte de teclado garante que Esc/E/P continuam funcionando mesmo com o foco dentro do site — sem ela o deck fica surdo depois do primeiro clique no iframe.
Barra de endereço fake (opcional, só se o usuário pedir contexto)
Reduz a área do site em ~48px no topo. Use quando o apresentador quiser mostrar QUAL é a URL. Neste caso a .webview-frame vai com top: 44px em vez de inset: 0.
<div style="position:absolute;inset:0 0 auto 0;z-index:3;height:44px;display:flex;align-items:center;gap:10px;padding:0 16px;background:rgba(10,10,10,0.85);border-bottom:1px solid rgba(255,144,77,0.35);font:600 13px/1 system-ui;color:#fff">
<span style="width:10px;height:10px;border-radius:50%;background:#FF904D"></span>
<span style="opacity:.8">URL</span>
</div>
Passos
- Receber destino + fonte. Slide novo ou slide N do deck X, e a URL (ou "quero offline"). Se faltar a URL, pergunte.
- Checar bloqueio. Se o domínio costuma bloquear incorporação (Google, YouTube home, Instagram, bancos, X/Twitter), avise e ofereça: (a) tentar ao vivo com fallback, ou (b) baixar para
assets/webview/<slug>/e rodar local. Para apresentação sem internet garantida, recomende a cópia local. - Se local: baixar o site (HTML + assets essenciais) para
assets/webview/<slug>/, apontarsrcpara o arquivo local. Guardar tudo emassets/, nada na raiz do deck. - Montar o slide full-bleed:
<section class="webview-slide">compadding:0eheight:100vh, iframeinset:0, guarda de interação e fallback, conforme o canônico. TrocarSLUG,URL,TITULO_DESCRITIVO. Inserir como<section>no deck preservando a navegação existente. - Conferir: o site toma o slide inteiro; clicar entra em interação; Esc volta a navegar o deck; fallback tem link válido; nada animado por cima do site.
- Reportar: caminho do arquivo, a URL/fonte (ao vivo ou local), se o domínio pode bloquear, e o loop interno em uma frase. Lembre o apresentador: clique para interagir, Esc para voltar ao deck.
Checklist
- Site ocupa o slide inteiro:
<section>compadding:0,height:100vh, iframeinset:0,border:0. - Guarda de interação presente: site só fica clicável após clique; Esc devolve a navegação ao deck (testado).
- Ponte de teclado presente: Esc/E/P funcionam com o foco dentro do iframe (testado clicando no site e apertando as três teclas).
- Camada de fallback com link "abrir em nova aba" caso o site recuse incorporação.
- Se o domínio costuma bloquear, o usuário foi avisado e a opção local/offline foi oferecida.
- Cópia local (quando usada) fica em
assets/webview/<slug>/; nada de arquivo de apoio na raiz do deck. - Loop interno só na guarda (pill pulsando + borda respirando); nada animado sobre o site; o site fica firme.
- Laranja (#FF904D) só na guarda; nunca sobreposto ao conteúdo do site.
-
iddo slide e do script batem (webview-SLUGúnico no deck); navegação do deck preservada. - Abre por
file://quando a fonte é local; quando é ao vivo, o fallback cobre o caso sem rede. - Nenhum travessão; textos visíveis acentuados corretamente (segue
agents/_shared/idioma.md).