openvibers/openvibe.live Commits
Home on phones: every section shows again; the rails scroll sideways instead of hiding behind tabs (#471)
Changes
@@@ -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 (thefeatured block above already shows it), and when it was the only stream there the grid's header goes too; the@@@ -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═══════════════════════════════════════════════════════════════ */@@@ -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 livecontent and creators): channels that were just on, their recordings, the clips people tookand 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>@@@ -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 phoneinitHomeIntro(); // the tour, the About banner and the network hub share one place on a phone-- initHomeCatchup();demoteHomeIntroSections();void loadHomeChangelog();_initHomeAbout();@@@ -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');@@@ -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 () => {
@@ -1268,20 +1268,19 @@ body.egg-rainbow { animation: eggRainbow 2.6s linear; }
@@ -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; }
#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; }
.home-tabs { display: none; }
@media (max-width: 759px) {
@media (max-width: 759px) {
- .home-catchup-tabs, .home-tabs {
+ .home-tabs {
display: flex; gap: 6px; margin: 30px -16px 6px; padding: 2px 16px 4px;
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;
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));
--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;
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;
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; }
@@ -1289,43 +1288,31 @@ body.egg-rainbow { animation: eggRainbow 2.6s linear; }
border: 1px solid var(--border); border-radius: 999px;
border: 1px solid var(--border); border-radius: 999px;
transition: color 0.18s, background 0.18s, border-color 0.18s;
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));
color: color-mix(in srgb, var(--sc-on) 30%, var(--text-primary));
background: color-mix(in srgb, var(--sc-on) 14%, transparent);
background: color-mix(in srgb, var(--sc-on) 14%, transparent);
border-color: color-mix(in srgb, var(--sc-on) 48%, 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="tour"]) [data-panel="tour"],
.home-tabbox:not([data-tab="about"]) [data-panel="about"],
.home-tabbox:not([data-tab="about"]) [data-panel="about"],
.home-tabbox:not([data-tab="network"]) [data-panel="network"] { display: none !important; }
.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="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
/* 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
featured block above already shows it), and when it was the only stream there the grid's header goes too; the
@@ -5931,12 +5931,6 @@ body.theme-light .btn-outline:hover {
@@ -5931,12 +5931,6 @@ body.theme-light .btn-outline:hover {
pointer-events: none;
pointer-events: none;
border-radius: 0 0 12px 12px;
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
UPDATES / CHANGELOG
═══════════════════════════════════════════════════════════════ */
═══════════════════════════════════════════════════════════════ */
@@ -101,21 +101,14 @@
@@ -101,21 +101,14 @@
</div>
</div>
</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 -->
<!-- "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) -->
<!-- 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">
<div class="hero-egg" id="hero-egg" style="display:none">
<button class="hero-egg-head" type="button" onclick="toggleHeroEgg()">
<button class="hero-egg-head" type="button" onclick="toggleHeroEgg()">
@@ -149,7 +142,7 @@
@@ -149,7 +142,7 @@
</div>
</div>
<!-- Community pulse: goals near completion, latest activity, weekly leaders -->
<!-- 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">
<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>
<h2><i class="fa-solid fa-bolt"></i> Happening Now<small class="section-kicker">the pulse</small></h2>
</div>
</div>
@@ -160,54 +153,46 @@
@@ -160,54 +153,46 @@
<!-- What people made, above the introduction to the site (roadmap 32.7: the home leads with live
<!-- 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
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). -->
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 -->
<!-- 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>
<h2><i class="fa-solid fa-clock-rotate-left"></i> Recently Online<small class="section-kicker">just missed them</small></h2>
</div>
</div>
- <div class="stream-grid" id="stream-grid-recent" data-catchup="recent">
+ <div class="stream-grid" id="stream-grid-recent">
<div class="empty-state">
<div class="empty-state">
<p class="muted">No recent streams</p>
<p class="muted">No recent streams</p>
</div>
</div>
</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 -->
<!-- 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>
<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>
<a href="/content?type=vods" onclick="return handleLinkClick(event, '/content?type=vods')" class="section-action">View all</a>
</div>
</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 -->
<!-- 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>
<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>
<a href="/content?type=clips" onclick="return handleLinkClick(event, '/content?type=clips')" class="section-action">View all</a>
</div>
</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 -->
<!-- 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>
<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>
<a href="/content?type=pastes" onclick="return handleLinkClick(event, '/content?type=pastes')" class="section-action">View all</a>
</div>
</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 -->
<!-- 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">
<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>
<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>
<a href="/moments" onclick="return handleLinkClick(event, '/moments')" class="section-action">See all</a>
@@ -1254,9 +1254,11 @@ function demoteHomeIntroSections() {
@@ -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 })
* 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.
* 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 }
@@ -1341,23 +1343,6 @@ function initHomeTabs(boxId, shownIds, storageKey, onOpen, { closable = false }
sync();
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. */
/** 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_KEY = 'ov_home_intro';
const _HOME_INTRO_SHOWN = { tour: 'home-tour-mount', about: 'home-cta-banner', network: 'home-network-hub' };
const _HOME_INTRO_SHOWN = { tour: 'home-tour-mount', about: 'home-cta-banner', network: 'home-network-hub' };
@@ -1372,9 +1357,7 @@ function initHomeIntro() {
@@ -1372,9 +1357,7 @@ function initHomeIntro() {
}
}
async function loadHome() {
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
initHomeIntro(); // the tour, the About banner and the network hub share one place on a phone
- initHomeCatchup();
demoteHomeIntroSections();
demoteHomeIntroSections();
void loadHomeChangelog();
void loadHomeChangelog();
_initHomeAbout();
_initHomeAbout();
@@ -1,8 +1,8 @@
@@ -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
* node test/home-catchup.test.js
*/
*/
@@ -16,6 +16,7 @@ try { assets.setSharedDir(require('openvibe-shared/files').dir); } catch { /* */
@@ -16,6 +16,7 @@ try { assets.setSharedDir(require('openvibe-shared/files').dir); } catch { /* */
const html = assets.renderRoute(assets.document('index.html').html, '/');
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 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 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');
const js = fs.readFileSync(path.join(__dirname, '../public/js/app-home.js'), 'utf8');
let failures = 0;
let failures = 0;
@@ -26,45 +27,35 @@ function check(name, fn) {
@@ -26,45 +27,35 @@ function check(name, fn) {
console.log('Home catch up');
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 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');
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');
@@ -1,14 +1,11 @@
@@ -1,14 +1,11 @@
/**
/**
* Home tabs (2026-10-30, the owner's short-pages rule: a phone page is one or two screens).
* 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
* node test/home-tabs.test.js
*/
*/
@@ -91,28 +88,27 @@ function openHome({ views = 0 } = {}) {
@@ -91,28 +88,27 @@ function openHome({ views = 0 } = {}) {
console.log('Home tabs');
console.log('Home tabs');
-const AROUND = ['lately', 'star', 'happening'];
const INTRO = ['tour', 'about', 'network'];
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 aroundEnd = home.indexOf('</div><!-- /home-around -->');
const around = home.slice(aroundStart, aroundEnd);
const around = home.slice(aroundStart, aroundEnd);
const introStart = home.indexOf('<div class="home-tabbox home-intro" id="home-intro"');
const introStart = home.indexOf('<div class="home-tabbox home-intro" id="home-intro"');
const introEnd = home.indexOf('</div><!-- /home-intro -->');
const introEnd = home.indexOf('</div><!-- /home-intro -->');
const intro = home.slice(introStart, introEnd);
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');
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));
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.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.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(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', () => {
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');
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];
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`);
assert.ok(tab, `the ${key} tab`);
const controls = (/aria-controls="([^"]+)"/.exec(tab) || [])[1];
const controls = (/aria-controls="([^"]+)"/.exec(tab) || [])[1];
@@ -121,35 +117,28 @@ check('one tab per section, named for screen readers, pointing at its section',
@@ -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(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(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', () => {
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');
assert.match(css, /\.home-tabs \{ display: none; \}/, 'no tabs by default');
const phone = css.slice(css.indexOf('.home-tabs { display: none; }'));
const phone = css.slice(css.indexOf('.home-tabs { display: none; }'));
const block = phone.slice(phone.indexOf('@media (max-width: 759px) {'));
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(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');
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="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', () => {
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);
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`);
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
@@ -167,44 +156,17 @@ check('the tour loads when its tab opens, not only when the mount nears the view
});
});
(async () => {
(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();
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();
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();
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();
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 () => {
await check('the tour loads when its tab opens', async () => {