Sentence-case headings and tabs, the headset named once on phones, sideloaded games get Play and a menu

Section headings, the tabs, the brand and the activity bar drop small capitals
for sentence case at one heading size. On a phone with a headset picker, the
connection pill keeps only its status dot so the name shows once. Sideloaded
titles get Play and a ⋯ menu (Add artwork, Remove) like installed apps. Manage
sources moves under Source & compatibility. Library artwork is a Settings
page, so Tools no longer lists it.

Refs #101, #106

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
saphidandClaude Opus 5.5 committed 2026-10-20 18:20:38 +11:00
1 parent fed2249776
commit 5c44d56f53
1 file changed
+25 -17
+25 -17
View File
@@ -43,10 +43,10 @@
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; }
.brand b { font-weight: 700; font-size: 15px; letter-spacing: 2.5px; }
.brand b { font-weight: 700; font-size: 16px; letter-spacing: -.1px; }
nav { display: flex; gap: 4px; height: 100%; }
nav a { display: flex; align-items: center; padding: 0 13px; height: 100%; color: var(--text); text-decoration: none;
font-weight: 500; font-size: 15px; letter-spacing: 1.2px; text-transform: uppercase;
font-weight: 500; font-size: 15px;
border-bottom: 3px solid transparent; transition: color .15s; }
nav a:hover { color: var(--bright); }
nav a.on { color: var(--blue); border-bottom-color: var(--blue); }
@@ -158,7 +158,7 @@
.panel { background: linear-gradient(180deg, rgba(42,47,56,.72) 0%, rgba(30,35,41,.9) 100%);
border-radius: 4px; padding: 18px; box-shadow: 0 8px 24px rgba(0,0,0,.25); min-width: 0; }
.shelf-head { display: flex; align-items: baseline; gap: 12px; margin: 0 0 14px; }
.shelf-head h2 { margin: 0; font-size: 13px; font-weight: 700; letter-spacing: 2px; text-transform: uppercase; color: var(--text); }
.shelf-head h2 { margin: 0; font-size: 17px; font-weight: 600; color: var(--bright); }
.shelf-head .count { color: var(--muted); font-size: 13px; }
.shelf-head .spacer { flex: 1; }
.sub { color: var(--muted); font-size: 12.5px; }
@@ -328,7 +328,7 @@
/* ---- bottom bar (Steam's DOWNLOADS strip) ---- */
.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 .label { font-size: 13px; font-weight: 600; 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; }
@@ -462,12 +462,16 @@
.hdr-end .pill, .hdr-end .devsel { min-width: 0; }
.hdr-end .pill { padding: 0 8px; gap: 6px; }
.hdr-end .pill > span:last-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* The picker names the headset, so the pill keeps only its status dot: the name shows once. */
.hdr-end:has(.devsel:not([hidden])) .pill { flex: none; min-width: 0; width: 38px; padding: 0; justify-content: center; }
.hdr-end:has(.devsel:not([hidden])) .pill .pt { display: none; }
.hdr-end .devsel { flex: 1 1 auto; max-width: none; }
#reportBtn, #refreshAll { width: 38px; padding: 0; justify-content: center; }
nav { position: fixed; left: 0; right: 0; bottom: 0; z-index: 25; gap: 0;
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); }
nav a { flex: 1; flex-direction: column; justify-content: center; gap: 3px; padding: 0; font-size: 11px;
border-bottom: 0; color: var(--muted); }
nav a.on { border-bottom: 0; }
nav svg { display: block; width: 23px; height: 23px; }
.bottombar, .drawer { display: none; }
@@ -650,7 +654,7 @@
.lib-menu button.danger { color:#ff9c8a; }
.lib-empty { grid-column:1/-1; text-align:center; padding:48px 20px; border:1px dashed #ffffff1c; border-radius:12px; color:#a8b5c2; }
.lib-empty h2 { color:var(--bright); text-transform:none; letter-spacing:-.3px; font-size:20px; margin:0 0 8px; }
.lib-section { font-size:13px; text-transform:uppercase; letter-spacing:1.6px; color:var(--muted); margin:0 0 14px; font-weight:700; }
.lib-section { font-size:17px; color:var(--bright); margin:0 0 14px; font-weight:600; }
.lib-manage { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,360px),1fr)); gap:18px; align-items:start; }
.store-empty { display:flex; flex-direction:column; align-items:center; text-align:center; padding:58px 20px 70px; }
.store-empty svg { width:110px; height:90px; color:var(--link); margin-bottom:14px; }
@@ -855,7 +859,7 @@
.store-heading h1 { font-size:21px; }
.store-heading p { display:block; max-width:none; }
.store-search { min-height:48px; }
.workspace-heading #sourcesOpen { min-height:44px; }
#sourcesOpen { min-height:44px; }
#nav a { min-width:0; }
.drawer.open { display:block; bottom:calc(58px + env(safe-area-inset-bottom)); }
}
@@ -937,7 +941,7 @@
<rect x="6.5" y="11" width="19" height="10" rx="4" fill="none" stroke="#fff" stroke-width="2"/>
<circle cx="12" cy="16" r="1.8" fill="#fff"/><circle cx="20" cy="16" r="1.8" fill="#fff"/>
</svg>
<b>FRAME CONTROL</b>
<b>Frame Control</b>
</a>
<nav id="nav" aria-label="Main navigation">
<a href="#apps" class="on" title="Apps and app installs (1)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><rect x="3" y="3" width="7" height="7" rx="2"/><rect x="14" y="3" width="7" height="7" rx="2"/><rect x="3" y="14" width="7" height="7" rx="2"/><rect x="14" y="14" width="7" height="7" rx="2"/></svg>Apps<kbd>1</kbd></a>
@@ -1220,7 +1224,7 @@
<div class="workspace-heading"><div><span class="eyebrow">Made for your Frame</span><h1>Apps</h1><p>VR experiences, Android apps and desktop apps.</p></div><button class="action add-install" data-open-install>+ Add / Install</button></div>
<div class="scope-tabs" id="appViews" aria-label="App library"><button data-app-view="installed" aria-pressed="true">Installed</button><button data-app-view="browse" aria-pressed="false">Browse</button><button data-app-view="linux" aria-pressed="false">Linux apps</button></div>
<div id="appsInstalled"></div><div id="appsLinux" hidden></div>
<div class="app-browse-heading" id="appBrowseHeading" hidden><p>Find something for your headset, then install from its app page.</p><button id="sourcesOpen" class="small">Manage sources</button></div><button class="small" id="repNew" hidden>Report an APK</button><span id="andCount" hidden></span>
<div class="app-browse-heading" id="appBrowseHeading" hidden><p>Find something for your headset, then install from its app page.</p></div><button class="small" id="repNew" hidden>Report an APK</button><span id="andCount" hidden></span>
<section id="appStore" aria-label="Android store">
<div class="store-layout">
<div class="store-main">
@@ -1229,6 +1233,7 @@
<label class="store-check"><input type="checkbox" id="storeFits"> Only apps that fit the Frame</label>
<label class="store-check" title="Apps built around a phone: calls and SMS, launchers, keyboards, widgets, fitness, system tools. Searching always finds them."><input type="checkbox" id="storePhone"> Show phone-only apps</label>
<label class="store-source-select"><span>Source</span><select id="sourceFilter"><option value="">All sources</option></select></label>
<button type="button" id="sourcesOpen" class="small">Manage sources</button>
</div>
</details></div>
<div id="sourceStatus" class="store-notice" role="status" aria-live="polite" hidden></div>
@@ -1243,7 +1248,7 @@
<div class="page" data-page="tools">
<div class="workspace-heading"><div><a href="#tools" id="toolBack" class="workspace-link" hidden>← All tools</a><h1 id="toolTitle">Tools</h1><p id="toolSubtitle">Choose a task for your Frame.</p></div></div>
<div id="toolDirectory"><section class="tool-group"><h2>Content & files</h2><div class="quick-routes"><a href="#transfer"><span>Send to Frame</span><small>Add files, APKs or your own games</small></a><a href="#media"><span>Media player</span><small>Play your own videos and media</small></a><a href="#artworkSettings"><span>Library artwork</span><small>Manage artwork and image sources</small></a><a href="#linuxApps"><span>Linux apps</span><small>Install and manage desktop apps</small></a></div></section><section class="tool-group"><h2>Play & control</h2><div class="quick-routes"><a href="#capture"><span>Capture & Live</span><small>See and control the headset view</small></a><a href="#pad"><span>Keyboard & trackpad</span><small>Type and point in desktop apps</small></a><a href="#panels"><span>Panel switcher</span><small>Bring an open panel into view</small></a><a href="#vrTools"><span>VR tools</span><small>Performance HUD and VR utilities</small></a><a href="#comfort"><span>Family & comfort</span><small>Session reminders and breaks</small></a><a href="#macview" id="macToolLink" hidden><span>Mac in the headset</span><small>Share Mac windows and displays</small></a></div></section><section class="tool-group"><h2>Your Frame</h2><a class="workspace-link" href="#settings">Connection, display &amp; preferences →</a></section><section class="tool-group"><h2>Assistant</h2><a class="workspace-link" href="/assistant">Open assistant &amp; AI agents →</a><p class="sub">Use your own model endpoint or review a proposed MCP action.</p></section></div>
<div id="toolDirectory"><section class="tool-group"><h2>Content & files</h2><div class="quick-routes"><a href="#transfer"><span>Send to Frame</span><small>Add files, APKs or your own games</small></a><a href="#media"><span>Media player</span><small>Play your own videos and media</small></a><a href="#linuxApps"><span>Linux apps</span><small>Install and manage desktop apps</small></a></div></section><section class="tool-group"><h2>Play & control</h2><div class="quick-routes"><a href="#capture"><span>Capture & Live</span><small>See and control the headset view</small></a><a href="#pad"><span>Keyboard & trackpad</span><small>Type and point in desktop apps</small></a><a href="#panels"><span>Panel switcher</span><small>Bring an open panel into view</small></a><a href="#vrTools"><span>VR tools</span><small>Performance HUD and VR utilities</small></a><a href="#comfort"><span>Family & comfort</span><small>Session reminders and breaks</small></a><a href="#macview" id="macToolLink" hidden><span>Mac in the headset</span><small>Share Mac windows and displays</small></a></div></section><section class="tool-group"><h2>Your Frame</h2><a class="workspace-link" href="#settings">Connection, display &amp; preferences →</a></section><section class="tool-group"><h2>Assistant</h2><a class="workspace-link" href="/assistant">Open assistant &amp; AI agents →</a><p class="sub">Use your own model endpoint or review a proposed MCP action.</p></section></div>
<div class="grid-3" id="toolWorkspace">
<section class="panel" id="vrTools">
<div class="shelf-head"><h2>VR comfort and performance</h2><span class="spacer"></span><span class="sub" id="vrUpdated">Waiting for the Frame</span></div>
@@ -1559,7 +1564,7 @@
<div class="drawer" id="drawer"><button class="small" id="closeLog">Close activity log</button><div class="log" id="log"></div></div>
<div class="bottombar" id="bottombar" role="button" aria-label="Show activity">
<span class="label">ACTIVITY</span>
<span class="label">Activity</span>
<span class="jobs" id="jobs" hidden><span class="spinner"></span><span id="jobsText"></span></span>
<span class="last" id="lastLog">Ready</span>
<span class="sub" id="drawerHint">Show ▴</span>
@@ -2167,7 +2172,7 @@ function render(s) {
<div class="over">
<div class="name">${esc(g.name)}</div>
<div class="size">${gb(g.size)}</div>
<button class="play small" data-launch="${g.appid}" data-name="${esc(g.name)}">▶ PLAY</button>
<button class="play small" data-launch="${g.appid}" data-name="${esc(g.name)}">▶ Play</button>
</div>
</div>`).join("") : `<div class="sub">No games installed.</div>`;
// Tall library art, falling back to the wide header, then to the name card.
@@ -3383,14 +3388,17 @@ async function loadTitles() {
<div class="item">
<div class="grow"><div class="t">${esc(t.id)}</div>
<div class="s" title="${esc(t.target)}">${esc(t.runtime_label)}${t.target ? " · " + esc(t.target) : ""}</div></div>
${t.art_missing ? `<button class="small" data-title="refresh-art" data-id="${esc(t.id)}" title="Its Steam library tile has no artwork yet">Add artwork</button>` : ""}
<button class="play small" data-title="launch" data-id="${esc(t.id)}" title="Launch in the headset">▶</button>
<button class="small danger" data-title="remove" data-id="${esc(t.id)}">Remove</button>
<button class="play small" data-title="launch" data-id="${esc(t.id)}" title="Launch in the headset">Play</button>
<details class="lib-more"><summary aria-label="More for ${esc(t.id)}">⋯</summary><div class="lib-menu">
${t.art_missing ? `<button data-title="refresh-art" data-id="${esc(t.id)}">Add artwork<small>Its Steam library tile has none yet</small></button>` : ""}
<button class="danger" data-title="remove" data-id="${esc(t.id)}">Remove…<small>Deletes the game and its files</small></button>
</div></details>
</div>`).join("") : `<div class="sub">None yet.</div>`;
}
$("titleList").onclick = async e => {
const b = e.target.closest("[data-title]"); if (!b) return;
const id = b.dataset.id;
b.closest(".lib-more")?.removeAttribute("open");
if (b.dataset.title === "remove") {
if (!confirm(`Remove ${id} and its files from the Frame?`)) return;
await act(`Remove ${id}`, () => api("/api/titles", { action: "remove", id }), b);
@@ -3476,7 +3484,7 @@ function gameButtons(id, name, free) {
const open = `<button class="small" data-gm="store" data-id="${id}" data-name="${esc(name)}" title="Open the store page in the headset">Store on Frame</button>`;
if (d && d.appid === id) return `<span class="tag">${d.paused ? "Paused" : "Downloading"} ${d.percent ?? 0}%</span>`;
if (g && g.installed) return `<span class="tag">Installed</span>` +
`<button class="play small" data-launch="${id}" data-name="${esc(name)}">▶ PLAY</button>`;
`<button class="play small" data-launch="${id}" data-name="${esc(name)}">▶ Play</button>`;
if (g) return `<button class="action small" data-gm="install" data-id="${id}" data-name="${esc(name)}">Install on Frame</button>`;
return `<a href="${storeUrl}" target="_blank" rel="noopener" title="Opens in your browser">${free ? "Get" : "Buy"} on Steam ↗</a>${open}`;
}