Put the game frame in the template, so its styles reach it

The frame rendered at 300x150 -- the HTML default for an unstyled iframe --
instead of the applet's 770x540. Astro's <style> is scoped: it compiles
.cn-frame to `.cn-frame[data-astro-cid-kr4ipkmt]` and stamps that attribute on
elements in the template. The iframe was built with document.createElement()
in the script, carried no such attribute, and therefore matched none of its
own rules.

So the iframe lives in the template now and the click only sets its src. An
iframe with no src loads nothing, so the payload still waits for the click,
which was the only reason it was being created dynamically.

It is hidden with a class rather than the `hidden` attribute, because the
`display: block` in the sizing rule outranks the user agent's
`[hidden] { display: none }` and it would have shown regardless.

Verified by rendering the staged branch locally against a stub manifest --
the page only builds its Play button when an export is present, so the
previous change shipped without its markup ever being looked at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S4wdbGSVzvUDHBWRjSVxfr
This commit is contained in:
wes
2026-08-28 22:01:55 -04:00
co-authored by Claude Opus 5
parent 71d2fd5c3c
commit 68da1f716b
+29 -11
View File
@@ -48,14 +48,18 @@ const overTheWire = build ? mb(build.wireBytes) : null;
{ {
build ? ( build ? (
<div class="cn-wrap"> <div class="cn-wrap" data-src={cannonsFrameUrl}>
<button class="cn-start" type="button" data-src={cannonsFrameUrl}> <button class="cn-start" type="button">
<span class="cn-start-label">Play</span> <span class="cn-start-label">Play</span>
<span class="cn-start-sub"> <span class="cn-start-sub">
downloads about {overTheWire} MB — the client, the server, and downloads about {overTheWire} MB — the client, the server, and
the game's own art, compressed the game's own art, compressed
</span> </span>
</button> </button>
{/* Rendered here rather than created in the script, so Astro's scoped
styles actually reach it -- see the note on .cn-frame below. With
no src it loads nothing, so the payload still waits for the click. */}
<iframe class="cn-frame" title="Cannons" allow="autoplay"></iframe>
</div> </div>
) : ( ) : (
<p class="cn-missing"> <p class="cn-missing">
@@ -78,15 +82,12 @@ const overTheWire = build ? mb(build.wireBytes) : null;
// the frame nears the viewport, and this frame is above the fold, so the // the frame nears the viewport, and this frame is above the fold, so the
// whole payload would download for anyone who opened the page. Several MB // whole payload would download for anyone who opened the page. Several MB
// is a lot to spend on a visitor who came here to read the paragraph. // is a lot to spend on a visitor who came here to read the paragraph.
const button = document.querySelector('.cn-start'); const wrap = document.querySelector('.cn-wrap');
const button = wrap?.querySelector('.cn-start');
const frame = wrap?.querySelector('.cn-frame');
button?.addEventListener('click', () => { button?.addEventListener('click', () => {
const frame = document.createElement('iframe'); frame.src = wrap.dataset.src;
frame.className = 'cn-frame'; wrap.classList.add('is-playing');
frame.src = button.dataset.src;
frame.title = 'Cannons';
// The client takes the keyboard for chat and aiming, and plays sound.
frame.allow = 'autoplay';
button.replaceWith(frame);
// The game only sees keystrokes once the frame has focus, and a visitor // The game only sees keystrokes once the frame has focus, and a visitor
// who just clicked Play should not have to click a second time to type. // who just clicked Play should not have to click a second time to type.
frame.addEventListener('load', () => frame.focus(), { once: true }); frame.addEventListener('load', () => frame.focus(), { once: true });
@@ -98,7 +99,15 @@ const overTheWire = build ? mb(build.wireBytes) : null;
margin: 1.5rem 0; margin: 1.5rem 0;
} }
/* 770x540 is the applet's own size; the frame is not responsive because the /* 770x540 is the applet's own size; the frame is not responsive because the
game's layout is fixed pixels and scaling it would blur the terrain. */ game's layout is fixed pixels and scaling it would blur the terrain.
⚠️ These styles are SCOPED -- Astro compiles them to
`.cn-frame[data-astro-cid-…]` and stamps that attribute on elements in
this template only. An iframe built with document.createElement() in the
script below carries no such attribute, matches nothing, and renders at
the HTML default of 300x150. That is exactly what shipped on 2026-08-28.
Hence the iframe living in the template with the script only setting its
src. */
.cn-start, .cn-start,
.cn-frame { .cn-frame {
display: block; display: block;
@@ -129,8 +138,17 @@ const overTheWire = build ? mb(build.wireBytes) : null;
font-size: 0.8rem; font-size: 0.8rem;
color: var(--muted); color: var(--muted);
} }
/* Not the `hidden` attribute: `display: block` above outranks the user
agent's `[hidden] { display: none }`, so the frame would show anyway. */
.cn-frame { .cn-frame {
background: #0b0d12; background: #0b0d12;
display: none;
}
.cn-wrap.is-playing .cn-start {
display: none;
}
.cn-wrap.is-playing .cn-frame {
display: block;
} }
.cn-note, .cn-note,
.cn-missing { .cn-missing {