Player full-screen: edge-to-edge themed stage + theme toggle in stage

Two fixes from on-device testing:

1. Fill the screen. The earlier stage capped the device at min(96vw,168vh)
   and centred it, leaving big margins on wide phones. Now the device frame
   goes transparent/borderless at position:absolute inset:0 and the OLED
   grows (flex:1) so the unit fills the whole viewport edge to edge.

2. Follow light/dark/system in full-screen. The full-screen skin is the
   themed page gradient (light in light mode, dark in dark, OS-driven on
   system), and a theme toggle (◐/☀/☾ — same cycle + "metronome.theme" key
   as the main page) now sits beside the exit ✕, since the top bar that
   normally holds it is hidden in stage.

The themed gradient is painted on the device element rather than the body
because a position:fixed body doesn't propagate its background to the
canvas (left a white area). The decorative PWR dot is hidden in stage so
it doesn't sit under the floating controls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Me Here 2026-05-25 21:42:30 -05:00
parent e22f267b83
commit d1bd996675

View file

@ -145,40 +145,49 @@
.hint{ font-size:11px; color:var(--muted) } .hint{ font-size:11px; color:var(--muted) }
code{ background:var(--field-bg); border:1px solid var(--field-bd); border-radius:4px; padding:1px 5px; font-size:11px } code{ background:var(--field-bg); border:1px solid var(--field-bd); border-radius:4px; padding:1px 5px; font-size:11px }
/* ---- full-screen "stage" mode ---- */ /* ---- full-screen "stage" mode: edge-to-edge, follows light/dark/system ---- */
.fs-exit{ display:none; position:fixed; top:14px; right:14px; z-index:80; .fs-ctrl{ display:none; position:fixed; top:max(12px,env(safe-area-inset-top)); z-index:80;
background:rgba(18,21,28,.7); color:#e7edf5; border:1px solid rgba(255,255,255,.28); background:rgba(127,139,154,.16); color:var(--txt); border:1px solid rgba(127,139,154,.5);
border-radius:50%; width:40px; height:40px; font-size:17px; line-height:1; cursor:pointer; border-radius:50%; width:40px; height:40px; font-size:17px; line-height:1; cursor:pointer;
backdrop-filter:blur(3px); -webkit-backdrop-filter:blur(3px) } backdrop-filter:blur(4px); -webkit-backdrop-filter:blur(4px) }
.fs-exit:hover{ background:rgba(40,48,62,.9) } .fs-ctrl:hover{ background:rgba(127,139,154,.32) }
#fsExit{ right:max(12px,env(safe-area-inset-right)) }
#fsThemeBtn{ right:calc(max(12px,env(safe-area-inset-right)) + 50px) }
.rotate-hint{ display:none } .rotate-hint{ display:none }
body.stage{ position:fixed; inset:0; padding:2.5vmin; gap:0; justify-content:center; overflow:hidden } /* the device frame goes transparent → the themed page background IS the full-screen
body.stage .topbar, body.stage .panel{ display:none } skin (light in light mode, dark in dark mode); children flex to fill the screen */
body.stage .fs-exit{ display:block } body.stage{ position:fixed; inset:0; padding:0; gap:0; overflow:hidden }
body.stage .device{ max-width:none; width:min(96vw, 168vh); margin:auto; border-radius:min(3vmin,22px) } body.stage .topbar, body.stage .panel, body.stage .grille{ display:none }
/* enlarge the glanceable bits with viewport-relative units */ body.stage .fs-ctrl{ display:block }
body.stage .screen{ padding:2vh 3vw } body.stage .device{ position:absolute; inset:0; width:auto; max-width:none; margin:0;
background:radial-gradient(circle at 50% -8%, var(--bg1), var(--bg2)); border:none; border-radius:0; box-shadow:none;
display:flex; flex-direction:column;
padding:max(2.2vh,env(safe-area-inset-top)) max(4vw,env(safe-area-inset-right))
max(2.4vh,env(safe-area-inset-bottom)) max(4vw,env(safe-area-inset-left)) }
body.stage .device::before, body.stage .device::after, body.stage .screw{ display:none }
body.stage .brandrow{ flex:0 0 auto; margin:0 0 2vh }
body.stage .pwr{ display:none } /* declutter the corner the floating controls sit in */
body.stage .logo .model, body.stage .knob-wrap{ color:var(--muted) }
body.stage .screen{ flex:1 1 auto; display:flex; flex-direction:column; justify-content:space-between; padding:2.4vh 3vw }
body.stage .scr-top{ font-size:2.8vh } body.stage .scr-top{ font-size:2.8vh }
body.stage .scr-top .tempo{ font-size:3.4vh } body.stage .scr-top .tempo{ font-size:3.6vh }
body.stage .scr-top .tempo b{ font-size:8.5vh } body.stage .scr-top .tempo b{ font-size:9vh }
body.stage .scr-name{ font-size:6vh; margin:1.4vh 0 1.2vh } body.stage .scr-name{ font-size:7vh; margin:0 }
body.stage .scr-bot{ font-size:2.8vh } body.stage .scr-bot{ font-size:2.8vh }
body.stage .leds{ gap:1.6vmin; margin:2.6vh 0 1vh } body.stage .leds{ flex:0 0 auto; gap:1.8vmin; margin:2.4vh 0 0 }
body.stage .led{ width:4.4vmin; height:4.4vmin } body.stage .led{ width:4.6vmin; height:4.6vmin }
body.stage .controls{ gap:1.6vmin; margin-top:2.2vh } body.stage .controls{ flex:0 0 auto; gap:1.8vmin; margin-top:2.2vh }
body.stage .controls .btn{ font-size:2.6vh; padding:1.5vh 1.8vw; min-width:7vw } body.stage .controls .btn{ font-size:2.6vh; padding:1.6vh 2vw; min-width:8vw }
body.stage .controls .btn.play{ min-width:11vw; font-size:3.4vh } body.stage .controls .btn.play{ min-width:12vw; font-size:3.6vh }
body.stage .controls .btn small{ font-size:1.3vh } body.stage .controls .btn small{ font-size:1.3vh }
body.stage .brandrow{ margin-bottom:2vh }
body.stage .grille{ display:none }
/* portrait while staged (mainly iPhone, which can't lock) → prompt to rotate */ /* portrait while staged (mainly iPhone, which can't lock) → prompt to rotate */
@media (orientation: portrait){ @media (orientation: portrait){
body.stage .device{ filter:blur(3px) brightness(.5); pointer-events:none } body.stage .device{ filter:blur(3px) brightness(.6); pointer-events:none }
body.stage .rotate-hint{ display:flex; position:fixed; inset:0; z-index:90; body.stage .rotate-hint{ display:flex; position:fixed; inset:0; z-index:90;
flex-direction:column; align-items:center; justify-content:center; gap:18px; flex-direction:column; align-items:center; justify-content:center; gap:18px;
background:var(--bg2); color:var(--txt); font-size:20px; text-align:center; padding:24px } background:var(--bg1); color:var(--txt); font-size:20px; text-align:center; padding:24px }
body.stage .rotate-hint .rh-icon{ font-size:64px; line-height:1; color:var(--cyan) } body.stage .rotate-hint .rh-icon{ font-size:64px; line-height:1; color:var(--cyan) }
} }
</style> </style>
@ -242,7 +251,8 @@
</div> </div>
<!-- stage-mode overlays (only visible in full-screen "stage" mode) --> <!-- stage-mode overlays (only visible in full-screen "stage" mode) -->
<button id="fsExit" class="fs-exit" title="Exit full screen (Esc)" aria-label="Exit full screen"></button> <button id="fsThemeBtn" class="fs-ctrl" title="Theme (system / light / dark)" aria-label="Toggle theme"></button>
<button id="fsExit" class="fs-ctrl" title="Exit full screen (Esc)" aria-label="Exit full screen"></button>
<div id="rotateHint" class="rotate-hint"> <div id="rotateHint" class="rotate-hint">
<span class="rh-icon"></span> <span class="rh-icon"></span>
<span>Rotate your device to landscape</span> <span>Rotate your device to landscape</span>
@ -417,10 +427,13 @@ function themePref(){ try{ const p=localStorage.getItem("metronome.theme"); retu
function applyTheme(p){ function applyTheme(p){
try{ localStorage.setItem("metronome.theme",p); }catch(e){} try{ localStorage.setItem("metronome.theme",p); }catch(e){}
document.documentElement.dataset.theme = effectiveTheme(p); document.documentElement.dataset.theme = effectiveTheme(p);
$("themeBtn").textContent = p==="system" ? "◐" : p==="light" ? "☀" : "☾"; const glyph = p==="system" ? "◐" : p==="light" ? "☀" : "☾";
$("themeBtn").title = "Theme: "+p+" (click to cycle: system → light → dark)"; const title = "Theme: "+p+" (click to cycle: system → light → dark)";
for(const id of ["themeBtn","fsThemeBtn"]){ const b=$(id); if(b){ b.textContent=glyph; b.title=title; } }
} }
$("themeBtn").onclick = ()=> applyTheme(THEMES[(THEMES.indexOf(themePref())+1)%THEMES.length]); const cycleTheme = ()=> applyTheme(THEMES[(THEMES.indexOf(themePref())+1)%THEMES.length]);
$("themeBtn").onclick = cycleTheme;
$("fsThemeBtn").onclick = cycleTheme;
matchMedia("(prefers-color-scheme: light)").addEventListener("change", ()=>{ if(themePref()==="system") applyTheme("system"); }); matchMedia("(prefers-color-scheme: light)").addEventListener("change", ()=>{ if(themePref()==="system") applyTheme("system"); });
applyTheme(themePref()); applyTheme(themePref());