openvibers/openvibe.tips Commits
The home's picture is Tips' own: a tip arriving on a stream (#243)
Changes
@@@ -117,7 +117,9 @@ ul.mod-list, ul.plain { list-style: none; padding: 0; }.tp-faq details { border: 1px solid var(--tips-border); border-radius: 12px; background: var(--tips-card); padding: 0 16px; }.tp-faq summary { cursor: pointer; font-weight: 600; padding: 12px 0; }.tp-faq p { margin: 0 0 14px; color: var(--tips-muted); }--@media (max-width: 600px) { .tp-cards > li:nth-child(n+5) { display: none; } }++/* On a phone the cards are a row to swipe, the edge of the next one showing: every card stays (the old rule hid++ every card after the fourth). */++@media (max-width: 600px) { .tp-cards { grid-template-columns: none; grid-auto-flow: column; grid-auto-columns: 82%; overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: none; } .tp-cards::-webkit-scrollbar { display: none; } .tp-cards > li { scroll-snap-align: start; } }/* Touch targets on phones (fleet audit 2026-10-29): the touch-only hit area for this group. Nothing moves with a mouse. */@media (pointer: coarse) {@@@ -165,3 +167,25 @@ ul.mod-list, ul.plain { list-style: none; padding: 0; }.tp-fold-block .tp-sec, .tp-fold-block .sc-sec { margin: 0; max-width: none; padding: 0; }}@media (max-width: 640px) { .tp-fold-block { margin-top: 24px; } .tp-fold-block > summary h2 { font-size: 1.05rem; } }++++/* ── The home's picture (server/web/pages.js tipScene): a tip arriving on a stream, and the goal it moves ── */++.tp-scene { width: min(100%, 380px); margin: 0 auto; display: grid; gap: 12px; }++.tp-scene-stream { position: relative; aspect-ratio: 16 / 10; border-radius: 18px; overflow: hidden; border: 1px solid var(--border, #1f2d47);++ background: radial-gradient(90% 80% at 72% 28%, color-mix(in srgb, var(--accent, #3b82f6) 42%, transparent), transparent 62%), linear-gradient(160deg, var(--bg-tertiary, #172238), var(--bg-card, #131c2e)); }++.tp-scene-live { position: absolute; left: 10px; top: 10px; padding: 2px 8px; border-radius: 6px; background: color-mix(in srgb, var(--live-red, #e11d48) 82%, #000); color: #fff; font-size: .7rem; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }++.tp-scene-alert { position: absolute; left: 10px; right: 10px; bottom: 10px; display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 14px; background: color-mix(in srgb, var(--bg-primary, #0a0f1c) 86%, transparent); border: 1px solid color-mix(in srgb, var(--accent, #3b82f6) 55%, transparent); box-shadow: 0 10px 28px rgba(0, 0, 0, .35);++ animation: tpSceneIn var(--ov-dur-4, .52s) var(--ov-ease-spring, cubic-bezier(.34, 1.56, .64, 1)) .5s both; }++.tp-scene-av { flex: none; display: grid; place-items: center; width: 36px; height: 36px; border-radius: 50%; background: hsl(200 60% 42%); color: #fff; font-weight: 800; }++.tp-scene-say { display: grid; gap: 1px; min-width: 0; }++.tp-scene-say b { font-size: .95rem; line-height: 1.25; color: var(--text-primary, #e6edf7); }++.tp-scene-say small { color: var(--text-secondary, #96a7c2); font-size: .84rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }++.tp-scene-goal { padding: 12px 14px; border-radius: 14px; border: 1px solid var(--border, #1f2d47); background: var(--bg-card, #131c2e); }++.tp-scene-goal p { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 2px 10px; margin: 0 0 8px; font-size: .88rem; color: var(--text-secondary, #96a7c2); }++.tp-scene-goal b { color: var(--text-primary, #e6edf7); }++.tp-scene-bar { height: 10px; border-radius: 999px; background: color-mix(in srgb, var(--text-muted, #7386a3) 22%, transparent); overflow: hidden; }++.tp-scene-bar i { display: block; height: 100%; width: 62%; border-radius: inherit; background: linear-gradient(90deg, var(--accent, #3b82f6), var(--accent-light, #60a5fa)); transform-origin: left center; animation: tpSceneFill 1.1s var(--ov-ease-out, cubic-bezier(.2, .8, .2, 1)) .9s both; }++.tp-scene figcaption { text-align: center; color: var(--text-muted, #7386a3); font-size: .8rem; }++@keyframes tpSceneIn { from { opacity: 0; transform: translateY(14px) scale(.96); } to { opacity: 1; transform: none; } }++@keyframes tpSceneFill { from { transform: scaleX(.84); } to { transform: none; } }++@media (prefers-reduced-motion: reduce) { .tp-scene-alert, .tp-scene-bar i { animation: none; } }++html[data-ov-motion=reduced] .tp-scene-alert, html[data-ov-motion=reduced] .tp-scene-bar i { animation: none; }@@@ -91,6 +91,20 @@ const HOME_PANE_IDS = ['send', 'for-creators', 'services', 'how', 'costs'];* supporter can send, what a creator gets (overlays for any streaming app, other ways to be paid), what it costs, how it* works and the questions people ask. live: clients/live-now.js; creators: switched-on pages with their active goal.*/++/**++ * The home's picture: a tip arriving on a stream. A frame that stands for the stream (a LIVE tag, a soft light), the++ * alert a creator's overlay shows when a tip lands, and the goal bar under it moving up by that tip. Plain markup and++ * CSS (public/css/tips.css, .tp-scene): the alert rises in and the bar fills once, and both are simply in place under++ * reduced motion. The names and numbers are an example, and the caption says so.++ */++function tipScene() {++ return `<figure class="tp-scene" aria-label="An example: a tip arriving on a creator's stream">++<div class="tp-scene-stream"><span class="tp-scene-live">Live</span>++<div class="tp-scene-alert"><span class="tp-scene-av" aria-hidden="true">M</span><span class="tp-scene-say"><b>maru tipped 500 Vibes</b><small>“play that song again!”</small></span></div></div>++<div class="tp-scene-goal"><p><b>Goal: a new microphone</b><span>3,100 of 5,000 Vibes</span></p><div class="tp-scene-bar" role="img" aria-label="62 percent of the goal"><i></i></div></div>++<figcaption>An example: the alert and the goal a creator's stream shows.</figcaption></figure>`;++}++function home({ creators, live = [] }) {const m = NEW_PAGE_MINIMUMS;const liveCards = live.length ? `<ul class="tp-cards">${live.map((c) => `<li class="tp-card"><p class="tp-card-top"><span class="tp-live">Live</span> <a href="${esc(c.url)}"><strong>${esc(c.name)}</strong></a></p>${c.title ? `<p>${esc(c.title)}</p>` : ''}<p class="meta">${[c.category, watching(c.viewers)].filter(Boolean).map(esc).join(' · ')}</p></li>`).join('')}</ul>` : '';@@@ -98,24 +112,9 @@ function home({ creators, live = [] }) {return showcase.hero({eyebrow: 'OpenVibe.Tips',title: 'Support the creators', accent: 'you watch',-- // The hero's picture is this site's own (2026-11-01): it drew the generic aura, the same on six homes.-- // What a viewer can send, around the creator.-- orbit: {-- label: 'What you can send',-- center: { title: 'The creator', sub: 'gets it' },-- inner: [-- { icon: 'ov:tips', label: 'A tip', href: '#send' },-- { icon: 'ov:chat', label: 'Paid message', href: '#send' },-- { icon: 'ov:audio', label: 'Read out', href: '#send' },-- { icon: 'ov:video', label: 'Video request', href: '#send' },-- { icon: 'ov:bell', label: 'A sound', href: '#send' },-- ],-- outer: [-- { icon: 'ov:live', label: 'Live', href: 'https://openvibe.live/' },-- { icon: 'ov:vip', label: 'VIP', href: 'https://openvibe.vip/' },-- { icon: 'ov:billing', label: 'Billing', href: 'https://billing.openvibe.network/' },-- ],-- },++ // The hero's picture is what Tips does, drawn (2026-11-10; the owner: no more "generic circled" homes): a++ // stream with a tip arriving on it, and the creator's goal filling. It is an example and says so.++ aside: { html: tipScene() },lede: 'Tip a creator with Vibes, send a highlighted paid message, have your message read out, request a video or play one of their sounds on stream. Creators set goals and show every tip with overlays that work on any streaming app.',actions: [...(creators.length ? [{ label: 'Find a creator', href: '#creators', primary: true }] : live.length ? [{ label: 'See who is live', href: '#live', primary: true }] : []),nnew file mode 100644@@@ -0,0 +1,31 @@++'use strict';++// The home's picture is Tips' own: a tip arriving on a stream and the goal it moves (the owner, 2026-11-09: no more++// "generic circled" homes). It is an example and says so. And the cards under it are a row to swipe on a phone: the++// stylesheet used to hide every card after the fourth.++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const { boot, check, done } = require('./helpers/app');++++(async () => {++ const t = await boot();++ await check('the home draws a tip arriving on a stream, labelled as an example', async () => {++ const home = await (await fetch(`${t.base}/`)).text();++ const scene = (home.match(/<figure class="tp-scene"[\s\S]*?<\/figure>/) || [''])[0];++ assert.ok(scene, 'the scene is in the hero');++ assert.match(scene, /aria-label="An example: a tip arriving on a creator's stream"|aria-label="An example: a tip arriving on a creator's stream"/);++ assert.match(scene, /<b>maru tipped 500 Vibes<\/b>/);++ assert.match(scene, /<div class="tp-scene-bar" role="img" aria-label="62 percent of the goal">/);++ 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');++ });++ await check('the stylesheet keeps the picture still under reduced motion and hides no card on a phone', async () => {++ const css = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'tips.css'), 'utf8');++ assert.match(css, /@media \(prefers-reduced-motion: reduce\) \{ \.tp-scene-alert, \.tp-scene-bar i \{ animation: none; \} \}/);++ assert.match(css, /html\[data-ov-motion=reduced\] \.tp-scene-alert/);++ assert.ok(!/\.tp-cards > li:nth-child\(n\+5\) \{ display: none/.test(css), 'no card is hidden on a phone');++ assert.match(css, /\.tp-cards \{ grid-template-columns: none; grid-auto-flow: column;/, 'they are a row to swipe');++ });++ await t.close();++ done();++})().catch((e) => { console.error(e); process.exit(1); });
@@ -117,7 +117,9 @@ ul.mod-list, ul.plain { list-style: none; padding: 0; }
@@ -117,7 +117,9 @@ ul.mod-list, ul.plain { list-style: none; padding: 0; }
.tp-faq details { border: 1px solid var(--tips-border); border-radius: 12px; background: var(--tips-card); padding: 0 16px; }
.tp-faq details { border: 1px solid var(--tips-border); border-radius: 12px; background: var(--tips-card); padding: 0 16px; }
.tp-faq summary { cursor: pointer; font-weight: 600; padding: 12px 0; }
.tp-faq summary { cursor: pointer; font-weight: 600; padding: 12px 0; }
.tp-faq p { margin: 0 0 14px; color: var(--tips-muted); }
.tp-faq p { margin: 0 0 14px; color: var(--tips-muted); }
-@media (max-width: 600px) { .tp-cards > li:nth-child(n+5) { display: none; } }
+/* On a phone the cards are a row to swipe, the edge of the next one showing: every card stays (the old rule hid
+ every card after the fourth). */
+@media (max-width: 600px) { .tp-cards { grid-template-columns: none; grid-auto-flow: column; grid-auto-columns: 82%; overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: none; } .tp-cards::-webkit-scrollbar { display: none; } .tp-cards > li { scroll-snap-align: start; } }
/* Touch targets on phones (fleet audit 2026-10-29): the touch-only hit area for this group. Nothing moves with a mouse. */
/* Touch targets on phones (fleet audit 2026-10-29): the touch-only hit area for this group. Nothing moves with a mouse. */
@media (pointer: coarse) {
@media (pointer: coarse) {
@@ -165,3 +167,25 @@ ul.mod-list, ul.plain { list-style: none; padding: 0; }
@@ -165,3 +167,25 @@ ul.mod-list, ul.plain { list-style: none; padding: 0; }
.tp-fold-block .tp-sec, .tp-fold-block .sc-sec { margin: 0; max-width: none; padding: 0; }
.tp-fold-block .tp-sec, .tp-fold-block .sc-sec { margin: 0; max-width: none; padding: 0; }
}
}
@media (max-width: 640px) { .tp-fold-block { margin-top: 24px; } .tp-fold-block > summary h2 { font-size: 1.05rem; } }
@media (max-width: 640px) { .tp-fold-block { margin-top: 24px; } .tp-fold-block > summary h2 { font-size: 1.05rem; } }
+
+/* ── The home's picture (server/web/pages.js tipScene): a tip arriving on a stream, and the goal it moves ── */
+.tp-scene { width: min(100%, 380px); margin: 0 auto; display: grid; gap: 12px; }
+.tp-scene-stream { position: relative; aspect-ratio: 16 / 10; border-radius: 18px; overflow: hidden; border: 1px solid var(--border, #1f2d47);
+ background: radial-gradient(90% 80% at 72% 28%, color-mix(in srgb, var(--accent, #3b82f6) 42%, transparent), transparent 62%), linear-gradient(160deg, var(--bg-tertiary, #172238), var(--bg-card, #131c2e)); }
+.tp-scene-live { position: absolute; left: 10px; top: 10px; padding: 2px 8px; border-radius: 6px; background: color-mix(in srgb, var(--live-red, #e11d48) 82%, #000); color: #fff; font-size: .7rem; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; }
+.tp-scene-alert { position: absolute; left: 10px; right: 10px; bottom: 10px; display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 14px; background: color-mix(in srgb, var(--bg-primary, #0a0f1c) 86%, transparent); border: 1px solid color-mix(in srgb, var(--accent, #3b82f6) 55%, transparent); box-shadow: 0 10px 28px rgba(0, 0, 0, .35);
+ animation: tpSceneIn var(--ov-dur-4, .52s) var(--ov-ease-spring, cubic-bezier(.34, 1.56, .64, 1)) .5s both; }
+.tp-scene-av { flex: none; display: grid; place-items: center; width: 36px; height: 36px; border-radius: 50%; background: hsl(200 60% 42%); color: #fff; font-weight: 800; }
+.tp-scene-say { display: grid; gap: 1px; min-width: 0; }
+.tp-scene-say b { font-size: .95rem; line-height: 1.25; color: var(--text-primary, #e6edf7); }
+.tp-scene-say small { color: var(--text-secondary, #96a7c2); font-size: .84rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.tp-scene-goal { padding: 12px 14px; border-radius: 14px; border: 1px solid var(--border, #1f2d47); background: var(--bg-card, #131c2e); }
+.tp-scene-goal p { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 2px 10px; margin: 0 0 8px; font-size: .88rem; color: var(--text-secondary, #96a7c2); }
+.tp-scene-goal b { color: var(--text-primary, #e6edf7); }
+.tp-scene-bar { height: 10px; border-radius: 999px; background: color-mix(in srgb, var(--text-muted, #7386a3) 22%, transparent); overflow: hidden; }
+.tp-scene-bar i { display: block; height: 100%; width: 62%; border-radius: inherit; background: linear-gradient(90deg, var(--accent, #3b82f6), var(--accent-light, #60a5fa)); transform-origin: left center; animation: tpSceneFill 1.1s var(--ov-ease-out, cubic-bezier(.2, .8, .2, 1)) .9s both; }
+.tp-scene figcaption { text-align: center; color: var(--text-muted, #7386a3); font-size: .8rem; }
+@keyframes tpSceneIn { from { opacity: 0; transform: translateY(14px) scale(.96); } to { opacity: 1; transform: none; } }
+@keyframes tpSceneFill { from { transform: scaleX(.84); } to { transform: none; } }
+@media (prefers-reduced-motion: reduce) { .tp-scene-alert, .tp-scene-bar i { animation: none; } }
+html[data-ov-motion=reduced] .tp-scene-alert, html[data-ov-motion=reduced] .tp-scene-bar i { animation: none; }
@@ -91,6 +91,20 @@ const HOME_PANE_IDS = ['send', 'for-creators', 'services', 'how', 'costs'];
@@ -91,6 +91,20 @@ const HOME_PANE_IDS = ['send', 'for-creators', 'services', 'how', 'costs'];
* supporter can send, what a creator gets (overlays for any streaming app, other ways to be paid), what it costs, how it
* supporter can send, what a creator gets (overlays for any streaming app, other ways to be paid), what it costs, how it
* works and the questions people ask. live: clients/live-now.js; creators: switched-on pages with their active goal.
* works and the questions people ask. live: clients/live-now.js; creators: switched-on pages with their active goal.
*/
*/
+/**
+ * The home's picture: a tip arriving on a stream. A frame that stands for the stream (a LIVE tag, a soft light), the
+ * alert a creator's overlay shows when a tip lands, and the goal bar under it moving up by that tip. Plain markup and
+ * CSS (public/css/tips.css, .tp-scene): the alert rises in and the bar fills once, and both are simply in place under
+ * reduced motion. The names and numbers are an example, and the caption says so.
+ */
+function tipScene() {
+ return `<figure class="tp-scene" aria-label="An example: a tip arriving on a creator's stream">
+<div class="tp-scene-stream"><span class="tp-scene-live">Live</span>
+<div class="tp-scene-alert"><span class="tp-scene-av" aria-hidden="true">M</span><span class="tp-scene-say"><b>maru tipped 500 Vibes</b><small>“play that song again!”</small></span></div></div>
+<div class="tp-scene-goal"><p><b>Goal: a new microphone</b><span>3,100 of 5,000 Vibes</span></p><div class="tp-scene-bar" role="img" aria-label="62 percent of the goal"><i></i></div></div>
+<figcaption>An example: the alert and the goal a creator's stream shows.</figcaption></figure>`;
+}
+
function home({ creators, live = [] }) {
function home({ creators, live = [] }) {
const m = NEW_PAGE_MINIMUMS;
const m = NEW_PAGE_MINIMUMS;
const liveCards = live.length ? `<ul class="tp-cards">${live.map((c) => `<li class="tp-card"><p class="tp-card-top"><span class="tp-live">Live</span> <a href="${esc(c.url)}"><strong>${esc(c.name)}</strong></a></p>${c.title ? `<p>${esc(c.title)}</p>` : ''}<p class="meta">${[c.category, watching(c.viewers)].filter(Boolean).map(esc).join(' · ')}</p></li>`).join('')}</ul>` : '';
const liveCards = live.length ? `<ul class="tp-cards">${live.map((c) => `<li class="tp-card"><p class="tp-card-top"><span class="tp-live">Live</span> <a href="${esc(c.url)}"><strong>${esc(c.name)}</strong></a></p>${c.title ? `<p>${esc(c.title)}</p>` : ''}<p class="meta">${[c.category, watching(c.viewers)].filter(Boolean).map(esc).join(' · ')}</p></li>`).join('')}</ul>` : '';
@@ -98,24 +112,9 @@ function home({ creators, live = [] }) {
@@ -98,24 +112,9 @@ function home({ creators, live = [] }) {
return showcase.hero({
return showcase.hero({
eyebrow: 'OpenVibe.Tips',
eyebrow: 'OpenVibe.Tips',
title: 'Support the creators', accent: 'you watch',
title: 'Support the creators', accent: 'you watch',
- // The hero's picture is this site's own (2026-11-01): it drew the generic aura, the same on six homes.
- // What a viewer can send, around the creator.
- orbit: {
- label: 'What you can send',
- center: { title: 'The creator', sub: 'gets it' },
- inner: [
- { icon: 'ov:tips', label: 'A tip', href: '#send' },
- { icon: 'ov:chat', label: 'Paid message', href: '#send' },
- { icon: 'ov:audio', label: 'Read out', href: '#send' },
- { icon: 'ov:video', label: 'Video request', href: '#send' },
- { icon: 'ov:bell', label: 'A sound', href: '#send' },
- ],
- outer: [
- { icon: 'ov:live', label: 'Live', href: 'https://openvibe.live/' },
- { icon: 'ov:vip', label: 'VIP', href: 'https://openvibe.vip/' },
- { icon: 'ov:billing', label: 'Billing', href: 'https://billing.openvibe.network/' },
- ],
- },
+ // The hero's picture is what Tips does, drawn (2026-11-10; the owner: no more "generic circled" homes): a
+ // stream with a tip arriving on it, and the creator's goal filling. It is an example and says so.
+ aside: { html: tipScene() },
lede: 'Tip a creator with Vibes, send a highlighted paid message, have your message read out, request a video or play one of their sounds on stream. Creators set goals and show every tip with overlays that work on any streaming app.',
lede: 'Tip a creator with Vibes, send a highlighted paid message, have your message read out, request a video or play one of their sounds on stream. Creators set goals and show every tip with overlays that work on any streaming app.',
actions: [
actions: [
...(creators.length ? [{ label: 'Find a creator', href: '#creators', primary: true }] : live.length ? [{ label: 'See who is live', href: '#live', primary: true }] : []),
...(creators.length ? [{ label: 'Find a creator', href: '#creators', primary: true }] : live.length ? [{ label: 'See who is live', href: '#live', primary: true }] : []),
new file mode 100644
new file mode 100644
@@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
+'use strict';
+// The home's picture is Tips' own: a tip arriving on a stream and the goal it moves (the owner, 2026-11-09: no more
+// "generic circled" homes). It is an example and says so. And the cards under it are a row to swipe on a phone: the
+// stylesheet used to hide every card after the fourth.
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const { boot, check, done } = require('./helpers/app');
+
+(async () => {
+ const t = await boot();
+ await check('the home draws a tip arriving on a stream, labelled as an example', async () => {
+ const home = await (await fetch(`${t.base}/`)).text();
+ const scene = (home.match(/<figure class="tp-scene"[\s\S]*?<\/figure>/) || [''])[0];
+ assert.ok(scene, 'the scene is in the hero');
+ assert.match(scene, /aria-label="An example: a tip arriving on a creator's stream"|aria-label="An example: a tip arriving on a creator's stream"/);
+ assert.match(scene, /<b>maru tipped 500 Vibes<\/b>/);
+ assert.match(scene, /<div class="tp-scene-bar" role="img" aria-label="62 percent of the goal">/);
+ 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');
+ });
+ await check('the stylesheet keeps the picture still under reduced motion and hides no card on a phone', async () => {
+ const css = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'tips.css'), 'utf8');
+ assert.match(css, /@media \(prefers-reduced-motion: reduce\) \{ \.tp-scene-alert, \.tp-scene-bar i \{ animation: none; \} \}/);
+ assert.match(css, /html\[data-ov-motion=reduced\] \.tp-scene-alert/);
+ assert.ok(!/\.tp-cards > li:nth-child\(n\+5\) \{ display: none/.test(css), 'no card is hidden on a phone');
+ assert.match(css, /\.tp-cards \{ grid-template-columns: none; grid-auto-flow: column;/, 'they are a row to swipe');
+ });
+ await t.close();
+ done();
+})().catch((e) => { console.error(e); process.exit(1); });