openvibers/openvibe.space Commits
The home's picture is what Space does: a sentence in, an app out (#267)
Changes
@@@ -120,3 +120,28 @@ body {.sp-fold-block .sc-sec { margin: 0; max-width: none; padding: 0; }}@media (max-width: 640px) { .sp-fold-block { margin-top: 24px; } .sp-fold-block > summary h2 { font-size: 1.05rem; } }++++/* ── The home's picture (server/render/pages.js spaceScene): a sentence in, an app out ── */++.sp-scene { width: min(100%, 400px); margin: 0 auto; display: grid; gap: 12px; }++.sp-scene-ask { display: grid; gap: 2px; margin: 0; padding: 10px 14px; border-radius: 14px 14px 14px 4px; border: 1px solid color-mix(in srgb, var(--accent, #3b82f6) 45%, transparent); background: color-mix(in srgb, var(--accent, #3b82f6) 12%, transparent); justify-self: start; max-width: 92%; }++.sp-scene-ask span { font-size: .72rem; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: var(--accent-light, #60a5fa); }++.sp-scene-ask b { color: var(--text-primary, #e6edf7); line-height: 1.3; }++.sp-scene-win { border: 1px solid var(--border, #1f2d47); border-radius: 16px; overflow: hidden; background: linear-gradient(var(--bg-card, #131c2e), var(--bg-card, #131c2e)), var(--bg-primary, #0a0f1c); box-shadow: 0 14px 34px rgba(0, 0, 0, .3);++ animation: spSceneIn var(--ov-dur-4, .52s) var(--ov-ease-spring, cubic-bezier(.34, 1.56, .64, 1)) .5s both; }++.sp-scene-bar { display: flex; align-items: center; gap: 6px; margin: 0; padding: 9px 12px; border-bottom: 1px solid var(--border, #1f2d47); background: color-mix(in srgb, var(--bg-primary, #0a0f1c) 60%, transparent); }++.sp-scene-bar i { width: 9px; height: 9px; border-radius: 50%; background: color-mix(in srgb, var(--text-muted, #7386a3) 45%, transparent); }++.sp-scene-bar span { margin-left: 6px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 500 .8rem/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: var(--text-secondary, #96a7c2); }++.sp-scene-app { display: grid; gap: 10px; padding: 14px; }++.sp-scene-app > b { font-size: 1.05rem; color: var(--text-primary, #e6edf7); }++.sp-scene-app ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }++.sp-scene-app li { display: flex; align-items: center; gap: 9px; min-height: 38px; padding: 4px 10px; border-radius: 10px; border: 1px solid var(--border, #1f2d47); color: var(--text-primary, #e6edf7); font-size: .92rem;++ animation: spSceneRow var(--ov-dur-3, .32s) var(--ov-ease-out, ease-out) both; }++.sp-scene-app li:nth-child(1) { animation-delay: .9s; } .sp-scene-app li:nth-child(2) { animation-delay: 1.05s; } .sp-scene-app li:nth-child(3) { animation-delay: 1.2s; }++.sp-scene-app em { margin-left: auto; font-style: normal; color: var(--text-muted, #7386a3); font-size: .82rem; }++.sp-scene-av { flex: none; display: grid; place-items: center; width: 24px; height: 24px; border-radius: 50%; background: hsl(var(--h) 55% 42%); color: #fff; font-size: .72rem; font-weight: 800; }++.sp-scene-btn { justify-self: start; padding: 8px 14px; border-radius: 10px; background: var(--accent, #3b82f6); color: #fff; font-weight: 700; font-size: .9rem; }++.sp-scene figcaption { text-align: center; color: var(--text-muted, #7386a3); font-size: .8rem; }++@keyframes spSceneIn { from { opacity: 0; transform: translateY(14px) scale(.97); } to { opacity: 1; transform: none; } }++@keyframes spSceneRow { from { opacity: 0; transform: translateX(-6px); } to { opacity: 1; transform: none; } }++@media (prefers-reduced-motion: reduce) { .sp-scene-win, .sp-scene-app li { animation: none; } }++html[data-ov-motion=reduced] .sp-scene-win, html[data-ov-motion=reduced] .sp-scene-app li { animation: none; }@@@ -39,6 +39,24 @@ function defaultQuotas() {return quotas;}++/**++ * The home's picture: a sentence in, an app out. The request a person types ("A sign-up sheet for our game night"),++ * and under it the space that comes back, as a small window: its address, a title, the list people add themselves++ * to, and its one button. Plain markup and CSS (public/css/space.css, .sp-scene): the window rises in and its rows++ * arrive once, and everything is simply in place under reduced motion. The words in it are an example, and the caption++ * says so.++ */++function spaceScene() {++ return `<figure class="sp-scene" aria-label="An example: a request, and the space it becomes">++<p class="sp-scene-ask"><span>You ask</span><b>A sign-up sheet for our game night</b></p>++<div class="sp-scene-win">++<p class="sp-scene-bar"><i></i><i></i><i></i><span>game-night.openvibe.space</span></p>++<div class="sp-scene-app"><b>Game night, Friday</b>++<ul><li><span class="sp-scene-av" style="--h:200">M</span>maru<em>bringing snacks</em></li><li><span class="sp-scene-av" style="--h:120">K</span>kiwi<em>two guests</em></li><li><span class="sp-scene-av" style="--h:320">T</span>teddy</li></ul>++<span class="sp-scene-btn">Add my name</span></div></div>++<figcaption>An example: one sentence in, a space people can sign in to and use.</figcaption></figure>`;++}++/** Hosting is working for staff; dynamic pages and spaces are the next stage. */function homePage() {const q = defaultQuotas();@@@ -107,24 +125,9 @@ curl -X POST "https://openvibe.space/api/v1/sites/$SITE_ID/deploys?activate=1" \const body = sc.hero({ pageHead: { icon: 'home' },eyebrow: 'OpenVibe.Space · spaces',title: 'Make a space.',-- // The hero's picture is this site's own (2026-11-01): it drew the generic aura, the same on six homes.-- // What a space is made of, around the space: sign-in, AI, data, hosting and the editor.-- orbit: {-- label: 'What a space has',-- center: { title: 'Your space', sub: 'an app on your account' },-- inner: [-- { icon: 'ov:account', label: 'Sign-in', href: '/spaces/new' },-- { icon: 'ov:ai', label: 'AI', href: '/spaces/new' },-- { icon: 'ov:drive', label: 'Your data', href: '/spaces/new' },-- { icon: 'ov:website', label: 'Hosting', href: '/dashboard' },-- { icon: 'ov:code', label: 'Editor', href: '/spaces/new' },-- ],-- outer: [-- { icon: 'ov:network', label: 'Network', href: 'https://openvibe.network/' },-- { icon: 'ov:media', label: 'Media', href: 'https://openvibe.media/' },-- { icon: 'ov:community', label: 'Community', href: 'https://openvibe.community/' },-- ],-- },++ // The hero's picture is what Space does, drawn (2026-11-10; the owner: no more "generic circled" homes): what++ // a person asks for, and the small app that comes back at its own address. An example, and it says so.++ aside: { html: spaceScene() },accent: 'An app on your OpenVibe account.',lede: 'Describe what you want and OpenVibe AI builds it, or start from a template and change anything in the editor. A space signs visitors in with OpenVibe, keeps shared lists and private data, and talks to OpenVibe AI.',actions: [nnew file mode 100644@@@ -0,0 +1,30 @@++'use strict';++// The home's picture is what Space does: a sentence in, an app out (the owner, 2026-11-09: no more "generic circled"++// homes). It is an example and says so, and it is still under reduced motion.++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const { boot } = require('./helpers/app');++++(async () => {++ const t = await boot();++ try {++ const res = await fetch(`${t.base || t.url}/`);++ assert.strictEqual(res.status, 200);++ const home = await res.text();++ const scene = (home.match(/<figure class="sp-scene"[\s\S]*?<\/figure>/) || [''])[0];++ assert.ok(scene, 'the picture is in the hero');++ assert.match(scene, /<p class="sp-scene-ask"><span>You ask<\/span><b>A sign-up sheet for our game night<\/b><\/p>/);++ assert.match(scene, /<span>game-night\.openvibe\.space<\/span>/, 'the space has its own address');++ assert.strictEqual((scene.match(/<li>/g) || []).length, 3, 'three people on the sheet');++ assert.match(scene, /<figcaption>An example: /, 'it says it is an example');++ assert.ok(!/class="sc-orbit|sc-orbit-/.test(home), 'the generic orbit is gone from this home');++ const css = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'space.css'), 'utf8');++ assert.match(css, /@media \(prefers-reduced-motion: reduce\) \{ \.sp-scene-win, \.sp-scene-app li \{ animation: none; \} \}/);++ assert.match(css, /html\[data-ov-motion=reduced\] \.sp-scene-win/);++ console.log('home-picture: a request and the space it becomes, labelled an example, still under reduced motion');++ } finally {++ if (t.close) await t.close();++ }++ process.exit(0);++})().catch((e) => { console.error(e); process.exit(1); });
@@ -120,3 +120,28 @@ body {
@@ -120,3 +120,28 @@ body {
.sp-fold-block .sc-sec { margin: 0; max-width: none; padding: 0; }
.sp-fold-block .sc-sec { margin: 0; max-width: none; padding: 0; }
}
}
@media (max-width: 640px) { .sp-fold-block { margin-top: 24px; } .sp-fold-block > summary h2 { font-size: 1.05rem; } }
@media (max-width: 640px) { .sp-fold-block { margin-top: 24px; } .sp-fold-block > summary h2 { font-size: 1.05rem; } }
+
+/* ── The home's picture (server/render/pages.js spaceScene): a sentence in, an app out ── */
+.sp-scene { width: min(100%, 400px); margin: 0 auto; display: grid; gap: 12px; }
+.sp-scene-ask { display: grid; gap: 2px; margin: 0; padding: 10px 14px; border-radius: 14px 14px 14px 4px; border: 1px solid color-mix(in srgb, var(--accent, #3b82f6) 45%, transparent); background: color-mix(in srgb, var(--accent, #3b82f6) 12%, transparent); justify-self: start; max-width: 92%; }
+.sp-scene-ask span { font-size: .72rem; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: var(--accent-light, #60a5fa); }
+.sp-scene-ask b { color: var(--text-primary, #e6edf7); line-height: 1.3; }
+.sp-scene-win { border: 1px solid var(--border, #1f2d47); border-radius: 16px; overflow: hidden; background: linear-gradient(var(--bg-card, #131c2e), var(--bg-card, #131c2e)), var(--bg-primary, #0a0f1c); box-shadow: 0 14px 34px rgba(0, 0, 0, .3);
+ animation: spSceneIn var(--ov-dur-4, .52s) var(--ov-ease-spring, cubic-bezier(.34, 1.56, .64, 1)) .5s both; }
+.sp-scene-bar { display: flex; align-items: center; gap: 6px; margin: 0; padding: 9px 12px; border-bottom: 1px solid var(--border, #1f2d47); background: color-mix(in srgb, var(--bg-primary, #0a0f1c) 60%, transparent); }
+.sp-scene-bar i { width: 9px; height: 9px; border-radius: 50%; background: color-mix(in srgb, var(--text-muted, #7386a3) 45%, transparent); }
+.sp-scene-bar span { margin-left: 6px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 500 .8rem/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: var(--text-secondary, #96a7c2); }
+.sp-scene-app { display: grid; gap: 10px; padding: 14px; }
+.sp-scene-app > b { font-size: 1.05rem; color: var(--text-primary, #e6edf7); }
+.sp-scene-app ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
+.sp-scene-app li { display: flex; align-items: center; gap: 9px; min-height: 38px; padding: 4px 10px; border-radius: 10px; border: 1px solid var(--border, #1f2d47); color: var(--text-primary, #e6edf7); font-size: .92rem;
+ animation: spSceneRow var(--ov-dur-3, .32s) var(--ov-ease-out, ease-out) both; }
+.sp-scene-app li:nth-child(1) { animation-delay: .9s; } .sp-scene-app li:nth-child(2) { animation-delay: 1.05s; } .sp-scene-app li:nth-child(3) { animation-delay: 1.2s; }
+.sp-scene-app em { margin-left: auto; font-style: normal; color: var(--text-muted, #7386a3); font-size: .82rem; }
+.sp-scene-av { flex: none; display: grid; place-items: center; width: 24px; height: 24px; border-radius: 50%; background: hsl(var(--h) 55% 42%); color: #fff; font-size: .72rem; font-weight: 800; }
+.sp-scene-btn { justify-self: start; padding: 8px 14px; border-radius: 10px; background: var(--accent, #3b82f6); color: #fff; font-weight: 700; font-size: .9rem; }
+.sp-scene figcaption { text-align: center; color: var(--text-muted, #7386a3); font-size: .8rem; }
+@keyframes spSceneIn { from { opacity: 0; transform: translateY(14px) scale(.97); } to { opacity: 1; transform: none; } }
+@keyframes spSceneRow { from { opacity: 0; transform: translateX(-6px); } to { opacity: 1; transform: none; } }
+@media (prefers-reduced-motion: reduce) { .sp-scene-win, .sp-scene-app li { animation: none; } }
+html[data-ov-motion=reduced] .sp-scene-win, html[data-ov-motion=reduced] .sp-scene-app li { animation: none; }
@@ -39,6 +39,24 @@ function defaultQuotas() {
@@ -39,6 +39,24 @@ function defaultQuotas() {
return quotas;
return quotas;
}
}
+/**
+ * The home's picture: a sentence in, an app out. The request a person types ("A sign-up sheet for our game night"),
+ * and under it the space that comes back, as a small window: its address, a title, the list people add themselves
+ * to, and its one button. Plain markup and CSS (public/css/space.css, .sp-scene): the window rises in and its rows
+ * arrive once, and everything is simply in place under reduced motion. The words in it are an example, and the caption
+ * says so.
+ */
+function spaceScene() {
+ return `<figure class="sp-scene" aria-label="An example: a request, and the space it becomes">
+<p class="sp-scene-ask"><span>You ask</span><b>A sign-up sheet for our game night</b></p>
+<div class="sp-scene-win">
+<p class="sp-scene-bar"><i></i><i></i><i></i><span>game-night.openvibe.space</span></p>
+<div class="sp-scene-app"><b>Game night, Friday</b>
+<ul><li><span class="sp-scene-av" style="--h:200">M</span>maru<em>bringing snacks</em></li><li><span class="sp-scene-av" style="--h:120">K</span>kiwi<em>two guests</em></li><li><span class="sp-scene-av" style="--h:320">T</span>teddy</li></ul>
+<span class="sp-scene-btn">Add my name</span></div></div>
+<figcaption>An example: one sentence in, a space people can sign in to and use.</figcaption></figure>`;
+}
+
/** Hosting is working for staff; dynamic pages and spaces are the next stage. */
/** Hosting is working for staff; dynamic pages and spaces are the next stage. */
function homePage() {
function homePage() {
const q = defaultQuotas();
const q = defaultQuotas();
@@ -107,24 +125,9 @@ curl -X POST "https://openvibe.space/api/v1/sites/$SITE_ID/deploys?activate=1" \
@@ -107,24 +125,9 @@ curl -X POST "https://openvibe.space/api/v1/sites/$SITE_ID/deploys?activate=1" \
const body = sc.hero({ pageHead: { icon: 'home' },
const body = sc.hero({ pageHead: { icon: 'home' },
eyebrow: 'OpenVibe.Space · spaces',
eyebrow: 'OpenVibe.Space · spaces',
title: 'Make a space.',
title: 'Make a space.',
- // The hero's picture is this site's own (2026-11-01): it drew the generic aura, the same on six homes.
- // What a space is made of, around the space: sign-in, AI, data, hosting and the editor.
- orbit: {
- label: 'What a space has',
- center: { title: 'Your space', sub: 'an app on your account' },
- inner: [
- { icon: 'ov:account', label: 'Sign-in', href: '/spaces/new' },
- { icon: 'ov:ai', label: 'AI', href: '/spaces/new' },
- { icon: 'ov:drive', label: 'Your data', href: '/spaces/new' },
- { icon: 'ov:website', label: 'Hosting', href: '/dashboard' },
- { icon: 'ov:code', label: 'Editor', href: '/spaces/new' },
- ],
- outer: [
- { icon: 'ov:network', label: 'Network', href: 'https://openvibe.network/' },
- { icon: 'ov:media', label: 'Media', href: 'https://openvibe.media/' },
- { icon: 'ov:community', label: 'Community', href: 'https://openvibe.community/' },
- ],
- },
+ // The hero's picture is what Space does, drawn (2026-11-10; the owner: no more "generic circled" homes): what
+ // a person asks for, and the small app that comes back at its own address. An example, and it says so.
+ aside: { html: spaceScene() },
accent: 'An app on your OpenVibe account.',
accent: 'An app on your OpenVibe account.',
lede: 'Describe what you want and OpenVibe AI builds it, or start from a template and change anything in the editor. A space signs visitors in with OpenVibe, keeps shared lists and private data, and talks to OpenVibe AI.',
lede: 'Describe what you want and OpenVibe AI builds it, or start from a template and change anything in the editor. A space signs visitors in with OpenVibe, keeps shared lists and private data, and talks to OpenVibe AI.',
actions: [
actions: [
new file mode 100644
new file mode 100644
@@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
+'use strict';
+// The home's picture is what Space does: a sentence in, an app out (the owner, 2026-11-09: no more "generic circled"
+// homes). It is an example and says so, and it is still under reduced motion.
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const { boot } = require('./helpers/app');
+
+(async () => {
+ const t = await boot();
+ try {
+ const res = await fetch(`${t.base || t.url}/`);
+ assert.strictEqual(res.status, 200);
+ const home = await res.text();
+ const scene = (home.match(/<figure class="sp-scene"[\s\S]*?<\/figure>/) || [''])[0];
+ assert.ok(scene, 'the picture is in the hero');
+ assert.match(scene, /<p class="sp-scene-ask"><span>You ask<\/span><b>A sign-up sheet for our game night<\/b><\/p>/);
+ assert.match(scene, /<span>game-night\.openvibe\.space<\/span>/, 'the space has its own address');
+ assert.strictEqual((scene.match(/<li>/g) || []).length, 3, 'three people on the sheet');
+ assert.match(scene, /<figcaption>An example: /, 'it says it is an example');
+ assert.ok(!/class="sc-orbit|sc-orbit-/.test(home), 'the generic orbit is gone from this home');
+ const css = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'space.css'), 'utf8');
+ assert.match(css, /@media \(prefers-reduced-motion: reduce\) \{ \.sp-scene-win, \.sp-scene-app li \{ animation: none; \} \}/);
+ assert.match(css, /html\[data-ov-motion=reduced\] \.sp-scene-win/);
+ console.log('home-picture: a request and the space it becomes, labelled an example, still under reduced motion');
+ } finally {
+ if (t.close) await t.close();
+ }
+ process.exit(0);
+})().catch((e) => { console.error(e); process.exit(1); });