openvibers/openvibe.live Commits

Hero chips: readable labels, centred icons, and fit verified across eight viewports

OP OpenVibers committed 16 Sept 2026 · 913fcd66c3 · 3 files, +48 −11

Labels were being truncated to "US…" and "HOURS WA…". Two changes: the three long ones are now
short words (Active, Visitors, Hours — the seven-day window is already stated by the delta pill
and the tooltip), and labels wrap rather than ellipsing, because an unreadable fragment is worse
than a second line. Sizes track the space a chip actually has rather than switching at
breakpoints.

Icon glyphs sat high in their tiles. Centring an icon-font glyph needs more than place-items: the
glyph lives in a line box sized by the inherited line-height, so the tile centred the box and the
glyph drifted inside it. Pinned to line-height 1 with the glyph in its own grid cell.

Measured at 320, 360, 412, 480, 768, 1024 and 1400 wide plus a 900x420 landscape phone: nine
chips, three rows, equal row heights, zero truncated labels, zero overflowing numbers or pills,
and every icon centred in its tile.

Also let the navbar's children shrink on narrow phones — they had no min-width: 0, so their
combined minimum pushed the bar past the right edge on a 320px screen.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CMxJm1dB8rdsaMSgNjVa82

Changes

ddiff --git a/public/css/style.css b/public/css/style.css
iindex 6743c760..81e3d9e8 100644
---- a/public/css/style.css
++++ b/public/css/style.css
@@@ -14819,11 +14819,11 @@ a .arena-thread:hover { color: var(--text-primary); background: color-mix(in srg
}
.hero-stat-strip .hero-stat { min-width: 0; padding: clamp(6px, 1.1vw, 10px) clamp(7px, 1.2vw, 12px); gap: clamp(5px, 0.9vw, 10px); }
.hero-stat-strip .hero-stat > i { font-size: clamp(0.72rem, 1.6vw, 1.05rem); }
--.hero-stat-strip .hero-stat-num { font-size: clamp(0.82rem, 2.3vw, 1.18rem); }
--.hero-stat-strip .hero-stat-label {
-- font-size: clamp(0.5rem, 1.15vw, 0.62rem); letter-spacing: 0.05em;
-- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
--}
++/* Sized against the chip, not the viewport: at three per row a chip is roughly a third of the
++ width minus gaps, so 3.2vw tracks the space actually available and clamps at both ends. */
++.hero-stat-strip .hero-stat-label { font-size: clamp(0.48rem, 1.5vw, 0.62rem); letter-spacing: 0.04em; }
++.hero-stat-strip .hero-stat-num { font-size: clamp(0.78rem, 3.4vw, 1.18rem); }
++.hero-stat-strip .hero-stat-delta { font-size: clamp(0.5rem, 1.9vw, 0.62rem); }
/* Below ~360px a label like "Hours watched" cannot fit on one line in a third of the screen.
Let it wrap rather than truncating it into something unreadable. */
@media (max-width: 900px) { .hero-stats { min-height: 218px; } }
@@@ -15102,8 +15102,45 @@ a .arena-thread:hover { color: var(--text-primary); background: color-mix(in srg
.hero-stat > i { grid-row: 1 / 2; align-self: center; }
.hero-stat-meta { grid-column: 2; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.hero-stat-num { white-space: nowrap; line-height: 1.12; }
--.hero-stat-label { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.2; }
++/* Never truncate a label — an unreadable "US…" is worse than a second line. Wrapping is the
++ safety net; the labels themselves are short enough that it rarely triggers. */
++.hero-stat-label { white-space: normal; overflow-wrap: anywhere; line-height: 1.15; }
.hero-stat-delta { grid-column: 1 / -1; justify-self: start; margin-top: 1px; }
.hero-stat-strip { grid-auto-rows: 1fr; align-items: stretch; }
/* The sparkline chip is one wide cell, not this shape. */
.hero-stat--spark { display: flex; }
++
++/* ── Icon tiles ──────────────────────────────────────────────────────────────
++ Centring an icon-font glyph needs more than place-items: the glyph sits inside a line box whose
++ height comes from the inherited line-height, so the tile centred the box and the glyph drifted
++ up inside it. Pinning line-height to 1 and giving the glyph its own grid cell puts it in the
++ middle of the tile at every size. */
++.hero-stat > i, .section-header h2 > i:first-child {
++ line-height: 1; font-style: normal;
++ display: grid; place-items: center; text-align: center;
++}
++.hero-stat > i::before, .section-header h2 > i:first-child::before { display: block; line-height: 1; }
++
++/* ── No sideways scroll on narrow phones ─────────────────────────────────────
++ The home page's decorative layers — the CTA banner's ambient glow and the tour card's core ring
++ — are sized in viewport units and bleed past the edge below about 400px, which turns the whole
++ page into a horizontal scroller. Clip rather than hide: overflow: clip does not create a scroll
++ container, so nothing inside loses its own scrolling. */
++#page-home { overflow-x: clip; }
++.home-cta-banner, .tour { overflow: hidden; }
++
++/* ── Navbar on very narrow phones ────────────────────────────────────────────
++ The navbar is fixed and spans the viewport, but its flex children could not shrink, so on a
++ 320-380px screen their combined minimum width pushed past the right edge and turned the entire
++ document into a horizontal scroller — every page, not just this one. Letting the children
++ shrink, tightening the gaps and dropping the brand wordmark (the mark itself stays) keeps it
++ inside the viewport without clipping anything people need to tap. */
++.navbar > * { min-width: 0; }
++.nav-brand { min-width: 0; overflow: hidden; }
++@media (max-width: 420px) {
++ .navbar { padding: 0 9px; gap: 8px; }
++ .nav-brand .brand-text, .nav-brand > span:not(.brand-mark) { display: none; }
++ .nav-right { gap: 6px; min-width: 0; }
++ .nav-coins, .nav-balance { max-width: 88px; overflow: hidden; }
++ .nav-avatar-wrap .nav-username { display: none; }
++}
ddiff --git a/public/index.html b/public/index.html
iindex c97d55af..aa7af5dd 100644
---- a/public/index.html
++++ b/public/index.html
@@@ -88,7 +88,7 @@
overlay) and is fetched without blocking the first paint: media="print" makes the browser
treat it as non-critical, and the onload flips it back to all once it has arrived. -->
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
-- <link rel="stylesheet" href="/css/style.css?v=184">
++ <link rel="stylesheet" href="/css/style.css?v=187">
<link rel="stylesheet" href="/css/home-fx.css?v=12">
<link rel="stylesheet" href="/css/home-tour.css?v=7">
<link rel="stylesheet" href="/css/i18n-star.css?v=5">
@@@ -5130,7 +5130,7 @@ curl -s https://openvibe.media/live/@Goosely/chat-insight.json | jq ".chat_insig
<script src="/live-notify.js?v=3" defer></script>
<script src="/js/ov-skeleton.js?v=1" defer></script>
<script src="/js/ov-odometer.js?v=3" defer></script>
--<script src="/js/app.js?v=159" defer></script>
++<script src="/js/app.js?v=160" defer></script>
<script src="/js/stream-player.js?v=22" defer></script>
<script src="/js/stream-pip.js?v=1" defer></script>
<script src="/js/chat-shared.js?v=3" defer></script>
ddiff --git a/public/js/app.js b/public/js/app.js
iindex f8456fbb..1642a167 100644
---- a/public/js/app.js
++++ b/public/js/app.js
@@@ -1354,8 +1354,8 @@ function renderHeroStats(stats) {
const now = [
{ key: 'liveNow', cls: stats.liveNow > 0 ? 'hero-stat--live' : '', icon: stats.liveNow > 0 ? 'fa-circle' : 'fa-circle-dot', num: stats.liveNow, label: 'Live', title: stats.liveNow > 0 ? 'Streams live right now' : 'Nobody is live right now — check Recently Online below' },
{ key: 'viewersNow', cls: stats.viewersNow > 0 ? 'hero-stat--live' : '', icon: 'fa-eye', num: stats.viewersNow, label: 'Watching', title: 'Viewers watching right now' },
-- { key: 'weeklyActive', icon: 'fa-fire', num: stats.weeklyActive, label: 'Active · 7d', title: 'People who chatted in the last 7 days', desc: 'Distinct chatters in the last 7 days — signed-in users, anonymous chatters and relayed (Twitch/Kick/YouTube) chatters, each counted once.', metric: 'active' },
-- { key: 'weeklyVisitors', icon: 'fa-user-plus', num: stats.weeklyVisitors, label: 'Visitors · 7d', title: 'First-time visitors in the last 7 days', desc: 'Browsers seen on the site for the first time in the last 7 days (a privacy-safe fingerprint, no account needed). A proxy for new people showing up, not just chatting.', metric: 'visitors' },
++ { key: 'weeklyActive', icon: 'fa-fire', num: stats.weeklyActive, label: 'Active', title: 'People who chatted in the last 7 days', desc: 'Distinct chatters in the last 7 days — signed-in users, anonymous chatters and relayed (Twitch/Kick/YouTube) chatters, each counted once.', metric: 'active' },
++ { key: 'weeklyVisitors', icon: 'fa-user-plus', num: stats.weeklyVisitors, label: 'Visitors', title: 'First-time visitors in the last 7 days', desc: 'Browsers seen on the site for the first time in the last 7 days (a privacy-safe fingerprint, no account needed). A proxy for new people showing up, not just chatting.', metric: 'visitors' },
];
// 24h viewer sparkline (5-minute samples) — trends read better than a snapshot.
const trend = Array.isArray(stats.viewerTrend) ? stats.viewerTrend : [];
@@@ -1484,7 +1484,7 @@ function renderHeroStats(stats) {
{ key: 'anons', icon: 'fa-user-secret', num: stats.anons, label: 'Anons', title: 'Anonymous chatters who have been given a name', recent: R.anons },
{ key: 'chatMessages', icon: 'fa-comments', num: stats.chatMessages, label: 'Messages', title: 'Chat messages sent, all time', recent: R.messages },
{ key: 'streamers', icon: 'fa-satellite-dish', num: stats.streamers, label: 'Streamers', title: 'People who have gone live here', recent: R.streamers },
-- { key: 'hoursWatched', icon: 'fa-couch', num: stats.hoursWatched, label: 'Hours watched', title: 'Hours the community has spent watching', recent: R.hours },
++ { key: 'hoursWatched', icon: 'fa-couch', num: stats.hoursWatched, label: 'Hours', title: 'Hours the community has spent watching', recent: R.hours },
];
// Always starts collapsed. Persisting "expanded" meant every reload rendered and counted up
// two dozen extra chips before the page had finished loading, for a view the reader asked for