Files
psysonic/src/styles/layout/main-content.css
T
Frank Stellmacher b0f35eabc9 fix: usable layout when the window is small (#981)
* fix(layout): collapse browse toolbar to icons on compact width

On a narrow window the labelled filter buttons wrapped into several rows
and, being a fixed-height sticky header, starved the album grid below them
down to a clipped strip. Wrap each toolbar label in a hideable span and drop
to icon-only in compact (mobile) mode; icons keep their tooltip and
aria-label so the action stays discoverable.

* fix(window): raise minimum window size to 520x640

The old 360x480 floor let the window shrink far past the point the layout
holds together. Floor it at a laptop-friendly size where the compact layout
(icon toolbar + scrollable lists) still works.

* fix(player): scale mobile cover to fit short windows

The cover width was viewport-derived with no height bound, so on a short
window the square grew past its slot and overlapped the title. Bind it to
the shrinkable wrap via max-height and keep it square with auto sizing.

* docs: changelog for small-window layout fixes (#981)
2026-06-04 11:16:48 +02:00

213 lines
6.0 KiB
CSS

/* ─── Main Content ─── */
.main-content {
grid-area: main;
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 0;
min-height: 0; /* allow 1fr row to shrink freely */
background: var(--bg-app);
z-index: 1;
}
/* Scoped UI scaling target. Sidebar / queue / player / titlebar live in
sibling grid cells of .app-shell and stay at 1:1. The wrapper inherits
.main-content's flex column layout so .content-header (fixed height) and
.content-body (flex: 1, scroll) keep working unchanged.
Zoom is applied via inline style only when uiScale !== 1, so the default
case has no extra layer or layout cost. */
.main-content-zoom {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
min-width: 0;
}
.content-header {
display: flex;
align-items: center;
gap: var(--space-4);
padding: 0 var(--space-6);
height: 64px;
box-sizing: border-box;
border-bottom: 1px solid var(--border-subtle);
background: var(--bg-app);
position: relative;
z-index: 10;
}
.content-header h1 {
font-family: var(--font-display);
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.content-header .spacer { flex: 1; }
.content-header > * {
transition: opacity 0.14s ease, visibility 0.14s ease;
}
.content-header[data-live-search-overlay] > :not(.live-search):not(.spacer) {
opacity: 0;
visibility: hidden;
pointer-events: none;
}
.content-body {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: var(--space-6);
contain: paint;
}
/* When floating player is active, add extra bottom padding so content doesn't
get hidden behind the fixed floating bar (player height + bottom offset + minimal spacing).
!important needed to override inline padding:0 on App.tsx content-body */
.app-shell.floating-player .content-body {
padding-bottom: calc(var(--player-height) + 12px) !important;
}
/* Every page re-uses .content-body as its outer wrapper, but App.tsx already
renders one around <Routes /> as the scroll container. The inner one must
not establish its own scroll ancestor — otherwise `position: sticky`
children anchor to a container that never scrolls, and the header just
scrolls along with the content. Padding stays: the outer App.tsx
content-body has `padding: 0` inline, the inner one is what gives each
page its breathing room. */
.content-body .content-body {
overflow: visible;
}
/* Main route stack: scroll + overlay thumb live in the inner viewport, not the
outer .content-body, so sticky headers still anchor to the scrolling element. */
.content-body.app-shell-route-host {
flex: 1;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
padding: 0;
position: relative;
contain: none;
}
.app-shell-route-scroll.overlay-scroll {
flex: 1;
min-height: 0;
}
.app-shell-route-scroll .app-shell-route-scroll__viewport {
contain: paint;
}
/* Browse pages: in-page overlay scroller — main route viewport does not scroll. */
.app-shell-route-scroll__viewport--inpage-split {
overflow: hidden !important;
}
/* Sticky toolbar + `OverlayScrollArea` body; shared by Artists, Albums, Composers, etc. */
.content-body.mainstage-inpage-split {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
overflow: hidden;
box-sizing: border-box;
padding-right: 0;
transition: padding-top 0.2s ease;
}
.mainstage-inpage-split .mainstage-inpage-toolbar {
flex-shrink: 0;
}
.content-body.mainstage-inpage-split .page-sticky-header {
position: relative;
top: auto;
z-index: auto;
margin-right: 0;
transition: padding 0.2s ease;
}
.content-body.mainstage-inpage-split.mainstage-inpage--header-tight {
padding-top: 0;
}
.content-body.mainstage-inpage-split.mainstage-inpage--header-tight .page-sticky-header {
padding-top: var(--space-2);
padding-bottom: var(--space-2);
}
.content-body.mainstage-inpage-split.mainstage-inpage--header-tight .page-sticky-header .page-title {
font-size: 1rem;
}
/* Toolbar rows: use these class names instead of inline gap so tight mode can tighten. */
.mainstage-inpage-toolbar-row {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.75rem;
}
.content-body.mainstage-inpage-split.mainstage-inpage--header-tight .mainstage-inpage-toolbar-row {
gap: 0.5rem;
}
.mainstage-inpage-toolbar-alpha-row {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
margin-top: var(--space-4);
}
.content-body.mainstage-inpage-split.mainstage-inpage--header-tight .mainstage-inpage-toolbar-alpha-row {
margin-top: var(--space-2);
}
.content-body.mainstage-inpage-split.mainstage-inpage--header-tight .artists-alpha-btn {
padding: 0.15rem 0.4rem;
font-size: 11px;
}
.content-body.mainstage-inpage-split > .mainstage-inpage-scroll.overlay-scroll {
flex: 1;
min-height: 0;
}
/* Compact mode: the sticky toolbar is `flex-shrink: 0`, so labelled filter
buttons wrap into several rows on a narrow window and starve the `flex: 1`
list below them, clipping the album grid (zunoz, RC2). Collapse the toolbar
buttons to icon-only (label hidden, icon + tooltip + aria-label kept) so the
row stays short and the list keeps its height. */
.app-shell[data-mobile] .mainstage-inpage-toolbar .toolbar-btn-label {
display: none;
}
.mainstage-inpage-scroll__viewport {
padding-bottom: var(--space-6);
padding-right: var(--space-6);
}
/* Sticky page header: keeps page title + filter/search bar visible while the
body scrolls. Negative horizontal margins (+ matching padding) make the
background flush with the scroll container edges so the sticky block looks
like a real bar, not a floating island. `top: 0` clamps below the content-
body's own padding-top; the residual 24px gap above the sticky block while
scrolling is intentional — gives the bar visual breathing room. */
.page-sticky-header {
position: sticky;
top: 0;
z-index: 5;
background: var(--bg-app);
margin: 0 calc(-1 * var(--space-6)) var(--space-5);
padding: var(--space-4) var(--space-6);
border-bottom: 1px solid var(--border-subtle);
}