openvibers/openvibe.tips Commits

The home's picture is Tips' own: a tip arriving on a stream (#243)

OP OpenVibers committed 3 days ago · e889bf39ad · 3 files, +73 −19

The hero drew the shared orbit. It now draws what Tips does: a stream with a tip landing on it and the creator's goal
moving up, labelled as an example, still under reduced motion. And the cards on a phone are a row to swipe: the
stylesheet hid every card after the fourth.

Changes

ddiff --git a/public/css/tips.css b/public/css/tips.css
iindex a4de949..9230608 100644
---- a/public/css/tips.css
++++ b/public/css/tips.css
@@@ -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; }
ddiff --git a/server/web/pages.js b/server/web/pages.js
iindex c466792..4adecb1 100644
---- a/server/web/pages.js
++++ b/server/web/pages.js
@@@ -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 }] : []),
ddiff --git a/test/home-picture.test.js b/test/home-picture.test.js
nnew file mode 100644
iindex 0000000..fc52751
---- /dev/null
++++ b/test/home-picture.test.js
@@@ -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&#39;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); });