openvibers/openvibe.live Commits

Mobile nav: a panel attached to the button, not a full-bleed sheet

OP OpenVibers committed 16 Sept 2026 · acb9854f18 · 2 files, +116 −41

The menu was pinned to both edges of the screen with its rows centred individually, so every pill
came out a different width and the column read as ragged. It is now anchored under the hamburger
at the right edge, sized to its content (208px min, capped at the viewport), and scaled up from
its top-right corner so the motion starts where the finger did, with a small tongue pointing back
at the button. Rows are a single stretched column with a fixed icon gutter so the labels line up,
and they arrive on a short capped stagger. Hover, active and current-page states are all
theme-derived, with transitions rather than instant swaps.

The glass is layered — a translucent card tint over blur and saturation, a hairline, and an inner
top highlight — instead of a flat wash.

Also: thumbnail badge type was scaling down to 8.96px on a two-across phone. Compact meant tight
padding, not unreadable numerals; the floor is now 0.7rem and the container query still shrinks
between floor and ceiling.

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 14b8ffef..cbd7d1e2 100644
---- a/public/css/style.css
++++ b/public/css/style.css
@@@ -1884,7 +1884,7 @@ body.egg-rainbow { animation: eggRainbow 2.6s linear; }
position: absolute; bottom: 5px;
display: inline-flex; align-items: center; gap: 4px;
padding: 2px 6px; border-radius: 7px;
-- font-size: clamp(0.56rem, 4.4cqw, 0.7rem);
++ font-size: clamp(0.7rem, 4.6cqw, 0.8rem);
font-weight: 700; line-height: 1.3;
color: #fff;
font-variant-numeric: tabular-nums;
@@@ -1914,12 +1914,12 @@ body.egg-rainbow { animation: eggRainbow 2.6s linear; }
}
/* Very small tiles: the badge would otherwise eat the corner of the frame. */
@container (max-width: 150px) {
-- .stream-card-viewers, .stream-card-duration { padding: 1px 4px; gap: 3px; bottom: 4px; border-radius: 6px; }
++ .stream-card-viewers, .stream-card-duration { padding: 1px 5px; gap: 3px; bottom: 4px; border-radius: 6px; font-size: 0.68rem; }
.stream-card-duration { left: 4px; }
.stream-card-viewers { right: 4px; }
}
@supports not (container-type: inline-size) {
-- .stream-card-viewers, .stream-card-duration { font-size: 0.68rem; }
++ .stream-card-viewers, .stream-card-duration { font-size: 0.74rem; }
}
.stream-card-nsfw {
position: absolute; top: 8px; right: 8px;
@@@ -6218,67 +6218,143 @@ textarea.form-input { resize: vertical; min-height: 60px; }
.nav-links-wrap .nav-scroll-btn { display: none !important; }
.nav-hamburger { display: flex; }
-- /* Mobile nav drawer */
++ /* ── Mobile nav menu ──────────────────────────────────────────────────────
++ This was a full-bleed sheet pinned to both edges of the screen, with the items centred
++ individually so every pill was a different width and the column read as ragged. It is now
++ a panel that belongs to the button that opened it: anchored under the hamburger at the
++ right edge, sized to its content rather than the viewport, and scaled up from its top-right
++ corner so the motion starts where the finger did.
++
++ Glass here is a layered one — a translucent card tint over a blur with a little saturation,
++ a hairline, and an inner top highlight — so it sits above the page instead of washing it
++ out. All of it is theme-derived; nothing is a fixed colour. */
.nav-links.show {
-- display: flex; flex-direction: column;
-- position: fixed; top: var(--navbar-h); left: 0; right: 0;
-- background: var(--glass-strong);
-- border-bottom: 1px solid var(--border);
-- padding: 10px 12px; z-index: 1001;
-- overflow-y: auto;
-- max-height: calc(100vh - var(--navbar-h));
-- max-height: calc(100dvh - var(--navbar-h));
-- gap: 4px;
-- box-shadow: 0 18px 40px rgba(0,0,0,0.35);
-- backdrop-filter: blur(18px);
-- -webkit-backdrop-filter: blur(18px);
-- animation: nav-drawer-in 0.2s ease-out;
++ display: flex; flex-direction: column; align-items: stretch;
++ position: fixed; top: calc(var(--navbar-h) + 7px);
++ right: 8px; left: auto; bottom: auto;
++ width: max-content;
++ min-width: 208px;
++ max-width: min(280px, calc(100vw - 16px));
++ padding: 7px; gap: 3px; z-index: 1001;
++ border-radius: 16px;
++ border: 1px solid color-mix(in srgb, var(--border-light) 85%, transparent);
++ background:
++ linear-gradient(180deg,
++ color-mix(in srgb, var(--bg-card) 88%, transparent),
++ color-mix(in srgb, var(--bg-secondary) 94%, transparent));
++ box-shadow:
++ 0 22px 48px rgba(0, 0, 0, 0.45),
++ 0 3px 10px rgba(0, 0, 0, 0.28),
++ inset 0 1px 0 rgba(255, 255, 255, 0.07);
++ backdrop-filter: blur(20px) saturate(1.6);
++ -webkit-backdrop-filter: blur(20px) saturate(1.6);
++ overflow-y: auto; overscroll-behavior: contain;
++ max-height: calc(100vh - var(--navbar-h) - 22px);
++ max-height: calc(100dvh - var(--navbar-h) - 22px);
++ transform-origin: top right;
++ animation: nav-drawer-in 0.26s cubic-bezier(.2, 1, .3, 1) both;
++ }
++ /* A small tongue pointing back at the hamburger, so the panel reads as attached to it. */
++ .nav-links.show::before {
++ content: ''; position: absolute; top: -5px; right: 17px;
++ width: 11px; height: 11px; transform: rotate(45deg);
++ background: color-mix(in srgb, var(--bg-card) 88%, transparent);
++ border-left: 1px solid color-mix(in srgb, var(--border-light) 85%, transparent);
++ border-top: 1px solid color-mix(in srgb, var(--border-light) 85%, transparent);
++ border-radius: 2px 0 0 0;
}
@keyframes nav-drawer-in {
-- from { opacity: 0; transform: translateY(-8px); }
-- to { opacity: 1; transform: translateY(0); }
++ from { opacity: 0; transform: translateY(-8px) scale(0.93); }
++ to { opacity: 1; transform: none; }
++ }
++ /* The rows arrive just after the panel, close together — a long stagger on a menu someone is
++ already reaching for reads as lag, so it is capped at six rows. */
++ @keyframes nav-item-in {
++ from { opacity: 0; transform: translateX(7px); }
++ to { opacity: 1; transform: none; }
}
++ .nav-links.show .nav-link,
++ .nav-links.show .nav-dropdown-menu a {
++ animation: nav-item-in 0.26s ease both;
++ animation-delay: calc(60ms + var(--nav-i, 0) * 26ms);
++ }
++ .nav-links.show > :nth-child(1) { --nav-i: 0; }
++ .nav-links.show > :nth-child(2) { --nav-i: 1; }
++ .nav-links.show > :nth-child(3) { --nav-i: 2; }
++ .nav-links.show > :nth-child(4) { --nav-i: 3; }
++ .nav-links.show > :nth-child(5) { --nav-i: 4; }
++ .nav-links.show > :nth-child(n+6) { --nav-i: 5; }
/* Restore labels & lay out vertically in dropdown */
.nav-links.show .nav-link {
-- flex-shrink: 1;
-- justify-content: flex-start;
-- padding: 11px 14px;
-- border-color: rgba(255,255,255,0.05);
-- background: rgba(255,255,255,0.03);
-- border-radius: 10px;
-- font-size: 0.92rem;
++ flex-shrink: 0;
++ display: flex; align-items: center; justify-content: flex-start; gap: 11px;
++ width: 100%;
++ padding: 10px 12px;
++ border: 1px solid transparent;
++ background: transparent;
++ border-radius: 11px;
++ font-size: 0.92rem; font-weight: 600;
++ white-space: nowrap;
++ transition: background-color 0.16s ease, border-color 0.16s ease, color 0.16s ease, transform 0.12s ease;
++ }
++ /* A fixed icon column so the labels start on one line instead of wherever each glyph ends. */
++ .nav-links.show .nav-link > i,
++ .nav-links.show .nav-dropdown-menu a > i {
++ width: 19px; flex: none; text-align: center; font-size: 0.94rem;
++ color: var(--text-secondary); transition: color 0.16s ease;
}
.nav-links.show .nav-link span { display: inline; }
.nav-links.show .nav-link[data-tooltip]::after { display: none; }
++ .nav-links.show .nav-link:hover,
++ .nav-links.show .nav-dropdown-menu a:hover {
++ background: color-mix(in srgb, var(--accent) 12%, transparent);
++ border-color: color-mix(in srgb, var(--accent) 24%, transparent);
++ color: var(--text-primary);
++ }
++ .nav-links.show .nav-link:hover > i,
++ .nav-links.show .nav-dropdown-menu a:hover > i { color: var(--accent); }
++ .nav-links.show .nav-link:active,
++ .nav-links.show .nav-dropdown-menu a:active { transform: scale(0.982); }
.nav-links.show .nav-link.active {
-- background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 14%, transparent), color-mix(in srgb, var(--accent) 6%, transparent));
-- border-color: color-mix(in srgb, var(--accent) 22%, transparent);
++ background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 20%, transparent), color-mix(in srgb, var(--accent) 9%, transparent));
++ border-color: color-mix(in srgb, var(--accent) 34%, transparent);
++ color: var(--text-primary);
}
++ .nav-links.show .nav-link.active > i { color: var(--accent); }
-- /* Nav dropdown in mobile: flatten to inline items sized like the other nav links. */
++ /* Nav dropdown in mobile: flatten into the same single column as everything else. */
.nav-links.show .nav-dropdown {
-- display: flex; flex-direction: column; align-items: center; gap: 4px;
++ display: flex; flex-direction: column; align-items: stretch; gap: 3px;
}
.nav-links.show .nav-dropdown .nav-dd-caret { display: none; }
.nav-links.show .nav-dropdown-menu {
display: flex;
flex-direction: column;
-- align-items: center;
++ align-items: stretch;
position: static;
background: none;
border: none;
box-shadow: none;
padding: 0;
-- gap: 4px;
++ gap: 3px;
margin-top: 0;
++ width: 100%;
}
.nav-links.show .nav-dropdown-menu a {
-- padding: 11px 14px;
-- border-radius: 10px;
-- border: 1px solid rgba(255,255,255,0.05);
-- background: rgba(255,255,255,0.03);
-- font-size: 0.92rem;
++ display: flex; align-items: center; gap: 11px;
++ padding: 10px 12px;
++ border-radius: 11px;
++ border: 1px solid transparent;
++ background: transparent;
++ font-size: 0.92rem; font-weight: 600;
++ white-space: nowrap;
++ transition: background-color 0.16s ease, border-color 0.16s ease, color 0.16s ease, transform 0.12s ease;
++ }
++ @media (prefers-reduced-motion: reduce) {
++ .nav-links.show,
++ .nav-links.show .nav-link,
++ .nav-links.show .nav-dropdown-menu a { animation: none !important; }
}
/* "Go Live" is the only remaining nav dropdown — keep its trigger visible. */
@@@ -6377,9 +6453,8 @@ textarea.form-input { resize: vertical; min-height: 60px; }
.user-dropdown a { padding: 12px 16px; font-size: 0.92rem; }
/* Nav drawer tighter */
-- .nav-links.show { padding: 8px 10px; gap: 3px; }
-- .nav-links.show .nav-link { padding: 10px 12px; font-size: 0.88rem; }
-- .nav-links.show .nav-dropdown-menu a { padding: 9px 12px; font-size: 0.86rem; }
++ /* Only the outer inset tightens on a very small screen; row size is set with the panel. */
++ .nav-links.show { right: 6px; max-width: calc(100vw - 12px); }
.hero h1 { font-size: 1.6rem; }
.hero { padding-left: 16px; padding-right: 16px; }
ddiff --git a/public/index.html b/public/index.html
iindex f8683f9f..3fd97e7a 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=201">
++ <link rel="stylesheet" href="/css/style.css?v=202">
<link rel="stylesheet" href="/css/home-fx.css?v=13">
<link rel="stylesheet" href="/css/home-tour.css?v=10">
<link rel="stylesheet" href="/css/i18n-star.css?v=11">