openvibers/openvibe.games Commits

The OpenVibe Frame on openvibe.games: public/chrome.js is frame.js (__ovgFrame, ovg-frame-hidden, ovg-frame-toggle); the portal shows what shipped (the network's shared shipped views) and the footer's Updates link opens the Games log on openvibe.network

OP OpenVibers committed 24 Sept 2026 · 3fe7f7b899 · 9 files, +192 −183


Changes

ddiff --git a/apps/client/editor.html b/apps/client/editor.html
iindex b4f280a..ef6265c 100644
---- a/apps/client/editor.html
++++ b/apps/client/editor.html
@@@ -21,14 +21,14 @@
<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. -->
++ <!-- The OpenVibe Frame: 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 }
++ window.__ovgFrame = { page: 'editor', collapsible: true, toggleFromStart: true }
</script>
-- <script src="/chrome.js" defer></script>
++ <script src="/frame.js" defer></script>
</head>
<body>
<!--
ddiff --git a/apps/client/index.html b/apps/client/index.html
iindex e64d263..431948f 100644
---- a/apps/client/index.html
++++ b/apps/client/index.html
@@@ -8,7 +8,7 @@
name="description"
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. -->
++ <!-- The OpenVibe Frame: 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>
@@@ -377,7 +377,7 @@
color: var(--text-faint);
}
-- /* ── shared chrome ───────────────────────────────────────────── */
++ /* ── the OpenVibe Frame ───────────────────────────────────────────── */
#navbar-mount {
position: relative;
z-index: 5;
@@@ -470,13 +470,21 @@
</div>
</section>
++ <!-- What shipped on OpenVibe.Games: the network's shared "shipped" views (openvibe-shared
++ shipped.js, which the shared footer loads), the same on every OpenVibe site. -->
++ <section class="ov-shipped-home" aria-label="What shipped" style="max-width:1100px;margin:0 auto;padding:0 20px">
++ <a data-ov-shipped="latest" data-service="games" href="https://openvibe.network/updates?site=games" hidden></a>
++ <div data-ov-shipped="list" data-service="games" data-limit="5" data-more="https://openvibe.network/updates?site=games" data-title="Recently shipped on OpenVibe.Games" hidden></div>
++ <noscript><p><a href="https://openvibe.network/updates?site=games">What shipped recently</a></p></noscript>
++ </section>
++
<!-- 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' }
++ window.__ovgFrame = { page: 'portal' }
</script>
-- <script src="/chrome.js" defer></script>
++ <script src="/frame.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).
ddiff --git a/apps/client/play.html b/apps/client/play.html
iindex e4c1e92..e7739a5 100644
---- a/apps/client/play.html
++++ b/apps/client/play.html
@@@ -8,7 +8,7 @@
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. -->
++ <!-- The OpenVibe Frame: 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>
@@@ -60,15 +60,15 @@
</script>
<link rel="stylesheet" href="/src/ui/styles.css" />
<script>
-- window.__ovgChrome = { page: 'play', title: 'Scraplandia', collapsible: true }
++ window.__ovgFrame = { page: 'play', title: 'Scraplandia', collapsible: true }
</script>
-- <script src="/chrome.js" defer></script>
++ <script src="/frame.js" defer></script>
</head>
<body>
<!--
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.
++ Once the player is in the world (the `ovg:playing` event) the Frame
++ folds away and a small floating button brings it back — see frame.js.
-->
<div id="navbar-mount"></div>
<div id="stage">
ddiff --git a/apps/client/public/chrome.js b/apps/client/public/chrome.js
ddeleted file mode 100644
iindex ee54881..0000000
---- a/apps/client/public/chrome.js
++++ /dev/null
@@@ -1,153 +0,0 @@
--/**
-- * 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/frame.js b/apps/client/public/frame.js
nnew file mode 100644
iindex 0000000..57eb6d4
---- /dev/null
++++ b/apps/client/public/frame.js
@@@ -0,0 +1,154 @@
++/**
++ * OpenVibe.Games — the OpenVibe Frame (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.__ovgFrame = { 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 Frame 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.__ovgFrame || {}
++ 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',
++ updates: 'https://openvibe.network/updates?site=games',
++ })
++ }
++
++ // ── Collapsible Frame for the game / editor pages ─────────────────────
++ // The canvas is sized by CSS below the navbar, so showing or hiding the
++ // Frame 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-frame-toggle'
++ btn.type = 'button'
++ btn.setAttribute('aria-label', 'Show or hide the OpenVibe navigation')
++ btn.innerHTML = '<span class="ovg-frame-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-frame-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-frame-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 Frame.
++ 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/src/editor/editor.css b/apps/client/src/editor/editor.css
iindex aeef4b0..3dcb8e0 100644
---- a/apps/client/src/editor/editor.css
++++ b/apps/client/src/editor/editor.css
@@@ -543,11 +543,11 @@ body {
padding-top: 6px !important;
padding-bottom: 6px !important;
}
--body.ovg-chrome-hidden #navbar-mount,
--body.ovg-chrome-hidden #ov-footer {
++body.ovg-frame-hidden #navbar-mount,
++body.ovg-frame-hidden #ov-footer {
display: none;
}
--#ovg-chrome-toggle {
++#ovg-frame-toggle {
position: absolute;
right: 10px;
bottom: 30px;
@@@ -566,11 +566,11 @@ body.ovg-chrome-hidden #ov-footer {
cursor: pointer;
opacity: 0.6;
}
--#ovg-chrome-toggle:hover {
++#ovg-frame-toggle:hover {
opacity: 1;
color: var(--ink, #d5dbe2);
}
--#ovg-chrome-toggle[hidden] {
++#ovg-frame-toggle[hidden] {
display: none;
}
ddiff --git a/apps/client/src/main.ts b/apps/client/src/main.ts
iindex 6ce04fd..4d74286 100644
---- a/apps/client/src/main.ts
++++ b/apps/client/src/main.ts
@@@ -152,8 +152,8 @@ 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).
++ // The player is heading into the world: the page folds the OpenVibe Frame
++ // away so the canvas gets the whole viewport (see public/frame.js).
window.dispatchEvent(new CustomEvent('ovg:playing'))
const havok = await havokPromise
ddiff --git a/apps/client/src/ui/styles.css b/apps/client/src/ui/styles.css
iindex f036c23..e68d645 100644
---- a/apps/client/src/ui/styles.css
++++ b/apps/client/src/ui/styles.css
@@@ -23,8 +23,8 @@ input {
/* ── 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
++ footer below. Once the player is in the world the Frame folds away
++ (body.ovg-frame-hidden, see public/frame.js) and the stage takes the
whole viewport; the engine resizes off the canvas's CSS box. */
body {
display: flex;
@@@ -65,13 +65,13 @@ body {
padding-bottom: 8px !important;
}
--body.ovg-chrome-hidden #navbar-mount,
--body.ovg-chrome-hidden #ov-footer {
++body.ovg-frame-hidden #navbar-mount,
++body.ovg-frame-hidden #ov-footer {
display: none;
}
--/* Floating "bring the chrome back" pill (chrome.js adds it once the game starts). */
--#ovg-chrome-toggle {
++/* Floating "bring the Frame back" pill (frame.js adds it once the game starts). */
++#ovg-frame-toggle {
position: absolute;
top: 10px;
left: 10px;
@@@ -91,12 +91,12 @@ body.ovg-chrome-hidden #ov-footer {
opacity: 0.55;
transition: opacity 0.15s;
}
--#ovg-chrome-toggle:hover,
--#ovg-chrome-toggle:focus-visible {
++#ovg-frame-toggle:hover,
++#ovg-frame-toggle:focus-visible {
opacity: 1;
color: var(--text-primary, #e6e6e6);
}
--#ovg-chrome-toggle[hidden] {
++#ovg-frame-toggle[hidden] {
display: none;
}
ddiff --git a/apps/server/src/net/httpServer.test.ts b/apps/server/src/net/httpServer.test.ts
iindex c281423..b11e3c9 100644
---- a/apps/server/src/net/httpServer.test.ts
++++ b/apps/server/src/net/httpServer.test.ts
@@@ -78,7 +78,7 @@ beforeAll(async () => {
writeFileSync(join(dist, 'index.html'), '<!doctype html><title>portal page</title>')
writeFileSync(join(dist, 'play.html'), '<!doctype html><title>game page</title>')
writeFileSync(join(dist, 'editor.html'), '<!doctype html><title>editor page</title>')
-- writeFileSync(join(dist, 'chrome.js'), 'void 0')
++ writeFileSync(join(dist, 'frame.js'), 'void 0')
writeFileSync(join(dist, ASSET.slice(1)), 'export {}')
writeFileSync(join(dist, 'assets', 'tex', 'wood_planks.jpg'), 'jpeg')
const mapPath = join(dir, 'data', 'map.json')
@@@ -141,7 +141,7 @@ describe('real routes on the apex', () => {
expect(asset.status).toBe(200)
expect(asset.headers['content-type']).toBe('text/javascript')
expect(asset.headers['cache-control']).toBe('public, max-age=31536000, immutable')
-- expect((await send('/chrome.js')).status).toBe(200)
++ expect((await send('/frame.js')).status).toBe(200)
expect((await send('/assets/tex/wood_planks.jpg')).status).toBe(200)
})