openvibers/openvibe.live Commits

Home on phones: every section shows again; the rails scroll sideways instead of hiding behind tabs (#471)

OP OpenVibers committed 13 days ago · d08519ad73 · 6 files, +100 −198

The owner, 2026-10-31: "now I can't see the videos and clips on the homepage of openvibe.live. So don't reduce size
by removing features." #461 and #462 had put the five Catch up rails (Recently Online, Recent VODs, Recent Clips,
Recent Pastes, AI Moments) and Around OpenVibe (the digest, the star spotlight, the pulse) behind phone tabs, and
#461 cut the changelog preview to its newest day.

- Both blocks show every section, with its own heading, on every screen: no tab row, no per-section switch.
- On a phone each rail is one row of cards that scrolls sideways (snapping, each card its own height), so the page
  stays a few screens tall without hiding anything.
- The changelog preview keeps its full collapsed height on a phone again.
- Only the explainer at the foot (How it works / About / The network) is still tabbed and starts closed.
- test/home-catchup.test.js now holds the rule (every rail in the page, no tabs, sideways on a phone);
  test/home-tabs.test.js covers the explainer only and checks that Around has no tabs.

Changes

ddiff --git a/public/css/features/home.css b/public/css/features/home.css
iindex bcd7f372..9d1dd1f6 100644
---- a/public/css/features/home.css
++++ b/public/css/features/home.css
@@@ -1268,20 +1268,19 @@ body.egg-rainbow { animation: eggRainbow 2.6s linear; }
#page-home .hero-philosophy-btn::after, #page-home .hero-philosophy-link::after, #page-home .star-name a::after, #page-home .hero-golive-note a::after { content: ''; position: absolute; left: 0; right: 0; top: -12px; bottom: -12px; }
}
--/* Catch up, Around OpenVibe and New here? (2026-10-30, the short-pages rule): on a phone a block of home
-- sections shares one place — a row of tabs and one of them at a time — so the page stays a couple of screens tall.
-- The tab names the section, so the section's own heading goes and its header keeps only what it does. Wider
-- screens never see the tabs and show every section. The breakpoint is app-home.js's _narrow. */
--.home-catchup-tabs { display: none; }
++/* New here? (2026-10-30, the short-pages rule): on a phone the explainer at the foot of the home is a row of tabs and
++ one section at a time. The content blocks above it (Catch up, Around OpenVibe) are never tabbed: every section shows
++ on every screen (the owner, 2026-10-31: "now I can't see the videos and clips on the homepage"). The breakpoint is
++ app-home.js's _narrow. */
.home-tabs { display: none; }
@media (max-width: 759px) {
-- .home-catchup-tabs, .home-tabs {
++ .home-tabs {
display: flex; gap: 6px; margin: 30px -16px 6px; padding: 2px 16px 4px;
overflow-x: auto; scrollbar-width: none; overscroll-behavior-x: contain; scroll-padding-inline: 16px;
}
-- .home-catchup-tabs::-webkit-scrollbar, .home-tabs::-webkit-scrollbar { display: none; }
-- .home-catchup-tabs[hidden], .home-tabs[hidden] { display: none !important; }
-- .home-catchup-tabs [role="tab"], .home-tabs [role="tab"] {
++ .home-tabs::-webkit-scrollbar { display: none; }
++ .home-tabs[hidden] { display: none !important; }
++ .home-tabs [role="tab"] {
--sc-on: color-mix(in srgb, var(--sc) 45%, var(--accent));
flex: none; display: inline-flex; align-items: center; gap: 8px; min-height: 44px; padding: 0 16px;
font: inherit; font-size: 0.86rem; font-weight: 750; white-space: nowrap; cursor: pointer;
@@@ -1289,43 +1288,31 @@ body.egg-rainbow { animation: eggRainbow 2.6s linear; }
border: 1px solid var(--border); border-radius: 999px;
transition: color 0.18s, background 0.18s, border-color 0.18s;
}
-- .home-catchup-tabs [role="tab"][hidden], .home-tabs [role="tab"][hidden] { display: none; }
-- .home-catchup-tabs [role="tab"] i, .home-tabs [role="tab"] i { font-size: 0.78rem; color: var(--sc-on); }
-- .home-catchup-tabs [role="tab"][aria-selected="true"], .home-tabs [role="tab"][aria-selected="true"] {
++ .home-tabs [role="tab"][hidden] { display: none; }
++ .home-tabs [role="tab"] i { font-size: 0.78rem; color: var(--sc-on); }
++ .home-tabs [role="tab"][aria-selected="true"] {
color: color-mix(in srgb, var(--sc-on) 30%, var(--text-primary));
background: color-mix(in srgb, var(--sc-on) 14%, transparent);
border-color: color-mix(in srgb, var(--sc-on) 48%, transparent);
}
-- .home-catchup-tabs [role="tab"]:focus-visible, .home-tabs [role="tab"]:focus-visible { outline: 2px solid var(--sc-on); outline-offset: 2px; }
-- .home-catchup:not([data-tab="recent"]) [data-catchup="recent"],
-- .home-catchup:not([data-tab="vods"]) [data-catchup="vods"],
-- .home-catchup:not([data-tab="clips"]) [data-catchup="clips"],
-- .home-catchup:not([data-tab="pastes"]) [data-catchup="pastes"],
-- .home-catchup:not([data-tab="moments"]) [data-catchup="moments"] { display: none !important; }
-- .home-catchup .section-header[data-catchup], .home-catchup [data-catchup] > .section-header { margin: 6px 0 10px; justify-content: flex-end; min-height: 0; }
-- .home-catchup .section-header[data-catchup] > h2, .home-catchup [data-catchup] > .section-header > h2,
-- .home-catchup .section-header[data-catchup]::after, .home-catchup [data-catchup] > .section-header::after { display: none; }
-- /* The two other blocks: one section at a time behind their own tabs. */
-- .home-tabbox:not([data-tab="lately"]) [data-panel="lately"],
-- .home-tabbox:not([data-tab="star"]) [data-panel="star"],
-- .home-tabbox:not([data-tab="happening"]) [data-panel="happening"],
++ .home-tabs [role="tab"]:focus-visible { outline: 2px solid var(--sc-on); outline-offset: 2px; }
.home-tabbox:not([data-tab="tour"]) [data-panel="tour"],
.home-tabbox:not([data-tab="about"]) [data-panel="about"],
.home-tabbox:not([data-tab="network"]) [data-panel="network"] { display: none !important; }
-- /* The tab names the section, so the section's own heading goes. What its header does stays. */
-- .home-tabbox [data-panel="star"] > .section-header > h2,
-- .home-tabbox [data-panel="lately"] .digest-title,
.home-tabbox [data-panel="network"] .network-hub-header h3 { display: none; }
-- .home-tabbox [data-panel="star"] > .section-header { margin: 6px 0 10px; min-height: 0; }
-- /* The rule the header draws across the page is the slack the note used to take; without the
-- heading there is nothing to rule out to, and the note would run past the section. */
-- .home-tabbox [data-panel="star"] > .section-header::after { display: none; }
-- /* The sentence under a phone header is indented to line up with the title it belongs to; with the
-- title gone it starts at the left and takes the width it needs. */
-- .home-tabbox [data-panel="star"] > .section-header > span.muted { margin-left: 0; flex: 1 1 auto; }
-- .home-tabbox [data-panel="happening"] > .section-header { display: none; }
--}
--@media (max-width: 759px) and (prefers-reduced-motion: reduce) { .home-catchup-tabs [role="tab"], .home-tabs [role="tab"] { transition: none; } }
++
++ /* Catch up on a phone: each rail is one row of cards that scrolls sideways, so all five fit in a few screens
++ and nothing is behind a tab. */
++ #home-catchup .stream-grid {
++ display: grid; grid-auto-flow: column; grid-template-columns: none; grid-auto-columns: minmax(232px, 74%); align-items: start;
++ gap: 12px; margin-inline: -16px; padding: 2px 16px 10px; scroll-padding-inline: 16px;
++ overflow-x: auto; overscroll-behavior-x: contain; scroll-snap-type: x mandatory; scrollbar-width: none;
++ }
++ #home-catchup .stream-grid::-webkit-scrollbar { display: none; }
++ #home-catchup .stream-grid > * { scroll-snap-align: start; min-width: 0; }
++ #home-catchup .stream-grid > .empty-state { grid-column: auto; min-width: calc(100vw - 32px); }
++}
++@media (max-width: 759px) and (prefers-reduced-motion: reduce) { .home-tabs [role="tab"] { transition: none; } }
/* 2026-10-31, the phone home, shorter again: the featured stream's card leaves the Live Now grid on a phone (the
featured block above already shows it), and when it was the only stream there the grid's header goes too; the
ddiff --git a/public/css/style.css b/public/css/style.css
iindex 2ed84926..aefd5a6b 100644
---- a/public/css/style.css
++++ b/public/css/style.css
@@@ -5931,12 +5931,6 @@ body.theme-light .btn-outline:hover {
pointer-events: none;
border-radius: 0 0 12px 12px;
}
--/* A phone shows the newest day only (2026-10-30, the short-pages rule): the hero and the footer already carry the
-- latest change, and "Show all" or "View all" opens the rest. */
--@media (max-width: 560px) {
-- .home-changelog-wrapper.collapsed { max-height: 200px; }
--}
--
/* ═══════════════════════════════════════════════════════════════
UPDATES / CHANGELOG
═══════════════════════════════════════════════════════════════ */
ddiff --git a/public/fragments/home.html b/public/fragments/home.html
iindex 2eea7967..1dac1092 100644
---- a/public/fragments/home.html
++++ b/public/fragments/home.html
@@@ -101,21 +101,14 @@
</div>
</div>
-- <!-- Around OpenVibe (2026-10-30, the short-pages rule): the digest, the star spotlight and the pulse are
-- three different things to look at. On a phone they share one place — a row of tabs and one of them at a
-- time — so the page stays a couple of screens tall. Wider screens hide the tabs and show all three.
-- A tab shows once its section has something: each loader hides a section that has not (initHomeTabs). -->
-- <div class="home-tabbox" id="home-around" data-tab="lately">
-- <div class="home-tabs" role="tablist" aria-label="Around OpenVibe">
-- <button type="button" role="tab" id="ha-tab-lately" data-tab="lately" aria-selected="true" aria-controls="home-digest" style="--sc:#60a5fa"><i class="fa-solid fa-chart-line" aria-hidden="true"></i><span>Lately</span></button>
-- <button type="button" role="tab" id="ha-tab-star" data-tab="star" aria-selected="false" aria-controls="home-star-section" tabindex="-1" style="--sc:#f59e0b"><i class="fa-solid fa-star" aria-hidden="true"></i><span>Star</span></button>
-- <button type="button" role="tab" id="ha-tab-happening" data-tab="happening" aria-selected="false" aria-controls="home-pulse-section" tabindex="-1" hidden style="--sc:#fbbf24"><i class="fa-solid fa-bolt" aria-hidden="true"></i><span>Happening</span></button>
-- </div>
++ <!-- Around OpenVibe: the digest, the star spotlight and the pulse, every one shown on every screen (the owner,
++ 2026-10-31: "don't reduce size by removing features"). -->
++ <div class="home-around" id="home-around">
<!-- "While you were away" (signed-in, returning) / "Lately on OpenVibe" (everyone) — site activity digest -->
-- <div class="home-digest" id="home-digest" data-panel="lately"><div class="ovsk ovsk-panel" aria-hidden="true"><div class="ovsk-bar" style="height:14px;width:45%"></div><div class="ovsk-bar" style="height:10px;width:92%"></div><div class="ovsk-bar" style="height:10px;width:68%"></div></div></div>
++ <div class="home-digest" id="home-digest"><div class="ovsk ovsk-panel" aria-hidden="true"><div class="ovsk-bar" style="height:14px;width:45%"></div><div class="ovsk-bar" style="height:10px;width:92%"></div><div class="ovsk-bar" style="height:10px;width:68%"></div></div></div>
<!-- Star of OpenVibe — featured streamer spotlight (star_streamer setting / STAR_STREAMER env) -->
-- <div class="home-star-section" id="home-star-section" data-panel="star"><div class="ovsk ovsk-panel" aria-hidden="true"><div class="ovsk-bar" style="height:14px;width:45%"></div><div class="ovsk-bar" style="height:10px;width:92%"></div><div class="ovsk-bar" style="height:10px;width:68%"></div><div class="ovsk-bar" style="height:10px;width:80%"></div></div></div>
++ <div class="home-star-section" id="home-star-section"><div class="ovsk ovsk-panel" aria-hidden="true"><div class="ovsk-bar" style="height:14px;width:45%"></div><div class="ovsk-bar" style="height:10px;width:92%"></div><div class="ovsk-bar" style="height:10px;width:68%"></div><div class="ovsk-bar" style="height:10px;width:80%"></div></div></div>
<div class="hero-egg" id="hero-egg" style="display:none">
<button class="hero-egg-head" type="button" onclick="toggleHeroEgg()">
@@@ -149,7 +142,7 @@
</div>
<!-- Community pulse: goals near completion, latest activity, weekly leaders -->
-- <div class="home-section-lazy" id="home-pulse-section" style="display:none" data-panel="happening">
++ <div class="home-section-lazy" id="home-pulse-section" style="display:none">
<div class="section-header" style="--sc:#f59e0b">
<h2><i class="fa-solid fa-bolt"></i> Happening Now<small class="section-kicker">the pulse</small></h2>
</div>
@@@ -160,54 +153,46 @@
<!-- What people made, above the introduction to the site (roadmap 32.7: the home leads with live
content and creators): channels that were just on, their recordings, the clips people took
and their pastes. What the AI made follows it (AI Moments; 33.6: made, then derived). -->
-- <!-- Catch up. On a phone the five rails below share one place: a row of tabs and one rail at a time, so the
-- home stays a couple of screens tall (the owner's short-pages rule, 2026-10-28). Wider screens hide the tabs and
-- show every rail. A tab shows once its rail has something (initHomeCatchup in app-home.js). -->
-- <div class="home-catchup" id="home-catchup" data-tab="recent">
-- <div class="home-catchup-tabs" role="tablist" aria-label="Catch up">
-- <button type="button" role="tab" id="hc-tab-recent" data-tab="recent" aria-selected="true" aria-controls="stream-grid-recent" style="--sc:#60a5fa"><i class="fa-solid fa-clock-rotate-left" aria-hidden="true"></i><span>Recently online</span></button>
-- <button type="button" role="tab" id="hc-tab-vods" data-tab="vods" aria-selected="false" aria-controls="home-recent-vods-grid" tabindex="-1" style="--sc:#a78bfa" hidden><i class="fa-solid fa-video" aria-hidden="true"></i><span>VODs</span></button>
-- <button type="button" role="tab" id="hc-tab-clips" data-tab="clips" aria-selected="false" aria-controls="home-clips-grid" tabindex="-1" style="--sc:#f472b6" hidden><i class="fa-solid fa-scissors" aria-hidden="true"></i><span>Clips</span></button>
-- <button type="button" role="tab" id="hc-tab-pastes" data-tab="pastes" aria-selected="false" aria-controls="home-pastes-list" tabindex="-1" style="--sc:#2dd4bf" hidden><i class="fa-solid fa-paste" aria-hidden="true"></i><span>Pastes</span></button>
-- <button type="button" role="tab" id="hc-tab-moments" data-tab="moments" aria-selected="false" aria-controls="home-moments-row" tabindex="-1" style="--sc:#22d3ee" hidden><i class="fa-solid fa-brain" aria-hidden="true"></i><span>AI Moments</span></button>
-- </div>
++ <!-- Catch up: the five rails, every one shown on every screen. On a phone each is one row that scrolls sideways
++ (features/home.css), so the page stays short without hiding anything (the owner, 2026-10-31). -->
++ <div class="home-catchup" id="home-catchup">
<!-- Recently Online -->
-- <div class="section-header" style="--sc:#60a5fa" data-catchup="recent">
++ <div class="section-header" style="--sc:#60a5fa">
<h2><i class="fa-solid fa-clock-rotate-left"></i> Recently Online<small class="section-kicker">just missed them</small></h2>
</div>
-- <div class="stream-grid" id="stream-grid-recent" data-catchup="recent">
++ <div class="stream-grid" id="stream-grid-recent">
<div class="empty-state">
<p class="muted">No recent streams</p>
</div>
</div>
-- <div id="stream-grid-recent-pagination" class="pastes-pagination" style="display:none" data-catchup="recent"></div>
++ <div id="stream-grid-recent-pagination" class="pastes-pagination" style="display:none"></div>
<!-- Recent VODs -->
-- <div class="section-header" style="--sc:#a78bfa;display:none" id="home-recent-vods-header" data-catchup="vods">
++ <div class="section-header" style="--sc:#a78bfa;display:none" id="home-recent-vods-header">
<h2><i class="fa-solid fa-video"></i> Recent VODs<small class="section-kicker">full streams</small></h2>
<a href="/content?type=vods" onclick="return handleLinkClick(event, '/content?type=vods')" class="section-action">View all</a>
</div>
-- <div class="stream-grid" id="home-recent-vods-grid" data-catchup="vods"></div>
-- <div id="home-recent-vods-pagination" class="pastes-pagination" style="display:none" data-catchup="vods"></div>
++ <div class="stream-grid" id="home-recent-vods-grid"></div>
++ <div id="home-recent-vods-pagination" class="pastes-pagination" style="display:none"></div>
<!-- Recent Clips -->
-- <div class="section-header" style="--sc:#f472b6;display:none" id="home-clips-header" data-catchup="clips">
++ <div class="section-header" style="--sc:#f472b6;display:none" id="home-clips-header">
<h2><i class="fa-solid fa-scissors"></i> Recent Clips<small class="section-kicker">the good bits</small></h2>
<a href="/content?type=clips" onclick="return handleLinkClick(event, '/content?type=clips')" class="section-action">View all</a>
</div>
-- <div class="stream-grid home-clips-grid" id="home-clips-grid" data-catchup="clips"></div>
-- <div id="home-clips-pagination" class="pastes-pagination" style="display:none" data-catchup="clips"></div>
++ <div class="stream-grid home-clips-grid" id="home-clips-grid"></div>
++ <div id="home-clips-pagination" class="pastes-pagination" style="display:none"></div>
<!-- Recent Pastes -->
-- <div class="section-header" style="--sc:#2dd4bf;display:none" id="home-pastes-header" data-catchup="pastes">
++ <div class="section-header" style="--sc:#2dd4bf;display:none" id="home-pastes-header">
<h2><i class="fa-solid fa-paste"></i> Recent Pastes<small class="section-kicker">shots & snippets</small></h2>
<a href="/content?type=pastes" onclick="return handleLinkClick(event, '/content?type=pastes')" class="section-action">View all</a>
</div>
-- <div class="home-pastes-list" id="home-pastes-list" data-catchup="pastes"></div>
-- <div id="home-pastes-pagination" class="pastes-pagination" style="display:none" data-catchup="pastes"></div>
++ <div class="home-pastes-list" id="home-pastes-list"></div>
++ <div id="home-pastes-pagination" class="pastes-pagination" style="display:none"></div>
<!-- AI Moments showcase -->
-- <div class="home-section-lazy" id="home-moments-section" style="display:none" data-catchup="moments">
++ <div class="home-section-lazy" id="home-moments-section" style="display:none">
<div class="section-header" style="--sc:#22d3ee">
<h2><i class="fa-solid fa-brain"></i> AI Moments<small class="section-kicker">picked by AI</small></h2>
<a href="/moments" onclick="return handleLinkClick(event, '/moments')" class="section-action">See all</a>
ddiff --git a/public/js/app-home.js b/public/js/app-home.js
iindex ad657b7d..a71efe10 100644
---- a/public/js/app-home.js
++++ b/public/js/app-home.js
@@@ -1254,9 +1254,11 @@ function demoteHomeIntroSections() {
}
/**
-- * Tabs for a block of home sections (2026-10-30, the owner's short-pages rule: a phone page is one or two screens).
-- * On a phone a block shows one of its sections at a time behind a row of tabs; wider screens never see the tabs
-- * and show every section. The tabs and the CSS live in the home fragment and features/home.css.
++ * Tabs for a block of home sections. Only the explainer at the foot (New here?) uses them: the content blocks (Catch
++ * up, Around OpenVibe) show every section on every screen again (the owner, 2026-10-31: "now I can't see the videos
++ * and clips on the homepage… don't reduce size by removing features"). On a phone the block shows one section at a
++ * time behind a row of tabs; wider screens never see the tabs. The tabs and the CSS live in the home fragment and
++ * features/home.css.
*
* initHomeTabs(boxId, shownIds, storageKey, onOpen, { closable })
* boxId the block's element id. It carries data-tab — the section that is open — and its tab row.
@@@ -1341,23 +1343,6 @@ function initHomeTabs(boxId, shownIds, storageKey, onOpen, { closable = false }
sync();
}
--/**
-- * Catch up: the five rails after Live Now share one place, one rail at a time. A tab shows once its rail has
-- * something: each loader already hides its header or section when it has nothing, so the tabs follow those.
-- */
--const HOME_CATCHUP_KEY = 'ov_home_catchup';
--const _HOME_CATCHUP_SHOWN = { vods: 'home-recent-vods-header', clips: 'home-clips-header', pastes: 'home-pastes-header', moments: 'home-moments-section' };
--function initHomeCatchup() {
-- initHomeTabs('home-catchup', _HOME_CATCHUP_SHOWN, HOME_CATCHUP_KEY);
--}
--
--/** Around OpenVibe: the digest, the star spotlight and the pulse share one place on a phone. */
--const HOME_AROUND_KEY = 'ov_home_around';
--const _HOME_AROUND_SHOWN = { lately: 'home-digest', star: 'home-star-section', happening: 'home-pulse-section' };
--function initHomeAround() {
-- initHomeTabs('home-around', _HOME_AROUND_SHOWN, HOME_AROUND_KEY);
--}
--
/** New here?: the tour, the About banner and the network hub share one place on a phone. */
const HOME_INTRO_KEY = 'ov_home_intro';
const _HOME_INTRO_SHOWN = { tour: 'home-tour-mount', about: 'home-cta-banner', network: 'home-network-hub' };
@@@ -1372,9 +1357,7 @@ function initHomeIntro() {
}
async function loadHome() {
-- initHomeAround(); // the digest, the star spotlight and the pulse share one place on a phone
initHomeIntro(); // the tour, the About banner and the network hub share one place on a phone
-- initHomeCatchup();
demoteHomeIntroSections();
void loadHomeChangelog();
_initHomeAbout();
ddiff --git a/test/home-catchup.test.js b/test/home-catchup.test.js
iindex 835c9963..ddaf9217 100644
---- a/test/home-catchup.test.js
++++ b/test/home-catchup.test.js
@@@ -1,8 +1,8 @@
/**
-- * Catch up (2026-10-30, the owner's short-pages rule: a phone page is one or two screens): on a phone the five rails
-- * after Live Now (Recently Online, Recent VODs, Recent Clips, Recent Pastes, AI Moments) share one place, a row of
-- * tabs and one rail at a time. Wider screens never see the tabs and show every rail. Nothing was removed: every
-- * rail is still in the page, in the same order.
++ * Catch up: the five rails after Live Now (Recently Online, Recent VODs, Recent Clips, Recent Pastes, AI Moments)
++ * show on every screen, each with its own heading. For a day they were phone tabs (2026-10-30), and the owner could
++ * no longer see the videos and clips on the home page (2026-10-31: "don't reduce size by removing features"). On a
++ * phone each rail is one row of cards that scrolls sideways, so the page stays short without hiding anything.
*
* node test/home-catchup.test.js
*/
@@@ -16,6 +16,7 @@ try { assets.setSharedDir(require('openvibe-shared/files').dir); } catch { /* */
const html = assets.renderRoute(assets.document('index.html').html, '/');
const home = html.slice(html.indexOf('<section id="page-home"'), html.indexOf('</section>', html.indexOf('<section id="page-home"')));
const css = fs.readFileSync(path.join(__dirname, '../public/css/features/home.css'), 'utf8');
++const style = fs.readFileSync(path.join(__dirname, '../public/css/style.css'), 'utf8');
const js = fs.readFileSync(path.join(__dirname, '../public/js/app-home.js'), 'utf8');
let failures = 0;
@@@ -26,45 +27,35 @@ function check(name, fn) {
console.log('Home catch up');
--const start = home.indexOf('<div class="home-catchup" id="home-catchup" data-tab="recent">');
++const start = home.indexOf('<div class="home-catchup" id="home-catchup">');
const end = home.indexOf('</div><!-- /home-catchup -->');
--const box = home.slice(start, end);
--const TABS = ['recent', 'vods', 'clips', 'pastes', 'moments'];
++const block = home.slice(start, end);
--check('the five rails sit inside one Catch up block, in their order', () => {
++check('every rail is in the block, in its order, with its own heading', () => {
assert.ok(start > 0 && end > start, 'the block opens and closes on the home page');
-- const order = ['id="stream-grid-recent"', 'id="home-recent-vods-grid"', 'id="home-clips-grid"', 'id="home-pastes-list"', 'id="home-moments-row"'].map((n) => box.indexOf(n));
-- assert.ok(order.every((i) => i > 0), 'every rail is inside it');
-- assert.deepStrictEqual([...order].sort((a, b) => a - b), order, 'in the same order as before');
++ const order = ['Recently Online', 'Recent VODs', 'Recent Clips', 'Recent Pastes', 'AI Moments'].map((h) => block.indexOf(h));
++ assert.ok(order.every((i) => i > 0), 'every heading is there');
++ assert.deepStrictEqual([...order].sort((a, b) => a - b), order, 'in the same order');
++ for (const id of ['stream-grid-recent', 'home-recent-vods-grid', 'home-clips-grid', 'home-pastes-list', 'home-moments-row']) {
++ assert.ok(block.includes(`id="${id}"`), `${id} is in the block`);
++ }
});
--check('one tab per rail, named for screen readers, pointing at its rail; only Recently online starts shown', () => {
-- assert.match(box, /<div class="home-catchup-tabs" role="tablist" aria-label="Catch up">/);
-- for (const key of TABS) {
-- const tab = (new RegExp(`<button type="button" role="tab" id="hc-tab-${key}" data-tab="${key}"[^>]*>`).exec(box) || [])[0];
-- assert.ok(tab, `the ${key} tab`);
-- const controls = (/aria-controls="([^"]+)"/.exec(tab) || [])[1];
-- assert.ok(controls && box.includes(`id="${controls}"`), `${key} controls a rail inside the block`);
-- assert.strictEqual(/ hidden\b/.test(tab), key !== 'recent', `${key} starts ${key === 'recent' ? 'shown' : 'hidden until its rail has something'}`);
-- assert.ok(new RegExp(`data-catchup="${key}"`).test(box), `${key}'s rail is marked`);
-- }
++check('no tabs and nothing that hides a rail on a phone', () => {
++ assert.ok(!block.includes('role="tablist"') && !block.includes('data-catchup'), 'no tab row and no per-rail switch');
++ assert.ok(!/home-catchup-tabs|data-catchup|\.home-catchup:not\(/.test(css), 'no CSS hides a rail');
++ assert.ok(!/initHomeCatchup|_HOME_CATCHUP_SHOWN/.test(js), 'no script sets tabs up');
});
--check('the tabs and the one-rail-at-a-time rule exist only under 760 px', () => {
-- assert.match(css, /\.home-catchup-tabs \{ display: none; \}/, 'no tabs by default');
-- const phone = css.slice(css.indexOf('.home-catchup-tabs { display: none; }'));
-- const block = phone.slice(phone.indexOf('@media (max-width: 759px) {'));
-- for (const key of TABS) assert.ok(block.includes(`.home-catchup:not([data-tab="${key}"]) [data-catchup="${key}"]`), `${key} hides when another tab is open`);
-- assert.match(block, /min-height: 44px/, 'a tab is a 44 px target');
-- assert.match(js, /const _narrow = typeof window !== 'undefined' && window\.matchMedia\('\(max-width: 759px\)'\)/, 'the same breakpoint as the rails\' phone sizing');
++check('on a phone each rail is one row that scrolls sideways', () => {
++ const phone = css.slice(css.indexOf('/* Catch up on a phone'));
++ assert.match(phone, /#home-catchup \.stream-grid \{[^}]*grid-auto-flow: column;[^}]*overflow-x: auto;[^}]*scroll-snap-type: x mandatory;/);
++ assert.match(phone, /#home-catchup \.stream-grid > \* \{ scroll-snap-align: start; min-width: 0; \}/);
});
--check('a tab shows once its rail does: the ids the script watches are the rails\' own', () => {
-- const watched = /const _HOME_CATCHUP_SHOWN = \{([^}]+)\}/.exec(js);
-- assert.ok(watched, 'the map of watched elements');
-- for (const id of watched[1].match(/'([a-z-]+)'/g).map((s) => s.slice(1, -1))) assert.ok(box.includes(`id="${id}"`), `${id} is in the block`);
-- assert.match(js, /initHomeCatchup\(\);\s*\n\s*demoteHomeIntroSections\(\);/, 'loadHome starts it');
++check('the changelog preview keeps its full height on a phone', () => {
++ assert.ok(!/\.home-changelog-wrapper\.collapsed \{ max-height: 200px; \}/.test(style), 'the newest-day-only cut is gone');
});
--console.log(failures ? `\n${failures} failed` : '\nall passed');
--process.exit(failures ? 1 : 0);
++if (failures) { console.log(`\n${failures} failed`); process.exit(1); }
++console.log('\nAll home catch up checks passed');
ddiff --git a/test/home-tabs.test.js b/test/home-tabs.test.js
iindex 94a9c7a8..d423acd8 100644
---- a/test/home-tabs.test.js
++++ b/test/home-tabs.test.js
@@@ -1,14 +1,11 @@
/**
* Home tabs (2026-10-30, the owner's short-pages rule: a phone page is one or two screens).
*
-- * Part 1 shipped Catch up: the five rails after Live Now share one place, a row of tabs and one rail at a time.
-- * This is the other two blocks, built on the same controller (initHomeTabs in public/js/app-home.js):
-- *
-- * Around OpenVibe the digest (Lately), the star spotlight (Star) and the pulse (Happening)
-- * New here? the restream tour (How it works), the About banner (About), the network hub (The network)
-- *
-- * Wider screens never see the tabs and show every section. Nothing was removed: every section is still in the
-- * page. The block is real here, not a string: the test opens the rendered home page in a DOM and drives it.
++ * Only the explainer at the foot is tabbed (New here?: the restream tour, the About banner, the network hub). The
++ * content blocks are never tabbed again (the owner, 2026-10-31: "now I can't see the videos and clips on the homepage…
++ * don't reduce size by removing features"): Around OpenVibe (the digest, the star spotlight, the pulse) shows every
++ * section on every screen, and test/home-catchup.test.js holds the rails. Wider screens never see the tabs. The
++ * block is real here, not a string: the test opens the rendered home page in a DOM and drives it.
*
* node test/home-tabs.test.js
*/
@@@ -91,28 +88,27 @@ function openHome({ views = 0 } = {}) {
console.log('Home tabs');
--const AROUND = ['lately', 'star', 'happening'];
const INTRO = ['tour', 'about', 'network'];
--const aroundStart = home.indexOf('<div class="home-tabbox" id="home-around"');
++const aroundStart = home.indexOf('<div class="home-around" id="home-around"');
const aroundEnd = home.indexOf('</div><!-- /home-around -->');
const around = home.slice(aroundStart, aroundEnd);
const introStart = home.indexOf('<div class="home-tabbox home-intro" id="home-intro"');
const introEnd = home.indexOf('</div><!-- /home-intro -->');
const intro = home.slice(introStart, introEnd);
--check('the three sections of Around OpenVibe share one block, in their order, with the egg where it was', () => {
++check('Around OpenVibe shows its three sections, in their order, with the egg where it was, and no tabs', () => {
assert.ok(aroundStart > 0 && aroundEnd > aroundStart, 'the block opens and closes on the home page');
const order = ['id="home-digest"', 'id="home-star-section"', 'id="hero-egg"', 'id="home-pulse-section"'].map((n) => around.indexOf(n));
assert.ok(order.every((i) => i > 0), 'every section is inside it');
assert.deepStrictEqual([...order].sort((a, b) => a - b), order, 'in the same order as before');
assert.ok(home.indexOf('id="home-around"') < home.indexOf('id="home-catchup"'), 'the block sits where the digest was');
++ assert.ok(!around.includes('role="tablist"') && !/data-panel="(lately|star|happening)"/.test(around), 'no tab row and no panels');
++ assert.ok(!/\[data-panel="(lately|star|happening)"\]/.test(css), 'no CSS hides one of them');
});
check('one tab per section, named for screen readers, pointing at its section', () => {
-- assert.match(around, /<div class="home-tabs" role="tablist" aria-label="Around OpenVibe">/);
assert.ok(intro.includes('<div class="home-tabs" role="tablist" aria-label="New here?">'), 'the New here? tab list');
-- for (const [key, block, box] of [['lately', around, 'home-digest'], ['star', around, 'home-star-section'], ['happening', around, 'home-pulse-section'],
-- ['tour', intro, 'home-tour-mount'], ['about', intro, 'home-cta-banner'], ['network', intro, 'home-network-hub']]) {
++ for (const [key, block, box] of [['tour', intro, 'home-tour-mount'], ['about', intro, 'home-cta-banner'], ['network', intro, 'home-network-hub']]) {
const tab = (new RegExp(`<button type="button" role="tab" id="[a-z]+-tab-${key}" data-tab="${key}"[^>]*>`).exec(block) || [])[0];
assert.ok(tab, `the ${key} tab`);
const controls = (/aria-controls="([^"]+)"/.exec(tab) || [])[1];
@@@ -121,35 +117,28 @@ check('one tab per section, named for screen readers, pointing at its section',
}
});
--check('How it works is the default tab of New here? and Lately the default of Around OpenVibe', () => {
++check('How it works is the default tab of New here?', () => {
assert.match(intro, /id="hi-tab-tour" data-tab="tour" aria-selected="true"/);
-- assert.match(around, /id="ha-tab-lately" data-tab="lately" aria-selected="true"/);
assert.match(intro, /<div class="home-tabbox home-intro" id="home-intro" data-tab="tour">/);
-- assert.match(around, /<div class="home-tabbox" id="home-around" data-tab="lately">/);
});
check('the tabs and the one-section-at-a-time rule exist only under 760 px', () => {
assert.match(css, /\.home-tabs \{ display: none; \}/, 'no tabs by default');
const phone = css.slice(css.indexOf('.home-tabs { display: none; }'));
const block = phone.slice(phone.indexOf('@media (max-width: 759px) {'));
-- for (const key of [...AROUND, ...INTRO]) assert.ok(block.includes(`.home-tabbox:not([data-tab="${key}"]) [data-panel="${key}"]`), `${key} hides when another tab is open`);
++ for (const key of INTRO) assert.ok(block.includes(`.home-tabbox:not([data-tab="${key}"]) [data-panel="${key}"]`), `${key} hides when another tab is open`);
assert.match(block, /min-height: 44px/, 'a tab is a 44 px target');
assert.match(js, /const _narrow = typeof window !== 'undefined' && window\.matchMedia\('\(max-width: 759px\)'\)/, 'the same breakpoint as the rails\' phone sizing');
});
--check('the section\'s own heading goes while its tab names it, and the header keeps what it does', () => {
-- assert.ok(css.includes('[data-panel="star"] > .section-header > h2'), 'the star heading goes');
-- assert.ok(css.includes('[data-panel="lately"] .digest-title'), 'the digest title goes');
++check('the network hub\'s heading goes while its tab names it', () => {
assert.ok(css.includes('[data-panel="network"] .network-hub-header h3'), 'the network heading goes');
-- assert.ok(css.includes('[data-panel="happening"] > .section-header { display: none; }'), 'the pulse header is only its heading');
-- assert.ok(css.includes('[data-panel="star"] > .section-header { margin: 6px 0 10px; min-height: 0; }'), 'the star header keeps its note');
});
check('a tab shows once its section does: the ids the script watches are the sections\' own', () => {
-- const aroundIds = /const _HOME_AROUND_SHOWN = \{([^}]+)\}/.exec(js);
const introIds = /const _HOME_INTRO_SHOWN = \{([^}]+)\}/.exec(js);
-- assert.ok(aroundIds && introIds, 'the maps of watched sections');
-- for (const id of aroundIds[1].match(/'([a-z-]+)'/g).map((s) => s.slice(1, -1))) assert.ok(around.includes(`id="${id}"`), `${id} is in the block`);
++ assert.ok(introIds, 'the map of watched sections');
++ assert.ok(!/initHomeAround|initHomeCatchup|_HOME_AROUND_SHOWN|_HOME_CATCHUP_SHOWN/.test(js), 'the content blocks have no tabs to set up');
for (const id of introIds[1].match(/'([a-z-]+)'/g).map((s) => s.slice(1, -1))) assert.ok(intro.includes(`id="${id}"`), `${id} is in the block`);
});
@@@ -167,44 +156,17 @@ check('the tour loads when its tab opens, not only when the mount nears the view
});
(async () => {
-- await check('a tab appears once its section has something (the loader shows the pulse)', async () => {
-- const p = openHome();
-- p.run('initHomeAround(); initHomeIntro();');
-- await tick();
-- assert.strictEqual(p.el('ha-tab-happening').hidden, true, 'the pulse has nothing yet, so its tab is hidden');
-- show(p.el('home-pulse-section'));
-- await tick();
-- assert.strictEqual(p.el('ha-tab-happening').hidden, false, 'the pulse arrived, so its tab is there');
-- assert.strictEqual(p.el('ha-tab-lately').hidden, false, 'the digest is there all along');
-- });
--
-- await check('a tab that is shown is one the reader can open, and it is remembered', async () => {
++ await check('a tab appears once its section has something, and a block with nothing drops its tab row', async () => {
const p = openHome();
-- p.run('initHomeAround();');
-- await tick();
-- p.click('ha-tab-star');
-- await tick();
-- assert.strictEqual(p.el('home-around').dataset.tab, 'star', 'the block shows the star');
-- assert.strictEqual(p.el('ha-tab-star').getAttribute('aria-selected'), 'true', 'the tab says so');
-- assert.strictEqual(p.el('ha-tab-lately').getAttribute('aria-selected'), 'false', 'the other one does not');
-- assert.strictEqual(p.store.get('ov_home_around'), 'star', 'the reader keeps it for next time');
-- });
--
-- await check('a section that empties out falls back, and a block with nothing drops its tab row', async () => {
-- const p = openHome();
-- p.run('initHomeAround();');
-- await tick();
-- p.click('ha-tab-star');
-- await tick();
-- show(p.el('home-pulse-section')); // the pulse arrives
++ p.run('initHomeIntro();');
await tick();
-- hide(p.el('home-digest')); // the loader found nothing
-- hide(p.el('home-star-section'));
++ for (const id of ['home-tour-mount', 'home-cta-banner', 'home-network-hub']) hide(p.el(id));
await tick();
-- assert.strictEqual(p.el('home-around').dataset.tab, 'happening', 'it fell back to the pulse');
-- hide(p.el('home-pulse-section'));
++ assert.strictEqual(p.el('home-intro').querySelector('[role="tablist"]').hidden, true, 'nothing to show, so no tab row');
++ show(p.el('home-cta-banner'));
await tick();
-- assert.strictEqual(p.el('home-around').querySelector('[role="tablist"]').hidden, true, 'nothing left to show, so no tab row');
++ assert.strictEqual(p.el('hi-tab-about').hidden, false, 'the banner arrived, so its tab is there');
++ assert.strictEqual(p.el('hi-tab-network').hidden, true, 'the hub has nothing, so its tab is hidden');
});
await check('the tour loads when its tab opens', async () => {