feat(web): iPhone PWA fixes (M1) + warm RTO redesign (M2)

M1 — PWA mechanics:
- Generate real app icons (apple-touch-icon + manifest 192/512/maskable)
  via pure-stdlib gen_icons.py; iOS uses apple-touch-icon, not manifest icons.
- viewport-fit=cover + env(safe-area-inset-*) on header/input/menu so content
  clears the notch and home indicator.
- Dynamic height pinned to the VisualViewport (height + offsetTop, re-measured
  across the keyboard animation) so the input stays above the iOS keyboard;
  100dvh fallback. Kills the squish/gap bugs in standalone mode.
- overscroll containment; flesh out manifest (scope, portrait, maskable).

M2 — visual redesign:
- Realign style.css to the warm low-glow RTO palette already used by the
  standalone pages (#0e0e0e panels, #2a1d12 borders); remove the neon
  saturated-orange borders and ~15 glow shadows.
- Reserve filled accent for one element (Send); glow only on status pulse +
  input focus. Flat warm message bubbles with tail corners.
- Reclaim the mobile header into [≡] Lyra · [status dot]; drop the redundant
  status bar (relay status now the header dot, updated in checkHealth).
- prefers-reduced-motion support; fix undefined var(--text); real light-mode tokens.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 23:20:11 +00:00
parent e75c4390b5
commit fa168271e1
8 changed files with 373 additions and 134 deletions
+47 -1
View File
@@ -5,10 +5,14 @@
<title>Lyra Core Chat</title>
<link rel="stylesheet" href="style.css" />
<!-- PWA -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Lyra" />
<meta name="theme-color" content="#070707" />
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
<link rel="icon" type="image/png" href="icon-192.png" />
<link rel="manifest" href="manifest.json" />
</head>
@@ -55,6 +59,8 @@
<span></span>
<span></span>
</button>
<span class="brand">Lyra</span>
<span class="brand-dot" id="brandDot" title="Relay status"></span>
<label for="mode">Mode:</label>
<select id="mode">
<option value="standard">Standard</option>
@@ -412,16 +418,56 @@
if (resp.ok) {
document.getElementById("status-dot").className = "dot ok";
document.getElementById("status-text").textContent = "Relay Online";
document.getElementById("brandDot").className = "brand-dot ok";
} else {
throw new Error("Bad status");
}
} catch (err) {
document.getElementById("status-dot").className = "dot fail";
document.getElementById("status-text").textContent = "Relay Offline";
document.getElementById("brandDot").className = "brand-dot fail";
}
}
document.addEventListener("DOMContentLoaded", () => {
// --- PWA: track the *visible* viewport height so the layout follows the
// iOS keyboard and the dynamic Safari toolbars (keeps the input bar visible
// instead of hiding behind the keyboard). Falls back to 100dvh via CSS.
function setAppHeight() {
const vv = window.visualViewport;
const h = (vv && vv.height) || window.innerHeight;
const off = (vv && vv.offsetTop) || 0;
const root = document.documentElement.style;
root.setProperty("--app-height", h + "px");
// iOS pans the visual viewport when the keyboard opens; follow its top
// edge so the pinned #chat sits exactly in the visible area.
root.setProperty("--app-offset", off + "px");
}
// Re-measure across the keyboard animation: iOS reports a stale (too-short)
// height mid-animation, so sample a few times until it settles.
function nudgeAppHeight() {
setAppHeight();
[50, 150, 300, 550].forEach((t) => setTimeout(setAppHeight, t));
}
setAppHeight();
if (window.visualViewport) {
window.visualViewport.addEventListener("resize", nudgeAppHeight);
window.visualViewport.addEventListener("scroll", setAppHeight);
}
window.addEventListener("resize", nudgeAppHeight);
window.addEventListener("orientationchange", nudgeAppHeight);
// Keep the latest message in view when the keyboard opens/closes.
const userInputEl = document.getElementById("userInput");
userInputEl.addEventListener("focus", () => {
nudgeAppHeight();
setTimeout(() => {
const m = document.getElementById("messages");
m.scrollTo({ top: m.scrollHeight, behavior: "smooth" });
}, 350);
});
userInputEl.addEventListener("blur", nudgeAppHeight);
// Mobile Menu Toggle
const hamburgerMenu = document.getElementById("hamburgerMenu");
const mobileMenu = document.getElementById("mobileMenu");