openvibers/openvibe.live Commits
Channel: every section keeps its header with its icon, and a phone's tab shows only its own section (#849)
Changes
@@@ -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-containedtabs … 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); } }@@@ -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>@@@ -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);@@@ -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>@@@ -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');
@@ -2219,6 +2219,13 @@ html[data-ov-motion="off"] #page-channel .ch-rec-dot { animation: none; }
@@ -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;
#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); }
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; }
#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-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 { 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; }
#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; }
@@ -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
/* ── 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
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 { 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 :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; }
#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); } }
@keyframes chPanelIn { from { opacity: 0; transform: translateY(6px); } }
@@ -457,7 +457,8 @@
@@ -457,7 +457,8 @@
<div id="ch-clips-of-pagination" class="pastes-pagination" style="display:none"></div>
<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). -->
<!-- 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>
<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 class="stream-grid" id="ch-ai-clips-grid"></div>
<div id="ch-ai-clips-pagination" class="pastes-pagination" style="display:none"></div>
<div id="ch-ai-clips-pagination" class="pastes-pagination" style="display:none"></div>
</div>
</div>
@@ -2329,7 +2329,9 @@ function _chApplyTabbed() {
@@ -2329,7 +2329,9 @@ function _chApplyTabbed() {
const page = document.getElementById('page-channel');
const page = document.getElementById('page-channel');
if (!page) return;
if (!page) return;
const on = _chIsTabbed();
const on = _chIsTabbed();
+ const was = page.classList.contains('ch-tabbed');
page.classList.toggle('ch-tabbed', on);
page.classList.toggle('ch-tabbed', on);
+ if (was !== on && typeof _renderAboutRecent === 'function') _renderAboutRecent();
if (on) _chShowPanel(_channelTab || _chFirstTab(), false);
if (on) _chShowPanel(_channelTab || _chFirstTab(), false);
else document.querySelectorAll('#page-channel .ch-tab-panel.is-current').forEach((p) => p.classList.remove('is-current'));
else document.querySelectorAll('#page-channel .ch-tab-panel.is-current').forEach((p) => p.classList.remove('is-current'));
}
}
@@ -3496,7 +3498,7 @@ function _aboutAiEffectiveShow() {
@@ -3496,7 +3498,7 @@ function _aboutAiEffectiveShow() {
function _aboutAiOverviewHTML() {
function _aboutAiOverviewHTML() {
if (!_aboutAiEffectiveShow()) return '';
if (!_aboutAiEffectiveShow()) return '';
return `<section class="ch-about-sec about-ai-overview" aria-label="AI summary">
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)}
${_collapsibleOverview(_aboutAiOverview)}
</section>`;
</section>`;
}
}
@@ -3518,13 +3520,14 @@ function _aboutRowCard(kind, it) {
@@ -3518,13 +3520,14 @@ function _aboutRowCard(kind, it) {
function _renderAboutRecent() {
function _renderAboutRecent() {
const host = document.getElementById('ch-about-recent');
const host = document.getElementById('ch-about-recent');
if (!host) return;
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;
const r = _aboutRecent;
if (!r || r.username !== currentChannelUsername) { host.innerHTML = ''; return; }
if (!r || r.username !== currentChannelUsername) { host.innerHTML = ''; return; }
const row = (title, icon, tab, kind, items) => items.length ? `<section class="ch-about-sec">
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>
<div class="ch-about-row">${items.map((it) => _aboutRowCard(kind, it)).join('')}</div>
</section>` : '';
</section>` : '';
host.innerHTML = row('Latest videos', 'fa-video', 'videos', 'vod', r.vods) + row('Popular clips', 'fa-scissors', 'clips', 'clip', r.clips);
host.innerHTML = row('Latest videos', 'fa-video', 'videos', 'vod', r.vods) + row('Popular clips', 'fa-scissors', 'clips', 'clip', r.clips);
@@ -69,7 +69,7 @@
@@ -69,7 +69,7 @@
if (!host) return;
if (!host) return;
if (!(links || []).length) { host.innerHTML = ''; host.hidden = true; return; }
if (!(links || []).length) { host.innerHTML = ''; host.hidden = true; return; }
host.hidden = false;
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}">
<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">
<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>
<span class="ch-social-ico"><i class="${escH(l.icon)}" aria-hidden="true"></i></span>
@@ -30,6 +30,16 @@ for (const fold of ['ai-timeline', 'analytics']) assert.match(below, new RegExp(
@@ -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.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.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');
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, /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, /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');
assert.match(js, /if \(passing && \(tab === 'analytics' \|\| tab === 'ai-timeline'\)\) return;/, 'the folds load when opened, not on the way past');