mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
b0f35eabc9
* 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)
213 lines
6.0 KiB
CSS
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);
|
|
}
|
|
|