diff --git a/web/scripts/e2e-player.mjs b/web/scripts/e2e-player.mjs index ac379ee..5576a40 100644 --- a/web/scripts/e2e-player.mjs +++ b/web/scripts/e2e-player.mjs @@ -67,17 +67,31 @@ if (await page.locator('canvas.vp-sphere').count()) { } await page.screenshot({ path: `${SHOTS}/h1-auto.png` }) -// No theme touches playback: under Retro the scanlines are off and the controls keep a plain font. +// No theme touches playback: for every theme the stylesheet has rules for, no full-window +// overlay is drawn, the picture isn't filtered, and the player's lettering is plain. { const was = await page.evaluate(() => document.documentElement.getAttribute('data-theme')) - await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'retro')) - const r = await page.evaluate(() => { - const app = document.querySelector('.app') - const over = app ? getComputedStyle(app, '::after').display : 'none' - const font = getComputedStyle(document.querySelector('.vp button')).fontFamily - return { over, font } + const themes = await page.evaluate(() => { + const ids = new Set() + for (const sh of document.styleSheets) { let rules; try { rules = sh.cssRules } catch { continue } + const walk = (rs) => { for (const r of rs) { if (r.cssRules) walk(r.cssRules); for (const m of (r.selectorText ?? '').matchAll(/data-theme="([^"]+)"/g)) ids.add(m[1]) } } + walk(rules) } + return [...ids] }) - check('Retro leaves the video alone', r.over === 'none' && !/mono|courier/i.test(r.font), JSON.stringify(r)) + const bad = [] + for (const t of themes) { + const r = await page.evaluate((t) => { + document.documentElement.setAttribute('data-theme', t) + const els = [document.documentElement, document.body, document.querySelector('.app'), document.querySelector('#root')].filter(Boolean) + const overlays = els.flatMap((e) => ['::before', '::after'].map((p) => getComputedStyle(e, p))).filter((c) => c.display !== 'none' && c.content !== 'none' && c.content !== 'normal' && (c.position === 'fixed' || c.position === 'absolute')).length + const pics = [...document.querySelectorAll('.vp video, .vp canvas')].map((e) => getComputedStyle(e).filter).filter((f) => f !== 'none').length + const c = getComputedStyle(document.querySelector('.vp')) + const b = getComputedStyle(document.querySelector('.vp button')) + return { overlays, pics, plain: !/mono|courier/i.test(b.fontFamily) && c.fontWeight === '400' && c.letterSpacing === 'normal' && c.textShadow === 'none' } + }, t) + if (r.overlays || r.pics || !r.plain) bad.push(`${t}: ${JSON.stringify(r)}`) + } + check(`no theme touches the video (${themes.length} themes with their own rules)`, themes.length > 0 && bad.length === 0, bad.join(' / ') || themes.join(', ')) await page.evaluate((t) => t ? document.documentElement.setAttribute('data-theme', t) : document.documentElement.removeAttribute('data-theme'), was) } diff --git a/web/src/styles.css b/web/src/styles.css index cfd9b89..b08e8e6 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -5,6 +5,10 @@ --s1: #3987e5; --s2: #c98500; --track: rgba(57, 135, 229, .18); --btn-fg: #111; --term-bg: var(--term-bg); --font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; --header-h: 44px; --footer-h: 28px; --rail-w: 220px; --tabs-h: 34px; + /* iOS 26+ draws a soft blur under the status bar of a Home Screen app that reaches ~16pt + below it (hamlet docs/gotchas.md). Whatever starts at the safe area starts below that band + too. Zero where there is no status bar to clear: desktop, a Safari tab. */ + --blur: min(env(safe-area-inset-top), 16px); } * { box-sizing: border-box; } html, body, #root { height: 100%; margin: 0; } @@ -30,7 +34,7 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi .svc-list > .error, .cron-list > .error { margin: 8px 12px; } /* layout */ -.app { height: 100%; display: flex; flex-direction: column; padding-top: env(safe-area-inset-top); } +.app { height: 100%; display: flex; flex-direction: column; padding-top: calc(env(safe-area-inset-top) + var(--blur)); } .header { height: var(--header-h); display: flex; align-items: center; gap: 12px; padding: 0 12px; background: var(--bg2); border-bottom: 1px solid var(--border); } .brand { font-weight: 700; color: var(--accent); letter-spacing: .5px; } .host { color: var(--muted); font-family: ui-monospace, monospace; font-size: 13px; } @@ -264,7 +268,7 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi than what is left, and scrolls inside itself. Its parts keep their height: in a column that may shrink, anything with its own scrollbar (a preview, a list) is squeezed to a sliver instead, and the dialog never scrolls at all. */ -.modal-back { position: fixed; inset: 0; z-index: 50; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; padding: max(16px, env(safe-area-inset-top)) max(16px, env(safe-area-inset-right)) max(16px, env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left)); } +.modal-back { position: fixed; inset: 0; z-index: 50; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; padding: max(16px, calc(env(safe-area-inset-top) + var(--blur))) max(16px, env(safe-area-inset-right)) max(16px, env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left)); } .modal { --modal-bg: var(--bg2); width: min(420px, 100%); max-height: 100%; overflow-y: auto; overscroll-behavior: contain; background: var(--modal-bg); border: 1px solid var(--border); border-radius: 10px; padding: 16px; display: flex; flex-direction: column; gap: 12px; } .modal > * { flex-shrink: 0; } /* a long dialog's buttons (and what went wrong) stay in reach while the rest scrolls */ @@ -372,6 +376,7 @@ h2.section { margin: 0; font-size: 13px; font-weight: 600; color: var(--muted); on an iPhone the whole window */ .vp { position: relative; flex: 1; min-height: 240px; background: #000; color: #fff; overflow: hidden; user-select: none; -webkit-user-select: none; outline: none; } .vp.max { position: fixed; inset: 0; z-index: 1000; } +.vp.max .vp-note { top: calc(env(safe-area-inset-top) + var(--blur) + 14px); } .vp-v { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; background: #000; } .vp-hidden { opacity: 0; pointer-events: none; } .vp-sphere { touch-action: none; cursor: grab; } @@ -957,7 +962,7 @@ pre.diff .add { color: var(--ok); background: color-mix(in srgb, var(--ok) 12%, @media (max-width: 800px) { .kvm-bar .kvm-url { display: none; } } .kvm-none { text-align: center; } .kvm-none p { margin: 4px 0; } /* No true fullscreen for a page element (Safari on iOS): fill the viewport in-page instead. */ -.kvm-frame.expanded { position: fixed; inset: 0; z-index: 60; } +.kvm-frame.expanded { position: fixed; inset: 0; z-index: 60; padding-top: calc(env(safe-area-inset-top) + var(--blur)); } .kvm-frame.expanded iframe { height: 100%; } .kvm-shrink { position: absolute; right: max(10px, env(safe-area-inset-right)); bottom: max(10px, env(safe-area-inset-bottom)); z-index: 61; background: var(--bg2); border: 1px solid var(--border); opacity: .75; } .kvm-shrink:hover, .kvm-shrink:active { opacity: 1; } @@ -972,7 +977,14 @@ pre.diff .add { color: var(--ok); background: color-mix(in srgb, var(--ok) 12%, .vp-time.inrow { display: none; } } -/* No theme touches playback: the Retro scanlines (a full-window overlay) are off while a video - is open, and the player keeps one plain font whatever the theme's. */ -[data-theme] .app:has(.vp)::after { display: none; } -.vp, .vp button, .vp input { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; } + +/* No theme touches playback, whatever it draws. A theme's full-window overlays (the Retro + scanlines today, any other tomorrow) are off while a player is open; the picture is never + filtered or blended; the player's text is plain, not the theme's lettering. Only the accent + colour of its controls follows the theme. */ +html:has(.vp)::before, html:has(.vp)::after, body:has(.vp)::before, body:has(.vp)::after, +.app:has(.vp)::before, .app:has(.vp)::after, #root:has(.vp)::before, #root:has(.vp)::after { display: none !important; } +.vp { isolation: isolate; filter: none !important; mix-blend-mode: normal !important; } +.vp video, .vp canvas { filter: none !important; mix-blend-mode: normal !important; } +.vp { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; font-weight: 400; letter-spacing: normal; text-shadow: none; } +.vp button, .vp input, .vp select { font-family: inherit !important; }