Keep the page clear of iOS safe areas everywhere; research typing, pointing and mirroring into the Frame

- Header, content, tab bar, status strip, drawer and toasts add the notch,
  Dynamic Island, rounded-corner and home-indicator insets on every side
  (zero on desktops). Phones on their side use the bottom tab bar layout.
- The phone tab bar hides while a text field has focus, instead of riding
  on the keyboard.
- DEBUG hook FRAME_TEST_LANDSCAPE for checking this in the Simulator.
- docs/streaming.md: iPhone mirroring (UxPlay, broadcast extension) and
  keyboard/mouse input (uinput needs no sudo on the Frame, verified).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
saphidandClaude Opus 5.5 committed 2026-09-27 21:26:32 +10:00
1 parent 3db311da13
commit fe87a9d826
3 files changed
+63 -25

No files matched your search

+24 -16
View File
@@ -27,8 +27,8 @@
}
* { box-sizing: border-box; }
[hidden] { display: none !important; } /* class display rules would otherwise win */
html { scroll-behavior: smooth; scroll-padding-top: 76px; }
body { margin: 0; color: var(--text); min-height: 100vh; padding-bottom: 46px;
html { scroll-behavior: smooth; scroll-padding-top: calc(76px + env(safe-area-inset-top)); }
body { margin: 0; color: var(--text); min-height: 100vh; padding-bottom: calc(46px + env(safe-area-inset-bottom));
font: 14px/1.45 "Motiva Sans", -apple-system, "Segoe UI", Arial, sans-serif;
background: radial-gradient(1400px 700px at 15% -15%, #2a475e 0%, transparent 60%),
linear-gradient(180deg, var(--bg) 0%, #16202d 40%, var(--bg-deep) 100%) fixed; }
@@ -39,7 +39,7 @@
/* ---- top bar (Steam's STORE / LIBRARY / COMMUNITY strip) ---- */
header { position: sticky; top: 0; z-index: 20; display: flex; align-items: center; gap: 30px;
height: 64px; padding: 0 28px; background: rgba(23, 29, 37, .94); backdrop-filter: blur(12px);
height: calc(64px + env(safe-area-inset-top)); padding: env(safe-area-inset-top) calc(28px + env(safe-area-inset-right)) 0 calc(28px + env(safe-area-inset-left)); background: rgba(23, 29, 37, .94); backdrop-filter: blur(12px);
box-shadow: 0 1px 0 rgba(255,255,255,.04), 0 6px 18px rgba(0,0,0,.35); }
.brand { display: flex; align-items: center; gap: 11px; color: var(--bright); text-decoration: none; }
.brand svg { width: 30px; height: 30px; }
@@ -57,10 +57,10 @@
.dot.on { background: var(--green); box-shadow: 0 0 6px rgba(89,191,64,.8); }
.dot.off { background: var(--bad); }
main { max-width: 1480px; margin: 0 auto; padding: 22px 28px 30px; display: flex; flex-direction: column; gap: 26px; }
main { max-width: 1480px; margin: 0 auto; padding: 22px calc(28px + env(safe-area-inset-right)) 30px calc(28px + env(safe-area-inset-left)); display: flex; flex-direction: column; gap: 26px; }
.grid-top { display: grid; grid-template-columns: minmax(0, 1.7fr) minmax(330px, 1fr); gap: 22px; }
.grid-3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 22px; }
@media (max-width: 1150px) { .grid-top, .grid-3 { grid-template-columns: 1fr; } header { gap: 16px; padding: 0 18px; } nav a { padding: 0 10px; } }
@media (max-width: 1150px) { .grid-top, .grid-3 { grid-template-columns: 1fr; } header { gap: 16px; padding: env(safe-area-inset-top) calc(18px + env(safe-area-inset-right)) 0 calc(18px + env(safe-area-inset-left)); } nav a { padding: 0 10px; } }
@media (max-width: 820px) { .brand b { display: none; } }
nav svg { display: none; }
html { -webkit-text-size-adjust: 100%; }
@@ -231,21 +231,21 @@
.links div { margin: 5px 0; }
/* ---- bottom bar (Steam's DOWNLOADS strip) ---- */
.bottombar { position: fixed; left: 0; right: 0; bottom: 0; z-index: 20; height: 42px; display: flex; align-items: center; gap: 14px;
padding: 0 28px; background: rgba(23,29,37,.96); box-shadow: 0 -1px 0 rgba(255,255,255,.04); cursor: pointer; }
.bottombar { position: fixed; left: 0; right: 0; bottom: 0; z-index: 20; height: calc(42px + env(safe-area-inset-bottom)); display: flex; align-items: center; gap: 14px;
padding: 0 calc(28px + env(safe-area-inset-right)) env(safe-area-inset-bottom) calc(28px + env(safe-area-inset-left)); background: rgba(23,29,37,.96); box-shadow: 0 -1px 0 rgba(255,255,255,.04); cursor: pointer; }
.bottombar .label { font-size: 12px; font-weight: 700; letter-spacing: 1.6px; color: var(--muted); }
.bottombar .jobs { display: inline-flex; align-items: center; gap: 8px; color: var(--link); font-size: 12.5px; }
.bottombar .jobs .spinner { width: 13px; height: 13px; border-width: 2px; }
.bottombar .last { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); font-size: 13px; }
.bottombar .last.e { color: #ff8a73; }
.drawer { position: fixed; left: 0; right: 0; bottom: 42px; z-index: 19; max-height: 0; overflow: hidden;
.drawer { position: fixed; left: 0; right: 0; bottom: calc(42px + env(safe-area-inset-bottom)); z-index: 19; max-height: 0; overflow: hidden;
background: rgba(14,20,27,.98); transition: max-height .25s ease; box-shadow: 0 -10px 30px rgba(0,0,0,.5); }
.drawer.open { max-height: 300px; }
.log { font: 12px/1.55 ui-monospace, "SF Mono", Menlo, monospace; color: #b8c4d2; max-height: 300px; overflow: auto;
white-space: pre-wrap; padding: 12px 28px; }
white-space: pre-wrap; padding: 12px calc(28px + env(safe-area-inset-right)) 12px calc(28px + env(safe-area-inset-left)); }
.log:empty::before { content: "Actions you take here are logged with their output."; color: var(--dim); }
.log .e { color: #ff8a73; } .log .ok { color: #a4d007; } .log time { color: var(--dim); }
.toast { position: fixed; bottom: 58px; right: 28px; z-index: 30; max-width: 460px; padding: 12px 16px; border-radius: 3px;
.toast { position: fixed; bottom: calc(58px + env(safe-area-inset-bottom)); right: calc(28px + env(safe-area-inset-right)); z-index: 30; max-width: 460px; padding: 12px 16px; border-radius: 3px;
background: #3d4450; color: #fff; box-shadow: 0 10px 30px rgba(0,0,0,.5); border-left: 3px solid var(--blue);
opacity: 0; transform: translateY(10px); transition: opacity .2s, transform .2s; pointer-events: none; }
.toast.show { opacity: 1; transform: none; }
@@ -322,15 +322,17 @@
border-radius: 3px; height: 32px; padding: 0 8px; font: inherit; font-size: 13px; }
/* Touch screens can't hover: keep the library's name and Play button showing. */
@media (hover: none) { .capsule .over { opacity: 1; } .capsule:hover { transform: none; } }
/* ---- phones: tabs move to a bottom bar, as in iOS apps; the page clears the notch and home indicator ---- */
@media (max-width: 640px) {
/* ---- phones, upright or on their side: tabs move to a bottom bar, as in iOS apps. Every edge keeps clear of
the safe area (notch or Dynamic Island, rounded corners, home indicator); env() is zero on desktops. ---- */
@media (max-width: 640px), (max-height: 500px) and (hover: none) {
html { scroll-padding-top: calc(64px + env(safe-area-inset-top)); }
/* No blur here: it would make the header the containing block of the fixed tab bar. */
header { height: calc(52px + env(safe-area-inset-top)); padding: env(safe-area-inset-top) 14px 0; gap: 10px;
header { height: calc(52px + env(safe-area-inset-top)); padding: env(safe-area-inset-top) calc(14px + env(safe-area-inset-right)) 0 calc(14px + env(safe-area-inset-left)); gap: 10px;
backdrop-filter: none; background: rgb(23, 29, 37); }
.brand svg { width: 26px; height: 26px; }
header .chip { height: 30px; padding: 0 10px; }
nav { position: fixed; left: 0; right: 0; bottom: 0; z-index: 25; gap: 0;
height: calc(58px + env(safe-area-inset-bottom)); padding-bottom: env(safe-area-inset-bottom);
height: calc(58px + env(safe-area-inset-bottom)); padding: 0 env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
background: rgba(23,29,37,.97); backdrop-filter: blur(12px); box-shadow: 0 -1px 0 rgba(255,255,255,.07); }
nav a { flex: 1; flex-direction: column; justify-content: center; gap: 3px; padding: 0; font-size: 10.5px;
letter-spacing: .5px; border-bottom: 0; color: var(--muted); }
@@ -338,9 +340,9 @@
nav svg { display: block; width: 23px; height: 23px; }
.bottombar, .drawer { display: none; }
body { padding-bottom: calc(70px + env(safe-area-inset-bottom)); }
.toast { left: 14px; right: 14px; max-width: none; bottom: calc(70px + env(safe-area-inset-bottom)); }
.toast { left: calc(14px + env(safe-area-inset-left)); right: calc(14px + env(safe-area-inset-right)); max-width: none; bottom: calc(70px + env(safe-area-inset-bottom)); }
main, .page { gap: 16px; }
main { padding: 12px 12px 20px; }
main { padding: 12px calc(12px + env(safe-area-inset-right)) 20px calc(12px + env(safe-area-inset-left)); }
.panel { padding: 14px; }
.grid-top, .grid-3, .and-grid, .and-col { gap: 14px; }
.toolbar { gap: 8px; }
@@ -356,6 +358,8 @@
button.small { height: 32px; }
.actions button { height: 46px; }
.cat-tools select { max-width: none; flex: 1 1 100%; }
/* The on-screen keyboard covers the bottom of the screen; the tab bar would ride on top of it. */
body.typing nav { display: none; }
}
</style>
</head>
@@ -2118,6 +2122,10 @@ function showPage() {
if (section) section.scrollIntoView(); else window.scrollTo(0, 0);
}
window.addEventListener("hashchange", showPage);
// While a text field has focus, phones hide the bottom tab bar (see body.typing in the CSS).
const typesText = el => el?.matches?.('textarea, input:not([type=range], [type=checkbox], [type=radio], [type=file], [type=button])');
document.addEventListener("focusin", e => document.body.classList.toggle("typing", !!typesText(e.target)));
document.addEventListener("focusout", () => document.body.classList.remove("typing"));
showPage();
document.addEventListener("keydown", e => {
if (e.metaKey || e.ctrlKey || e.altKey || /INPUT|TEXTAREA|SELECT/.test(document.activeElement.tagName) || document.querySelector("dialog[open]")) return;