openvibers/openvibe.games Commits

Shared network chrome on every page, SEO, open-voice verbiage

OP OpenVibers committed 18 Sept 2026 · db5d52d8d4 · 8 files, +546 −167

Portal, play and editor pages load openvibe.network's theme-loader (before
paint), navbar and footer; public/chrome.js wires them: site links, sign-in
/ sign-out via /auth/*, the ovg_sso token + /auth/me fallback, silentLogin,
and the play page's game history entry. The game and editor pages get a
#navbar-mount above a #stage that holds the canvas; once the player is in
the world (ovg:playing) the chrome folds away and a floating pill brings it
back. Canonical, Open Graph, Twitter card and JSON-LD (WebSite + VideoGame)
on the portal and play page; the editor is noindex. The portal palette now
follows the shared theme tokens. Cost claims dropped from the copy.

Changes

ddiff --git a/apps/client/editor.html b/apps/client/editor.html
iindex b0e413d..b4f280a 100644
---- a/apps/client/editor.html
++++ b/apps/client/editor.html
@@@ -3,7 +3,32 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
-- <title>Scraplandia Map Editor</title>
++ <title>Scraplandia Map Editor — OpenVibe.Games</title>
++ <meta
++ name="description"
++ content="Collaborative map editor for Scraplandia on OpenVibe.Games."
++ />
++ <!-- An admin tool: not for search engines. -->
++ <meta name="robots" content="noindex, nofollow" />
++ <link rel="canonical" href="https://openvibe.games/editor" />
++ <meta property="og:type" content="website" />
++ <meta property="og:site_name" content="OpenVibe.Games" />
++ <meta property="og:title" content="Scraplandia Map Editor" />
++ <meta
++ property="og:description"
++ content="Collaborative map editor for Scraplandia on OpenVibe.Games."
++ />
++ <meta property="og:url" content="https://openvibe.games/editor" />
++ <meta property="og:image" content="https://openvibe.games/og-image.png" />
++ <meta name="twitter:card" content="summary_large_image" />
++ <!-- Shared network chrome: the user's theme before first paint, then navbar + footer. -->
++ <script src="https://openvibe.network/shared/theme-loader.js"></script>
++ <script src="https://openvibe.network/shared/navbar.js" defer></script>
++ <script src="https://openvibe.network/shared/footer.js" defer></script>
++ <script>
++ window.__ovgChrome = { page: 'editor', collapsible: true, toggleFromStart: true }
++ </script>
++ <script src="/chrome.js" defer></script>
</head>
<body>
<!--
@@@ -15,39 +40,47 @@
What stays here is what must exist before any script runs (the canvas)
and the overlays that are not part of the workspace grid.
-->
-- <canvas id="game"></canvas>
-- <div id="shell"></div>
++ <div id="navbar-mount"></div>
++ <div id="stage">
++ <canvas id="game"></canvas>
++ <div id="shell"></div>
-- <!-- Revision conflict. Nothing here merges, and it says so. -->
-- <div id="conflict" class="overlay" style="display: none">
-- <div class="overlay-card">
-- <h2>⚠ Newer map saved by another admin</h2>
-- <div id="conflict-summary" class="hint"></div>
-- <div class="overlay-actions">
-- <button id="conflict-load" class="mini" type="button">Discard mine · load theirs</button>
-- <button id="conflict-keep" class="mini" type="button">Keep mine · dismiss</button>
-- <button id="conflict-export" class="mini" type="button">💾 Download my local copy</button>
-- </div>
-- <div class="hint">
-- "Keep mine" only dismisses this panel — Save stays refused until you reload.
++ <!-- Revision conflict. Nothing here merges, and it says so. -->
++ <div id="conflict" class="overlay" style="display: none">
++ <div class="overlay-card">
++ <h2>⚠ Newer map saved by another admin</h2>
++ <div id="conflict-summary" class="hint"></div>
++ <div class="overlay-actions">
++ <button id="conflict-load" class="mini" type="button">
++ Discard mine · load theirs
++ </button>
++ <button id="conflict-keep" class="mini" type="button">Keep mine · dismiss</button>
++ <button id="conflict-export" class="mini" type="button">
++ 💾 Download my local copy
++ </button>
++ </div>
++ <div class="hint">
++ "Keep mine" only dismisses this panel — Save stays refused until you reload.
++ </div>
</div>
</div>
-- </div>
-- <!-- Keyboard shortcuts. Every action is here; none are hardcoded. -->
-- <div id="settings" class="overlay" style="display: none">
-- <div class="overlay-card">
-- <h2>⌨ Keyboard shortcuts</h2>
-- <div id="keys-list" class="keys-list"></div>
-- <div class="overlay-actions">
-- <button id="keys-reset" class="mini" type="button">Reset to defaults</button>
-- <button id="settings-close" class="mini" type="button">Close</button>
++ <!-- Keyboard shortcuts. Every action is here; none are hardcoded. -->
++ <div id="settings" class="overlay" style="display: none">
++ <div class="overlay-card">
++ <h2>⌨ Keyboard shortcuts</h2>
++ <div id="keys-list" class="keys-list"></div>
++ <div class="overlay-actions">
++ <button id="keys-reset" class="mini" type="button">Reset to defaults</button>
++ <button id="settings-close" class="mini" type="button">Close</button>
++ </div>
</div>
</div>
-- </div>
-- <!-- Co-editor name tags, positioned over the viewport. -->
-- <div id="peers"></div>
++ <!-- Co-editor name tags, positioned over the viewport. -->
++ <div id="peers"></div>
++ </div>
++ <footer id="ov-footer" class="ovf"></footer>
<script type="module" src="/src/editor/main.ts"></script>
</body>
ddiff --git a/apps/client/index.html b/apps/client/index.html
iindex 569db1d..76761c4 100644
---- a/apps/client/index.html
++++ b/apps/client/index.html
@@@ -3,28 +3,93 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
-- <title>OpenVibe.Games — free browser games on the OpenVibe network</title>
++ <title>OpenVibe.Games — browser games on the OpenVibe network</title>
<meta
name="description"
-- content="The games arm of the OpenVibe network. Play Scraplandia — scrappy multiplayer survival-building in Scrap City — free, in your browser, no download."
++ content="The games arm of the OpenVibe network. Play Scraplandia — scrappy multiplayer survival-building in Scrap City — in your browser, no download."
/>
++ <!-- Shared network chrome: the user's theme before first paint, then navbar + footer. -->
++ <script src="https://openvibe.network/shared/theme-loader.js"></script>
++ <script src="https://openvibe.network/shared/navbar.js" defer></script>
++ <script src="https://openvibe.network/shared/footer.js" defer></script>
++ <link rel="canonical" href="https://openvibe.games/" />
++ <meta name="robots" content="index, follow" />
++ <meta property="og:type" content="website" />
++ <meta property="og:site_name" content="OpenVibe.Games" />
++ <meta property="og:title" content="OpenVibe.Games — browser games on the OpenVibe network" />
++ <meta
++ property="og:description"
++ content="Play Scraplandia — scrappy multiplayer survival-building in Scrap City — in your browser, no download. One OpenVibe account across the whole network."
++ />
++ <meta property="og:url" content="https://openvibe.games/" />
++ <meta property="og:image" content="https://openvibe.games/og-image.png" />
++ <meta property="og:image:width" content="1200" />
++ <meta property="og:image:height" content="630" />
++ <meta name="twitter:card" content="summary_large_image" />
++ <meta name="twitter:title" content="OpenVibe.Games — browser games on the OpenVibe network" />
++ <meta
++ name="twitter:description"
++ content="Play Scraplandia — multiplayer survival-building in your browser, no download."
++ />
++ <meta name="twitter:image" content="https://openvibe.games/og-image.png" />
++ <script type="application/ld+json">
++ {
++ "@context": "https://schema.org",
++ "@graph": [
++ {
++ "@type": "WebSite",
++ "@id": "https://openvibe.games/#website",
++ "name": "OpenVibe.Games",
++ "url": "https://openvibe.games/",
++ "description": "Browser games on the OpenVibe network — multiplayer, in your browser, no download.",
++ "publisher": {
++ "@type": "Organization",
++ "name": "OpenVibe",
++ "url": "https://openvibe.network/"
++ }
++ },
++ {
++ "@type": "VideoGame",
++ "@id": "https://play.openvibe.games/#game",
++ "name": "Scraplandia",
++ "url": "https://play.openvibe.games/",
++ "image": "https://openvibe.games/og-image.png",
++ "description": "Scrappy multiplayer survival-building in and around Scrap City: physgun anything, weld it to anything else, chase supply drops in the wilds and haul your salvage home.",
++ "genre": ["Survival", "Sandbox", "Building"],
++ "playMode": "MultiPlayer",
++ "gamePlatform": "Web browser",
++ "applicationCategory": "Game",
++ "operatingSystem": "Any",
++ "isPartOf": { "@id": "https://openvibe.games/#website" },
++ "publisher": {
++ "@type": "Organization",
++ "name": "OpenVibe",
++ "url": "https://openvibe.network/"
++ }
++ }
++ ]
++ }
++ </script>
<style>
* {
margin: 0;
box-sizing: border-box;
}
++ /* Page palette. The shared theme-loader sets --accent, --accent-light, --accent-dark,
++ --accent-rgb, --bg-primary, --text-primary… on <html>, so the portal follows the
++ signed-in user's theme; the values here are the fallbacks before it runs. */
:root {
-- --accent: #8b5cf6;
-- --accent-light: #a78bfa;
-- --accent-dark: #6d28d9;
++ --accent: #3b82f6;
++ --accent-light: #60a5fa;
++ --accent-dark: #1d4ed8;
--secondary: #22d3ee;
-- --bg: #131318;
-- --bg-raised: #1a1a22;
-- --bg-card: rgba(26, 26, 34, 0.88);
-- --border: #2a2a36;
-- --text: #e8e8f0;
-- --text-dim: #a0a0b2;
-- --text-faint: #6a6a7a;
++ --bg: var(--bg-primary, #0a0f1c);
++ --bg-raised: var(--bg-secondary, #101828);
++ --bg-card: var(--bg-card-page, rgba(19, 28, 46, 0.88));
++ --border: #1f2d47;
++ --text: var(--text-primary, #e6edf7);
++ --text-dim: var(--text-secondary, #96a7c2);
++ --text-faint: var(--text-muted, #7386a3);
}
html,
body {
@@@ -45,9 +110,13 @@
inset: 0;
z-index: -2;
background:
-- radial-gradient(1100px 520px at 50% -8%, rgba(139, 92, 246, 0.22), transparent 68%),
++ radial-gradient(
++ 1100px 520px at 50% -8%,
++ rgba(var(--accent-rgb, 59, 130, 246), 0.22),
++ transparent 68%
++ ),
radial-gradient(800px 420px at 88% 28%, rgba(34, 211, 238, 0.07), transparent 60%),
-- linear-gradient(180deg, #16121f 0%, var(--bg) 46%);
++ linear-gradient(180deg, var(--bg-raised) 0%, var(--bg) 46%);
}
.stars {
position: fixed;
@@@ -56,59 +125,6 @@
opacity: 0.7;
}
-- /* ── nav ─────────────────────────────────────────────────────── */
-- nav {
-- display: flex;
-- align-items: center;
-- gap: 20px;
-- padding: 16px clamp(16px, 4vw, 40px);
-- max-width: 1200px;
-- margin: 0 auto;
-- }
-- .brand {
-- font-weight: 900;
-- font-size: 19px;
-- letter-spacing: 0.02em;
-- text-decoration: none;
-- color: var(--text);
-- white-space: nowrap;
-- }
-- .brand b {
-- background: linear-gradient(120deg, var(--accent-light), var(--accent));
-- -webkit-background-clip: text;
-- background-clip: text;
-- color: transparent;
-- }
-- .nav-links {
-- margin-left: auto;
-- display: flex;
-- align-items: center;
-- gap: 6px;
-- flex-wrap: wrap;
-- }
-- .nav-links a {
-- color: var(--text-dim);
-- text-decoration: none;
-- font-size: 14px;
-- padding: 8px 12px;
-- border-radius: 8px;
-- transition:
-- color 0.15s,
-- background 0.15s;
-- }
-- .nav-links a:hover {
-- color: var(--text);
-- background: rgba(139, 92, 246, 0.12);
-- }
-- .nav-links a.signin {
-- color: #fff;
-- background: linear-gradient(120deg, var(--accent), var(--accent-dark));
-- font-weight: 700;
-- }
-- .nav-links a.signin:hover {
-- background: linear-gradient(120deg, var(--accent-light), var(--accent));
-- }
--
/* ── hero ────────────────────────────────────────────────────── */
header {
text-align: center;
@@@ -122,11 +138,16 @@
animation: rise 0.9s cubic-bezier(0.2, 0.8, 0.2, 1) both;
}
.hero-title b {
-- background: linear-gradient(120deg, var(--accent-light) 10%, var(--accent) 55%, var(--secondary) 110%);
++ background: linear-gradient(
++ 120deg,
++ var(--accent-light) 10%,
++ var(--accent) 55%,
++ var(--secondary) 110%
++ );
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-- filter: drop-shadow(0 6px 34px rgba(139, 92, 246, 0.35));
++ filter: drop-shadow(0 6px 34px rgba(var(--accent-rgb, 59, 130, 246), 0.35));
}
.hero-sub {
margin: 18px auto 0;
@@@ -171,7 +192,7 @@
.game-card:hover {
transform: translateY(-5px);
border-color: var(--accent);
-- box-shadow: 0 14px 44px rgba(139, 92, 246, 0.18);
++ box-shadow: 0 14px 44px rgba(var(--accent-rgb, 59, 130, 246), 0.18);
}
.game-card.featured {
grid-column: 1 / -1;
@@@ -190,17 +211,21 @@
align-items: center;
justify-content: center;
background:
-- radial-gradient(420px 200px at 30% 20%, rgba(139, 92, 246, 0.35), transparent 70%),
++ radial-gradient(
++ 420px 200px at 30% 20%,
++ rgba(var(--accent-rgb, 59, 130, 246), 0.35),
++ transparent 70%
++ ),
radial-gradient(380px 220px at 75% 85%, rgba(34, 211, 238, 0.18), transparent 70%),
-- linear-gradient(160deg, #221a33, #141420 70%);
++ linear-gradient(160deg, var(--bg-raised), var(--bg) 70%);
}
.shot::after {
content: '';
position: absolute;
inset: 0;
background-image:
-- linear-gradient(rgba(167, 139, 250, 0.06) 1px, transparent 1px),
-- linear-gradient(90deg, rgba(167, 139, 250, 0.06) 1px, transparent 1px);
++ linear-gradient(rgba(var(--accent-rgb, 59, 130, 246), 0.06) 1px, transparent 1px),
++ linear-gradient(90deg, rgba(var(--accent-rgb, 59, 130, 246), 0.06) 1px, transparent 1px);
background-size: 34px 34px;
mask-image: radial-gradient(closest-side, #000, transparent);
-webkit-mask-image: radial-gradient(closest-side, #000, transparent);
@@@ -231,8 +256,8 @@
}
.badge.soon {
color: var(--accent-light);
-- background: rgba(139, 92, 246, 0.14);
-- border: 1px solid rgba(139, 92, 246, 0.35);
++ background: rgba(var(--accent-rgb, 59, 130, 246), 0.14);
++ border: 1px solid rgba(var(--accent-rgb, 59, 130, 246), 0.35);
}
.game-body h2 {
font-size: clamp(26px, 3.4vw, 38px);
@@@ -273,7 +298,7 @@
text-decoration: none;
color: #fff;
background: linear-gradient(120deg, var(--accent), var(--accent-dark));
-- box-shadow: 0 8px 30px rgba(139, 92, 246, 0.4);
++ box-shadow: 0 8px 30px rgba(var(--accent-rgb, 59, 130, 246), 0.4);
transition:
transform 0.15s ease,
box-shadow 0.15s ease;
@@@ -285,10 +310,10 @@
@keyframes pulse {
0%,
100% {
-- box-shadow: 0 8px 30px rgba(139, 92, 246, 0.4);
++ box-shadow: 0 8px 30px rgba(var(--accent-rgb, 59, 130, 246), 0.4);
}
50% {
-- box-shadow: 0 8px 48px rgba(139, 92, 246, 0.7);
++ box-shadow: 0 8px 48px rgba(var(--accent-rgb, 59, 130, 246), 0.7);
}
}
.game-card.soon {
@@@ -309,8 +334,12 @@
padding: 0 20px;
}
.account-inner {
-- background: linear-gradient(160deg, rgba(139, 92, 246, 0.12), rgba(26, 26, 34, 0.9) 55%);
-- border: 1px solid rgba(139, 92, 246, 0.3);
++ background: linear-gradient(
++ 160deg,
++ rgba(var(--accent-rgb, 59, 130, 246), 0.12),
++ var(--bg-card) 55%
++ );
++ border: 1px solid rgba(var(--accent-rgb, 59, 130, 246), 0.3);
border-radius: 20px;
padding: clamp(26px, 5vw, 44px);
text-align: center;
@@@ -348,27 +377,14 @@
color: var(--text-faint);
}
-- /* ── footer ──────────────────────────────────────────────────── */
-- footer {
-- border-top: 1px solid var(--border);
-- padding: 30px 20px 40px;
-- text-align: center;
-- color: var(--text-faint);
-- font-size: 13px;
-- line-height: 2;
-- }
-- footer a {
-- color: var(--text-dim);
-- text-decoration: none;
-- margin: 0 10px;
-- }
-- footer a:hover {
-- color: var(--accent-light);
++ /* ── shared chrome ───────────────────────────────────────────── */
++ #navbar-mount {
++ position: relative;
++ z-index: 5;
}
-- .foot-brand {
-- font-weight: 800;
-- color: var(--text-dim);
-- letter-spacing: 0.06em;
++ #ov-footer {
++ max-width: 1200px;
++ margin: 0 auto;
}
</style>
</head>
@@@ -376,24 +392,13 @@
<div class="backdrop"></div>
<canvas class="stars" id="stars"></canvas>
-- <nav>
-- <a class="brand" href="/">Open<b>Vibe</b>.Games</a>
-- <div class="nav-links">
-- <a href="https://openvibe.live" target="_blank" rel="noopener">openvibe.live</a>
-- <a href="https://openvibe.tools" target="_blank" rel="noopener">openvibe.tools</a>
-- <a
-- class="signin"
-- href="https://openvibe.network/login?redirect=https%3A%2F%2Fplay.openvibe.games"
-- >Sign in</a
-- >
-- </div>
-- </nav>
++ <div id="navbar-mount"></div>
<header>
<h1 class="hero-title">Open<b>Vibe.Games</b></h1>
<p class="hero-sub">
-- Free multiplayer games that run in your browser. No installs, no launchers — one OpenVibe
-- account across the whole network.
++ Multiplayer games that run in your browser. No installs, no launchers — one OpenVibe account
++ across the whole network.
</p>
</header>
@@@ -416,7 +421,7 @@
><span>PvP wilds</span><span>bottle caps</span>
</div>
<a class="play-cta" href="https://play.openvibe.games" data-dev-href="/play"
-- >▶ PLAY FREE — NO DOWNLOAD</a
++ >▶ PLAY — NO DOWNLOAD</a
>
</div>
</div>
@@@ -427,7 +432,9 @@
<div class="game-body">
<span class="badge soon">COMING SOON</span>
<h3>Vibe Tables</h3>
-- <p>Casual card and board nights with your stream chat — join from a link, no account walls.</p>
++ <p>
++ Casual card and board nights with your stream chat — join from a link, no account walls.
++ </p>
</div>
</div>
<div class="game-card soon">
@@@ -453,31 +460,22 @@
<h2>One character as a guest. <b>Three</b> with an OpenVibe account.</h2>
<p>
Jump into any game instantly as a guest — your character is tied to this browser and
-- connection. Sign in with your free <b>OpenVibe</b> account to unlock three character
-- slots, keep progress safe across devices and IP changes, and carry one identity across
++ connection. Sign in with your <b>OpenVibe</b> account to unlock three character slots,
++ keep progress safe across devices and IP changes, and carry one identity across
openvibe.live, openvibe.tools and every game here.
</p>
-- <a
-- class="signin"
-- href="https://openvibe.network/login?redirect=https%3A%2F%2Fplay.openvibe.games"
-- >Sign in with OpenVibe</a
-- >
++ <a class="signin" href="/auth/login?next=%2F">Sign in with OpenVibe</a>
<div class="fine">One account. All of OpenVibe.</div>
</div>
</section>
-- <footer>
-- <div>
-- <a href="https://openvibe.network">openvibe.network</a>
-- <a href="https://openvibe.live">openvibe.live</a>
-- <a href="https://openvibe.tools">openvibe.tools</a>
-- <a href="https://openvibe.media">openvibe.media</a>
-- <a href="https://openvibe.community">openvibe.community</a>
-- </div>
-- <div class="foot-brand">OPENVIBE.GAMES</div>
-- <div>Part of the OpenVibe network · One Account. All of OpenVibe.</div>
-- </footer>
++ <!-- Shared cross-site footer (openvibe.network/shared/footer.js): network links, legal, signed-in row. -->
++ <footer id="ov-footer" class="ovf"></footer>
++ <script>
++ window.__ovgChrome = { page: 'portal' }
++ </script>
++ <script src="/chrome.js" defer></script>
<script>
// Dev convenience: on localhost the play. vhost doesn't exist, so PLAY
// buttons fall back to the same-origin /play route (Vite alias / server).
@@@ -506,7 +504,11 @@
ctx.clearRect(0, 0, c.width, c.height)
for (const [x, y, r, tint] of stars) {
ctx.globalAlpha = 0.25 + Math.random() * 0.5
-- ctx.fillStyle = tint ? '#a78bfa' : '#ffffff'
++ ctx.fillStyle = tint
++ ? getComputedStyle(document.documentElement)
++ .getPropertyValue('--accent-light')
++ .trim() || '#60a5fa'
++ : '#ffffff'
ctx.beginPath()
ctx.arc(x * c.width, y * c.height, r, 0, 7)
ctx.fill()
ddiff --git a/apps/client/play.html b/apps/client/play.html
iindex 8183d45..e4c1e92 100644
---- a/apps/client/play.html
++++ b/apps/client/play.html
@@@ -3,12 +3,79 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
-- <title>Scraplandia</title>
++ <title>Scraplandia — OpenVibe.Games</title>
++ <meta
++ name="description"
++ content="Play Scraplandia: scrappy multiplayer survival-building in and around Scrap City, in your browser, no download. Part of the OpenVibe network."
++ />
++ <!-- Shared network chrome: the user's theme before first paint, then navbar + footer. -->
++ <script src="https://openvibe.network/shared/theme-loader.js"></script>
++ <script src="https://openvibe.network/shared/navbar.js" defer></script>
++ <script src="https://openvibe.network/shared/footer.js" defer></script>
++ <link rel="canonical" href="https://play.openvibe.games/" />
++ <meta name="robots" content="index, follow" />
++ <meta property="og:type" content="website" />
++ <meta property="og:site_name" content="OpenVibe.Games" />
++ <meta property="og:title" content="Scraplandia — play in your browser" />
++ <meta
++ property="og:description"
++ content="Scrappy multiplayer survival-building in and around Scrap City. Physgun anything, weld it to anything else, haul your salvage home. No download."
++ />
++ <meta property="og:url" content="https://play.openvibe.games/" />
++ <meta property="og:image" content="https://openvibe.games/og-image.png" />
++ <meta property="og:image:width" content="1200" />
++ <meta property="og:image:height" content="630" />
++ <meta name="twitter:card" content="summary_large_image" />
++ <meta name="twitter:title" content="Scraplandia — play in your browser" />
++ <meta
++ name="twitter:description"
++ content="Multiplayer survival-building on the OpenVibe network. In your browser, no download."
++ />
++ <meta name="twitter:image" content="https://openvibe.games/og-image.png" />
++ <script type="application/ld+json">
++ {
++ "@context": "https://schema.org",
++ "@type": "VideoGame",
++ "@id": "https://play.openvibe.games/#game",
++ "name": "Scraplandia",
++ "url": "https://play.openvibe.games/",
++ "image": "https://openvibe.games/og-image.png",
++ "description": "Scrappy multiplayer survival-building in and around Scrap City: physgun anything, weld it to anything else, chase supply drops in the wilds and haul your salvage home.",
++ "genre": ["Survival", "Sandbox", "Building"],
++ "playMode": "MultiPlayer",
++ "gamePlatform": "Web browser",
++ "applicationCategory": "Game",
++ "operatingSystem": "Any",
++ "isPartOf": {
++ "@type": "WebSite",
++ "name": "OpenVibe.Games",
++ "url": "https://openvibe.games/"
++ },
++ "publisher": {
++ "@type": "Organization",
++ "name": "OpenVibe",
++ "url": "https://openvibe.network/"
++ }
++ }
++ </script>
<link rel="stylesheet" href="/src/ui/styles.css" />
++ <script>
++ window.__ovgChrome = { page: 'play', title: 'Scraplandia', collapsible: true }
++ </script>
++ <script src="/chrome.js" defer></script>
</head>
<body>
-- <canvas id="game"></canvas>
-- <div id="ui"></div>
++ <!--
++ The shared navbar mounts at the top; the game fills the stage below it.
++ Once the player is in the world (the `ovg:playing` event) the chrome
++ folds away and a small floating button brings it back — see chrome.js.
++ -->
++ <div id="navbar-mount"></div>
++ <div id="stage">
++ <canvas id="game"></canvas>
++ <div id="ui"></div>
++ </div>
++ <footer id="ov-footer" class="ovf"></footer>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
ddiff --git a/apps/client/public/chrome.js b/apps/client/public/chrome.js
nnew file mode 100644
iindex 0000000..ee54881
---- /dev/null
++++ b/apps/client/public/chrome.js
@@@ -0,0 +1,153 @@
++/**
++ * OpenVibe.Games — the shared network chrome (navbar + footer) on every page.
++ *
++ * The navbar and footer themselves come from openvibe.network/shared/*.js,
++ * loaded by each page's <head>; this file only tells them who we are: the
++ * site's links, where sign-in/sign-out live, and the session the game keeps
++ * in the `ovg_sso` cookie / localStorage entry (set by /auth/callback).
++ *
++ * Pages configure it with `window.__ovgChrome = { page, title, collapsible }`
++ * before this script runs:
++ * page 'portal' | 'play' | 'editor'
++ * title recorded to the signed-in user's network history (play only)
++ * collapsible the chrome folds away once the game starts (the `ovg:playing`
++ * event) and a small floating button brings it back.
++ * toggleFromStart show that button immediately (the editor), not only after
++ * `ovg:playing`.
++ */
++;(function () {
++ 'use strict'
++ var NETWORK = 'https://openvibe.network'
++ var cfg = window.__ovgChrome || {}
++ var local = ['localhost', '127.0.0.1'].indexOf(location.hostname) !== -1
++ var onPlayHost = location.hostname.indexOf('play.') === 0
++ var PORTAL = local || !onPlayHost ? '/' : 'https://openvibe.games/'
++ var PLAY = local ? '/play' : onPlayHost ? '/' : 'https://play.openvibe.games/'
++ var EDITOR = local || !onPlayHost ? '/editor' : 'https://openvibe.games/editor'
++
++ function cookie(name) {
++ var m = document.cookie.match(new RegExp('(?:^|;\\s*)' + name + '=([^;]*)'))
++ try {
++ return m ? decodeURIComponent(m[1]) : null
++ } catch (e) {
++ return m ? m[1] : null
++ }
++ }
++ /** The openvibe.network access token this site holds, if any. */
++ function sessionToken() {
++ var t = null
++ try {
++ t = localStorage.getItem('ovg_sso')
++ } catch (e) {
++ /* storage unavailable */
++ }
++ return t || cookie('ovg_sso') || undefined
++ }
++
++ var here = location.pathname + location.search
++ var links = [
++ { label: 'Games', href: PORTAL },
++ { label: 'Scraplandia', href: PLAY },
++ ]
++ var footerLinks = [
++ {
++ heading: 'Games',
++ items: [
++ { label: 'All games', href: PORTAL },
++ { label: 'Play Scraplandia', href: PLAY },
++ { label: 'Map editor', href: EDITOR },
++ ],
++ },
++ {
++ heading: 'Account',
++ items: [
++ { label: 'Sign in', href: '/auth/login?next=' + encodeURIComponent(here) },
++ { label: 'Sign out', href: '/auth/logout?next=' + encodeURIComponent(location.pathname) },
++ { label: 'Themes', href: NETWORK + '/themes' },
++ ],
++ },
++ ]
++
++ function initNavbar() {
++ var opts = {
++ service: 'games',
++ apiBase: NETWORK,
++ token: sessionToken(),
++ // Same-origin fallback: resolves the ovg_sso cookie server-side when the
++ // token is not readable here (or the Network rejects it client-side).
++ sessionUrl: '/auth/me',
++ loginUrl: '/auth/login?next=' + encodeURIComponent(here),
++ onLogout: function () {
++ location.href = '/auth/logout?next=' + encodeURIComponent(location.pathname)
++ },
++ links: links,
++ menu: { after: [{ label: 'Map editor', href: EDITOR, icon: 'fa-map' }] },
++ // One prompt=none attempt per tab when the browser says it has an OpenVibe account.
++ silentLogin: '/auth/login?silent=1&next={url}',
++ }
++ if (cfg.page === 'play') opts.history = { type: 'game', title: cfg.title || 'Scraplandia' }
++ window.OpenVibeNavbar.init(opts)
++ }
++
++ function initFooter() {
++ if (!document.getElementById('ov-footer')) return
++ window.OpenVibeFooter.init({
++ service: 'games',
++ variant: cfg.page === 'portal' ? 'full' : 'compact',
++ links: footerLinks,
++ mount: '#ov-footer',
++ })
++ }
++
++ // ── Collapsible chrome for the game / editor pages ─────────────────────
++ // The canvas is sized by CSS below the navbar, so showing or hiding the
++ // chrome is a class flip plus a resize event for the engine.
++ function setupCollapse() {
++ var stage = document.getElementById('stage')
++ if (!stage) return
++ var btn = document.createElement('button')
++ btn.id = 'ovg-chrome-toggle'
++ btn.type = 'button'
++ btn.setAttribute('aria-label', 'Show or hide the OpenVibe navigation')
++ btn.innerHTML = '<span class="ovg-chrome-toggle-mark">☰</span> OpenVibe'
++ // The game page reveals it once play starts; the editor has it from the start.
++ btn.hidden = !cfg.toggleFromStart
++ function apply(hidden) {
++ document.body.classList.toggle('ovg-chrome-hidden', hidden)
++ btn.classList.toggle('is-hidden', hidden)
++ btn.title = hidden ? 'Show the OpenVibe navigation' : 'Hide the OpenVibe navigation'
++ window.dispatchEvent(new Event('resize'))
++ }
++ btn.addEventListener('click', function () {
++ apply(!document.body.classList.contains('ovg-chrome-hidden'))
++ })
++ stage.appendChild(btn)
++ window.addEventListener('ovg:playing', function () {
++ btn.hidden = false
++ apply(true)
++ })
++ }
++
++ var tries = 0
++ function boot() {
++ if (!window.OpenVibeNavbar || !window.OpenVibeFooter) {
++ // The shared scripts are deferred; if the Network is unreachable the
++ // page simply runs without its chrome.
++ if (++tries < 60) setTimeout(boot, 100)
++ return
++ }
++ try {
++ initNavbar()
++ } catch (e) {
++ /* navbar optional */
++ }
++ try {
++ initFooter()
++ } catch (e) {
++ /* footer optional */
++ }
++ }
++ if (cfg.collapsible) setupCollapse()
++ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot)
++ else boot()
++})()
ddiff --git a/apps/client/public/og-image.png b/apps/client/public/og-image.png
nnew file mode 100644
iindex 0000000..775abdb
BBinary files /dev/null and b/apps/client/public/og-image.png differ
ddiff --git a/apps/client/src/editor/editor.css b/apps/client/src/editor/editor.css
iindex c641ced..aeef4b0 100644
---- a/apps/client/src/editor/editor.css
++++ b/apps/client/src/editor/editor.css
@@@ -519,6 +519,61 @@ body {
sans-serif;
}
++/* Page frame: shared navbar, the editor stage, a one-line footer. The stage
++ is the positioning root for the canvas, the workspace grid and the dialogs. */
++body {
++ display: flex;
++ flex-direction: column;
++}
++#navbar-mount {
++ flex: none;
++ position: relative;
++ z-index: 30;
++}
++#stage {
++ position: relative;
++ flex: 1 1 auto;
++ min-height: 0;
++}
++#ov-footer {
++ flex: none;
++}
++#ov-footer.ovf {
++ margin-top: 0 !important;
++ padding-top: 6px !important;
++ padding-bottom: 6px !important;
++}
++body.ovg-chrome-hidden #navbar-mount,
++body.ovg-chrome-hidden #ov-footer {
++ display: none;
++}
++#ovg-chrome-toggle {
++ position: absolute;
++ right: 10px;
++ bottom: 30px;
++ z-index: 40;
++ display: inline-flex;
++ align-items: center;
++ gap: 6px;
++ padding: 4px 10px;
++ border-radius: 999px;
++ border: 1px solid var(--line, #2a3340);
++ background: var(--panel-bg, rgba(10, 14, 18, 0.8));
++ color: var(--dim, #78828e);
++ font:
++ 600 11px/1 system-ui,
++ sans-serif;
++ cursor: pointer;
++ opacity: 0.6;
++}
++#ovg-chrome-toggle:hover {
++ opacity: 1;
++ color: var(--ink, #d5dbe2);
++}
++#ovg-chrome-toggle[hidden] {
++ display: none;
++}
++
#game {
position: absolute;
inset: 0;
ddiff --git a/apps/client/src/main.ts b/apps/client/src/main.ts
iindex af0dd57..6ce04fd 100644
---- a/apps/client/src/main.ts
++++ b/apps/client/src/main.ts
@@@ -152,6 +152,9 @@ async function start(): Promise<void> {
}
const slot = choice.slot
saveName(name)
++ // The player is heading into the world: the page folds the network chrome
++ // away so the canvas gets the whole viewport (see public/chrome.js).
++ window.dispatchEvent(new CustomEvent('ovg:playing'))
const havok = await havokPromise
const physics = createHavokWorldForScene(scene, havok)
ddiff --git a/apps/client/src/ui/styles.css b/apps/client/src/ui/styles.css
iindex 190f982..f036c23 100644
---- a/apps/client/src/ui/styles.css
++++ b/apps/client/src/ui/styles.css
@@@ -21,6 +21,28 @@ input {
-webkit-user-select: text;
}
++/* ── Page frame ────────────────────────────────────────────────────
++ Shared navbar on top, the game stage filling the rest, a one-line
++ footer below. Once the player is in the world the chrome folds away
++ (body.ovg-chrome-hidden, see public/chrome.js) and the stage takes the
++ whole viewport; the engine resizes off the canvas's CSS box. */
++body {
++ display: flex;
++ flex-direction: column;
++}
++
++#navbar-mount {
++ flex: none;
++ position: relative;
++ z-index: 30;
++}
++
++#stage {
++ position: relative;
++ flex: 1 1 auto;
++ min-height: 0;
++}
++
#game {
width: 100%;
height: 100%;
@@@ -29,11 +51,55 @@ input {
}
#ui {
-- position: fixed;
++ position: absolute;
inset: 0;
pointer-events: none;
}
++#ov-footer {
++ flex: none;
++}
++#ov-footer.ovf {
++ margin-top: 0 !important;
++ padding-top: 8px !important;
++ padding-bottom: 8px !important;
++}
++
++body.ovg-chrome-hidden #navbar-mount,
++body.ovg-chrome-hidden #ov-footer {
++ display: none;
++}
++
++/* Floating "bring the chrome back" pill (chrome.js adds it once the game starts). */
++#ovg-chrome-toggle {
++ position: absolute;
++ top: 10px;
++ left: 10px;
++ z-index: 40;
++ display: inline-flex;
++ align-items: center;
++ gap: 6px;
++ padding: 5px 11px;
++ border-radius: 999px;
++ border: 1px solid var(--border, #2a3340);
++ background: var(--bg-secondary, rgba(10, 14, 18, 0.8));
++ color: var(--text-secondary, #b0b0b8);
++ font:
++ 600 12px/1 system-ui,
++ sans-serif;
++ cursor: pointer;
++ opacity: 0.55;
++ transition: opacity 0.15s;
++}
++#ovg-chrome-toggle:hover,
++#ovg-chrome-toggle:focus-visible {
++ opacity: 1;
++ color: var(--text-primary, #e6e6e6);
++}
++#ovg-chrome-toggle[hidden] {
++ display: none;
++}
++
.crosshair {
position: absolute;
left: 50%;