feat(shadertoy): add fullscreen support + ditch insets for max visual effect (#278)

* feat(shadertoy): add fullscreen support

Add fullscreen toggle button that appears when the host supports fullscreen
display mode. Uses the same pattern as pdf-server:
- Check availableDisplayModes in host context
- Show/hide button based on capability
- Toggle between inline and fullscreen via requestDisplayMode()
- Update UI state on host context changes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(shadertoy): button visibility after exiting fullscreen + Escape key

- Only update button visibility when availableDisplayModes is present
  in context (partial updates don't include all fields)
- Add Escape key handler to exit fullscreen mode

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Olivier Chafik 2026-01-15 13:01:55 +00:00 • committed by GitHub
commit b78cd74c1b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 115 additions and 6 deletions

View file

@ -5,8 +5,61 @@ html, body {
overflow: hidden;
}
.main {
position: relative;
width: 100%;
height: 100%;
}
#canvas {
width: 100%;
height: 100%;
display: block;
}
/* Fullscreen button */
.fullscreen-btn {
position: absolute;
bottom: 16px;
right: 16px;
width: 40px;
height: 40px;
border: none;
border-radius: 8px;
background: rgba(0, 0, 0, 0.6);
color: white;
cursor: pointer;
display: none; /* Hidden by default, shown when fullscreen available */
align-items: center;
justify-content: center;
transition: background 0.2s, opacity 0.2s;
opacity: 0.7;
z-index: 100;
}
.fullscreen-btn:hover {
background: rgba(0, 0, 0, 0.8);
opacity: 1;
}
.fullscreen-btn svg {
width: 20px;
height: 20px;
}
.fullscreen-btn .collapse-icon {
display: none;
}
.fullscreen-btn.available {
display: flex;
}
/* Fullscreen mode */
.main.fullscreen .fullscreen-btn .expand-icon {
display: none;
}
.main.fullscreen .fullscreen-btn .collapse-icon {
display: block;
}

View file

@ -34,6 +34,13 @@ const log = {
// Get element references
const mainEl = document.querySelector(".main") as HTMLElement;
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
const fullscreenBtn = document.getElementById(
"fullscreen-btn",
) as HTMLButtonElement;
// Display mode state
let currentDisplayMode: "inline" | "fullscreen" = "inline";
// Resize canvas to fill viewport
function resizeCanvas() {
canvas.width = window.innerWidth;
@ -42,16 +49,55 @@ function resizeCanvas() {
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
// Handle host context changes (safe area insets)
// Handle host context changes (display mode)
function handleHostContextChanged(ctx: McpUiHostContext) {
if (ctx.safeAreaInsets) {
mainEl.style.paddingTop = `${ctx.safeAreaInsets.top}px`;
mainEl.style.paddingRight = `${ctx.safeAreaInsets.right}px`;
mainEl.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`;
mainEl.style.paddingLeft = `${ctx.safeAreaInsets.left}px`;
// Note: We ignore safeAreaInsets to maximize shader display area
// Show fullscreen button if available (only update if field is present)
if (ctx.availableDisplayModes !== undefined) {
if (ctx.availableDisplayModes.includes("fullscreen")) {
fullscreenBtn.classList.add("available");
} else {
fullscreenBtn.classList.remove("available");
}
}
// Update display mode state and UI
if (ctx.displayMode) {
currentDisplayMode = ctx.displayMode as "inline" | "fullscreen";
if (currentDisplayMode === "fullscreen") {
mainEl.classList.add("fullscreen");
} else {
mainEl.classList.remove("fullscreen");
}
}
}
// Handle Escape key to exit fullscreen
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && currentDisplayMode === "fullscreen") {
toggleFullscreen();
}
});
// Toggle fullscreen mode
async function toggleFullscreen() {
const newMode = currentDisplayMode === "fullscreen" ? "inline" : "fullscreen";
try {
const result = await app.requestDisplayMode({ mode: newMode });
currentDisplayMode = result.mode as "inline" | "fullscreen";
if (currentDisplayMode === "fullscreen") {
mainEl.classList.add("fullscreen");
} else {
mainEl.classList.remove("fullscreen");
}
} catch (err) {
log.error("Failed to change display mode:", err);
}
}
fullscreenBtn.addEventListener("click", toggleFullscreen);
// ShaderToyLite instance
let shaderToy: ShaderToyLiteInstance | null = null;