openvibers/openvibe.live Commits

Channel: every section keeps its header with its icon, and a phone's tab shows only its own section (#849)

OP OpenVibers committed 1 days ago · 43f4fc529d · 5 files, +39 −12

The owner, 11-12, with a phone screenshot of a channel: "a bunch of sections are
missing a header, and they need a nice header with icon".

Two rules had met. The rails' grid (#page-channel :is(#ch-panel-videos, ...), two
ids) outranked the tabs' hiding of the sections not chosen (one id), so on a phone
Videos, Clips, Clips taken and Pastes showed under every tab, About included, while
the tabs' block hid each section's heading.

- In a phone's tabs a section not chosen never shows, whatever a later rule says.
- Every section keeps its heading there: the icon tile, the name, the count, its
  tools beside or under it; a fold's summary is its heading, not a toggle.
- About standing alone in a tab gets its "Latest videos" and "Popular clips" rows
  back, and About's parts (AI summary, Links, the rows) and Clips' AI Moments use one
  sub-header: the same icon tile a size down.
- test/channel-sections.test.js pins both.

Changes

ddiff --git a/public/css/features/channel.css b/public/css/features/channel.css
iindex bf883e5b..c531a63d 100644
---- a/public/css/features/channel.css
++++ b/public/css/features/channel.css
@@@ -2219,6 +2219,13 @@ html[data-ov-motion="off"] #page-channel .ch-rec-dot { animation: none; }
#page-channel .ch-sec-ic { display: inline-grid; place-items: center; flex: none; width: 30px; height: 30px; border-radius: 9px; font-size: .9rem;
color: var(--accent-light, var(--accent)); background: color-mix(in srgb, var(--accent) 14%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 22%, transparent); }
#page-channel svg.ch-sec-ic { padding: 7px; box-sizing: border-box; }
++/* A part of a section (AI Moments in Clips, About's latest videos and popular clips): the same tile, a size down. */
++#page-channel .ch-sub-h { display: flex; align-items: center; gap: 9px; margin: 22px 0 6px; font-size: 1rem; font-weight: 800; line-height: 1.2; letter-spacing: -.005em; text-transform: none; color: var(--text-primary); }
++#page-channel .ch-sub-h > .ch-sec-ic { width: 26px; height: 26px; border-radius: 8px; font-size: .78rem; }
++#page-channel .ch-about-h.ch-sub-h { margin-top: 18px; }
++#page-channel .ch-sub-h .ch-about-h-note { font-size: .8rem; font-weight: 600; color: var(--text-muted); opacity: 1; }
++#page-channel .ch-socials-head.ch-sub-h { margin: 0 0 10px; }
++#page-channel .ch-about-content > :first-child .ch-sub-h:first-child, #page-channel .ch-about-content > .ch-sub-h:first-child { margin-top: 0; }
#page-channel .ch-sec-n { display: inline-flex; align-items: center; height: 22px; padding: 0 8px; border-radius: 999px; font-size: .74rem; background: color-mix(in srgb, var(--text-primary, #fff) 7%, transparent); }
#page-channel .ch-ib { gap: 6px; border-color: color-mix(in srgb, var(--text-primary, #fff) 8%, transparent); }
#page-channel .ch-ib .ch-ib-caret { display: none; font-size: .6rem; opacity: .7; }
@@@ -2239,12 +2246,18 @@ html[data-ov-motion="off"] #page-channel .ch-rec-dot { animation: none; }
/* ── One section at a time on a phone or a tablet (ask 367, 2026-11-11: "consolidate these into tabs, self-contained
tabs … at least on mobile"). The page's HTML keeps every section; at 900 px or narrower app-channel.js marks the
-- page .ch-tabbed and the chosen one .is-current. The tab names the section, so its own heading steps aside and its
-- tools lead its first row; folds open by themselves. ─── */
--#page-channel.ch-tabbed .ch-tab-panel:not(.is-current) { display: none; }
++ page .ch-tabbed and the chosen one .is-current. Folds open by themselves.
++ 11-12, the owner, with a phone screenshot of About followed by headless rows of videos and clips: "a bunch of sections
++ are missing a header, and they need a nice header with icon". Two rules had met: the rails' grid
++ (#page-channel :is(#ch-panel-videos, …), two ids) outranked the hiding of the sections not chosen (one id), so Videos,
++ Clips, Clips taken and Pastes showed under every tab, while this block took their headings away. Now a section not
++ chosen never shows, whatever a later rule says, and every section keeps its heading: the icon tile, the name, the
++ count, its tools beside or under it. ─── */
++#page-channel.ch-tabbed .ch-tab-panel:not(.is-current) { display: none !important; }
#page-channel.ch-tabbed .ch-tab-panel { margin: 0 0 18px; min-height: 42vh; }
--#page-channel.ch-tabbed .ch-tab-panel > .ch-sec-h, #page-channel.ch-tabbed .ch-tab-panel .ch-fold > summary { display: none; }
--#page-channel.ch-tabbed :is(#ch-panel-videos, #ch-panel-clips, #ch-panel-clips-taken, #ch-panel-pastes) > .ch-panel-bar { grid-column: 1 / -1; grid-row: 1; justify-content: flex-start; }
++/* A fold is open in its own tab: its summary is the section's heading, not a toggle. */
++#page-channel.ch-tabbed .ch-tab-panel .ch-fold > summary { pointer-events: none; min-height: 0; padding: 0; border: 0; background: none; }
++#page-channel.ch-tabbed .ch-tab-panel .ch-fold > summary .ch-fold-say { display: none; }
#page-channel.ch-tabbed :is(#ch-panel-videos, #ch-panel-clips, #ch-panel-clips-taken, #ch-panel-pastes) > .ch-panel-bar:not(:has(> :not([style*="display:none"]):not([style*="display: none"]):not(:empty))) { display: none; }
#page-channel.ch-tabbed .ch-tab-panel.is-entering { animation: chPanelIn var(--ov-dur-2, .22s) var(--ov-ease-out, cubic-bezier(.22, 1, .36, 1)) both; }
@keyframes chPanelIn { from { opacity: 0; transform: translateY(6px); } }
ddiff --git a/public/fragments/channel.html b/public/fragments/channel.html
iindex 5d6cbe12..1a2b0160 100644
---- a/public/fragments/channel.html
++++ b/public/fragments/channel.html
@@@ -457,7 +457,8 @@
<div id="ch-clips-of-pagination" class="pastes-pagination" style="display:none"></div>
<!-- What the AI cut from these streams, after what people made (roadmap 33.6). -->
<div class="ch-ai-clips" id="ch-ai-clips-section" hidden>
-- <p class="ch-panel-hint"><i class="fa-solid fa-wand-magic-sparkles"></i> <b>AI Moments</b> — clips OpenVibe's AI cut automatically when chat reacted. No one clipped these.</p>
++ <h3 class="ch-sub-h"><i class="fa-solid fa-wand-magic-sparkles ch-sec-ic" aria-hidden="true"></i>AI Moments</h3>
++ <p class="ch-panel-hint">Clips OpenVibe's AI cut automatically when chat reacted. No one clipped these.</p>
<div class="stream-grid" id="ch-ai-clips-grid"></div>
<div id="ch-ai-clips-pagination" class="pastes-pagination" style="display:none"></div>
</div>
ddiff --git a/public/js/app-channel.js b/public/js/app-channel.js
iindex 4c730251..df6635bb 100644
---- a/public/js/app-channel.js
++++ b/public/js/app-channel.js
@@@ -2329,7 +2329,9 @@ function _chApplyTabbed() {
const page = document.getElementById('page-channel');
if (!page) return;
const on = _chIsTabbed();
++ const was = page.classList.contains('ch-tabbed');
page.classList.toggle('ch-tabbed', on);
++ if (was !== on && typeof _renderAboutRecent === 'function') _renderAboutRecent();
if (on) _chShowPanel(_channelTab || _chFirstTab(), false);
else document.querySelectorAll('#page-channel .ch-tab-panel.is-current').forEach((p) => p.classList.remove('is-current'));
}
@@@ -3496,7 +3498,7 @@ function _aboutAiEffectiveShow() {
function _aboutAiOverviewHTML() {
if (!_aboutAiEffectiveShow()) return '';
return `<section class="ch-about-sec about-ai-overview" aria-label="AI summary">
-- <h3 class="ch-about-h"><i class="fa-solid fa-wand-magic-sparkles"></i> AI summary <span class="ch-about-h-note">from past streams</span></h3>
++ <h3 class="ch-about-h ch-sub-h"><i class="fa-solid fa-wand-magic-sparkles ch-sec-ic" aria-hidden="true"></i>AI summary <span class="ch-about-h-note">from past streams</span></h3>
${_collapsibleOverview(_aboutAiOverview)}
</section>`;
}
@@@ -3518,13 +3520,14 @@ function _aboutRowCard(kind, it) {
function _renderAboutRecent() {
const host = document.getElementById('ch-about-recent');
if (!host) return;
-- // Sections, not tabs (11-07): the Videos and Clips sections follow About on the page, so these preview rows would
-- // show the same cards twice. They were there so an About tab was never a one-line bio over empty space.
-- if (document.getElementById('ch-panel-videos')) { host.innerHTML = ''; return; }
++ // On a wide screen the Videos and Clips sections follow About on the page, so these preview rows would show the
++ // same cards twice. In a phone's tabs About stands alone (ask 367), and the rows are back, so About is never a
++ // one-line bio over empty space.
++ if (document.getElementById('ch-panel-videos') && !_chIsTabbed()) { host.innerHTML = ''; return; }
const r = _aboutRecent;
if (!r || r.username !== currentChannelUsername) { host.innerHTML = ''; return; }
const row = (title, icon, tab, kind, items) => items.length ? `<section class="ch-about-sec">
-- <h3 class="ch-about-h"><i class="fa-solid ${icon}"></i> ${title}<button type="button" class="ch-about-more" onclick="switchChannelTab('${tab}', document.querySelector('.ch-tab[data-tab=${tab}]'), true)">See all <i class="fa-solid fa-arrow-right"></i></button></h3>
++ <h3 class="ch-about-h ch-sub-h"><i class="fa-solid ${icon} ch-sec-ic" aria-hidden="true"></i>${title}<button type="button" class="ch-about-more" onclick="switchChannelTab('${tab}', document.querySelector('.ch-tab[data-tab=${tab}]'), true)">See all <i class="fa-solid fa-arrow-right"></i></button></h3>
<div class="ch-about-row">${items.map((it) => _aboutRowCard(kind, it)).join('')}</div>
</section>` : '';
host.innerHTML = row('Latest videos', 'fa-video', 'videos', 'vod', r.vods) + row('Popular clips', 'fa-scissors', 'clips', 'clip', r.clips);
ddiff --git a/public/js/channel-socials.js b/public/js/channel-socials.js
iindex d522e79e..e4bdb93a 100644
---- a/public/js/channel-socials.js
++++ b/public/js/channel-socials.js
@@@ -69,7 +69,7 @@
if (!host) return;
if (!(links || []).length) { host.innerHTML = ''; host.hidden = true; return; }
host.hidden = false;
-- host.innerHTML = `<div class="ch-socials-head"><i class="fa-solid fa-link"></i> Links</div><div class="ch-socials-grid">${links.map((l, i) => `
++ host.innerHTML = `<h3 class="ch-socials-head ch-sub-h"><i class="fa-solid fa-link ch-sec-ic" aria-hidden="true"></i>Links</h3><div class="ch-socials-grid">${links.map((l, i) => `
<article class="ch-social-card" style="--sc:${escH(l.color)}" data-i="${i}">
<a class="ch-social-card-top" href="${escH(l.url)}" target="_blank" rel="noopener me">
<span class="ch-social-ico"><i class="${escH(l.icon)}" aria-hidden="true"></i></span>
ddiff --git a/test/channel-sections.test.js b/test/channel-sections.test.js
iindex f8131dd9..eb2284f0 100644
---- a/test/channel-sections.test.js
++++ b/test/channel-sections.test.js
@@@ -30,6 +30,16 @@ for (const fold of ['ai-timeline', 'analytics']) assert.match(below, new RegExp(
assert.match(css, /#page-channel \.ch-tab-panel \{ display: block;/);
assert.ok(!/#page-channel \.ch-tab-panel \{[^}]*display: none/.test(css), 'no channel section is display:none by rule');
assert.match(js, /panel\.hidden = b\.style\.display === 'none';/, 'a section hides only with its jump link');
++// Tabs on a phone (ask 367): a section not chosen never shows, whatever a later rule says (11-12: the rails' two-id grid
++// outranked the one-id hiding, so Videos, Clips, Clips taken and Pastes showed under About with no heading); and every
++// section keeps its heading there (the owner: "they need a nice header with icon").
++assert.match(css, /#page-channel\.ch-tabbed \.ch-tab-panel:not\(\.is-current\) \{ display: none !important; \}/);
++assert.ok(!/\.ch-tabbed[^{}]*\.ch-sec-h[^{}]*\{[^}]*display: none/.test(css), 'no heading is hidden in tabs');
++assert.ok(!/\.ch-tabbed[^{}]*\.ch-fold > summary \{[^}]*display: none/.test(css), 'a fold\'s heading shows in tabs');
++for (const tab of SECTIONS) {
++ const h = below.match(new RegExp(`<h2 class="ch-sec-h" id="ch-h-${tab}">([\\s\\S]*?)</h2>`));
++ assert.ok(h && /class="fa-solid [a-z-]+ ch-sec-ic"/.test(h[1]), `${tab}'s heading has its icon tile`);
++}
assert.match(js, /function _watchChannelSections\(\)/);
assert.match(js, /new IntersectionObserver\([\s\S]{0,200}_ensureChannelSection\(e\.target\.id\.replace\('ch-panel-', ''\), true\)/, 'a section loads as it nears the screen');
assert.match(js, /if \(passing && \(tab === 'analytics' \|\| tab === 'ai-timeline'\)\) return;/, 'the folds load when opened, not on the way past');